aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 17:14:21 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 17:14:21 +0200
commit3524c2eb1267baf44301ea888abc784ecf85e76a (patch)
tree28cb95387a1426c1d2fe5feec765378376b774ef /src
parent0402b5b683c7412daace2414f4b903b677b5405c (diff)
downloadqtmaildir-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')
-rw-r--r--src/mainwindow.cpp101
-rw-r--r--src/mainwindow.h33
-rw-r--r--src/notmuchworker.h2
3 files changed, 134 insertions, 2 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)
{
diff --git a/src/mainwindow.h b/src/mainwindow.h
index e965de2..6b841fa 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -32,6 +32,7 @@
#include "config.h"
#include "htmlbuilder.h"
#include "keymap.h"
+#include "launchselectors.h"
// Included rather than forward-declared: SyncPhaseTracker is held by value, so
// its size must be known here. MailSync itself stays a forward declaration.
#include "mailsync.h"
@@ -131,6 +132,27 @@ public:
QVector<PendingChange> pendingChangeSnapshot() const;
+ /// Applies what a launch asked for (item 200).
+ ///
+ /// ONE entry point, called both by main() at startup and by the socket
+ /// handler when a later launch arrives. Two paths through separate code
+ /// would drift, which is the lesson this file has already learned from
+ /// every other pair.
+ ///
+ /// An EMPTY selector set deliberately changes nothing: a bare launch
+ /// against a running window means "raise yourself", and a raise is not a
+ /// navigation. The user is looking at something.
+ ///
+ /// A selector that matches nothing leaves the window on its configured
+ /// view and names the miss in the status bar. Not an empty result, which
+ /// makes a stale link look like a broken client; not a refusal, which is
+ /// right for a script and wrong for a desktop launch.
+ ///
+ /// A thread id that is not hex is such 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 argv.
+ void applySelectors(const LaunchSelectors &selectors);
+
/// Opens the list behind the unsynced-changes count.
///
/// Takes the snapshot, asks the worker to resolve its subjects, and shows
@@ -169,6 +191,13 @@ public:
/// preserve comments or key order).
static QString uiStatePath();
+ /// Path of the single-instance socket (item 200).
+ ///
+ /// Beside uiStatePath() and built the same way, so the two cannot drift.
+ /// GenericStateLocation, not StateLocation: the latter appends both the
+ /// organization and the application name, and both are "qtmaildir".
+ static QString singleInstanceSocketPath();
+
/// Kernel lock table every MainWindow's SyncMonitor watches, "/proc/locks"
/// unless a test overrides it.
///
@@ -628,6 +657,10 @@ private slots:
/// point the button uses.
void recoverStaleThread(const QString &threadId, const QString &messageId);
+ /// The worker's answer to a --message selector.
+ void onThreadForMessageResolved(const QString &messageId,
+ const QString &threadId);
+
/// Drills into the double-clicked row: the whole thread, expanded, alone in
/// the view, with that row's own message in the pane.
void onRowDoubleClicked(const QModelIndex &index);
diff --git a/src/notmuchworker.h b/src/notmuchworker.h
index 3183729..ad5f616 100644
--- a/src/notmuchworker.h
+++ b/src/notmuchworker.h
@@ -143,8 +143,6 @@ private:
/// threadIdForTesting() and resolveThreadForMessage().
QString firstThreadIdMatching(const QString &query);
-public:
-
public slots:
/// Loads ONE message, for a message row selected in the list.