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