From e83bcd07f8b9994120d06b5f4d5a0b471566d8b1 Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 30 Mar 2025 03:04:33 +0000 Subject: [PATCH] sane-input-handler: port to oil shell --- .../programs/sane-input-handler/default.nix | 5 +- .../sane-input-handler/sane-input-handler | 420 +++++++++++------- 2 files changed, 250 insertions(+), 175 deletions(-) diff --git a/hosts/common/programs/sane-input-handler/default.nix b/hosts/common/programs/sane-input-handler/default.nix index 259385b9c..55f903854 100644 --- a/hosts/common/programs/sane-input-handler/default.nix +++ b/hosts/common/programs/sane-input-handler/default.nix @@ -73,18 +73,17 @@ in }; }; }; - packageUnwrapped = pkgs.static-nix-shell.mkBash { + packageUnwrapped = pkgs.static-nix-shell.mkYsh { pname = "sane-input-handler"; srcRoot = ./.; pkgs = { - inherit (pkgs) coreutils jq killall playerctl procps sane-open util-linux wireplumber; + inherit (pkgs) coreutils killall playerctl procps sane-open util-linux wireplumber; sway = config.sane.programs.sway.package; }; }; suggestedPrograms = [ "bonsai" # dependencies which get pulled in unconditionally: - "jq" "killall" "playerctl" "procps" #< TODO: reduce to just those parts of procps which are really needed diff --git a/hosts/common/programs/sane-input-handler/sane-input-handler b/hosts/common/programs/sane-input-handler/sane-input-handler index 5256ac2d4..252cef302 100755 --- a/hosts/common/programs/sane-input-handler/sane-input-handler +++ b/hosts/common/programs/sane-input-handler/sane-input-handler @@ -1,5 +1,5 @@ #!/usr/bin/env nix-shell -#!nix-shell -i bash -p bash -p coreutils -p jq -p killall -p playerctl -p procps -p sane-open -p sway -p util-linux -p wireplumber +#!nix-shell -i ysh -p coreutils -p killall -p oils-for-unix -p playerctl -p procps -p sane-open -p sway -p util-linux -p wireplumber # vim: set filetype=bash : # input map considerations @@ -56,12 +56,14 @@ # increments to use for volume adjustment (in %) -VOL_INCR=5 -KEYBOARD="${KEYBOARD:-wvkbd-mobintl}" -CAMERA="${CAMERA:-org.postmarketos.Megapixels.desktop}" +var VOL_INCR = 5 +var KEYBOARD = "${KEYBOARD:-wvkbd-mobintl}" +var CAMERA = "${CAMERA:-org.postmarketos.Megapixels.desktop}" +var VERBOSITY = 0 +var DRY_RUN = false -showHelp() { - echo "usage: sane-input-handler [--verbose] [--dry-run] " +proc showHelp { + echo "usage: sane-input-handler [--verbose [--verbose]] [--dry-run] " echo "" echo "where action is one of:" echo "- power_tap_{1,2}" @@ -78,280 +80,354 @@ showHelp() { echo "- voldown_start" } -log() { - printf "sane-input-handler: %s\n" "$1" >&2 +proc log (; ...stmts; level, context) { + var prefix = " $context:" if context else "" + var formatted = "$(pp value (stmts))" + echo "[$level] sane-input-handler:$prefix $formatted" >&2 } -VERBOSE= -debug() { - if [ -n "$VERBOSE" ]; then - log "$@" - fi +proc info (context=""; ...stmts) { + log (level="INFO", context=context, ...stmts) } -trace() { - debug "$*" - "$@" +proc debug (context=""; ...stmts) { + if (VERBOSITY >= 1) { + log (level="DEBG", context=context, ...stmts) + } } -DRY_RUN= -effect() { - if [ -n "$DRY_RUN" ]; then - log "SKIP(dry run): $*" - else - trace "$@" - fi +proc verbose (context=""; ...stmts) { + if (VERBOSITY >= 2) { + log (level="VERB", context=context, ...stmts) + } +} + +proc trace (...args) { + debug (...args) + @[args] +} + +proc effect (...args) { + if (DRY_RUN) { + info "SKIP(dry run)" (...args) + } else { + trace @[args] + } } ## HELPERS # swaySetOutput true|false # turns the display on or off -swaySetOutput() { - effect swaymsg -- output '*' power "$1" +proc swaySetOutput (value) { + effect swaymsg -- output '*' power "$value" } + # swaySetTouch enabled|disabled # turns touch input on or off -swaySetTouch() { +proc swaySetTouch (value) { # XXX(2024/06/09): `type:touch` method is documented, but now silently fails # swaymsg -- input type:touch events "$1" - local inputs=($(swaymsg -t get_inputs --raw | jq '. | map(select(.type == "touch")) | map(.identifier) | join(" ")' --raw-output)) - debug "detected ${#inputs[@]} sway inputs" - for id in "${inputs[@]}"; do - effect swaymsg -- input "$id" events "$1" - done + var inputs = null + swaymsg -t get_inputs --raw | json read (&inputs) + for input in (inputs) { + if (input.type === "touch") { + effect swaymsg -- input "$[input.identifier]" events "$value" + } + } } -# success if all touch inputs have their events enabled -swayGetTouch() { - swaymsg -t get_inputs --raw \ - | jq --exit-status '. | map(select(.type == "touch")) | all(.libinput.send_events == "enabled")' \ - > /dev/null -} -# success if all outputs have power -swayGetOutput() { - swaymsg -t get_outputs --raw \ - | jq --exit-status '. | all(.power)' \ - > /dev/null +# true if all touch inputs have their events enabled +func swayGetTouch () { + var inputs = null + swaymsg -t get_inputs --raw | json read (&inputs) + + var num_touch_enabled = 0 + var num_touch_disabled = 0 + for input in (inputs) { + if (input.type === "touch") { + var send_events = input.libinput.send_events + case (send_events) { + ("enabled") { + setvar num_touch_enabled += 1 + } + ("disabled") { + setvar num_touch_disabled += 1 + } + (else) { + info "swayGetTouch" ("unknown 'libinput.send_events' value:", send_events) + } + } + } + } + return (num_touch_disabled === 0) } -isAllOn() { - swayGetOutput && swayGetTouch +# true if all outputs have power +func swayGetOutput () { + var outputs = null + swaymsg -t get_outputs --raw | json read (&outputs) + + var num_power_true = 0 + var num_power_false = 0 + for output in (outputs) { + case (output.power) { + (true) { + setvar num_power_true += 1 + } + (false) { + setvar num_power_false += 1 + } + (else) { + info "swayGetOutput" ("unknown 'power' value:", output.power) + } + } + } + return (num_power_false === 0) } -isInhibited() { - pidof rofi +# true if rofi is visible +func rofiGet () { + if pidof rofi { + return (true) + } else { + return (false) + } } -handleWith() { - local state= - if [ -n "$_isInhibited" ]; then - state="inhibited+" - fi - if [ -n "$_isAllOn" ]; then - state="${state}on" - else - state="${state}off" - fi - log "state=$state action=$action: handleWith: $*" - "$@" - exit $? +var MEMOIZED = {} +func memoize (name, f) { + var expr = null + if (name in MEMOIZED) { + setvar expr = MEMOIZED[name] + verbose "memoize(cached)" (name, expr) + } else { + verbose "memoize(uncached)" (name) + # setvar expr = f() + setvar expr = io->evalExpr (f); + verbose "memoize(uncached -> cached)" (name, expr) + setglobal MEMOIZED[name] = expr + } + return (expr) +} + +func isAllOn () { + return (memoize ("isAllOn", ^[swayGetOutput() and swayGetTouch()])) +} + +func isInhibited () { + return (memoize ("rofiGet", ^[rofiGet()])) +} + +proc handleWith (...args) { + var state = "" + if (isInhibited()) { + setvar state = "inhibited+" + } + if (isAllOn()) { + setvar state = "${state}on" + } else { + setvar state = "${state}off" + } + + info "handleWith" ("state=$state", "action=$action", ...args) + @[args] + exit 0 } ## HANDLERS -ignore() { - true +proc ignore { + : } -inhibited() { - true +proc inhibited { + : } -unmapped() { - true +proc unmapped { + : } -allOn() { +proc allOn { swaySetOutput true swaySetTouch enabled } -allOff() { +proc allOff { swaySetOutput false swaySetTouch disabled } -toggleKeyboard() { - local keyboardPid=$(pidof "$KEYBOARD") - if [ -z "$keyboardPid" ]; then - log "cannot find $KEYBOARD" +proc toggleKeyboard { + var keyboardPids = $(pidof "$KEYBOARD") => split(" ") + if (not keyboardPids) { + log ("cannot find $KEYBOARD") return - fi + } - for p in $keyboardPid; do + for p in (keyboardPids) { # `env` so that we get the right `kill` binary instead of bash's builtin # `kill` only one keyboard process. in the case of e.g. sandboxing, # the keyboard might consist of multiple processes and each one we signal would cause a toggle - if effect env kill -s RTMIN+0 "$p"; then + if effect env kill -s RTMIN+0 "$p" { break - fi - done + } + } } ## DISPATCHERS -dispatchDefault() { - case "$action" in - "power_tap_2") +proc dispatchDefault (action) { + case (action) { + ("power_tap_2") { # power twice => screenoff handleWith allOff - ;; - "power_hold") + } + ("power_hold") { # power twice => toggle media player handleWith effect playerctl play-pause - ;; + } - volup_tap*) - handleWith effect wpctl set-volume @DEFAULT_AUDIO_SINK@ "$VOL_INCR"%+ - ;; - voldown_tap*) - handleWith effect wpctl set-volume @DEFAULT_AUDIO_SINK@ "$VOL_INCR"%- - ;; - esac + / ^ 'volup_tap_' d+ $ / { + handleWith effect wpctl set-volume '@DEFAULT_AUDIO_SINK@' "$VOL_INCR"%+ + } + / ^ 'voldown_tap_' d+ $ / { + handleWith effect wpctl set-volume '@DEFAULT_AUDIO_SINK@' "$VOL_INCR"%- + } + } } -dispatchOff() { - case "$action" in - "power_tap_1") +proc dispatchOff (action) { + case (action) { + ("power_tap_1") { # power once => unlock handleWith allOn - ;; - "power_tap_1_hold") + } + ("power_tap_1_hold") { # power tap->hold: escape hatch for when bonsaid locks up handleWith effect systemctl restart bonsaid - ;; - volup_hold*) + } + / ^ 'volup_hold_' d+ $ / { handleWith effect playerctl position 30+ - ;; - voldown_hold*) + } + / ^ 'voldown_hold_' d+ $ / { handleWith effect playerctl position 10- - ;; - esac + } + } } -dispatchOn() { - case "$action" in +proc dispatchOn (action) { + case (action) { # power_tap_1: intentional default to no-op (it's important this be unmapped, because events can be misordered with power_tap_1 arriving *after* power_tap_2) # power_tap_2: intentional default to screenoff - "power_tap_1_hold") + ("power_tap_1_hold") { # power tap->hold: kill active window # TODO: disable this if locked (with e.g. schlock, swaylock, etc) handleWith effect swaymsg kill - ;; - "power_and_volup") + } + ("power_and_volup") { # power (hold) -> volup: take screenshot handleWith effect sane-open --application sane-screenshot.desktop - ;; - "power_and_voldown") + } + ("power_and_voldown") { # power (hold) -> voldown: open camera handleWith effect sane-open --auto-keyboard --application "$CAMERA" - ;; - "power_then_volup") + } + ("power_then_volup") { # power (tap) -> volup: rotate CCW handleWith effect swaymsg -- output '-' transform 90 anticlockwise - ;; - "power_then_voldown") + } + ("power_then_voldown") { # power (tap) -> voldown: rotate CW handleWith effect swaymsg -- output '-' transform 90 clockwise - ;; + } - "volup_tap_1") + ("volup_tap_1") { # volume up once: filesystem browser handleWith effect sane-open --auto-keyboard --application rofi-filebrowser.desktop - ;; - "volup_hold_1") + } + ("volup_hold_1") { # volume up hold: browse files and apps # reset fs directory: useful in case you get stuck in broken directory (e.g. one which lacks a `..` entry) rm -f ~/.cache/rofi/rofi3.filebrowsercache handleWith effect sane-open --auto-keyboard --application rofi.desktop - ;; + } - "voldown_start") + ("voldown_start") { # volume down once: toggle keyboard handleWith toggleKeyboard - ;; - "voldown_hold_1") + } + ("voldown_hold_1") { # hold voldown to launch terminal # note we already triggered the keyboard; that's fine: usually keyboard + terminal go together :) handleWith effect sane-open --auto-keyboard --application xdg-terminal-exec.desktop - ;; - "voldown_tap_1") + } + ("voldown_tap_1") { # swallow, to prevent keyboard from also triggering media controls handleWith ignore - ;; - voldown_hold_*) + } + / ^ 'voldown_hold_' d+ $ / { # swallow, to prevent terminal from also triggering media controls handleWith ignore - ;; - esac + } + } } -dispatchInhibited() { - case "$action" in - "power_tap_1_hold") +proc dispatchInhibited (action) { + case (action) { + ("power_tap_1_hold") { # power hold: escape hatch in case rofi has hung handleWith effect killall -9 rofi - ;; - *) + } + (else) { # eat everything else (and let rofi consume it) handleWith inhibited - ;; - esac + } + } } -dispatchToplevel() { - _isAllOn="$(isAllOn && echo 1 || true)" +proc dispatchToplevel (action) { + if (not isAllOn()) { + trace dispatchOff "$action" + } else { + if (isInhibited()) { + trace dispatchInhibited "$action" + } else { + trace dispatchOn "$action" + } + } - if [ -z "$_isAllOn" ]; then - trace dispatchOff - else - _isInhibited="$(isInhibited && echo 1 || true)" - if [ -n "$_isInhibited" ]; then - trace dispatchInhibited - else - trace dispatchOn - fi - fi - - trace dispatchDefault + trace dispatchDefault "$action" } -action= -doShowHelp= -parseArgs() { - for arg in "$@"; do - case "$arg" in - (--dry-run) - DRY_RUN=1 - ;; - (-h|--help) - doShowHelp=1 - ;; - (-v|--verbose) - VERBOSE=1 - ;; - (*) - action="$arg" - ;; - esac - done +var action = null +var doShowHelp = false +proc parseArgs (; ...args) { + for arg in (args) { + case (arg) { + ("--dry-run") { + setglobal DRY_RUN = true + } + ("--help") { + setglobal doShowHelp = true + } + ("--verbose") { + setglobal VERBOSITY += 1 + } + (else) { + setglobal action = "$arg" + } + } + } } -parseArgs "$@" +if is-main { + parseArgs (...ARGV) -if [ -n "$doShowHelp" ]; then - showHelp - exit 0 -fi + if (doShowHelp) { + showHelp + exit 0 + } -trace dispatchToplevel -handleWith unmapped + trace dispatchToplevel "$action" + handleWith unmapped +}