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:
@@ -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;
|
||||
|
||||
|
@@ -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
|
||||
|
120
hosts/common/programs/sane-input-handler/default.nix
Normal file
120
hosts/common/programs/sane-input-handler/default.nix
Normal 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
|
||||
'';
|
||||
}
|
228
hosts/common/programs/sane-input-handler/sane-input-handler
Executable file
228
hosts/common/programs/sane-input-handler/sane-input-handler
Executable 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
|
20
hosts/common/programs/wvkbd.nix
Normal file
20
hosts/common/programs/wvkbd.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user