diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-14 12:27:41 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-14 12:27:41 +0200 |
| commit | d513418a2e8854bfe8702577cda6db2cbf0ce292 (patch) | |
| tree | 5e95c58030058ed5d0b986d1e0f25b7e6e005b90 | |
| parent | d203d938f005d7542bf13e217ac51917866a8ace (diff) | |
| download | quickshell-d513418a2e8854bfe8702577cda6db2cbf0ce292.tar.gz quickshell-d513418a2e8854bfe8702577cda6db2cbf0ce292.zip | |
feat(desktop): move mail-overview in as the mail module
Accounts.qml is unchanged and MailPanel's body becomes the page. The
three scripts move with it, which changes the absolute paths in autostart
and in the waybar module; both are outside this repo and listed in the
plan's final task. Button.qml, byte-identical here and in vm-manager,
lands at the shell root as the single copy.
| -rw-r--r-- | desktop/Button.qml (renamed from mail-overview/Button.qml) | 0 | ||||
| -rw-r--r-- | desktop/modules/mail/Accounts.qml (renamed from mail-overview/Accounts.qml) | 0 | ||||
| -rw-r--r-- | desktop/modules/mail/MailModule.qml | 40 | ||||
| -rw-r--r-- | desktop/modules/mail/MailPage.qml | 220 | ||||
| -rw-r--r-- | desktop/modules/mail/MailTile.qml (renamed from mail-overview/shell.qml) | 22 | ||||
| -rw-r--r-- | desktop/modules/mail/README.md (renamed from mail-overview/README.md) | 0 | ||||
| -rwxr-xr-x | desktop/modules/mail/mail-notify.sh (renamed from mail-overview/mail-notify.sh) | 0 | ||||
| -rwxr-xr-x | desktop/modules/mail/test-mail-notify.sh (renamed from mail-overview/test-mail-notify.sh) | 0 | ||||
| -rwxr-xr-x | desktop/modules/mail/waybar-mail.sh (renamed from mail-overview/waybar-mail.sh) | 2 | ||||
| -rw-r--r-- | desktop/shell.qml | 2 | ||||
| -rw-r--r-- | mail-overview/MailPanel.qml | 318 | ||||
| l--------- | mail-overview/Theme.qml | 1 |
12 files changed, 274 insertions, 331 deletions
diff --git a/mail-overview/Button.qml b/desktop/Button.qml index d6ca78f..d6ca78f 100644 --- a/mail-overview/Button.qml +++ b/desktop/Button.qml diff --git a/mail-overview/Accounts.qml b/desktop/modules/mail/Accounts.qml index 0ae6886..0ae6886 100644 --- a/mail-overview/Accounts.qml +++ b/desktop/modules/mail/Accounts.qml diff --git a/desktop/modules/mail/MailModule.qml b/desktop/modules/mail/MailModule.qml new file mode 100644 index 0000000..99ddaff --- /dev/null +++ b/desktop/modules/mail/MailModule.qml @@ -0,0 +1,40 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick +import "../.." + +// Always active: the unread count outlives the drawer, and Accounts watches +// qtmaildir.conf so a new account appears without a restart. +Module { + id: mod + + name: "mail" + icon: "" + label: "Mail" + alwaysActive: true + + tileContent: Component { MailTile {} } + + page: Component { + Page { + id: mailPage + title: "Mail" + // Mail has almost certainly arrived since this was last opened, + // and for an autostarted shell that is the whole session. + Component.onCompleted: Accounts.refresh() + MailPage { + width: parent.width + onClose: mailPage.back() + } + } + } +} diff --git a/desktop/modules/mail/MailPage.qml b/desktop/modules/mail/MailPage.qml new file mode 100644 index 0000000..85b0876 --- /dev/null +++ b/desktop/modules/mail/MailPage.qml @@ -0,0 +1,220 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import Quickshell +import Quickshell.Io +import QtQuick +import "../.." + +Column { + id: page + + signal close + + // mail-watcher's heartbeat, written every 60s. Read once per open: the + // page is behind a Loader, so opening it rebuilds the FileView and reads + // the current file. Watch is deliberately not used, because the heartbeat + // is written by atomic replace (tmpfile + rename) and an inotify watch + // held on the old inode dies with it. + property bool watcherAlive: false + property int watcherDead: 0 + property int watcherExpected: 0 + + // The same staleness rule as mail-watcher's heartbeat_is_healthy: dead + // threads or a heartbeat older than 300s mean the watcher needs a look. + // Backoff is healthy, so it never turns the dot. + function readHeartbeat(payload) { + page.watcherAlive = false; + page.watcherDead = 0; + page.watcherExpected = 0; + + let data = null; + try { data = JSON.parse(payload); } catch (e) { return; } + if (!data || typeof data.ts !== "string") return; + + const ts = Date.parse(data.ts); + if (isNaN(ts) || (Date.now() - ts) / 1000 > 300) return; + + page.watcherAlive = true; + page.watcherDead = Number(data.dead) || 0; + page.watcherExpected = Number(data.expected) || 0; + } + + readonly property color watcherColor: + !watcherAlive ? Theme.red + : watcherDead > 0 ? Theme.yellow + : Theme.green + + readonly property string watcherText: + !watcherAlive ? "watcher not running" + : watcherDead > 0 ? `${watcherDead} folder(s) dead, check the log` + : `watcher ok · ${watcherExpected} folders` + + spacing: 10 + + FileView { + id: heartbeat + path: `${Quickshell.env("HOME")}/.local/state/mail-watcher.heartbeat` + onLoaded: page.readHeartbeat(text()) + onLoadFailed: page.readHeartbeat("") + } + + Item { + width: parent.width + implicitHeight: totalText.implicitHeight + + Text { + id: totalText + anchors.right: parent.right + // A dash rather than a possibly-wrong number while any account is + // still unknown. + text: Accounts.anyUnknown ? "—" : `${Accounts.total} unread` + color: Theme.subtext + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + } + } + + Text { + visible: Accounts.error !== "" + width: parent.width + text: Accounts.error + color: Theme.red + wrapMode: Text.WordWrap + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 2 + } + + Repeater { + model: Accounts.accounts + + Column { + required property var modelData + width: page.width + spacing: 4 + + Item { + width: parent.width + implicitHeight: 26 + + Rectangle { + id: dot + anchors.verticalCenter: parent.verticalCenter + width: 8; height: 8; radius: 4 + // The account's own colour from the config. Per-account + // identity, not a palette. + color: modelData.color || Theme.accent + } + + Text { + anchors { left: dot.right; leftMargin: 10; verticalCenter: parent.verticalCenter } + text: modelData.label + color: Theme.text + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + } + + Text { + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + text: modelData.count < 0 ? "—" : String(modelData.count) + color: modelData.count > 0 ? Theme.text : Theme.subtext + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + font.bold: modelData.count > 0 + } + } + + // The newest three unread threads. Read-only: qtmaildir takes no + // arguments, so there is no way to ask it for a particular thread. + Repeater { + model: modelData.threads + + Column { + required property var modelData + width: page.width - 18 + x: 18 + spacing: 1 + bottomPadding: 4 + + Item { + width: parent.width + implicitHeight: who.implicitHeight + + Text { + id: who + anchors.left: parent.left + width: parent.width - when.implicitWidth - 10 + text: modelData.authors + elide: Text.ElideRight + color: Theme.subtext + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 3 + } + + Text { + id: when + anchors.right: parent.right + text: modelData.date + color: Theme.overlay + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 3 + } + } + + Text { + width: parent.width + text: modelData.subject + elide: Text.ElideRight + color: Theme.text + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 2 + } + } + } + } + } + + Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } + + // Watcher health. Green when idling, yellow when a folder gave up, red + // when there is no fresh heartbeat at all. + Item { + width: parent.width + implicitHeight: 22 + + Rectangle { + id: watcherDot + anchors.verticalCenter: parent.verticalCenter + width: 8; height: 8; radius: 4 + color: page.watcherColor + } + + Text { + anchors { left: watcherDot.right; leftMargin: 10; verticalCenter: parent.verticalCenter } + text: page.watcherText + color: Theme.subtext + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 2 + } + } + + Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } + + Row { + anchors.right: parent.right + spacing: 10 + + Button { + text: "Open qtmaildir" + onClicked: { Accounts.openClient(); page.close(); } + } + } +} diff --git a/mail-overview/shell.qml b/desktop/modules/mail/MailTile.qml index cd2e907..640dad0 100644 --- a/mail-overview/shell.qml +++ b/desktop/modules/mail/MailTile.qml @@ -9,16 +9,16 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -import Quickshell -import Quickshell.Io +import QtQuick +import "../.." -ShellRoot { - MailPanel { id: panel } - - IpcHandler { - target: "mail" - function toggle() { panel.toggle(); } - function show() { panel.show(); } - function close() { panel.close(); } - } +Text { + elide: Text.ElideRight + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Accounts.total > 0 ? Theme.text : Theme.subtext + // A dash rather than a possibly-wrong number while any account is unknown, + // the same rule the page header uses. + text: Accounts.anyUnknown ? "—" + : Accounts.total === 0 ? "no unread" + : `${Accounts.total} unread` } diff --git a/mail-overview/README.md b/desktop/modules/mail/README.md index 34eaa53..34eaa53 100644 --- a/mail-overview/README.md +++ b/desktop/modules/mail/README.md diff --git a/mail-overview/mail-notify.sh b/desktop/modules/mail/mail-notify.sh index b132775..b132775 100755 --- a/mail-overview/mail-notify.sh +++ b/desktop/modules/mail/mail-notify.sh diff --git a/mail-overview/test-mail-notify.sh b/desktop/modules/mail/test-mail-notify.sh index 1c3e01a..1c3e01a 100755 --- a/mail-overview/test-mail-notify.sh +++ b/desktop/modules/mail/test-mail-notify.sh diff --git a/mail-overview/waybar-mail.sh b/desktop/modules/mail/waybar-mail.sh index ee07ecb..4942f54 100755 --- a/mail-overview/waybar-mail.sh +++ b/desktop/modules/mail/waybar-mail.sh @@ -16,7 +16,7 @@ # which is why there is no daemon to supervise and no interval to tune. # # The count is the total across every account. A per account breakdown is the -# drawer's job; see mail-overview/README.md. +# drawer's job; see modules/mail/README.md. set -u diff --git a/desktop/shell.qml b/desktop/shell.qml index 2168a1b..d27583d 100644 --- a/desktop/shell.qml +++ b/desktop/shell.qml @@ -13,6 +13,7 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import "modules/appearance" +import "modules/mail" import "modules/sound" ShellRoot { @@ -32,6 +33,7 @@ ShellRoot { id: drawer modules: [ SoundModule {}, + MailModule {}, AppearanceModule {}, ] } diff --git a/mail-overview/MailPanel.qml b/mail-overview/MailPanel.qml deleted file mode 100644 index 0a7f36a..0000000 --- a/mail-overview/MailPanel.qml +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright (C) 2026 Danilo M. <danix@danix.xyz> -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 2 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import QtQuick - -Scope { - id: root - - // Waybar runs on this screen, so the drawer belongs here too. - property string monitor: "DP-1" - property bool open: false - - // mail-watcher's heartbeat, written every 60s. Read once per open: the - // drawer is a LazyLoader, so closing and reopening rebuilds the FileView - // and reads the current file. Watch is deliberately not used, because the - // heartbeat is written by atomic replace (tmpfile + rename) and an inotify - // watch held on the old inode dies with it. - property bool watcherAlive: false - property int watcherDead: 0 - property int watcherExpected: 0 - - // The same staleness rule as mail-watcher's heartbeat_is_healthy: dead - // threads or a heartbeat older than 300s mean the watcher needs a look. - // Backoff is healthy, so it never turns the dot. - function readHeartbeat(payload) { - root.watcherAlive = false; - root.watcherDead = 0; - root.watcherExpected = 0; - - let data = null; - try { data = JSON.parse(payload); } catch (e) { return; } - if (!data || typeof data.ts !== "string") return; - - const ts = Date.parse(data.ts); - if (isNaN(ts) || (Date.now() - ts) / 1000 > 300) return; - - root.watcherAlive = true; - root.watcherDead = Number(data.dead) || 0; - root.watcherExpected = Number(data.expected) || 0; - } - - readonly property color watcherColor: - !watcherAlive ? Theme.red - : watcherDead > 0 ? Theme.yellow - : Theme.green - - readonly property string watcherText: - !watcherAlive ? "watcher not running" - : watcherDead > 0 ? `${watcherDead} folder(s) dead, check the log` - : `watcher ok · ${watcherExpected} folders` - - readonly property var screenObj: - Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] - - function show() { - // Mail has almost certainly arrived since this was last opened, and - // for an autostarted shell that is the whole session. - Accounts.refresh(); - root.open = true; - } - function close() { root.open = false; } - function toggle() { root.open ? close() : show(); } - - // Quickshell exits once no window is visible, and this drawer is closed - // most of the time. See AGENTS.md. - PanelWindow { - visible: true - implicitWidth: 1 - implicitHeight: 1 - color: "transparent" - exclusionMode: ExclusionMode.Ignore - mask: Region {} - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - } - - LazyLoader { - active: root.open - - PanelWindow { - id: win - screen: root.screenObj - - anchors { top: true; left: true; right: true; bottom: true } - color: "transparent" - - // Normal, not Ignore: waybar claims an exclusive zone at the top - // of this screen, so respecting it puts the drawer below the bar - // without this file knowing the bar's height. The backdrop then - // starts below waybar too, which is why waybar stays un-dimmed. - exclusionMode: ExclusionMode.Normal - - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.namespace: "quickshell-mail" - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - - Rectangle { - anchors.fill: parent - color: "#000000" - opacity: 0.5 - MouseArea { anchors.fill: parent; onClicked: root.close() } - } - - // Keys reach a focused item, never the window itself: setting - // keyboardFocus above is necessary but not sufficient, and - // Keys.onEscapePressed on a PanelWindow never fires. - Item { - anchors.fill: parent - focus: true - Keys.onEscapePressed: root.close() - } - - // Top right, under the waybar icon, which sits in modules-right. - Rectangle { - id: drawer - anchors { top: parent.top; right: parent.right; topMargin: 8; rightMargin: 8 } - width: 460 - height: Math.min(content.implicitHeight + 32, win.height - 24) - radius: 14 - color: Qt.alpha(Theme.base, 0.72) - border.width: 1 - border.color: Qt.alpha(Theme.text, 0.12) - - // Swallows clicks so they do not reach the backdrop and close - // the drawer. - MouseArea { anchors.fill: parent } - - FileView { - id: heartbeat - path: `${Quickshell.env("HOME")}/.local/state/mail-watcher.heartbeat` - onLoaded: root.readHeartbeat(text()) - onLoadFailed: root.readHeartbeat("") - } - - Column { - id: content - anchors { left: parent.left; right: parent.right; top: parent.top; margins: 16 } - spacing: 10 - - Item { - width: parent.width - implicitHeight: Math.max(title.implicitHeight, totalText.implicitHeight) - - Text { - id: title - anchors.verticalCenter: parent.verticalCenter - text: "Mail" - color: Theme.text - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize + 2 - font.bold: true - } - - Text { - id: totalText - anchors { right: parent.right; verticalCenter: parent.verticalCenter } - // A dash rather than a possibly-wrong number while - // any account is still unknown. - text: Accounts.anyUnknown ? "—" : `${Accounts.total} unread` - color: Theme.subtext - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - } - } - - Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } - - Text { - visible: Accounts.error !== "" - width: parent.width - text: Accounts.error - color: Theme.red - wrapMode: Text.WordWrap - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - 2 - } - - Repeater { - model: Accounts.accounts - - Column { - required property var modelData - width: content.width - spacing: 4 - - Item { - width: parent.width - implicitHeight: 26 - - Rectangle { - id: dot - anchors.verticalCenter: parent.verticalCenter - width: 8; height: 8; radius: 4 - // The account's own colour from the config. - // Per-account identity, not a palette. - color: modelData.color || Theme.accent - } - - Text { - anchors { left: dot.right; leftMargin: 10; verticalCenter: parent.verticalCenter } - text: modelData.label - color: Theme.text - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - } - - Text { - anchors { right: parent.right; verticalCenter: parent.verticalCenter } - text: modelData.count < 0 ? "—" : String(modelData.count) - color: modelData.count > 0 ? Theme.text : Theme.subtext - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - font.bold: modelData.count > 0 - } - } - - // The newest three unread threads. Read-only: - // qtmaildir takes no arguments, so there is no way - // to ask it for a particular thread. - Repeater { - model: modelData.threads - - Column { - required property var modelData - width: content.width - 18 - x: 18 - spacing: 1 - bottomPadding: 4 - - Item { - width: parent.width - implicitHeight: who.implicitHeight - - Text { - id: who - anchors.left: parent.left - width: parent.width - when.implicitWidth - 10 - text: modelData.authors - elide: Text.ElideRight - color: Theme.subtext - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - 3 - } - - Text { - id: when - anchors.right: parent.right - text: modelData.date - color: Theme.overlay - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - 3 - } - } - - Text { - width: parent.width - text: modelData.subject - elide: Text.ElideRight - color: Theme.text - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - 2 - } - } - } - } - } - - Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } - - // Watcher health, above the button. Green when idling, - // yellow when a folder gave up, red when there is no fresh - // heartbeat at all. - Item { - width: parent.width - implicitHeight: 22 - - Rectangle { - id: watcherDot - anchors.verticalCenter: parent.verticalCenter - width: 8; height: 8; radius: 4 - color: root.watcherColor - } - - Text { - anchors { left: watcherDot.right; leftMargin: 10; verticalCenter: parent.verticalCenter } - text: root.watcherText - color: Theme.subtext - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSize - 2 - } - } - - Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } - - Row { - anchors.right: parent.right - spacing: 10 - - Button { - text: "Open qtmaildir" - onClicked: { Accounts.openClient(); root.close(); } - } - } - } - } - } - } -} diff --git a/mail-overview/Theme.qml b/mail-overview/Theme.qml deleted file mode 120000 index 3d2e40f..0000000 --- a/mail-overview/Theme.qml +++ /dev/null @@ -1 +0,0 @@ -../shared/Theme.qml
\ No newline at end of file |
