aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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.qml40
-rw-r--r--desktop/modules/mail/MailPage.qml220
-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-xdesktop/modules/mail/mail-notify.sh (renamed from mail-overview/mail-notify.sh)0
-rwxr-xr-xdesktop/modules/mail/test-mail-notify.sh (renamed from mail-overview/test-mail-notify.sh)0
-rwxr-xr-xdesktop/modules/mail/waybar-mail.sh (renamed from mail-overview/waybar-mail.sh)2
-rw-r--r--desktop/shell.qml2
-rw-r--r--mail-overview/MailPanel.qml318
l---------mail-overview/Theme.qml1
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