diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-03 08:52:21 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-04 12:52:39 +0200 |
| commit | 23e2b007c76dbd1569b1f97c17100eefed4988af (patch) | |
| tree | fe121e499b6595d563f5756929cb1d24f43ad31d /src/notmuchworker.h | |
| parent | f752848629572a8396ddb6f50748f7accf227312 (diff) | |
| download | qtmaildir-23e2b007c76dbd1569b1f97c17100eefed4988af.tar.gz qtmaildir-23e2b007c76dbd1569b1f97c17100eefed4988af.zip | |
feat: add NotmuchWorker with batched queries and tag mutation
Owns the only notmuch database handle. Queries run read-only and emit
threads in batches of 200 with a generation counter so the UI can discard
superseded results. Tag mutation closes the read-only handle, opens
read-write, applies, and closes, holding the process-wide write lock for
milliseconds rather than blocking a concurrent `notmuch new`.
Tested against a throwaway database built in a QTemporaryDir, superseding
the spec's original "no unit test" position: applyTags is the only code
here that writes to a notmuch index. The fixture never touches ~/Mail or
~/.notmuch-config.
Two fixes against the drafted implementation, both caught by mutating the
code and confirming exactly one test failed:
- loadThread conflated "no query given" with "query matched nothing in
this thread", so filtering a thread down to zero matches rendered every
message expanded. Tracked with an explicit haveMatchSet flag.
- applyTags now documents why a stale message id must skip rather than
abort: notmuch_database_find_message reports SUCCESS with a null message
for an unknown id, and the live ids alongside it still need tagging.
Note for fixture authors: notmuch synchronizes maildir flags with tags at
index time, so a file named `...:2,S` is indexed without the unread tag no
matter what [new] tags requests. Unread fixture messages go in new/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/notmuchworker.h')
| -rw-r--r-- | src/notmuchworker.h | 69 |
1 files changed, 69 insertions, 0 deletions
diff --git a/src/notmuchworker.h b/src/notmuchworker.h new file mode 100644 index 0000000..2551004 --- /dev/null +++ b/src/notmuchworker.h @@ -0,0 +1,69 @@ +#pragma once + +#include <QObject> +#include <QStringList> +#include <QVector> + +#include "types.h" + +struct _notmuch_database; +typedef struct _notmuch_database notmuch_database_t; + +/// Owns the only notmuch database handle in the process. +/// +/// libnotmuch is not thread-safe and queries over a large database block, so +/// this object lives on its own thread and the UI reaches it only through +/// queued signals. No notmuch pointer ever leaves this class. +class NotmuchWorker : public QObject +{ + Q_OBJECT +public: + /// notmuchConfigPath may be empty, in which case notmuch resolves its own + /// config and therefore its own database.path. + explicit NotmuchWorker(const QString ¬muchConfigPath, QObject *parent = nullptr); + ~NotmuchWorker() override; + + /// Threads emitted per threadsReady() signal. + static constexpr int kBatchSize = 200; + +public slots: + /// Runs a query. generation lets the UI discard results from a superseded + /// query without the worker needing to know about cancellation. + void runQuery(const QString &query, quint64 generation); + + /// Loads the messages of one thread, oldest first. matchQuery is the + /// user's current query; messages matching it render expanded, the rest + /// as stubs. + void loadThread(const QString &threadId, const QString &matchQuery, + quint64 generation); + + /// Applies tag changes. Opens the database read-write, applies, and closes + /// immediately: notmuch's write lock is exclusive process-wide, so holding + /// it would block the user's cron `notmuch new`. + void applyTags(const TagChange &change); + + /// Batch tagging over whole threads. The UI holds thread ids, not message + /// ids, for rows it has not opened, so the resolution happens here where + /// the database handle lives. This is the path the archive/flag/delete + /// actions use on a multi-row selection. + void applyTagsToThreads(const QStringList &threadIds, + const QStringList &add, + const QStringList &remove, + const QString &description); + +signals: + void threadsReady(const QVector<ThreadSummary> &threads, quint64 generation); + void queryFinished(int totalThreads, quint64 generation); + void threadLoaded(const QVector<MessageRef> &messages, quint64 generation); + void tagsApplied(const TagChange &change); + void errorOccurred(const QString &message); + +private: + bool openReadOnly(); + void close(); + + QByteArray configPathArg() const; + + QString m_configPath; + notmuch_database_t *m_db = nullptr; +}; |
