aboutsummaryrefslogtreecommitdiffstats
path: root/mail-overview
diff options
context:
space:
mode:
Diffstat (limited to 'mail-overview')
-rw-r--r--mail-overview/Accounts.qml24
-rw-r--r--mail-overview/MailPanel.qml78
-rw-r--r--mail-overview/README.md36
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