#!/bin/bash set -u readonly PANAMA_OSD_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" readonly PANAMA_OSD_BLIP_DEFAULT="/usr/share/sounds/freedesktop/stereo/audio-volume-change.oga" # ── Panama's own preferences ──────────────────────────────────────────────── # # These run as keybinds, so the shell may not be up and there is no IPC to ask. # settings.json is read straight off disk instead, and every failure -- no file # on a fresh install, a file written before the key existed, a file that is not # JSON at all -- means the schema default rather than a broken volume key. settings_file() { printf '%s/panama/settings.json\n' "${XDG_CONFIG_HOME:-$HOME/.config}" } setting_bool() { local key="$1" fallback="$2" file value file="$(settings_file)" [[ -r $file ]] || { printf '%s\n' "$fallback"; return 0; } command -v jq >/dev/null 2>&1 || { printf '%s\n' "$fallback"; return 0; } value="$(jq -r --arg key "$key" ' if type == "object" and (.[$key] | type) == "boolean" then .[$key] else empty end ' "$file" 2>/dev/null)" || value="" [[ $value == true || $value == false ]] || value="$fallback" printf '%s\n' "$value" } # wpctl clamps to 1.0 by default, and it clamps the *result* -- so the ceiling # has to be passed on the way down as well. Without it, stepping down from 130% # would snap to 100% instead of 124%, which reads as the slider jumping on its # own. The microphone never gets this: gain past 100% on a capture device buys # noise, not signal, and the Sound page's input slider stays 0-100 to match. volume_limit() { if [[ $(setting_bool overAmplification false) == true ]]; then printf '1.5\n' else printf '1\n' fi } # The click that says the volume moved. Backgrounded and never waited on: it is # feedback about something that has already happened, so it must not sit between # the key press and the OSD. A distribution without the freedesktop sound theme # has no file to play, which is a silent desktop rather than a broken key. play_blip() { local sound="${PANAMA_OSD_BLIP_SOUND:-$PANAMA_OSD_BLIP_DEFAULT}" [[ $(setting_bool volumeChangeBlip true) == true ]] || return 0 [[ -f $sound ]] || return 0 pw-play "$sound" >/dev/null 2>&1 & disown 2>/dev/null || true } strict_delivery() { [[ ${PANAMA_OSD_STRICT:-false} == true || ${PANAMA_OSD_STRICT:-false} == 1 ]] } show_progress() { if ! qs ipc call osd progress "$1" "$2" 100 "$3" >/dev/null 2>&1; then strict_delivery && return 1 fi return 0 } show_message() { if ! qs ipc call osd message "$1" "$2" >/dev/null 2>&1; then strict_delivery && return 1 fi return 0 } volume_state() { local target="$1" output level percent muted=false output="$(wpctl get-volume "$target" 2>/dev/null)" || return 1 if [[ $output =~ Volume:[[:space:]]*([0-9]+([.][0-9]+)?) ]]; then level="${BASH_REMATCH[1]}" else return 1 fi [[ $output == *"[MUTED]"* ]] && muted=true percent="$(awk -v value="$level" 'BEGIN { printf "%d", value * 100 + 0.5 }')" printf '%s %s\n' "$percent" "$muted" } show_volume() { local target="$1" kind="$2" state percent muted label state="$(volume_state "$target")" || return 0 read -r percent muted <<<"$state" if [[ $muted == true ]]; then show_progress "${kind}-muted" "$percent" "Muted" else label="${percent}%" show_progress "$kind" "$percent" "$label" fi } adjust_volume() { local action="${1:-}" step="${2:-6}" target="@DEFAULT_AUDIO_SINK@" limit limit="$(volume_limit)" case "$action" in up) wpctl set-volume -l "$limit" "$target" "${step}%+" || return ;; down) wpctl set-volume -l "$limit" "$target" "${step}%-" || return ;; toggle) wpctl set-mute "$target" toggle || return ;; *) printf 'Usage: panama-osd volume up|down|toggle [step]\n' >&2; return 2 ;; esac play_blip show_volume "$target" volume } adjust_microphone() { local action="${1:-}" step="${2:-6}" target="@DEFAULT_AUDIO_SOURCE@" case "$action" in up) wpctl set-volume -l 1 "$target" "${step}%+" || return ;; down) wpctl set-volume -l 1 "$target" "${step}%-" || return ;; toggle) wpctl set-mute "$target" toggle || return ;; *) printf 'Usage: panama-osd microphone up|down|toggle [step]\n' >&2; return 2 ;; esac show_volume "$target" microphone } brightness_percent() { local output="$1" percent percent="$(awk -F, 'NR == 1 { value=$4; gsub(/%/, "", value); print value }' <<<"$output")" [[ $percent =~ ^[0-9]+$ ]] || return 1 printf '%s\n' "$percent" } brightness_error() { local detail="$1" label="External brightness unavailable" if [[ $detail == *udev* || $detail == *accessible* || $detail == *permission* ]]; then label="Brightness needs permission" fi show_message dialog-warning-symbolic "$label" || true if command -v notify-send >/dev/null 2>&1; then notify-send --app-name=Panama --icon=display-brightness-symbolic \ "Brightness unavailable" "$detail" >/dev/null 2>&1 || true fi } discover_ddc_bus() { local helper="$1" cache_file="$2" list_json focused selected error bus connector command -v jq >/dev/null 2>&1 || { brightness_error "jq is required to discover DDC/CI displays." return 1 } list_json="$("$helper" list 2>/dev/null)" || { brightness_error "The external brightness helper could not inspect connected displays." return 1 } if ! jq -e 'type == "object" and (.displays | type == "array")' >/dev/null 2>&1 <<<"$list_json"; then brightness_error "The external brightness helper returned invalid display information." return 1 fi error="$(jq -r '.error // empty' <<<"$list_json")" if [[ -n $error ]]; then brightness_error "$error" return 1 fi focused="$(hyprctl -j monitors 2>/dev/null \ | jq -r '.[] | select(.focused == true) | .name' 2>/dev/null \ | head -n1)" selected="$(jq -r --arg connector "$focused" ' ([.displays[] | select(.connector == $connector)][0] // .displays[0] // empty) | [.bus, .connector] | @tsv ' <<<"$list_json")" IFS=$'\t' read -r bus connector <<<"$selected" if [[ ! $bus =~ ^[0-9]+$ ]]; then brightness_error "No connected monitor exposes DDC/CI brightness control." return 1 fi umask 077 printf '%s\t%s\n' "$bus" "$connector" >"$cache_file" printf '%s\n' "$bus" } adjust_ddc_brightness() { local action="$1" step="$2" local helper="${PANAMA_OSD_BRIGHTNESS_HELPER:-$PANAMA_OSD_SCRIPT_DIR/panama-brightness}" local runtime_dir="${PANAMA_OSD_RUNTIME_DIR:-${XDG_RUNTIME_DIR:-/tmp}/panama-osd-${UID}}" local cache_file="$runtime_dir/brightness-bus" lock_file="$runtime_dir/brightness.lock" local bus="" connector="" current target lock_fd [[ -x $helper ]] || { brightness_error "The external brightness helper is not installed." return 0 } mkdir -p "$runtime_dir" || return 0 chmod 700 "$runtime_dir" 2>/dev/null || true exec {lock_fd}>"$lock_file" || return 0 # DDC transactions on one I2C bus cannot safely overlap. A short wait also # sheds an excessive key-repeat backlog instead of replaying it seconds later. flock -w 2 "$lock_fd" || return 0 if [[ -r $cache_file ]]; then IFS=$'\t' read -r bus connector <"$cache_file" || true [[ $bus =~ ^[0-9]+$ ]] || bus="" fi if [[ -n $bus ]]; then current="$("$helper" get "$bus" 2>/dev/null)" || current="" if [[ ! $current =~ ^[0-9]+$ ]]; then : >"$cache_file" bus="" fi fi if [[ -z $bus ]]; then bus="$(discover_ddc_bus "$helper" "$cache_file")" || return 0 current="$("$helper" get "$bus" 2>/dev/null)" || current="" fi if [[ ! $current =~ ^[0-9]+$ ]]; then brightness_error "The selected monitor stopped responding over DDC/CI." return 0 fi if [[ $action == up ]]; then target=$(( current + step )) else target=$(( current - step )) fi (( target > 100 )) && target=100 (( target < 0 )) && target=0 if ! "$helper" set "$bus" "$target" >/dev/null 2>&1; then brightness_error "The selected monitor did not accept the brightness change." return 0 fi show_progress brightness "$target" "${target}%" } adjust_brightness() { local action="${1:-}" step="${2:-5}" output percent [[ $step =~ ^[0-9]+$ ]] || { printf 'Usage: panama-osd brightness up|down [step]\n' >&2 return 2 } case "$action" in up|down) ;; *) printf 'Usage: panama-osd brightness up|down [step]\n' >&2; return 2 ;; esac # Laptop panels expose a kernel backlight class and remain the fastest, # most reliable path. Desktops fall through to DDC/CI monitor control. output="$(brightnessctl -m -c backlight 2>/dev/null)" || output="" if percent="$(brightness_percent "$output")"; then if [[ $action == up ]]; then brightnessctl -e4 -n2 -c backlight set "${step}%+" >/dev/null || return 0 else brightnessctl -e4 -n2 -c backlight set "${step}%-" >/dev/null || return 0 fi output="$(brightnessctl -m -c backlight 2>/dev/null)" || return 0 percent="$(brightness_percent "$output")" || return 0 show_progress brightness "$percent" "${percent}%" return fi adjust_ddc_brightness "$action" "$step" } media_action() { local action="${1:-}" kind label fallback case "$action" in play-pause) playerctl play-pause || return if [[ $(playerctl status 2>/dev/null) == "Playing" ]]; then kind="media-play" fallback="Playing" else kind="media-pause" fallback="Paused" fi ;; next) playerctl next || return; kind="media-next"; fallback="Next track" ;; previous) playerctl previous || return; kind="media-previous"; fallback="Previous track" ;; stop) playerctl stop || return; kind="media-stop"; fallback="Stopped" ;; *) printf 'Usage: panama-osd media play-pause|next|previous|stop\n' >&2; return 2 ;; esac label="$(playerctl metadata --format '{{ title }} — {{ artist }}' 2>/dev/null)" [[ -n $label ]] || label="$fallback" show_message "$kind" "$label" } # Airplane mode: one rfkill state for every radio, with an OSD saying which # way it went. "Any radio unblocked" reads as radios-on, so toggle means # "block everything" from a mixed state -- the direction a person reaching # for airplane mode wants. airplane_action() { case "${1:-toggle}" in toggle) if rfkill list 2>/dev/null | grep -q 'Soft blocked: no'; then rfkill block all 2>/dev/null \ && show_message "airplane-mode-symbolic" "Airplane mode on" else rfkill unblock all 2>/dev/null \ && show_message "network-wireless-symbolic" "Airplane mode off" fi ;; *) printf 'Usage: panama-osd airplane toggle\n' >&2 return 2 ;; esac } case "${1:-}" in volume) shift; adjust_volume "$@" ;; microphone) shift; adjust_microphone "$@" ;; brightness) shift; adjust_brightness "$@" ;; media) shift; media_action "$@" ;; airplane) shift; airplane_action "$@" ;; message) shift kind="${1:-}" [[ -n $kind && $# -ge 2 ]] || { printf 'Usage: panama-osd message ICON LABEL\n' >&2 exit 2 } shift show_message "$kind" "$*" ;; *) printf 'Usage: panama-osd volume|microphone|brightness|media|airplane|message ACTION [value]\n' >&2 exit 2 ;; esac