aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-13 16:07:18 +0200
committerDanilo M. <danix@danix.xyz>2026-08-13 16:07:18 +0200
commita52e4e41c2bc8fc8eb23c4bc99f9b8724212bd23 (patch)
tree96baca838f24de8dabfa95391eaf9cd1d0c6d20f /src
parente7cdd7fd3ae960572976cb4a053ecf192baa17c2 (diff)
downloadqtmaildir-a52e4e41c2bc8fc8eb23c4bc99f9b8724212bd23.tar.gz
qtmaildir-a52e4e41c2bc8fc8eb23c4bc99f9b8724212bd23.zip
feat(rules): every Maildir folder in the Folder dropdown
The dropdown was built from config, which names one subtree per account and nothing below it, so it offered five entries and no way to say Drafts or Sent. A rule wants to target those as often as a whole account. NotmuchWorker gains requestFolders/foldersReady, walking the tree from notmuch_database_get_path() and listing every directory holding cur/. It belongs there because the database root is notmuch's database.path and the worker owns the only handle that can answer for it; putting the root in config would be the second source of truth the design refuses. From the disk rather than from the index: a folder mbsync created and nothing has landed in yet is still a folder a rule may target, and a list derived from indexed message paths would not offer it. The two tests build their own fixture rather than extending the shared one, which needs a nested folder and would otherwise move seven count assertions in unrelated tests. Mutation-checked: flattening the walk to non-recursive fails the listing test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/mainwindow.cpp27
-rw-r--r--src/notmuchworker.cpp37
-rw-r--r--src/notmuchworker.h17
3 files changed, 71 insertions, 10 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index f622036..e8b4dda 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1314,18 +1314,16 @@ void MainWindow::showTagRulesDialog()
auto *dialog = new TagRulesDialog(this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
- // The Folder row's dropdown. Config holds the account subdirectories and
- // the dialog holds no Config, so they are handed over here. Account::maildir
- // is the subtree name the Folder term compiles a path: against; the account
- // key is a config section name and would match nothing.
- QStringList folders;
- for (const Account &account : m_config.accounts()) {
- if (!account.maildir.isEmpty() && !folders.contains(account.maildir))
- folders.append(account.maildir);
- }
- dialog->setFolders(folders);
m_tagRulesDialog = dialog;
+ // The Folder row's dropdown, filled from the Maildir tree on disk rather
+ // than from config. Config names one subtree per account and nothing
+ // below it, so the dropdown offered five entries and no way to say Drafts
+ // or Sent, which is a folder a rule wants to target as often as a whole
+ // account. The answer comes back queued, after the dialog is already up;
+ // setFolders refills the rows that exist by then.
+ QMetaObject::invokeMethod(m_worker, "requestFolders", Qt::QueuedConnection);
+
connect(dialog, &TagRulesDialog::countsRequested, this, [this, dialog]() {
QMetaObject::invokeMethod(
m_worker, "requestMessageCounts", Qt::QueuedConnection,
@@ -1425,6 +1423,15 @@ void MainWindow::wireWorker()
connect(m_worker, &NotmuchWorker::messageCountsReady,
this, &MainWindow::onRuleCountsReady);
+ // The rules dialog is the only consumer, and it may have been closed while
+ // the scan was in flight. No generation counter: the tree on disk does not
+ // change under a query, so a late answer is still the right one.
+ connect(m_worker, &NotmuchWorker::foldersReady, this,
+ [this](const QStringList &folders) {
+ if (m_tagRulesDialog)
+ m_tagRulesDialog->setFolders(folders);
+ });
+
// A confirmed write clears the pending revert: without this, a later
// unrelated error would roll back a change that actually succeeded.
connect(m_worker, &NotmuchWorker::tagsApplied,
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp
index 94fb79c..6aae397 100644
--- a/src/notmuchworker.cpp
+++ b/src/notmuchworker.cpp
@@ -20,6 +20,9 @@
#include <notmuch.h>
+#include <QDir>
+#include <QDirIterator>
+#include <QFileInfo>
#include <QSet>
#include <cstdlib>
@@ -678,3 +681,37 @@ void NotmuchWorker::requestMessageCounts(const QStringList &queries,
emit messageCountsReady(counts, generation);
}
+
+void NotmuchWorker::requestFolders()
+{
+ if (!openReadOnly())
+ return;
+
+ const QString root = QString::fromUtf8(notmuch_database_get_path(m_db));
+ if (root.isEmpty()) {
+ emit errorOccurred(
+ QStringLiteral("notmuch reports no database path."));
+ return;
+ }
+
+ // A Maildir folder is a directory holding cur/. Testing for that rather
+ // than listing every directory keeps the plumbing (cur, new, tmp) and an
+ // account's container directory out of the list; neither is somewhere mail
+ // is filed. Hidden directories are skipped, which is what excludes
+ // .notmuch itself.
+ QStringList folders;
+ QDirIterator it(root, QDir::Dirs | QDir::NoDotAndDotDot,
+ QDirIterator::Subdirectories);
+ const QDir rootDir(root);
+ while (it.hasNext()) {
+ const QString path = it.next();
+ if (!QFileInfo::exists(path + QStringLiteral("/cur")))
+ continue;
+ folders.append(rootDir.relativeFilePath(path));
+ }
+
+ // Sorted, so the dropdown keeps one order across openings. QDirIterator
+ // walks in filesystem order, which is neither stable nor alphabetical.
+ folders.sort();
+ emit foldersReady(folders);
+}
diff --git a/src/notmuchworker.h b/src/notmuchworker.h
index b3fbed3..9736ab8 100644
--- a/src/notmuchworker.h
+++ b/src/notmuchworker.h
@@ -151,6 +151,18 @@ public slots:
/// called when the dialog is opened and never on a timer.
void requestDatabaseStats(quint64 generation);
+ /// Every Maildir folder under the database root, as paths relative to it.
+ ///
+ /// From the DISK, not from the index: a folder mbsync created and nothing
+ /// has landed in yet is still a folder a tagging rule may target, and one
+ /// derived from indexed message paths would not offer it.
+ ///
+ /// Here rather than in MainWindow because the database root is
+ /// notmuch's `database.path` and this class owns the only handle that can
+ /// answer for it. Duplicating the path into config is exactly the second
+ /// source of truth the design refuses.
+ void requestFolders();
+
signals:
void threadsReady(const QVector<ThreadSummary> &threads, quint64 generation);
void queryFinished(int totalThreads, quint64 generation);
@@ -175,6 +187,11 @@ signals:
/// renders as unknown rather than as zero.
void databaseStatsReady(const DatabaseStats &stats, quint64 generation);
+ /// Maildir folders relative to the database root, sorted. No generation:
+ /// the tree on disk does not change under a query, and the one consumer
+ /// asks once when its dialog opens.
+ void foldersReady(const QStringList &folders);
+
void errorOccurred(const QString &message);
private: