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";
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

View File

@@ -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] <action>"
proc showHelp {
echo "usage: sane-input-handler [--verbose [--verbose]] [--dry-run] <action>"
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
}