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:
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user