From a880134140f7ab8195c6651c2f64f614855c8dce Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sun, 18 Oct 2026 11:00:53 +0200 Subject: mail: open accounts and messages in qtmaildir from the drawer and notifications qtmaildir now takes --account, --thread and --message, and hands them to a running window over its socket, so the drawer and the notifications can open the exact thing they show instead of just launching the client. mail-open.sh is the one place that turns an account and a thread into selectors, shared by both callers. It looks up the thread's newest unread message at click time rather than when the row or notification was built: by then it may have been read, and an empty lookup falls back to the whole thread instead of a miss in qtmaildir's status bar. The notification never had a working click: dunstify is not installed, so mail-notify.sh always took its notify-send fallback, which carried no action. It now uses notify-send -A NAME=Text, one action per listed thread (keyed by thread id, which is hex and so never holds the "=" separator) plus the account as default. The round trip through notifyd was checked: notifyctl action makes notify-send print the picked name. On the page, the account header opens the account and a thread row its newest unread message. Rows use TapHandler and HoverHandler because a MouseArea cannot fill a Column. Launches are detached: a first launch is the client itself, and the old reused Process would have killed it on the next click. Opening anything now closes the drawer rather than going back. Co-Authored-By: Claude Opus 5.5 --- desktop/modules/mail/Accounts.qml | 19 ++++++--- desktop/modules/mail/MailModule.qml | 2 +- desktop/modules/mail/MailPage.qml | 17 +++++++- desktop/modules/mail/README.md | 38 +++++++++-------- desktop/modules/mail/mail-notify.sh | 73 ++++++++++++++++++++------------ desktop/modules/mail/mail-open.sh | 46 ++++++++++++++++++++ desktop/modules/mail/test-mail-notify.sh | 13 ++++++ 7 files changed, 153 insertions(+), 55 deletions(-) create mode 100755 desktop/modules/mail/mail-open.sh diff --git a/desktop/modules/mail/Accounts.qml b/desktop/modules/mail/Accounts.qml index c2bc9d8..4f8b114 100644 --- a/desktop/modules/mail/Accounts.qml +++ b/desktop/modules/mail/Accounts.qml @@ -35,7 +35,7 @@ Singleton { // it is singular and the rows always sum to the header. readonly property string scope: "tag:unread and tag:inbox" - // [{ key, label, color, count, threads: [{ authors, date, subject }] }] + // [{ key, label, color, count, threads: [{ thread, authors, date, subject }] }] // count is -1 until known, and stays -1 on failure: a zero there would // read as an empty inbox. property var accounts: [] @@ -170,6 +170,7 @@ Singleton { const rows = JSON.parse(parts[1]); if (Array.isArray(rows)) { acct.threads = rows.map(r => ({ + thread: String(r.thread ?? ""), authors: String(r.authors ?? ""), date: String(r.date_relative ?? ""), subject: String(r.subject ?? "(no subject)"), @@ -206,12 +207,16 @@ Singleton { Timer { id: debounce; interval: 300; onTriggered: root.refresh() } Timer { id: respawn; interval: 5000; onTriggered: watcher.running = true } - // Launching qtmaildir. It takes no arguments, so there is nothing to tell - // it about the account or thread clicked. - Process { id: openProc; command: [`${Quickshell.env("HOME")}/bin/qtmaildir`] } - + // Launching qtmaildir, detached: a running client takes the selectors + // over its socket and the launch exits, but a first launch is the client + // itself and must not belong to a Process that the next click restarts. function openClient() { - openProc.running = false; - openProc.running = true; + Quickshell.execDetached([`${Quickshell.env("HOME")}/bin/qtmaildir`]); + } + + // An account, or a thread's newest unread message in it. mail-open.sh is + // shared with the notifications, so both open mail the same way. + function open(key, thread) { + Quickshell.execDetached([`${Quickshell.shellDir}/modules/mail/mail-open.sh`, key, thread || ""]); } } diff --git a/desktop/modules/mail/MailModule.qml b/desktop/modules/mail/MailModule.qml index 5f501ba..0c71b2c 100644 --- a/desktop/modules/mail/MailModule.qml +++ b/desktop/modules/mail/MailModule.qml @@ -40,7 +40,7 @@ Module { Component.onCompleted: Accounts.refresh() MailPage { width: parent.width - onClose: mailPage.back() + onClose: mailPage.dismiss() } } } diff --git a/desktop/modules/mail/MailPage.qml b/desktop/modules/mail/MailPage.qml index 85b0876..1b41412 100644 --- a/desktop/modules/mail/MailPage.qml +++ b/desktop/modules/mail/MailPage.qml @@ -97,6 +97,7 @@ Column { model: Accounts.accounts Column { + id: account required property var modelData width: page.width spacing: 4 @@ -105,6 +106,12 @@ Column { width: parent.width implicitHeight: 26 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { Accounts.open(modelData.key); page.close(); } + } + Rectangle { id: dot anchors.verticalCenter: parent.verticalCenter @@ -132,12 +139,13 @@ Column { } } - // The newest three unread threads. Read-only: qtmaildir takes no - // arguments, so there is no way to ask it for a particular thread. + // The newest three unread threads. A click opens the thread's + // newest unread message, in this account. Repeater { model: modelData.threads Column { + id: row required property var modelData width: page.width - 18 x: 18 @@ -177,6 +185,11 @@ Column { font.family: Theme.fontFamily font.pixelSize: Theme.fontSize - 2 } + + // Handlers, not a MouseArea: a Column would lay one out + // as a row of its own rather than let it fill. + HoverHandler { cursorShape: Qt.PointingHandCursor } + TapHandler { onTapped: { Accounts.open(account.modelData.key, row.modelData.thread); page.close(); } } } } } diff --git a/desktop/modules/mail/README.md b/desktop/modules/mail/README.md index 2349da6..5083c34 100644 --- a/desktop/modules/mail/README.md +++ b/desktop/modules/mail/README.md @@ -156,15 +156,19 @@ login. The same applies to a restart, so mail that arrived while it was down is never notified. The drawer count is still right and still shows it, so nothing is lost but a stale popup. -The notification is sent with dunstify. The icon is passed as an **absolute -path** to the active icon theme's `mail-unread-multiple`, because this dunst -resolves an icon *name* only through the `icon_path` in `dunstrc`, and that -path holds no mail icon, so a name renders no icon at all. The click action -(`-A default,open`) makes dunst prepend an `(A)` action indicator when -`show_indicators` is on, so `dunstrc` sets `show_indicators = no`. A middle -click (dunst's `do_action`) opens qtmaildir, but not the account it belongs -to: `qtmaildir` takes no arguments and `startup_account` is a static setting -rather than a flag, the same limitation the thread rows have. +The notification is sent with `notify-send` (libnotify 0.8), whose `-A +NAME=Text` prints the picked action's name; dunstify is not installed. The +icon is passed as an **absolute path** to the active icon theme's +`mail-unread-multiple`, so it does not depend on how the daemon resolves icon +names. + +Each listed thread is an action, keyed by its thread id, and the account is +the `default` one. A click on the balloon offers them in the notifications' +picker. The picked one runs `mail-open.sh`: the account opens qtmaildir on that +account, a thread on its newest unread message. Thread ids rather than +Message-IDs are the keys because they are hex, so a key never carries the `=` +that separates it from its label; the message is looked up when the action is +picked, and if it was read in the meantime the whole thread opens instead. `test-mail-notify.sh` is the check. It sources the script as a library and asserts on the functions that only move text around, so it needs no mail and no @@ -191,15 +195,15 @@ respected. `Keys.onEscapePressed` on a `PanelWindow` never fires, so the focus sits on an inner `Item`. -## Thread rows are read-only +## Opening mail from the page -Each account shows its three newest unread threads as author, relative date and -subject. Clicking one launches qtmaildir and nothing more, because `qtmaildir` -accepts no command line arguments: there is no way to tell it which thread to -open. `startup_account` in its config is a static setting, not a flag, so -"open on this account" is unavailable for the same reason. Marking read from -here was rejected rather than deferred: it would write the database behind a -possibly running client. +A click on an account's header opens qtmaildir on that account, and a click on +a thread row opens that thread's newest unread message, through the same +`mail-open.sh` the notifications use. qtmaildir hands a second launch's +selectors to the running window, so neither opens a second client. Launches +are detached: a first launch *is* the client, and a reused `Process` would kill +it on the next click. Marking read from here was rejected rather than +deferred: it would write the database behind a possibly running client. "Open qtmaildir" launches the client. There is no "Sync now" button: the watcher triggers a sync the moment mail lands, the cron tick is the backstop, diff --git a/desktop/modules/mail/mail-notify.sh b/desktop/modules/mail/mail-notify.sh index 968799c..0052a8d 100755 --- a/desktop/modules/mail/mail-notify.sh +++ b/desktop/modules/mail/mail-notify.sh @@ -32,6 +32,7 @@ set -u STATE="${MAIL_NOTIFY_STATE:-$HOME/.local/state/mail-notify.lastmod}" CONFIG="${MAIL_NOTIFY_CONFIG:-$HOME/.config/qtmaildir/qtmaildir.conf}" SCOPE='tag:unread and tag:inbox' +OPEN="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/mail-open.sh" # How many threads a notification body lists before eliding into "+N more". # Three matches the drawer's own --limit=3. @@ -119,6 +120,25 @@ build_body() { printf '\n' } +# One action per listed thread, "=