aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/qtmaildir/Qtm.qml
blob: 554841b02eb6fc5c4989e1d6e8971b3931c61c04 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// 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 = "changed on disk: Discard to load it, then redo";
            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: " + FileViewError.toString(err)
    }

    Process {
        id: pg
        command: ["pgrep", "-x", "qtmaildir"]
        onExited: code => root.running = code === 0
    }
}