aboutsummaryrefslogtreecommitdiffstats
path: root/src/mailsync.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailsync.cpp')
-rw-r--r--src/mailsync.cpp129
1 files changed, 129 insertions, 0 deletions
diff --git a/src/mailsync.cpp b/src/mailsync.cpp
index b1a5208..005580c 100644
--- a/src/mailsync.cpp
+++ b/src/mailsync.cpp
@@ -18,6 +18,135 @@
#include "mailsync.h"
+#include <QCoreApplication>
+#include <QRegularExpression>
+
+namespace {
+
+/// Longest status text put into the label. Sync output is unstructured and
+/// arrives from a script rather than from this code, so a line is truncated
+/// rather than trusted to be a sensible length: an unbounded one would resize
+/// the status bar and push the permanent widgets beside it off.
+constexpr int kMaxStatusChars = 120;
+
+/// Strips the script's "HH:MM:SS " prefix and anything that could disturb a
+/// single-line label. Returns plain text, never markup.
+QString sanitiseLine(const QString &line)
+{
+ QString text = line.trimmed();
+
+ static const QRegularExpression timestamp(
+ QStringLiteral("^\\d{2}:\\d{2}:\\d{2}\\s+"));
+ text.remove(timestamp);
+
+ // Collapse every control character, not just newlines: a stray \r would
+ // otherwise leave the label showing the tail of the line only.
+ static const QRegularExpression controls(QStringLiteral("[\\x00-\\x1f\\x7f]+"));
+ text.replace(controls, QStringLiteral(" "));
+ text = text.simplified();
+
+ if (text.size() > kMaxStatusChars)
+ text = text.left(kMaxStatusChars - 1) + QStringLiteral("…");
+
+ return text;
+}
+
+} // namespace
+
+void SyncPhaseTracker::reset()
+{
+ m_phase = SyncPhase::Starting;
+ m_status.clear();
+}
+
+bool SyncPhaseTracker::feed(const QString &line)
+{
+ const QString text = sanitiseLine(line);
+ if (text.isEmpty())
+ return false;
+
+ // The script's own banners. Skipped before anything else: RUN START would
+ // otherwise read as mbsync output, and RUN END carries a status= field that
+ // must not be parsed, since the exit code is the authority on the outcome.
+ if (text.startsWith(QLatin1String("=====")))
+ return false;
+
+ const QString before = m_status;
+
+ // notmuch new announces itself by what it reports, since it prints no
+ // banner. Any of these means mbsync is done and the reindex is running.
+ // Matched loosely and case-insensitively: the wording varies by version.
+ static const QRegularExpression notmuchLine(
+ QStringLiteral("^(processed \\d|added \\d|no new mail|found \\d)"),
+ QRegularExpression::CaseInsensitiveOption);
+
+ if (notmuchLine.match(text).hasMatch()) {
+ m_phase = SyncPhase::Notmuch;
+ m_status = QCoreApplication::translate("SyncPhaseTracker",
+ "Reindexing (notmuch)...");
+ return m_status != before;
+ }
+
+ // The channel mbsync is working on, which is the account name the user
+ // wants to see. Only printed under -V, which is why the shipped script
+ // passes it: without -V mbsync is silent until it exits.
+ //
+ // Taken from the output rather than from config, so what is shown is what
+ // is actually happening, and in the order it actually happens.
+ static const QRegularExpression channel(
+ QStringLiteral("^Channel\\s+(\\S.*)$"),
+ QRegularExpression::CaseInsensitiveOption);
+
+ if (const auto match = channel.match(text); match.hasMatch()) {
+ m_phase = SyncPhase::Mbsync;
+ // The name comes from a config file this app does not own and lands in
+ // a label, so it is truncated on its own before being interpolated:
+ // bounding only the finished string would let a long name push the
+ // wording out instead of itself.
+ QString name = match.captured(1).trimmed();
+ constexpr int kMaxNameChars = 60;
+ if (name.size() > kMaxNameChars)
+ name = name.left(kMaxNameChars - 1) + QStringLiteral("…");
+
+ m_status = QCoreApplication::translate("SyncPhaseTracker",
+ "Syncing %1...").arg(name);
+ return m_status != before;
+ }
+
+ // mbsync's end-of-run summary, printed with or without -V. It arrives after
+ // every channel is done, so it reports rather than progresses; the
+ // "Far:/Near:" tail is dropped as unreadable at a glance.
+ static const QRegularExpression summary(
+ QStringLiteral("^Channels:\\s*(\\d+)\\s+Boxes:\\s*(\\d+)"),
+ QRegularExpression::CaseInsensitiveOption);
+
+ if (const auto match = summary.match(text); match.hasMatch()) {
+ m_phase = SyncPhase::Mbsync;
+ m_status = QCoreApplication::translate(
+ "SyncPhaseTracker", "Syncing mail: %1 channels, %2 boxes")
+ .arg(match.captured(1), match.captured(2));
+ if (m_status.size() > kMaxStatusChars)
+ m_status = m_status.left(kMaxStatusChars - 1) + QStringLiteral("…");
+ return m_status != before;
+ }
+
+ // Everything else while mbsync runs. The bulk of a real run is one
+ // "Ignoring non-mail file" line per Maildir, so individual lines are never
+ // shown: only the fact that mbsync is the phase.
+ //
+ // Only ever an upgrade from Starting, never a downgrade. Once the summary
+ // has given real counts, a later noise line must not overwrite them with
+ // the generic wording: the label would flicker back to saying less than it
+ // already said, for every one of thousands of ignored files.
+ if (m_phase == SyncPhase::Starting) {
+ m_phase = SyncPhase::Mbsync;
+ m_status = QCoreApplication::translate("SyncPhaseTracker",
+ "Syncing mail (mbsync)...");
+ }
+
+ return m_status != before;
+}
+
MailSync::MailSync(const QString &command, QObject *parent)
: QObject(parent), m_command(command)
{