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
|
#!/bin/bash
#
# 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.
#
# Read, set and watch desktop modes. The modes are files under
# XDG_RUNTIME_DIR holding 0 or 1; a missing file means off.
#
# This talks to the files, not to the shell, so it works while quickshell is
# down. Setting a mode that way records the state without firing its effects;
# the shell sees the change through its own watch and reasserts them.
#
# statusctl <mode> get prints 0 or 1
# statusctl <mode> set 0|1
# statusctl <mode> toggle
# statusctl <mode> watch waybar JSON on every change
set -u
MODES="dnd presentation"
DIR="${XDG_RUNTIME_DIR:-/tmp}"
usage() {
printf 'usage: %s <%s> <get|set 0|1|toggle|watch>\n' \
"${0##*/}" "$(printf '%s' "$MODES" | tr ' ' '|')" >&2
exit 1
}
[[ $# -ge 2 ]] || usage
mode="$1"
action="$2"
# A typo must fail loudly rather than read as a mode that happens to be off.
case " $MODES " in
*" $mode "*) ;;
*) printf '%s: unknown mode: %s\n' "${0##*/}" "$mode" >&2; exit 1 ;;
esac
file="$DIR/status.$mode"
read_mode() {
local v=""
# A missing file is the normal state before anything has written one and
# reads as off. Guarding on existence keeps the shell's redirection error
# off stderr: `2>/dev/null` on the command cannot suppress a failure of
# its own input redirect.
[[ -e "$file" ]] && v="$(tr -d '[:space:]' < "$file" 2>/dev/null)"
[[ "$v" == "1" ]] && printf '1' || printf '0'
}
# Write through a temporary file and rename, so no reader ever sees a
# half-written value. This is also what FileView does on the QML side, and it
# is why a watcher has to listen for moved_to as well as close_write.
write_mode() {
local want="$1" tmp
tmp="$(mktemp "$DIR/.status.$mode.XXXXXX")" || exit 1
printf '%s\n' "$want" > "$tmp"
# The temp file is made in the same directory as the target, so this is a
# rename rather than a copy, and therefore atomic. A failure here has to
# be loud: reporting success on a write that did not land would leave the
# caller and the shell disagreeing about the mode, with an orphan temp
# file as the only trace.
mv -f "$tmp" "$file" || { rm -f "$tmp"; exit 1; }
}
emit() {
local state="$1"
printf '{"text": "", "alt": "%s", "class": "%s", "tooltip": "%s"}\n' \
"$state" "$state" "$(tooltip "$state")"
}
tooltip() {
case "$1" in
activated) printf '%s: on' "$mode" ;;
deactivated) printf '%s: off' "$mode" ;;
down) printf '%s: no state file' "$mode" ;;
esac
}
state_now() {
[[ -e "$file" ]] || { printf 'down'; return; }
[[ "$(read_mode)" == "1" ]] && printf 'activated' || printf 'deactivated'
}
case "$action" in
get)
read_mode
printf '\n'
;;
set)
[[ $# -eq 3 ]] || usage
case "$3" in
0|1) write_mode "$3" ;;
*) usage ;;
esac
;;
toggle)
[[ "$(read_mode)" == "1" ]] && write_mode 0 || write_mode 1
;;
watch)
emit "$(state_now)"
# Watch the directory rather than the file: an atomic write replaces
# the file, so a watch held on the old inode dies with it. This is the
# same trap the mail watcher hit with Xapian.
#
# inotifywait must die with us. Piped straight into the while loop it
# would be a pipeline sibling, not a child, so a plain kill on this
# process (which is exactly how waybar stops and respawns its exec
# children on every reload) leaves it running, watching a directory
# nobody reads anymore. Process substitution makes it a real child
# whose PID we can hold and kill from a trap.
exec 3< <(inotifywait -q -m -e close_write,moved_to,delete --format '%f' "$DIR" 2>/dev/null)
watcher=$!
trap 'kill "$watcher" 2>/dev/null' EXIT TERM INT
while read -r changed <&3; do
[[ "$changed" == "status.$mode" ]] || continue
emit "$(state_now)"
done
;;
*)
usage
;;
esac
|