#!/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
