diff options
| -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 |
