diff options
Diffstat (limited to 'src/mailsync.cpp')
| -rw-r--r-- | src/mailsync.cpp | 129 |
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) { |
