port sxmo_hook_inputhandler.sh -> sane-input-handler

this one can run outside the SXMO environment.
major thing missing at the moment is that rofi doesn't get volume
control inputs because bonsai out-competes it for exclusive control.
This commit is contained in:
2024-02-29 01:26:38 +00:00
parent d8a8038cae
commit 6253d1799a
5 changed files with 376 additions and 0 deletions

View File

@@ -719,11 +719,17 @@ in
"/sys/kernel"
];
procps = {};
psmisc = {};
pstree.sandbox.method = "landlock";
pstree.sandbox.extraPaths = [
"/proc"
];
pulseaudio = {};
pulsemixer.sandbox.method = "landlock";
pulsemixer.sandbox.whitelistAudio = true;

View File

@@ -81,6 +81,7 @@
./rhythmbox.nix
./ripgrep.nix
./rofi
./sane-input-handler
./sane-scripts.nix
./sfeed.nix
./signal-desktop.nix
@@ -108,6 +109,7 @@
./wireplumber.nix
./wireshark.nix
./wob
./wvkbd.nix
./xarchiver.nix
./xdg-desktop-portal.nix
./xdg-desktop-portal-gtk.nix

View File

@@ -0,0 +1,120 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.sane-input-handler;
doExec = inputName: transitions: {
type = "exec";
command = [
"setsid"
"-f"
"sane-input-handler"
inputName
];
inherit transitions;
};
onDelay = ms: transitions: {
type = "delay";
delay_duration = ms * 1000000;
inherit transitions;
};
onEvent = eventName: transitions: {
type = "event";
event_name = eventName;
inherit transitions;
};
friendlyToBonsai = { trigger ? null, terminal ? false, timeout ? {}, power_pressed ? {}, power_released ? {}, voldown_pressed ? {}, voldown_released ? {}, volup_pressed ? {}, volup_released ? {} }@args:
if trigger != null then [
(doExec trigger (friendlyToBonsai (builtins.removeAttrs args ["trigger"])))
] else let
events = [ ]
++ (lib.optional (timeout != {}) (onDelay (timeout.ms or 400) (friendlyToBonsai (builtins.removeAttrs timeout ["ms"]))))
++ (lib.optional (power_pressed != {}) (onEvent "power_pressed" (friendlyToBonsai power_pressed)))
++ (lib.optional (power_released != {}) (onEvent "power_released" (friendlyToBonsai power_released)))
++ (lib.optional (voldown_pressed != {}) (onEvent "voldown_pressed" (friendlyToBonsai voldown_pressed)))
++ (lib.optional (voldown_released != {}) (onEvent "voldown_released" (friendlyToBonsai voldown_released)))
++ (lib.optional (volup_pressed != {}) (onEvent "volup_pressed" (friendlyToBonsai volup_pressed)))
++ (lib.optional (volup_released != {}) (onEvent "volup_released" (friendlyToBonsai volup_released)))
;
in assert terminal -> events == []; events;
# trigger ${button}_hold_N every `holdTime` ms until ${button} is released
recurseHold = button: { count ? 1, maxHolds ? 5, prefix ? "", holdTime ? 600, ... }@opts: lib.optionalAttrs (count <= maxHolds) {
"${button}_released".terminal = true; # end the hold -> back to root state
timeout = {
ms = holdTime;
trigger = "${prefix}${button}_hold_${builtins.toString count}";
} // (recurseHold button (opts // { count = count+1; }));
};
# trigger volup_tap_N or voldown_tap_N on every tap.
# if a volume button is held, then switch into `recurseHold`'s handling instead
volumeActions = { count ? 1, maxTaps ? 5, prefix ? "", timeout ? 600, ... }@opts: lib.optionalAttrs (count != maxTaps) {
volup_pressed = (recurseHold "volup" opts) // {
volup_released = {
trigger = "${prefix}volup_tap_${builtins.toString count}";
timeout.ms = timeout;
} // (volumeActions (opts // { count = count+1; }));
};
voldown_pressed = (recurseHold "voldown" opts) // {
voldown_released = {
trigger = "${prefix}voldown_tap_${builtins.toString count}";
timeout.ms = timeout;
} // (volumeActions (opts // { count = count+1; }));
};
};
in
{
sane.programs.sane-input-handler = {
packageUnwrapped = pkgs.static-nix-shell.mkBash {
pname = "sane-input-handler";
srcRoot = ./.;
pkgs = {
inherit (pkgs) coreutils playerctl procps psmisc pulseaudio util-linux wvkbd;
sway = config.sane.programs.sway.package.sway-unwrapped;
};
};
suggestedPrograms = [ "bonsai" "playerctl" "procps" "psmisc" "pulseaudio" "sway" "wvkbd" ];
};
sane.programs.bonsai.config.transitions = lib.mkIf cfg.enabled (friendlyToBonsai {
# map sequences of "events" to an argument to pass to sane-input-handler
# map: power (short), power (short) x2, power (long)
power_pressed.timeout.ms = 900; # press w/o release. this is a long timeout because it's tied to the "kill window" action.
power_pressed.timeout.trigger = "powerhold";
power_pressed.power_released.timeout.trigger = "powerbutton_one";
power_pressed.power_released.timeout.ms = 300;
power_pressed.power_released.power_pressed.trigger = "powerbutton_two";
# map: volume taps and holds
volup_pressed = (recurseHold "volup" {}) // {
# this either becomes volup_hold_* (via recurseHold, above) or:
# - a short volup_tap_1 followed by:
# - a *finalized* volup_1 (i.e. end of action)
# - more taps/holds, in which case we prefix it with `modal_<action>`
# to denote that we very explicitly entered this state.
#
# it's clunky: i do it this way so that voldown can map to keyboard/terminal in unlock mode
# but trigger media controls in screenoff
# in a way which *still* allows media controls if explicitly entered into via a tap on volup first
volup_released = (volumeActions { prefix = "modal_"; }) // {
trigger = "volup_tap_1";
timeout.ms = 300;
timeout.trigger = "volup_1";
};
};
voldown_pressed = (volumeActions {}).voldown_pressed // {
trigger = "voldown_start";
};
});
sane.programs.sway.config.extra_lines = lib.mkIf cfg.enabled ''
# TODO: `bindsym` at this level CONSUMES the event globally;
# need some way to allow rofi to see these events when it is in focus
bindsym --locked --no-repeat XF86PowerOff exec bonsaictl -e power_pressed
bindsym --locked --release XF86PowerOff exec bonsaictl -e power_released
bindsym --locked --no-repeat XF86AudioRaiseVolume exec bonsaictl -e volup_pressed
bindsym --locked --release XF86AudioRaiseVolume exec bonsaictl -e volup_released
bindsym --locked --no-repeat XF86AudioLowerVolume exec bonsaictl -e voldown_pressed
bindsym --locked --release XF86AudioLowerVolume exec bonsaictl -e voldown_released
'';
}

