diff options
Diffstat (limited to 'mail-overview')
| -rw-r--r-- | mail-overview/Accounts.qml | 24 | ||||
| -rw-r--r-- | mail-overview/MailPanel.qml | 78 | ||||
| -rw-r--r-- | mail-overview/README.md | 36 |
3 files changed, 107 insertions, 31 deletions
diff --git a/mail-overview/Accounts.qml b/mail-overview/Accounts.qml index 27cdcf9..0ae6886 100644 --- a/mail-overview/Accounts.qml +++ b/mail-overview/Accounts.qml @@ -184,32 +184,12 @@ Singleton { accounts = next; } - // Launching qtmaildir, and syncing. qtmaildir takes no arguments, so - // there is nothing to tell it about the account or thread clicked. + // 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`] } function openClient() { openProc.running = false; openProc.running = true; } - - property bool syncing: false - - // mailsync.sh is already lock-protected against a concurrent cron run, so - // this does not need its own guard beyond not stacking clicks. - Process { - id: syncProc - command: [`${Quickshell.env("HOME")}/bin/mailsync.sh`] - onExited: { - root.syncing = false; - root.refresh(); - } - } - - function sync() { - if (syncing) return; - syncing = true; - syncProc.running = false; - syncProc.running = true; - } } diff --git a/mail-overview/MailPanel.qml b/mail-overview/MailPanel.qml index 7457fb2..0a7f36a 100644 --- a/mail-overview/MailPanel.qml +++ b/mail-overview/MailPanel.qml @@ -10,6 +10,7 @@ // GNU General Public License for more details. import Quickshell +import Quickshell.Io import Quickshell.Wayland import QtQuick @@ -20,6 +21,45 @@ Scope { 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] @@ -95,6 +135,13 @@ Scope { // 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 } @@ -230,17 +277,36 @@ Scope { 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: Accounts.syncing ? "Syncing..." : "Sync now" - enabled: !Accounts.syncing - onClicked: Accounts.sync() - } - - Button { text: "Open qtmaildir" onClicked: { Accounts.openClient(); root.close(); } } diff --git a/mail-overview/README.md b/mail-overview/README.md index daf2bf9..68ad051 100644 --- a/mail-overview/README.md +++ b/mail-overview/README.md @@ -16,7 +16,9 @@ Clicking the icon opens the drawer; Escape or a click outside closes it. │ ● Account D 0 │ │ ● Account E 0 │ ├──────────────────────────────────────────────────┤ - │ [Sync now] [Open qtmaildir] │ + │ ● watcher ok · 25 folders │ + ├──────────────────────────────────────────────────┤ + │ [Open qtmaildir] │ └──────────────────────────────────────────────────┘ ## Running it @@ -145,8 +147,36 @@ open. `startup_account` in its config is a static setting, not a flag, so here was rejected rather than deferred: it would write the database behind a possibly running client. -"Sync now" runs `~/bin/mailsync.sh`, which is already lock-protected against a -concurrent cron run, and the watcher picks up whatever it commits. +"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, +and `on-click-right` on the waybar module runs `~/bin/mailsync.sh` for a manual +pull. + +## The watcher status dot + +Below the accounts, above the button, a coloured dot reports whether +`mail-watcher` is alive and sane, read from its heartbeat at +`~/.local/state/mail-watcher.heartbeat`: + +- **green** (`watcher ok · N folders`) heartbeat fresh, no dead threads. +- **yellow** (`N folder(s) dead, check the log`) heartbeat fresh, but a folder + gave up permanently. +- **red** (`watcher not running`) heartbeat missing, unparseable, or older than + 300s. + +Backoff never turns the dot: it is normal recovery from a dropped IDLE +connection, and the watcher's own health check treats it as healthy. The +staleness rule is the same one `mail-watcher` uses (`heartbeat_is_healthy`), +reimplemented here in a few lines rather than shelling out to +`mail-watcher.py --status` on every open. + +The heartbeat is read once per open. The drawer is a `LazyLoader`, so closing +and reopening rebuilds the `FileView` and reads the file current. A file watch +is deliberately not used: the heartbeat is written by atomic replace (tmpfile +then rename), so an inotify watch held on the old inode dies with it, which is +the same trap as watching a file inside the Xapian directory. The cost is that +a drawer left open does not update until reopened, which for 60s heartbeat data +is not worth a timer. ## Theme and blur |
