sane-input-handler: express the logic in a way thats more immune to the previous class of quoting error

had to introduce some hacks to keep the debuggability though. i don't like it.
This commit is contained in:
2025-03-31 07:10:31 +00:00
parent 6da4a5ab9d
commit 9fcaba8bf3

View File

@@ -57,11 +57,38 @@
# increments to use for volume adjustment (in %) # increments to use for volume adjustment (in %)
var VOL_INCR = 5 var VOL_INCR = 5
var SEEK_INCR = 30
var SEEK_DECR = 10
var KEYBOARD = "${KEYBOARD:-wvkbd-mobintl}" var KEYBOARD = "${KEYBOARD:-wvkbd-mobintl}"
var CAMERA = "${CAMERA:-org.postmarketos.Megapixels.desktop}" var CAMERA = "${CAMERA:-org.postmarketos.Megapixels.desktop}"
var VERBOSITY = 0 var VERBOSITY = 0
var DRY_RUN = false var DRY_RUN = false
# all known triggers:
var Triggers = {
PowerTap1 : "power_tap_1",
PowerTap2 : "power_tap_2",
PowerHold : "power_hold",
PowerTap1Hold : "power_tap_1_hold",
PowerAndVolup : "power_and_volup",
PowerAndVoldown : "power_and_voldown",
PowerThenVolup : "power_then_volup",
PowerThenVoldown : "power_and_voldown",
VolupTap1 : "volup_tap_1",
VolupTap2 : "volup_tap_2",
VolupTap3 : "volup_tap_3",
VolupHold1 : "volup_hold_1",
VolupHold2 : "volup_hold_2",
VolupHold3 : "volup_hold_3",
VoldownTap1 : "voldown_tap_1",
VoldownTap2 : "voldown_tap_2",
VoldownTap3 : "voldown_tap_3",
VoldownHold1 : "voldown_hold_1",
VoldownHold2 : "voldown_hold_2",
VoldownHold3 : "voldown_hold_3",
VoldownStart : "voldown_start",
}
proc showHelp { proc showHelp {
echo "usage: sane-input-handler [--verbose [--verbose]] [--dry-run] <action>" echo "usage: sane-input-handler [--verbose [--verbose]] [--dry-run] <action>"
echo "" echo ""
@@ -81,9 +108,21 @@ proc showHelp {
} }
proc log (; ...stmts; level, context) { proc log (; ...stmts; level, context) {
var prefix = " $context:" if context else "" var prefix = "";
var formatted = "$(pp value (stmts))" var formatted = "";
echo "[$level] sane-input-handler:$prefix $formatted" >&2 if (stmts) {
setvar prefix = "$context: " if context else ""
setvar formatted = "$(pp value (stmts))"
} else {
# bare log statement; nothing to pretty-print
setvar formatted = context;
}
echo "[$level] sane-input-handler: $prefix$formatted" >&2
}
proc die (context=""; ...stmts) {
log (level="ERRR", context=context, ...stmts)
exit 1
} }
proc info (context=""; ...stmts) { proc info (context=""; ...stmts) {
@@ -115,13 +154,6 @@ proc effect (...args) {
} }
} }
func traceFunc (funcName, fn, ...args) {
debug "$funcName" (...args)
var r = fn(...args)
debug "$[funcName] -> return" (r)
return (r)
}
## HELPERS ## HELPERS
# swaySetOutput true|false # swaySetOutput true|false
@@ -271,25 +303,6 @@ func isFullscreen () {
return (memoize ("swayIsFullscreen", ^[swayIsFullscreen()])) return (memoize ("swayIsFullscreen", ^[swayIsFullscreen()]))
} }
proc handleWith (; ...args) {
var state = ""
if (isFullscreen()) {
setvar state = "fullscreen+${state}"
}
if (isInhibited()) {
setvar state = "inhibited+${state}"
}
if (isAllOn()) {
setvar state = "${state}on"
} else {
setvar state = "${state}off"
}
info "handleWith" ("state=$state", "action=$action", ...args)
@[args]
}
## HANDLERS ## HANDLERS
proc ignore { proc ignore {
: :
@@ -297,9 +310,6 @@ proc ignore {
proc inhibited { proc inhibited {
: :
} }
proc unmapped {
:
}
proc allOn { proc allOn {
swaySetOutput true swaySetOutput true
@@ -311,9 +321,9 @@ proc allOff {
} }
proc toggleKeyboard { proc toggleKeyboard {
var keyboardPids = $(pidof "$KEYBOARD") => split(" ") var keyboardPids = $(pidof "$KEYBOARD" || echo "") => split(" ")
if (not keyboardPids) { if (not keyboardPids) {
log ("cannot find $KEYBOARD") info "toggleKeyboard: cannot find keyboard" (KEYBOARD)
return return
} }
@@ -344,11 +354,11 @@ proc restartBonsai {
} }
proc seekForward { proc seekForward {
effect playerctl position 30+ effect playerctl position "$SEEK_INCR"+
} }
proc seekBackward { proc seekBackward {
effect playerctl position 10- effect playerctl position "$SEEK_DECR"-
} }
proc killWindow { proc killWindow {
@@ -389,158 +399,161 @@ proc killRofi {
effect killall -9 rofi effect killall -9 rofi
} }
## DISPATCHERS # index of all possible responses, to allow lookup by-name
var Responses = {
func mapDefault (action) { allOn: allOn,
case (action) { allOff: allOff,
("power_tap_2") { toggleKeyboard: toggleKeyboard,
# power twice => screenoff togglePlayback: togglePlayback,
return ("allOff") volumeUp: volumeUp,
} volumeDown: volumeDown,
("power_hold") { restartBonsai: restartBonsai,
# power twice => toggle media player seekForward: seekForward,
return ("togglePlayback") seekBackward: seekBackward,
} killWindow: killWindow,
/ ^ 'volup_hold_' d+ $ / { takeScreenshot: takeScreenshot,
return ("seekForward") openCamera: openCamera,
} rotateCCW: rotateCCW,
/ ^ 'voldown_hold_' d+ $ / { rotateCW: rotateCW,
return ("seekBackward") openFilebrowser: openFilebrowser,
} openFilebrowserWithApps: openFilebrowserWithApps,
openTerminal: openTerminal,
/ ^ 'volup_tap_' d+ $ / { killRofi: killRofi,
return ("volumeUp")
}
/ ^ 'voldown_tap_' d+ $ / {
return ("volumeDown")
}
}
} }
func mapOff (action) { func Handler_exec(self) {
case (action) { var resp = self.response
("power_tap_1") { resp #< this executes the response
# power once => unlock
return ("allOn")
}
("power_tap_1_hold") {
# power tap->hold: escape hatch for when bonsaid locks up
return ("restartBonsai")
}
}
} }
func mapOn (action) { var Handler = Object(null, {
case (action) { # methods
# 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) exec: Handler_exec,
# power_tap_2: intentional default to screenoff # instance state
("power_tap_1_hold") { trigger: null, #< name of action the _user_ performed
# power tap->hold: kill active window response: null, #< bound proc to invoke in response to the trigger
# TODO: disable this if locked (with e.g. schlock, swaylock, etc) name: null, #< friendly name of response; should just be the stringified form of `response`
return ("killWindow") screen: false, #< screen must be on for this response to trigger
} fullscreen: false, #< desktop view must be fullscreen for this response to trigger
("power_and_volup") { sys_menu: false, #< system menu must be active for this response to trigger
# power (hold) -> volup: take screenshot off: false, #< screen must be off for this response to trigger
return ("screenshot") })
}
("power_and_voldown") {
# power (hold) -> voldown: open camera
return ("openCamera")
}
("power_then_volup") {
# power (tap) -> volup: rotate CCW
return ("rotateCCW")
}
("power_then_voldown") {
# power (tap) -> voldown: rotate CW
return ("rotateCW")
}
("volup_tap_1") { func Dispatcher_add_handler(self, trigger, response; screen=false, fullscreen=false, sys_menu=false, off=false) {
# volume up once: filesystem browser call self.handlers[trigger]->append(Object(Handler, {
return ("openFilebrowser") trigger: trigger,
} response: Responses[response],
("volup_hold_1") { name: response,
# volume up hold: browse files and apps screen: screen,
return ("openFilebrowserWithApps") fullscreen: fullscreen,
} sys_menu: sys_menu,
off: off,
("voldown_start") { }))
# volume down once: toggle keyboard
return ("toggleKeyboard")
}
("voldown_hold_1") {
# hold voldown to launch terminal
# note we already triggered the keyboard; that's fine: usually keyboard + terminal go together :)
return ("openTerminal")
}
("voldown_tap_1") {
# swallow, to prevent keyboard from also triggering media controls
return ("ignore")
}
/ ^ 'volup_hold_' d+ $ / {
# swallow, to prevent app launcher from also triggering media controls
return ("ignore")
}
/ ^ 'voldown_hold_' d+ $ / {
# swallow, to prevent terminal from also triggering media controls
return ("ignore")
}
}
} }
func mapFullscreen (action) { func Dispatcher_new() {
case (action) { var handlers = {}
("power_tap_1_hold") { for _k, v in (Triggers) {
# power tap->hold: kill active window setvar handlers[v] = []
return ("killWindow")
}
("power_then_volup") {
# power (tap) -> volup: rotate CCW
return ("rotateCCW")
}
("power_then_voldown") {
# power (tap) -> voldown: rotate CW
return ("rotateCW")
}
} }
return (Object(Dispatcher, { handlers: handlers }))
} }
func mapInhibited (action) { func Dispatcher_default() {
case (action) { var inst = Dispatcher.new()
("power_tap_1_hold") {
# power hold: escape hatch in case rofi has hung call inst->add_handler(Triggers.PowerHold, "togglePlayback", fullscreen=true, screen=true, off=true)
return ("killRofi") call inst->add_handler(Triggers.VolupHold1, "seekForward", fullscreen=true, off=true)
call inst->add_handler(Triggers.VolupHold2, "seekForward", fullscreen=true, off=true)
call inst->add_handler(Triggers.VolupHold3, "seekForward", fullscreen=true, off=true)
call inst->add_handler(Triggers.VoldownHold1, "seekBackward", fullscreen=true, off=true)
call inst->add_handler(Triggers.VoldownHold2, "seekBackward", fullscreen=true, off=true)
call inst->add_handler(Triggers.VoldownHold3, "seekBackward", fullscreen=true, off=true)
call inst->add_handler(Triggers.VolupTap1, "volumeUp", fullscreen=true, off=true)
call inst->add_handler(Triggers.VolupTap2, "volumeUp", fullscreen=true, off=true)
call inst->add_handler(Triggers.VolupTap3, "volumeUp", fullscreen=true, off=true)
call inst->add_handler(Triggers.VoldownTap1, "volumeDown", fullscreen=true, off=true)
call inst->add_handler(Triggers.VoldownTap2, "volumeDown", fullscreen=true, off=true)
call inst->add_handler(Triggers.VoldownTap3, "volumeDown", fullscreen=true, off=true)
call inst->add_handler(Triggers.PowerTap1, "allOn", off=true)
call inst->add_handler(Triggers.PowerTap1Hold, "restartBonsai", off=true)
call inst->add_handler(Triggers.PowerTap2, "allOff", fullscreen=true, screen=true, off=true)
call inst->add_handler(Triggers.PowerTap1Hold, "killWindow", fullscreen=true, screen=true)
call inst->add_handler(Triggers.PowerThenVolup, "rotateCCW", fullscreen=true, screen=true)
call inst->add_handler(Triggers.PowerThenVoldown, "rotateCW", fullscreen=true, screen=true)
call inst->add_handler(Triggers.PowerAndVolup, "takeScreenshot", screen=true)
call inst->add_handler(Triggers.PowerAndVoldown, "openCamera", screen=true)
call inst->add_handler(Triggers.VolupTap1, "openFilebrowser", screen=true)
call inst->add_handler(Triggers.VolupHold1, "openFilebrowserWithApps", screen=true)
call inst->add_handler(Triggers.VoldownStart, "toggleKeyboard", screen=true)
call inst->add_handler(Triggers.VoldownHold1, "openTerminal", screen=true)
call inst->add_handler(Triggers.PowerTap1Hold, "killRofi", sys_menu=true)
return (inst)
}
func Dispatcher_get(self, trigger) {
var candidates = self.handlers[trigger]
var applicable = []
for c in (candidates) {
# TODO: this logic can be optimized!
var match = false
if (isAllOn()) {
if (isFullscreen()) {
verbose "state = fullscreen"
setvar match = c.fullscreen
} elif (isInhibited()) {
verbose "state = inhibited"
setvar match = c.sys_menu
} else {
verbose "state = screen"
setvar match = c.screen
}
} else {
verbose "state = off"
setvar match = c.off
}
if (match) {
debug "Dispatcher.get: found applicable" (c)
call applicable->append(c)
}
}
case (len(applicable)) {
(0) {
debug "Dispatcher.get: no applicable candidates for trigger" (trigger)
return (null)
}
(1) {
var a = applicable[0]
verbose "Dispatcher.get: filtered to 1 candidate" (trigger, a)
return (a)
} }
(else) { (else) {
# eat everything else (and let rofi consume it) # TODO: this should be a static assertion, not a runtime check!
return ("inhibited") die "Dispatcher.get: filtered to multiple candidates" (trigger, applicable)
} }
} }
} }
func mapToplevel (action) { var Dispatcher = Object(null, {
var mappedTo = null ## class methods
if (isAllOn()) { default: Dispatcher_default,
if (isInhibited()) { new: Dispatcher_new,
setvar mappedTo = traceFunc("mapInhibited", mapInhibited, action) ## methods
} elif (isFullscreen()) { "M/add_handler": Dispatcher_add_handler,
setvar mappedTo = traceFunc("mapFullscreen", mapFullscreen, action) get: Dispatcher_get,
} else { ## instance state
setvar mappedTo = traceFunc("mapOn", mapOn, action) handlers: {} # trigger -> List[Handler]
} })
} else {
setvar mappedTo = traceFunc("mapOff", mapOff, action)
}
if (mappedTo === null) {
setvar mappedTo = traceFunc("mapDefault", mapDefault, action)
}
return (mappedTo) var trigger = null
}
var action = null
var doShowHelp = false var doShowHelp = false
proc parseArgs (; ...args) { proc parseArgs (; ...args) {
for arg in (args) { for arg in (args) {
@@ -555,7 +568,7 @@ proc parseArgs (; ...args) {
setglobal VERBOSITY += 1 setglobal VERBOSITY += 1
} }
(else) { (else) {
setglobal action = "$arg" setglobal trigger = "$arg"
} }
} }
} }
@@ -569,7 +582,10 @@ if is-main {
exit 0 exit 0
} }
var handler = mapToplevel (action) or "unmapped" var dispatcher = Dispatcher.default()
var handler = dispatcher.get(trigger)
handleWith (handler) info "handling" (trigger, handler and handler.name)
if (handler) {
call handler.exec()
}
} }