sane-input-handler: port to oil shell
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user