aboutsummaryrefslogtreecommitdiffstats
path: root/src/main.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 17:33:53 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 17:33:53 +0200
commite2d677d82ee52f940fda018de9372330c87fb49a (patch)
tree2159a3087794f5d19a860048e0039e800c56fba0 /src/main.cpp
parent4439dffbefcb4d2e3cff0c9e330effc47c6cb02a (diff)
downloadqtmaildir-e2d677d82ee52f940fda018de9372330c87fb49a.tar.gz
qtmaildir-e2d677d82ee52f940fda018de9372330c87fb49a.zip
feat: accept the launch selectors on the command line
Connect first, become the server only if that fails. A live instance is handed the selectors and this process exits without ever opening a database, so two launches no longer mean two notmuch handles. When no instance answers and no socket can be created either, the window still opens: losing single-instance behaviour is a degradation, losing the mail client is not. --version and --help stay hand-checked before QApplication exists: Qt's own versions exit through QCoreApplication, and --version has to work where the GUI cannot open. The selectors themselves are parsed after QApplication, from app.arguments(), because the constructor strips Qt's standard options such as -platform and -style; parsing the raw argv would reject them as unknown. An unknown option prints the error on stderr and exits with status 2 before any window is shown. The socket's parent directory is created first, since the state directory does not exist on a first run and listen() cannot create a socket inside a missing directory. Raising under Wayland is a request rather than a command. The compositor may honour it as a focus hint or ignore it by policy; the selectors apply either way, which is the half that has to work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/main.cpp')
-rw-r--r--src/main.cpp81
1 files changed, 69 insertions, 12 deletions
diff --git a/src/main.cpp b/src/main.cpp
index a7908d0..56e016a 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -19,6 +19,7 @@
#include <QApplication>
#include <QCoreApplication>
#include <QDir>
+#include <QFileInfo>
#include <QIcon>
#include <QLocale>
#include <QMessageBox>
@@ -32,7 +33,9 @@
#include <cstring>
#include "config.h"
+#include "launchselectors.h"
#include "mainwindow.h"
+#include "singleinstance.h"
#include "version.h"
int main(int argc, char *argv[])
@@ -48,18 +51,13 @@ int main(int argc, char *argv[])
}
if (std::strcmp(argv[i], "--help") == 0
|| std::strcmp(argv[i], "-h") == 0) {
- std::printf(
- "qtmaildir %s - a Qt6 mail client for notmuch-indexed Maildirs\n"
- "\n"
- "Usage: qtmaildir [options]\n"
- "\n"
- " -h, --help Show this help and exit\n"
- " -v, --version Show the version and exit\n"
- "\n"
- "Configuration: ~/.config/qtmaildir/qtmaildir.conf\n"
- "qtmaildir reads a notmuch-indexed Maildir. It does no network\n"
- "protocol work: fetching and sending are external commands.\n",
- QTMAILDIR_VERSION_DISPLAY);
+ // The text lives with the parser, so the options and their
+ // descriptions cannot drift apart.
+ std::printf("%s",
+ LaunchSelectors::helpText(
+ QStringLiteral(QTMAILDIR_VERSION_DISPLAY))
+ .toLocal8Bit()
+ .constData());
return 0;
}
}
@@ -82,6 +80,40 @@ int main(int argc, char *argv[])
app.setOrganizationName(QStringLiteral("qtmaildir"));
app.setApplicationVersion(QStringLiteral(QTMAILDIR_VERSION));
+ // Parsed AFTER QApplication, from app.arguments() rather than argv: the
+ // constructor consumes Qt's own options (-platform, -style and the rest)
+ // and removes them, so parsing the raw argv would reject a standard Qt
+ // option as unknown. No window exists yet, so a bad option still exits
+ // before anything is shown.
+ QString selectorError;
+ const LaunchSelectors selectors =
+ LaunchSelectors::parse(app.arguments(), &selectorError);
+ if (!selectorError.isEmpty()) {
+ std::fprintf(stderr, "qtmaildir: %s\n",
+ selectorError.toLocal8Bit().constData());
+ return 2;
+ }
+
+ // Connect first, become the server only if that fails. A live instance is
+ // handed the selectors and this process exits without ever opening a
+ // database: notmuch permits one handle per process, so two windows are two
+ // handles, which this avoids as a side effect of the feature.
+ //
+ // On main's stack, like the QTranslator below: it owns the socket for the
+ // life of the process and must outlive exec().
+ const QString socketPath = MainWindow::singleInstanceSocketPath();
+ // The state directory does not exist on a first run, and listen() cannot
+ // create a socket in a missing directory.
+ QDir().mkpath(QFileInfo(socketPath).absolutePath());
+ SingleInstance instance(socketPath);
+ if (!instance.tryBecomeServer()) {
+ if (instance.sendToRunningInstance(selectors))
+ return 0;
+ // No running instance answered and no socket could be created either.
+ // Carry on and open a window: losing single-instance behaviour is a
+ // degradation, and losing the mail client is not acceptable.
+ }
+
// Compiled in rather than read from disk, so the icon is there whether or
// not the app was installed. setDesktopFileName() is what lets a Wayland
// compositor match the window to its .desktop entry, which is where the
@@ -138,6 +170,31 @@ int main(int argc, char *argv[])
MainWindow window(config);
window.show();
+ // What this launch asked for. After show(), so the window is up before a
+ // query starts running against it.
+ window.applySelectors(selectors);
+
+ // A later launch. The selectors arrive on the socket and go through the
+ // same applySelectors() this startup path just used.
+ QObject::connect(&instance, &SingleInstance::selectorsReceived, &window,
+ [&window](const LaunchSelectors &arrived) {
+ // Raised whatever the selectors say, an empty set
+ // included: a bare launch against a running window
+ // means "show me the window".
+ //
+ // Under Wayland this is a REQUEST, not a command. The
+ // compositor may honour it as a focus hint or ignore
+ // it by policy, which is its decision and not a defect
+ // to work around: the selectors still apply and the
+ // window still shows the right thing.
+ window.setWindowState(window.windowState()
+ & ~Qt::WindowMinimized);
+ window.show();
+ window.raise();
+ window.activateWindow();
+ window.applySelectors(arrived);
+ });
+
// After show(), and out here rather than inside the constructor. A modal
// raised from the constructor cannot be dismissed under the offscreen
// platform, so it hung the test suite with no output (item 84). Showing it