diff --git a/hosts/common/programs/sane-input-handler/sane-input-handler b/hosts/common/programs/sane-input-handler/sane-input-handler index 18813d17e..be534b4a8 100755 --- a/hosts/common/programs/sane-input-handler/sane-input-handler +++ b/hosts/common/programs/sane-input-handler/sane-input-handler @@ -57,11 +57,38 @@ # increments to use for volume adjustment (in %) var VOL_INCR = 5 +var SEEK_INCR = 30 +var SEEK_DECR = 10 var KEYBOARD = "${KEYBOARD:-wvkbd-mobintl}" var CAMERA = "${CAMERA:-org.postmarketos.Megapixels.desktop}" var VERBOSITY = 0 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 { echo "usage: sane-input-handler [--verbose [--verbose]] [--dry-run] " echo "" @@ -81,9 +108,21 @@ proc showHelp { } proc log (; ...stmts; level, context) { - var prefix = " $context:" if context else "" - var formatted = "$(pp value (stmts))" - echo "[$level] sane-input-handler:$prefix $formatted" >&2 + var prefix = ""; + var formatted = ""; + 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) { @@ -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 # swaySetOutput true|false @@ -271,25 +303,6 @@ func isFullscreen () { 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 proc ignore { : @@ -297,9 +310,6 @@ proc ignore { proc inhibited { : } -proc unmapped { - : -} proc allOn { swaySetOutput true @@ -311,9 +321,9 @@ proc allOff { } proc toggleKeyboard { - var keyboardPids = $(pidof "$KEYBOARD") => split(" ") + var keyboardPids = $(pidof "$KEYBOARD" || echo "") => split(" ") if (not keyboardPids) { - log ("cannot find $KEYBOARD") + info "toggleKeyboard: cannot find keyboard" (KEYBOARD) return } @@ -344,11 +354,11 @@ proc restartBonsai { } proc seekForward { - effect playerctl position 30+ + effect playerctl position "$SEEK_INCR"+ } proc seekBackward { - effect playerctl position 10- + effect playerctl position "$SEEK_DECR"- } proc killWindow { @@ -389,158 +399,161 @@ proc killRofi { effect killall -9 rofi } -## DISPATCHERS - -func mapDefault (action) { - case (action) { - ("power_tap_2") { - # power twice => screenoff - return ("allOff") - } - ("power_hold") { - # power twice => toggle media player - return ("togglePlayback") - } - / ^ 'volup_hold_' d+ $ / { - return ("seekForward") - } - / ^ 'voldown_hold_' d+ $ / { - return ("seekBackward") - } - - / ^ 'volup_tap_' d+ $ / { - return ("volumeUp") - } - / ^ 'voldown_tap_' d+ $ / { - return ("volumeDown") - } - } +# index of all possible responses, to allow lookup by-name +var Responses = { + allOn: allOn, + allOff: allOff, + toggleKeyboard: toggleKeyboard, + togglePlayback: togglePlayback, + volumeUp: volumeUp, + volumeDown: volumeDown, + restartBonsai: restartBonsai, + seekForward: seekForward, + seekBackward: seekBackward, + killWindow: killWindow, + takeScreenshot: takeScreenshot, + openCamera: openCamera, + rotateCCW: rotateCCW, + rotateCW: rotateCW, + openFilebrowser: openFilebrowser, + openFilebrowserWithApps: openFilebrowserWithApps, + openTerminal: openTerminal, + killRofi: killRofi, } -func mapOff (action) { - case (action) { - ("power_tap_1") { - # power once => unlock - return ("allOn") - } - ("power_tap_1_hold") { - # power tap->hold: escape hatch for when bonsaid locks up - return ("restartBonsai") - } - } +func Handler_exec(self) { + var resp = self.response + resp #< this executes the response } -func mapOn (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->hold: kill active window - # TODO: disable this if locked (with e.g. schlock, swaylock, etc) - return ("killWindow") - } - ("power_and_volup") { - # power (hold) -> volup: take screenshot - 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") - } +var Handler = Object(null, { + # methods + exec: Handler_exec, + # instance state + trigger: null, #< name of action the _user_ performed + response: null, #< bound proc to invoke in response to the trigger + name: null, #< friendly name of response; should just be the stringified form of `response` + screen: false, #< screen must be on for this response to trigger + fullscreen: false, #< desktop view must be fullscreen for this response to trigger + sys_menu: false, #< system menu must be active for this response to trigger + off: false, #< screen must be off for this response to trigger +}) - ("volup_tap_1") { - # volume up once: filesystem browser - return ("openFilebrowser") - } - ("volup_hold_1") { - # volume up hold: browse files and apps - return ("openFilebrowserWithApps") - } - - ("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 Dispatcher_add_handler(self, trigger, response; screen=false, fullscreen=false, sys_menu=false, off=false) { + call self.handlers[trigger]->append(Object(Handler, { + trigger: trigger, + response: Responses[response], + name: response, + screen: screen, + fullscreen: fullscreen, + sys_menu: sys_menu, + off: off, + })) } -func mapFullscreen (action) { - case (action) { - ("power_tap_1_hold") { - # power tap->hold: kill active window - return ("killWindow") - } - ("power_then_volup") { - # power (tap) -> volup: rotate CCW - return ("rotateCCW") - } - ("power_then_voldown") { - # power (tap) -> voldown: rotate CW - return ("rotateCW") - } +func Dispatcher_new() { + var handlers = {} + for _k, v in (Triggers) { + setvar handlers[v] = [] } + return (Object(Dispatcher, { handlers: handlers })) } -func mapInhibited (action) { - case (action) { - ("power_tap_1_hold") { - # power hold: escape hatch in case rofi has hung - return ("killRofi") +func Dispatcher_default() { + var inst = Dispatcher.new() + + call inst->add_handler(Triggers.PowerHold, "togglePlayback", fullscreen=true, screen=true, off=true) + 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) { - # eat everything else (and let rofi consume it) - return ("inhibited") + # TODO: this should be a static assertion, not a runtime check! + die "Dispatcher.get: filtered to multiple candidates" (trigger, applicable) } } } -func mapToplevel (action) { - var mappedTo = null - if (isAllOn()) { - if (isInhibited()) { - setvar mappedTo = traceFunc("mapInhibited", mapInhibited, action) - } elif (isFullscreen()) { - setvar mappedTo = traceFunc("mapFullscreen", mapFullscreen, action) - } else { - setvar mappedTo = traceFunc("mapOn", mapOn, action) - } - } else { - setvar mappedTo = traceFunc("mapOff", mapOff, action) - } +var Dispatcher = Object(null, { + ## class methods + default: Dispatcher_default, + new: Dispatcher_new, + ## methods + "M/add_handler": Dispatcher_add_handler, + get: Dispatcher_get, + ## instance state + handlers: {} # trigger -> List[Handler] +}) - if (mappedTo === null) { - setvar mappedTo = traceFunc("mapDefault", mapDefault, action) - } - return (mappedTo) -} - -var action = null +var trigger = null var doShowHelp = false proc parseArgs (; ...args) { for arg in (args) { @@ -555,7 +568,7 @@ proc parseArgs (; ...args) { setglobal VERBOSITY += 1 } (else) { - setglobal action = "$arg" + setglobal trigger = "$arg" } } } @@ -569,7 +582,10 @@ if is-main { exit 0 } - var handler = mapToplevel (action) or "unmapped" - - handleWith (handler) + var dispatcher = Dispatcher.default() + var handler = dispatcher.get(trigger) + info "handling" (trigger, handler and handler.name) + if (handler) { + call handler.exec() + } }