aboutsummaryrefslogtreecommitdiffstats
path: root/shared/Status.qml
blob: 8f80b9a4c8ed6e6de9f393ffed44901b44047115 (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
131
132
133
134
135
136
137
138
139
// 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 Quickshell.Wayland
import QtQuick

// Desktop modes as state. One file per mode under $XDG_RUNTIME_DIR, holding
// "0" or "1"; a missing file means off. The runtime directory is tmpfs, so a
// reboot resets every mode with no cleanup code here.
//
// The files are the interface, not this singleton: statusctl reads and writes
// them directly so it works while the shell is down, and the FileView watch
// means an external write repaints the drawer with no polling. It also means
// a mode set from outside still fires its effects, because the watch reaches
// the same handler a tile click would.
Singleton {
    id: root

    readonly property string dir: Quickshell.env("XDG_RUNTIME_DIR") || "/tmp"

    readonly property bool dnd: dndFile.value
    readonly property bool presentation: presFile.value

    readonly property int activeCount: (root.dnd ? 1 : 0) + (root.presentation ? 1 : 0)

    // Set once by shell.qml. IdleInhibitor does nothing with a null window,
    // and the singleton has no window of its own to offer.
    property var inhibitWindow: null

    // What dnd was before presentation mode turned it on, so turning
    // presentation mode off restores it rather than clearing it. Held here
    // rather than in a file: it is meaningful only while presentation mode is
    // on, and presentation mode does not survive a reboot.
    //
    // Known limit: on a shell restart while presentation is already on,
    // onPresentationChanged can run before the dnd FileView has loaded, so the
    // recorded prior value depends on which file loads first.
    property bool dndBeforePresentation: false

    function setMode(name, on) {
        if (name === "dnd") {
            dndFile.write(on);
        } else if (name === "presentation") {
            presFile.write(on);
        }
    }

    function toggleMode(name) {
        if (name === "dnd") root.setMode("dnd", !root.dnd);
        else if (name === "presentation") root.setMode("presentation", !root.presentation);
    }

    // Effects follow the mode rather than the setter, so a mode set by
    // statusctl while the drawer is closed asserts them too.
    onPresentationChanged: {
        if (root.presentation) {
            root.dndBeforePresentation = root.dnd;
            root.setMode("dnd", true);
            root.runBreaktimer("pause");
        } else {
            root.setMode("dnd", root.dndBeforePresentation);
            root.runBreaktimer("resume");
        }
    }

    function runBreaktimer(verb) {
        breakProc.command = ["breaktimer.sh", verb];
        breakProc.running = false;
        breakProc.running = true;
    }

    // breaktimer owns its own state file; this only calls its verbs. Two
    // writers on that file would race with its daemon loop, which rewrites it
    // on every phase change.
    Process {
        id: breakProc
        onExited: code => {
            if (code !== 0)
                console.warn("status: breaktimer.sh " + breakProc.command[1] + " exited " + code);
        }
    }

    // Wayland idle inhibit. The compositor advertises
    // zwp_idle_inhibit_manager_v1 and hypridle honours it, so no D-Bus path
    // is needed even though elogind runs here.
    IdleInhibitor {
        window: root.inhibitWindow
        enabled: root.presentation && root.inhibitWindow !== null
    }

    component ModeFile: FileView {
        id: mf

        property bool value: false

        // FileView is documented to fire fileChanged on its own setText, so a
        // write would re-enter this handler. Comparing before assigning makes
        // that harmless: the reparse yields the value just written and the
        // binding does not change.
        function reparse() {
            const t = mf.text().trim();
            const v = (t === "1");
            if (v !== mf.value) mf.value = v;
        }

        function write(on) {
            const s = on ? "1\n" : "0\n";
            mf.value = on;
            mf.setText(s);
        }

        // Both are the documented defaults in 0.3.1, set explicitly because
        // the CLI depends on them: statusctl watches close_write,moved_to
        // precisely because an atomic write lands as a rename, so a future
        // release flipping this default would break the watcher silently.
        atomicWrites: true
        watchChanges: true
        printErrors: false
        onFileChanged: mf.reload()
        onLoaded: mf.reparse()
        // A missing file is the off state, not an error worth logging.
        onLoadFailed: mf.value = false
    }

    ModeFile { id: dndFile; path: root.dir + "/status.dnd" }
    ModeFile { id: presFile; path: root.dir + "/status.presentation" }
}