// 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. pragma Singleton import Quickshell import Quickshell.Io import QtQuick import "Ini.js" as I // qtmaildir.conf, shared by the four Mail settings pages. qtmaildir reads // it at startup and never writes it, so editing it under a running // qtmaildir is safe and applies on its next launch. // // The pages edit a draft, never the file: every field writes to it as it is // typed, and save() writes it out. The draft is shared, so one Save writes // what all four pages changed, and it survives moving between pages. Singleton { id: root readonly property string path: (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/qtmaildir/qtmaildir.conf" // The file as last read or written. property string text: "" // The file as the draft started from, and the draft. property string base: "" property string draft: "" readonly property bool dirty: root.draft !== root.base // Fields holding a value that cannot be written, by section/key; Save // waits until there are none. property var invalid: ({}) readonly property bool valid: Object.keys(root.invalid).length === 0 property string error: "" // Whether qtmaildir runs now, so a page can say a change waits for a // restart. Rechecked on every refresh(). property bool running: false // The row keys the pages repeat over: account keys, tags, shortcuts. // Assigned only when the set changes, never bound to the draft: a new // array rebuilds every row of a Repeater, and since every keystroke // makes a new draft, a bound model would rebuild the row being typed in // and drop its focus after each character. Rows read their values live. property var accounts: [] property var tags: [] property var shortcuts: [] function same(a, b) { return JSON.stringify(a) === JSON.stringify(b); } onDraftChanged: { const acc = I.sections(root.draft).filter(s => s.startsWith("account.")).map(s => s.slice(8)); const tg = I.entries(root.draft, "tagcolors").map(e => e.key); const sc = I.entries(root.draft, "keys").map(e => e.key); if (!root.same(acc, root.accounts)) root.accounts = acc; if (!root.same(tg, root.tags)) root.tags = tg; if (!root.same(sc, root.shortcuts)) root.shortcuts = sc; } function get(section, key) { return I.get(root.draft, section, key); } function entries(section) { return I.entries(root.draft, section); } // val null unsets: the line is commented out where it stands. function set(section, key, val) { root.draft = I.set(root.draft, section, key, val); } function remove(section, key) { root.draft = I.remove(root.draft, section, key); } function addAccount(key, pairs) { root.draft = I.addSection(root.draft, "account." + key, pairs); } function removeAccount(key) { root.draft = I.removeSection(root.draft, "account." + key); } function markInvalid(id, bad) { if (!!root.invalid[id] === bad) return; const next = Object.assign({}, root.invalid); if (bad) next[id] = true; else delete next[id]; root.invalid = next; } // Refused when the file changed on disk since the draft began: writing // would discard that change unseen. Discard then shows the new file. // Never empty, since a header always remains, so FileView.setText is safe // here (AGENTS.md: it writes nothing for empty text on an unloaded path). function save() { if (root.text !== root.base) { root.error = "qtmaildir.conf changed on disk since these edits began. Discard to load it, then redo them."; return; } root.error = ""; root.text = root.draft; root.base = root.draft; fv.setText(root.draft); } function discard() { root.error = ""; root.invalid = ({}); root.base = root.text; root.draft = root.text; } function refresh() { fv.reload(); pg.running = false; pg.running = true; } FileView { id: fv path: root.path atomicWrites: true watchChanges: true printErrors: false // Its own writes come back here too, as the text already held. onFileChanged: reload() // A clean draft follows the file; a dirty one is kept, and save() // notices the file moved under it. onLoaded: { root.text = text(); if (!root.dirty) { root.base = root.text; root.draft = root.text; } } onLoadFailed: if (!root.dirty) { root.text = ""; root.base = ""; root.draft = ""; } onSaveFailed: err => root.error = "Cannot write " + root.path + ": " + FileViewError.toString(err) } Process { id: pg command: ["pgrep", "-x", "qtmaildir"] onExited: code => root.running = code === 0 } }