aboutsummaryrefslogtreecommitdiffstats
path: root/shared/Status.qml
blob: 5df1847f8816593cea85bda0bae4c03f9471f17b (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
// 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.
    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 }

    // 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" }
}