diff options
Diffstat (limited to 'desktop/modules/status/statusctl')
| -rwxr-xr-x | desktop/modules/status/statusctl | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/desktop/modules/status/statusctl b/desktop/modules/status/statusctl new file mode 100755 index 0000000..71ada07 --- /dev/null +++ b/desktop/modules/status/statusctl @@ -0,0 +1,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 + # An unreadable file reads as off, deliberately: a missing mode file is + # the normal state before anything has written one, and the redirect + # makes that fallback explicit rather than a side effect of a pipeline + # swallowing cat's exit status. + 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 |
