// Copyright (C) 2026 Danilo M. // // 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(); } } } }