aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/qtmaildir/Qtm.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/qtmaildir/Qtm.qml')
-rw-r--r--desktop/modules/qtmaildir/Qtm.qml83
1 files changed, 68 insertions, 15 deletions
diff --git a/desktop/modules/qtmaildir/Qtm.qml b/desktop/modules/qtmaildir/Qtm.qml
index 744bcb4..3f6175b 100644
--- a/desktop/modules/qtmaildir/Qtm.qml
+++ b/desktop/modules/qtmaildir/Qtm.qml
@@ -19,35 +19,83 @@ 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
- // Keys of the [account.<key>] sections, in file order.
- readonly property var accounts: I.sections(root.text).filter(s => s.startsWith("account.")).map(s => s.slice(8))
+ // 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.text, section, key); }
- function entries(section) { return I.entries(root.text, section); }
+ 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.write(I.set(root.text, section, key, val)); }
- function remove(section, key) { root.write(I.remove(root.text, section, key)); }
- function addAccount(key, pairs) { root.write(I.addSection(root.text, "account." + key, pairs)); }
- function removeAccount(key) { root.write(I.removeSection(root.text, "account." + key)); }
+ 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 write(t) {
- if (t === root.text) return;
+ 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.text = t;
- fv.setText(t);
+ root.invalid = ({});
+ root.base = root.text;
+ root.draft = root.text;
}
function refresh() {
@@ -62,10 +110,15 @@ Singleton {
atomicWrites: true
watchChanges: true
printErrors: false
- // Its own writes come back here too; reparsing the same text is a no-op.
+ // Its own writes come back here too, as the text already held.
onFileChanged: reload()
- onLoaded: root.text = text()
- onLoadFailed: root.text = ""
+ // 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)
}