View File

@@ -0,0 +1,228 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p coreutils -p playerctl -p procps -p psmisc -p pulseaudio -p sway -p util-linux -p wvkbd
# input map considerations
# - using compound actions causes delays.
# e.g. if volup->volup is a distinct action from volup, then single-volup action is forced to wait the maximum button delay.
# - actions which are to be responsive should therefore have a dedicated key.
# - a dedicated "kill" combo is important for unresponsive fullscreen apps, because appmenu doesn't show in those
# - although better may be to force appmenu to show over FS apps
# - bonsai mappings are static, so buttons can't benefit from non-compounding unless they're mapped accordingly for all lock states
# - this limitation could be removed, but with work
#
# example of a design which considers these things:
# - when unlocked:
# - volup toggle -> app menu
# - voldown press -> keyboard
# - voldown hold -> terminal
# - power x2 -> screenoff
# - hold power -> kill app
# - when locked:
# - volup tap -> volume up
# - volup hold -> media seek forward
# - voldown tap -> volume down
# - voldown hold -> media seek backward
# - power x1 -> screen on
# - power x2 -> play/pause media
# some trickiness allows for media controls in unlocked mode:
# - volup tap -> enter media mode
# - i.e. in this state, vol tap/hold is mapped to volume/seek
# - if, after entering media mode, no more taps occur, then we trigger the default app-menu action
# limitations/downsides:
# - power mappings means phone is artificially slow to unlock.
# - media controls when unlocked have quirks:
# - mashing voldown to decrease the volume will leave you with a toggled keyboard.
# - seeking backward isn't possible except by first tapping volup.
# increments to use for volume adjustment
VOL_INCR=5
KEYBOARD="${KEYBOARD:-wvkbd-mobintl}"
action="$1"
isTouchOn() {
# success if all touch inputs have their events enabled
swaymsg -t get_inputs --raw \
| jq --exit-status '. | map(select(.type == "touch")) | all(.libinput.send_events == "enabled")' \
> /dev/null
}
isScreenOn() {
# success if all outputs have power
swaymsg -t get_outputs --raw \
| jq --exit-status '. | all(.power)' \
> /dev/null
}
isAllOn() {
isTouchOn && isScreenOn
}
isInhibited() {
pidof rofi
}
ignore() {
true
}
inhibited() {
true
}
unmapped() {
true
}
openDesktop() {
# open a .desktop file via the xdg-desktop-portal
gdbus call --session --timeout 10 \
--dest org.freedesktop.portal.Desktop \
--object-path /org/freedesktop/portal/desktop \
--method org.freedesktop.portal.DynamicLauncher.Launch \
"$1" {}
}
allOn() {
swaymsg -- output '*' power true
swaymsg -- input type:touch events enabled
}
allOff() {
swaymsg -- output '*' power false
swaymsg -- input type:touch events disabled
}
toggleKeyboard() {
local kbpid=$(pidof "$KEYBOARD")
if [ -z "$kbpid" ] || ! ( env kill -s RTMIN+0 "$kbpid" ); then
echo "sane-input-handler: failed to toggle keyboard; launching manually: $KEYBOARD"
# manually launch the keyboard, as a fallback
"$KEYBOARD" &
fi
}
handleWith() {
state=
if [ -n "$_isInhibited" ]; then
state="inhibited+"
fi
if [ -n "$_isAllOn" ]; then
state="${state}on"
else
state="${state}off"
fi
echo "sane-input-handler: state=$state action=$action: handleWith: $@"
"$@"
exit 0
}
dispatchDefault() {
case "$action" in
"powerbutton_one")
# power once => unlock
handleWith allOn
;;
"powerbutton_two")
# power twice => screenoff
handleWith allOff
;;
# powerbutton_three: intentional no-op because overloading the kill-window handler is risky
volup_tap*|modal_volup_tap*)
handleWith pactl set-sink-volume @DEFAULT_SINK@ +"$VOL_INCR%"
;;
voldown_tap*|modal_voldown_tap*)
handleWith pactl set-sink-volume @DEFAULT_SINK@ -"$VOL_INCR%"
;;
volup_hold*|modal_volup_hold*)
handleWith playerctl position 30+
;;
voldown_hold*|modal_voldown_hold*)
handleWith playerctl position 10-
;;
esac
}
dispatchOff() {
case "$action" in
"powerbutton_two")
# power twice => toggle media player
handleWith playerctl play-pause
;;
"powerhold")
# power toggle during deep sleep often gets misread as power hold, so treat same
handleWith allOn
;;
esac
}
dispatchOn() {
case "$action" in
# powerbutton_one: intentional default to no-op
# powerbutton_two: intentional default to screenoff
"powerhold")
# power thrice: kill active window
handleWith swaymsg kill
;;
"volup_tap_1")
# swallow: this could be the start to a media control (multi taps / holds),
# or it could be just a single tap -> release, handled next/below
handleWith ignore
;;
"volup_1")
# volume up once: system menu
handleWith openDesktop rofi.desktop
;;
"voldown_start")
# volume down once: toggle keyboard
handleWith toggleKeyboard
;;
"voldown_hold_2")
# hold voldown to launch terminal
# note we already triggered the keyboard; that's fine: usually keyboard + terminal go together :)
# voldown_hold_1 frequently triggers during short taps meant only to reveal the keyboard,
# so prefer a longer hold duration
handleWith openDesktop xdg-terminal-exec.desktop
;;
"voldown_tap_1")
# swallow, to prevent keyboard from also triggering media controls
handleWith ignore
;;
voldown_hold_*)
# swallow, to prevent terminal from also triggering media controls
handleWith ignore
;;
esac
}
dispatchInhibited() {
case "$action" in
"powerhold")
# power thrice: escape hatch in case rofi has hung
handleWith killall -9 rofi
;;
*)
# eat everything else (and let rofi consume it)
handleWith inhibited
;;
esac
}
_isAllOn="$(isAllOn && echo 1 || true)"
_isInhibited="$(isInhibited && echo 1 || true)"
if [ -n "$_isInhibited" ]; then
dispatchInhibited
fi
if [ -n "$_isAllOn" ]; then
dispatchOn
else
dispatchOff
fi
dispatchDefault
handleWith unmapped

View File

@@ -0,0 +1,20 @@
{ config, ... }:
let
cfg = config.sane.programs.wvkbd;
in
{
sane.programs.wvkbd = {
services.wvkbd = {
description = "wvkbd: wayland virtual keyboard";
after = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
# --hidden: send SIGUSR2 to unhide
Execstart = "${cfg.package}/bin/wvkbd-mobintl --hidden";
Type = "simple";
Restart = "always";
RestartSec = "3s";
};
};
};
}