From 64d3138ba923071069da6c9bc458a25a9cc7d27f Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 11 Aug 2026 19:41:16 +0200 Subject: feat(sync): sync a tag change automatically after a short delay Item 71. A tag edit reached the notmuch index at edit time and then sat there until the user clicked Sync or their cron job fired, so "mark all read" updated the view while the change itself waited, sometimes for ten minutes. A confirmed edit now arms a debounce that runs the existing sync path. The delay is auto_sync_delay_ms in [general], defaulting to 2000, and follows mark_read_delay_ms exactly, including that zero and negative are not errors: zero syncs on the next trip through the event loop, and any negative value disables the behaviour, which is the switch for a user who wants only their cron job. It is armed from onTagsApplied, where a write is confirmed and the pending count is already current, rather than where one is sent: a sync scheduled for a write the worker went on to reject would run for nothing. A debounce rather than a schedule, restarted by each edit, because "mark all read" confirms one write per thread in the view and an arm-per-edit timer would be the storm of syncs the debounce exists to prevent. Nothing is armed when no sync command is configured or when the pending count is zero, the case where an edit was netted against its own inverse. When the timer fires with a sync already running, local or cron, it skips rather than queues: mbsync's own answer to a second run is to fail on it, and the edits stay pending rather than being lost. Also fixes a pane blanked out from under the reader, found by hand testing this feature. onSyncFinished called runCurrentQuery() where the cron path calls refreshCurrentQuery(), and a re-run clears the model, the undo stack and the message pane. The stale-thread notice handles a thread that stops matching the query and has since item 35, but a re-run left nothing for it to describe. The two paths had no reason to differ; before this item a local sync only followed a click on Sync, so the difference went unnoticed. Reading a message in the Unread view, having it marked read, and watching the pane go blank two seconds later is what surfaced it. Its test asserts on the undo stack rather than the pane: both paths issue a queued query test_mainwindow has no worker to answer, so the pane ends up blank either way and an assertion on it would pass against both, while the undo stack is cleared by one and kept by the other. Nine tests, four in test_config and five in test_mainwindow, each mutation-checked: removing the schedule call, honouring a negative delay, dropping the nothing-pending guard, dropping the already-running guard, and restoring runCurrentQuery() each fail a test. Co-Authored-By: Claude Opus 5 --- src/config.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/config.cpp') diff --git a/src/config.cpp b/src/config.cpp index a81651f..b1f730c 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -271,6 +271,23 @@ void Config::load(const QString &path) } } + // Item 71. Same shape as mark_read_delay_ms above, including that zero and + // negative are not errors: 0 syncs on the next trip through the event loop + // and negative disables the automatic sync entirely, which is how a user who + // wants only their cron job turns this off. + const QVariant autoSync = settings.value(QStringLiteral("auto_sync_delay_ms")); + if (autoSync.isValid()) { + bool ok = false; + const int value = autoSync.toString().toInt(&ok); + if (ok) { + m_autoSyncDelayMs = value; + } else { + addProblem(QStringLiteral("Auto-sync delay '%1' is not a number; " + "using the default.") + .arg(autoSync.toString())); + } + } + // [completion] is an ordinary section, so this one DOES take its prefix. // ',' separates entries and '|' separates a value from its description: // two different characters because QSettings splits comma lists itself, -- cgit v1.2.3