diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-29 17:33:53 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-29 17:33:53 +0200 |
| commit | e2d677d82ee52f940fda018de9372330c87fb49a (patch) | |
| tree | 2159a3087794f5d19a860048e0039e800c56fba0 /src/main.cpp | |
| parent | 4439dffbefcb4d2e3cff0c9e330effc47c6cb02a (diff) | |
| download | qtmaildir-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.cpp | 81 |
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 |
