sane-input-handler: port to oil shell

This commit is contained in:
2025-03-30 03:04:33 +00:00
parent 03635fcf31
commit e83bcd07f8
2 changed files with 250 additions and 175 deletions

View File

@@ -73,18 +73,17 @@ in
}; };
}; };
}; };
packageUnwrapped = pkgs.static-nix-shell.mkBash { packageUnwrapped = pkgs.static-nix-shell.mkYsh {
pname = "sane-input-handler"; pname = "sane-input-handler";
srcRoot = ./.; srcRoot = ./.;
pkgs = { 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; sway = config.sane.programs.sway.package;
}; };
}; };
suggestedPrograms = [ suggestedPrograms = [
"bonsai" "bonsai"
# dependencies which get pulled in unconditionally: # dependencies which get pulled in unconditionally:
"jq"
"killall" "killall"
"playerctl" "playerctl"
"procps" #< TODO: reduce to just those parts of procps which are really needed "procps" #< TODO: reduce to just those parts of procps which are really needed

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env nix-shell #!/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 : # vim: set filetype=bash :
# input map considerations # input map considerations
@@ -56,12 +56,14 @@
# increments to use for volume adjustment (in %) # increments to use for volume adjustment (in %)
VOL_INCR=5 var VOL_INCR = 5
KEYBOARD="${KEYBOARD:-wvkbd-mobintl}" var KEYBOARD = "${KEYBOARD:-wvkbd-mobintl}"
CAMERA="${CAMERA:-org.postmarketos.Megapixels.desktop}" var CAMERA = "${CAMERA:-org.postmarketos.Megapixels.desktop}"
var VERBOSITY = 0
var DRY_RUN = false
showHelp() { proc showHelp {
echo "usage: sane-input-handler [--verbose] [--dry-run] <action>" echo "usage: sane-input-handler [--verbose [--verbose]] [--dry-run] <action>"
echo "" echo ""
echo "where action is one of:" echo "where action is one of:"
echo "- power_tap_{1,2}" echo "- power_tap_{1,2}"
@@ -78,280 +80,354 @@ showHelp() {
echo "- voldown_start" echo "- voldown_start"
} }
log() { proc log (; ...stmts; level, context) {
printf "sane-input-handler: %s\n" "$1" >&2 var prefix = " $context:" if context else ""
var formatted = "$(pp value (stmts))"
echo "[$level] sane-input-handler:$prefix $formatted" >&2
} }
VERBOSE= proc info (context=""; ...stmts) {
debug() { log (level="INFO", context=context, ...stmts)
if [ -n "$VERBOSE" ]; then
log "$@"
fi
} }
trace() { proc debug (context=""; ...stmts) {
debug "$*" if (VERBOSITY >= 1) {
"$@" log (level="DEBG", context=context, ...stmts)
}
} }
DRY_RUN= proc verbose (context=""; ...stmts) {
effect() { if (VERBOSITY >= 2) {
if [ -n "$DRY_RUN" ]; then log (level="VERB", context=context, ...stmts)
log "SKIP(dry run): $*" }
else }
trace "$@"
fi proc trace (...args) {
debug (...args)
@[args]
}
proc effect (...args) {
if (DRY_RUN) {
info "SKIP(dry run)" (...args)
} else {
trace @[args]
}
} }
## HELPERS ## HELPERS
# swaySetOutput true|false # swaySetOutput true|false
# turns the display on or off # turns the display on or off
swaySetOutput() { proc swaySetOutput (value) {
effect swaymsg -- output '*' power "$1" effect swaymsg -- output '*' power "$value"
} }
# swaySetTouch enabled|disabled # swaySetTouch enabled|disabled
# turns touch input on or off # turns touch input on or off
swaySetTouch() { proc swaySetTouch (value) {
# XXX(2024/06/09): `type:touch` method is documented, but now silently fails # XXX(2024/06/09): `type:touch` method is documented, but now silently fails
# swaymsg -- input type:touch events "$1" # swaymsg -- input type:touch events "$1"
local inputs=($(swaymsg -t get_inputs --raw | jq '. | map(select(.type == "touch")) | map(.identifier) | join(" ")' --raw-output)) var inputs = null
debug "detected ${#inputs[@]} sway inputs" swaymsg -t get_inputs --raw | json read (&inputs)
for id in "${inputs[@]}"; do for input in (inputs) {
effect swaymsg -- input "$id" events "$1" if (input.type === "touch") {
done effect swaymsg -- input "$[input.identifier]" events "$value"
}
}
} }
# success if all touch inputs have their events enabled # true if all touch inputs have their events enabled
swayGetTouch() { func swayGetTouch () {
swaymsg -t get_inputs --raw \ var inputs = null
| jq --exit-status '. | map(select(.type == "touch")) | all(.libinput.send_events == "enabled")' \ swaymsg -t get_inputs --raw | json read (&inputs)
> /dev/null
} var num_touch_enabled = 0
# success if all outputs have power var num_touch_disabled = 0
swayGetOutput() { for input in (inputs) {
swaymsg -t get_outputs --raw \ if (input.type === "touch") {
| jq --exit-status '. | all(.power)' \ var send_events = input.libinput.send_events
> /dev/null 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() { # true if all outputs have power
swayGetOutput && swayGetTouch 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() { # true if rofi is visible
pidof rofi func rofiGet () {
if pidof rofi {
return (true)
} else {
return (false)
}
} }
handleWith() { var MEMOIZED = {}
local state= func memoize (name, f) {
if [ -n "$_isInhibited" ]; then var expr = null
state="inhibited+" if (name in MEMOIZED) {
fi setvar expr = MEMOIZED[name]
if [ -n "$_isAllOn" ]; then verbose "memoize(cached)" (name, expr)
state="${state}on" } else {
else verbose "memoize(uncached)" (name)
state="${state}off" # setvar expr = f()
fi setvar expr = io->evalExpr (f);
log "state=$state action=$action: handleWith: $*" verbose "memoize(uncached -> cached)" (name, expr)
"$@" setglobal MEMOIZED[name] = expr
exit $? }
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 ## HANDLERS
ignore() { proc ignore {
true :
} }
inhibited() { proc inhibited {
true :
} }
unmapped() { proc unmapped {
true :
} }
allOn() { proc allOn {
swaySetOutput true swaySetOutput true
swaySetTouch enabled swaySetTouch enabled
} }
allOff() { proc allOff {
swaySetOutput false swaySetOutput false
swaySetTouch disabled swaySetTouch disabled
} }
toggleKeyboard() { proc toggleKeyboard {
local keyboardPid=$(pidof "$KEYBOARD") var keyboardPids = $(pidof "$KEYBOARD") => split(" ")
if [ -z "$keyboardPid" ]; then if (not keyboardPids) {
log "cannot find $KEYBOARD" log ("cannot find $KEYBOARD")
return 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 # `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, # `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 # 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 break
fi }
done }
} }
## DISPATCHERS ## DISPATCHERS
dispatchDefault() { proc dispatchDefault (action) {
case "$action" in case (action) {
"power_tap_2") ("power_tap_2") {
# power twice => screenoff # power twice => screenoff
handleWith allOff handleWith allOff
;; }
"power_hold") ("power_hold") {
# power twice => toggle media player # power twice => toggle media player
handleWith effect playerctl play-pause handleWith effect playerctl play-pause
;; }
volup_tap*) / ^ 'volup_tap_' d+ $ / {
handleWith effect wpctl set-volume @DEFAULT_AUDIO_SINK@ "$VOL_INCR"%+ handleWith effect wpctl set-volume '@DEFAULT_AUDIO_SINK@' "$VOL_INCR"%+
;; }
voldown_tap*) / ^ 'voldown_tap_' d+ $ / {
handleWith effect wpctl set-volume @DEFAULT_AUDIO_SINK@ "$VOL_INCR"%- handleWith effect wpctl set-volume '@DEFAULT_AUDIO_SINK@' "$VOL_INCR"%-
;; }
esac }
} }
dispatchOff() { proc dispatchOff (action) {
case "$action" in case (action) {
"power_tap_1") ("power_tap_1") {
# power once => unlock # power once => unlock
handleWith allOn handleWith allOn
;; }
"power_tap_1_hold") ("power_tap_1_hold") {
# power tap->hold: escape hatch for when bonsaid locks up # power tap->hold: escape hatch for when bonsaid locks up
handleWith effect systemctl restart bonsaid handleWith effect systemctl restart bonsaid
;; }
volup_hold*) / ^ 'volup_hold_' d+ $ / {
handleWith effect playerctl position 30+ handleWith effect playerctl position 30+
;; }
voldown_hold*) / ^ 'voldown_hold_' d+ $ / {
handleWith effect playerctl position 10- handleWith effect playerctl position 10-
;; }
esac }
} }
dispatchOn() { proc dispatchOn (action) {
case "$action" in 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_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_2: intentional default to screenoff
"power_tap_1_hold") ("power_tap_1_hold") {
# power tap->hold: kill active window # power tap->hold: kill active window
# TODO: disable this if locked (with e.g. schlock, swaylock, etc) # TODO: disable this if locked (with e.g. schlock, swaylock, etc)
handleWith effect swaymsg kill handleWith effect swaymsg kill
;; }
"power_and_volup") ("power_and_volup") {
# power (hold) -> volup: take screenshot # power (hold) -> volup: take screenshot
handleWith effect sane-open --application sane-screenshot.desktop handleWith effect sane-open --application sane-screenshot.desktop
;; }
"power_and_voldown") ("power_and_voldown") {
# power (hold) -> voldown: open camera # power (hold) -> voldown: open camera
handleWith effect sane-open --auto-keyboard --application "$CAMERA" handleWith effect sane-open --auto-keyboard --application "$CAMERA"
;; }
"power_then_volup") ("power_then_volup") {
# power (tap) -> volup: rotate CCW # power (tap) -> volup: rotate CCW
handleWith effect swaymsg -- output '-' transform 90 anticlockwise handleWith effect swaymsg -- output '-' transform 90 anticlockwise
;; }
"power_then_voldown") ("power_then_voldown") {
# power (tap) -> voldown: rotate CW # power (tap) -> voldown: rotate CW
handleWith effect swaymsg -- output '-' transform 90 clockwise handleWith effect swaymsg -- output '-' transform 90 clockwise
;; }
"volup_tap_1") ("volup_tap_1") {
# volume up once: filesystem browser # volume up once: filesystem browser
handleWith effect sane-open --auto-keyboard --application rofi-filebrowser.desktop handleWith effect sane-open --auto-keyboard --application rofi-filebrowser.desktop
;; }
"volup_hold_1") ("volup_hold_1") {
# volume up hold: browse files and apps # 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) # 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 rm -f ~/.cache/rofi/rofi3.filebrowsercache
handleWith effect sane-open --auto-keyboard --application rofi.desktop handleWith effect sane-open --auto-keyboard --application rofi.desktop
;; }
"voldown_start") ("voldown_start") {
# volume down once: toggle keyboard # volume down once: toggle keyboard
handleWith toggleKeyboard handleWith toggleKeyboard
;; }
"voldown_hold_1") ("voldown_hold_1") {
# hold voldown to launch terminal # hold voldown to launch terminal
# note we already triggered the keyboard; that's fine: usually keyboard + terminal go together :) # 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 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 # swallow, to prevent keyboard from also triggering media controls
handleWith ignore handleWith ignore
;; }
voldown_hold_*) / ^ 'voldown_hold_' d+ $ / {
# swallow, to prevent terminal from also triggering media controls # swallow, to prevent terminal from also triggering media controls
handleWith ignore handleWith ignore
;; }
esac }
} }
dispatchInhibited() { proc dispatchInhibited (action) {
case "$action" in case (action) {
"power_tap_1_hold") ("power_tap_1_hold") {
# power hold: escape hatch in case rofi has hung # power hold: escape hatch in case rofi has hung
handleWith effect killall -9 rofi handleWith effect killall -9 rofi
;; }
*) (else) {
# eat everything else (and let rofi consume it) # eat everything else (and let rofi consume it)
handleWith inhibited handleWith inhibited
;; }
esac }
} }
dispatchToplevel() { proc dispatchToplevel (action) {
_isAllOn="$(isAllOn && echo 1 || true)" if (not isAllOn()) {
trace dispatchOff "$action"
} else {
if (isInhibited()) {
trace dispatchInhibited "$action"
} else {
trace dispatchOn "$action"
}
}
if [ -z "$_isAllOn" ]; then trace dispatchDefault "$action"
trace dispatchOff
else
_isInhibited="$(isInhibited && echo 1 || true)"
if [ -n "$_isInhibited" ]; then
trace dispatchInhibited
else
trace dispatchOn
fi
fi
trace dispatchDefault
} }
action= var action = null
doShowHelp= var doShowHelp = false
parseArgs() { proc parseArgs (; ...args) {
for arg in "$@"; do for arg in (args) {
case "$arg" in case (arg) {
(--dry-run) ("--dry-run") {
DRY_RUN=1 setglobal DRY_RUN = true
;; }
(-h|--help) ("--help") {
doShowHelp=1 setglobal doShowHelp = true
;; }
(-v|--verbose) ("--verbose") {
VERBOSE=1 setglobal VERBOSITY += 1
;; }
(*) (else) {
action="$arg" setglobal action = "$arg"
;; }
esac }
done }
} }
parseArgs "$@" if is-main {
parseArgs (...ARGV)
if [ -n "$doShowHelp" ]; then if (doShowHelp) {
showHelp showHelp
exit 0 exit 0
fi }
trace dispatchToplevel trace dispatchToplevel "$action"
handleWith unmapped handleWith unmapped
}