diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-29 17:14:21 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-29 17:14:21 +0200 |
| commit | 3524c2eb1267baf44301ea888abc784ecf85e76a (patch) | |
| tree | 28cb95387a1426c1d2fe5feec765378376b774ef /src/mainwindow.cpp | |
| parent | 0402b5b683c7412daace2414f4b903b677b5405c (diff) | |
| download | qtmaildir-3524c2eb1267baf44301ea888abc784ecf85e76a.tar.gz qtmaildir-3524c2eb1267baf44301ea888abc784ecf85e76a.zip | |
feat: apply the launch selectors to the window
One entry point, called both at startup and by the socket handler when a later
launch arrives. Two paths would drift, which is the lesson this file has
already learned from every other pair.
The account moves first, because a built-in filter composes with the dropdown
and a query run before it would carry the old scope. The thread case reuses
recoverStaleThread() whole, as item 91's double-click already does. A message
id resolves to its thread first through the worker: opening a message means
opening its conversation with that message selected.
A thread id must be hex or it is treated as a miss and never reaches notmuch.
recoverStaleThread() builds thread:<id> unquoted, which is safe for ids notmuch
handed out and not for ones from another program's command line, where
"0000 or tag:inbox" would widen the query to the whole inbox. The check uses
anchoredPattern() because a PCRE $ also matches before a trailing newline.
An empty selector set changes nothing. A bare launch against a running window
means raise yourself, and a raise is not a navigation.
Also drops an empty public: label left in notmuchworker.h by the previous
commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/mainwindow.cpp')
| -rw-r--r-- | src/mainwindow.cpp | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 26643f4..4e0c487 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -43,6 +43,7 @@ #include <QMessageBox> #include <QPlainTextEdit> #include <QPushButton> +#include <QRegularExpression> #include <QSettings> #include <QSplitter> #include <QStandardPaths> @@ -101,6 +102,17 @@ QString MainWindow::uiStatePath() return base + QStringLiteral("/qtmaildir/uistate.conf"); } +QString MainWindow::singleInstanceSocketPath() +{ + // Built exactly like uiStatePath(), including the GenericStateLocation + // choice and the reason for it. A socket is machine-written state, so it + // belongs beside the UI state and never in the hand-edited config + // directory. + const QString base = + QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation); + return base + QStringLiteral("/qtmaildir/qtmaildir.sock"); +} + namespace { /// Overridden only by setLocksPathForTesting(); "/proc/locks" in every real run. QString g_locksPath = QStringLiteral("/proc/locks"); @@ -2845,6 +2857,8 @@ void MainWindow::wireWorker() this, &MainWindow::onThreadTreeLoaded); connect(m_worker, &NotmuchWorker::messageLoaded, this, &MainWindow::onMessageLoaded); + connect(m_worker, &NotmuchWorker::threadForMessageResolved, + this, &MainWindow::onThreadForMessageResolved); connect(m_worker, &NotmuchWorker::threadDigestLoaded, this, &MainWindow::onThreadDigestLoaded); connect(m_worker, &NotmuchWorker::errorOccurred, @@ -5209,6 +5223,93 @@ void MainWindow::updateStaleThreadNotice() m_messageView->setStaleThread(threadId, messageId); } +void MainWindow::applySelectors(const LaunchSelectors &selectors) +{ + // Nothing asked for. A bare launch against a running window means "raise + // yourself", which main() and the socket handler do around this call; from + // here there is nothing to change, and re-running a query would take the + // user off whatever they were reading. + if (selectors.isEmpty()) + return; + + // The account FIRST, and the order matters: a built-in filter composes + // with the dropdown, so a query run before the account moved would carry + // the old scope. This is the same ordering the startup path uses. + if (!selectors.account.isEmpty()) { + const int index = m_accountBox->findData(selectors.account); + if (index >= 0) { + m_accountBox->setCurrentIndex(index); + } else { + // Named, so a caller passing a stale key can be debugged from the + // client rather than from the caller. + showTransientStatus( + tr("No account named '%1'.").arg(selectors.account)); + } + } + + // A message id names a message INSIDE a conversation, so it has to be + // resolved to its thread before anything can be opened. Asked of the + // worker, which owns the only database handle; the answer arrives in + // onThreadForMessageResolved(). + if (!selectors.messageId.isEmpty()) { + if (m_worker) { + QMetaObject::invokeMethod( + m_worker, "resolveThreadForMessage", Qt::QueuedConnection, + Q_ARG(QString, selectors.messageId)); + } + // The thread selector, if any, is deliberately NOT also applied here: + // the message's own thread is what will open, and running a second + // query underneath it would race the one the resolve is about to + // start. + return; + } + + if (!selectors.threadId.isEmpty()) { + // recoverStaleThread() builds thread:<id> UNQUOTED, which is safe for + // an id notmuch handed out and not for one from another program's + // command line: "0000 or tag:inbox" would widen the query to the whole + // inbox. notmuch thread ids are hex, so anything else is a miss and + // never reaches a query at all. + // + // anchoredPattern(), not ^...$: in PCRE a $ also matches before a + // trailing newline, so "0000\n" would pass a ^...$ check. + static const QRegularExpression hex( + QRegularExpression::anchoredPattern(QStringLiteral("[0-9a-fA-F]+"))); + if (!hex.match(selectors.threadId).hasMatch()) { + showTransientStatus( + tr("No thread matched '%1'.").arg(selectors.threadId)); + return; + } + + // recoverStaleThread() is reused whole. It runs thread:<id>, remembers + // the target across the queued round trips the load takes, expands + // the thread when its row arrives and selects it. Item 91's + // double-click already reuses it; this is the third caller. + // + // The empty message id is meaningful to it: land on the ROOT row, + // which is the thread's first message. + recoverStaleThread(selectors.threadId, QString()); + } +} + +void MainWindow::onThreadForMessageResolved(const QString &messageId, + const QString &threadId) +{ + if (threadId.isEmpty()) { + // The miss path, and the window stays where it is. A message id from + // another program can be stale for every ordinary reason: the mail was + // deleted, moved by another client, or never indexed here. + showTransientStatus(tr("No message matched '%1'.").arg(messageId)); + return; + } + + // The THREAD, with that message selected. An id: query on the message + // alone would show one card out of its conversation, which item 91 settled + // is the wrong reading of "open this message". The thread id came from + // notmuch, so it is safe to hand on unquoted. + recoverStaleThread(threadId, messageId); +} + void MainWindow::recoverStaleThread(const QString &threadId, const QString &messageId) { |
