#!/bin/sh # SPDX-FileCopyrightText: 2023-2025 The Remph # SPDX-License-Identifier: GPL-3.0-or-later set ${BASH_VERSION:+-o pipefail} -efu # $wobdir is a `wrapper' directory that prevents a race between `test -p # $wobfifo` and `>$wobfifo', during which $wobfifo can be removed, causing # it to be created again as a regular file. If the removal also removes the # parent directory $wobdir, then sh's > operator can't creat(2). Of course, # the race could still happen between removing $wobfifo and $wobdir, but # this is less likely. wobdir=$XDG_RUNTIME_DIR/swob.$WAYLAND_DISPLAY wobfifo=$wobdir/pipe wobini= SWOB_AUDIO= SWOB_MAX_VOLUME=${SWOB_MAX_VOLUME:-100} scan_confdirs() { for dir in ${XDG_CONFIG_HOME:+"$XDG_CONFIG_HOME"} ~/.config /etc "${0%/*}"/../etc; do test -d "$dir"/swob || continue if test -z "$wobini" -a -r "$dir"/swob/wob.ini; then wobini=$dir/swob/wob.ini fi if test -z "$SWOB_AUDIO" -a -r "$dir"/swob/audio; then SWOB_AUDIO=`cat "$dir"/swob/audio` fi if test -n "$wobini" -a -n "$SWOB_AUDIO"; then return fi done } new_wobini() { # fine, I will make my own wob.ini(5) wobini=${XDG_CONFIG_HOME:-~/.config}/swob/wob.ini echo >&2 "$0: no swob/wob.ini found; writing default to $wobini" mkdir -p "${wobini%/*}" cat >$wobini </dev/null 2>&1 && SWOB_AUDIO=$sound_sys } get_audio_type() { # MUST be called with set +fu case $SWOB_AUDIO in pipewire|pulse|alsa) return ;; '') ;; *) echo >&2 "$0: warning: unrecognised SWOB_AUDIO: $SWOB_AUDIO" ;; esac select_audio pipewire "$PIPEWIRE_RUNTIME_DIR" wpctl && return select_audio pulse "$PULSE_RUNTIME_PATH" pactl && return SWOB_AUDIO=alsa # default to ALSA } set_vol() { set +fu get_audio_type set -fu case $SWOB_AUDIO in pipewire) case $1 in toggle) to_set=mute ;; *) to_set="volume -l $(awk -v percent="$SWOB_MAX_VOLUME" 'BEGIN{printf "%.1f", percent/100}')" ;; esac wpctl set-$to_set @DEFAULT_AUDIO_SINK@ "$1" wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed -E \ -e 's/^Volume: ([0-9]+)\.([0-9][0-9])/\1\2/' \ -e 's/\[MUTED\]/mute/' ;; pulse) case $1 in toggle) to_set=mute ;; *) to_set=volume ;; esac sign="${1:(-1)}" percent=`pactl get-sink-volume @DEFAULT_SINK@ | sed -En 's/.* ([0-9]+)%.*/\1/p'` if test "$to_set" = "mute" \ -o \( "$percent" -lt "$SWOB_MAX_VOLUME" \) \ -o \( "$percent" -eq "$SWOB_MAX_VOLUME" -a "$sign" = "-" \); then pactl set-sink-$to_set @DEFAULT_SINK@ "$(echo "$1" | sed -E 's/(.*)([+-])$/\2\1/')" else pactl set-sink-$to_set @DEFAULT_SINK@ "${SWOB_MAX_VOLUME}%" fi mute_cmd='pactl get-sink-mute @DEFAULT_SINK@' mute_out=`$mute_cmd` case $mute_out in 'Mute: yes') muted=1 ;; 'Mute: no') muted= ;; *) echo >&2 "$0: warning: bad output from $mute_cmd: $mute_out" ;; esac echo "$percent ${muted:+mute}" ;; *) if test "$SWOB_AUDIO" != alsa; then echo >&2 "$0: WARNING: internal inconsistency! SWOB_AUDIO: \`$SWOB_AUDIO'" fi amixer sset Master "$1" | sed -E ' # Extract percentage, keep original line for later h s/.*\[([0-9]+)%\].*/\1/ t match d : match # Get original line back x # Test if audio is muted /\[off\]/ { x s/$/ mute/ b } # else x s/$/ volume/' ;; esac } do_cmd_get_percent() { case $1 in volume|vol) set_vol "$2" ;; brightness|brt) brightnessctl -m set "$2" | sed -En 's/(.*[^0-9])?([0-9]+)%.*/\2 brightness/p' ;; *) echo >&2 "$0: error: unrecognised argument: $target" exit -1 ;; esac } ## MAIN ## scan_confdirs start_wob { do_cmd_get_percent "$@" sleep 3 # Needs to be long enough that there aren't too many wob(1) # processes spawned and respawned consecutively on repeated # taps, but short enough that there aren't too many /bin/sh # processes hanging around simultaneously running sleep(1) # from this script! 3 seconds is an uneducated guess. } >$wobfifo # Don't let wob die if it's still receiving input from another process; wait # until it says it's finished # To solve the above mentioned problem of too many /bin/sh processes hanging # around, we could setsid(1) wob so the script can exit without waiting as # soon as it's done sleeping (the existing situation is that as long as one # script sleeps, the shell that spawned the wob process will wait until that # sleep is done) test -z ${!-} || wait $! # surprisingly, $! could be unset (not just zero-length)