diff --git a/home/fuji/att_menu.nix b/home/fuji/att_menu.nix new file mode 100644 index 0000000..6ee5cad --- /dev/null +++ b/home/fuji/att_menu.nix @@ -0,0 +1,67 @@ +{ pkgs +, lib +, inputs +, system +, ... +}: +let + att_menu = inputs.att_menu.packages.${system}.default; + att_wm = inputs.att_wm.packages.${system}.default; + + attwmctl = pkgs.writeShellScriptBin "attwmctl" '' + exec ${att_wm}/bin/att_wmctl "$@" + ''; + + tablet_mode = + pkgs.writeShellScriptBin "tablet-mode" + (builtins.readFile ./tablet-mode.sh); +in +{ + home.packages = [ + att_menu + attwmctl + tablet_mode + ]; + + systemd.user.services = { + att_menu = { + Unit = { + Description = "att_menu touch panel (tablet mode)"; + PartOf = [ "graphical-session.target" ]; + After = [ "graphical-session.target" ]; + StartLimitIntervalSec = 0; + }; + Service = { + ExecStart = lib.getExe att_menu; + Restart = "on-failure"; + RestartSec = 3; + }; + }; + + tablet-mode = { + Unit = { + Description = "Run the tablet-only services while the machine is folded"; + PartOf = [ "graphical-session.target" ]; + After = [ "graphical-session.target" ]; + }; + Service = { + Environment = [ + "PATH=${lib.makeBinPath [ + pkgs.evtest + pkgs.coreutils + pkgs.systemd + pkgs.bash + ]}" + ]; + ExecStart = '' + ${tablet_mode}/bin/tablet-mode \ + --on 'systemctl --user start att_menu.service squeekboard.service' \ + --off 'systemctl --user stop att_menu.service squeekboard.service' + ''; + Restart = "always"; + RestartSec = 5; + }; + Install.WantedBy = [ "graphical-session.target" ]; + }; + }; +} diff --git a/home/fuji/auto-rotate.sh b/home/fuji/auto-rotate.sh new file mode 100755 index 0000000..c195965 --- /dev/null +++ b/home/fuji/auto-rotate.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# auto-rotate — rotate a wlroots output from iio-sensor-proxy orientation events. +# +# Unlike rot8 this does not read the accelerometer itself. iio-sensor-proxy owns +# sensor selection (fuji has two accel_3d devices), applies its own hysteresis, +# and reports `undefined` when the device is too flat to have a meaningful +# orientation — so there is no dead zone in which we silently keep a stale state. +# +# SIGUSR1 toggle rotation lock (current transform is kept) +# SIGUSR2 re-apply the last known orientation (e.g. after kanshi resets it) + +set -uo pipefail + +OUTPUT="${AUTOROTATE_OUTPUT:-eDP-1}" +HOOKS=() +VERBOSE=0 + +usage() { + cat <<-EOF + usage: auto-rotate [-o OUTPUT] [-H HOOK]... [-v] + + -o, --output OUTPUT output to rotate (default: ${OUTPUT}) + -H, --hook CMD run CMD after each rotation; may be repeated. + \$ORIENTATION and \$PREV_ORIENTATION are exported. + -v, --verbose log every sensor event, not just rotations + EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + -o | --output) + OUTPUT="${2:?--output needs a value}" + shift 2 + ;; + -H | --hook) + HOOKS+=("${2:?--hook needs a value}") + shift 2 + ;; + -v | --verbose) + VERBOSE=1 + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + printf 'auto-rotate: unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +for cmd in monitor-sensor wlr-randr; do + command -v "$cmd" >/dev/null || { + printf 'auto-rotate: %s not found in PATH\n' "$cmd" >&2 + exit 1 + } +done + +log() { printf '[auto-rotate] %s\n' "$*"; } +vlog() { [ "$VERBOSE" = 1 ] && log "$@"; return 0; } + +# iio-sensor-proxy orientation -> wlr-randr transform. +# +# NOTE: if the screen ends up 180 degrees off, swap the left-up and right-up +# lines. This mapping is the common convention but it is not something the +# sensor tells us, so it has to be confirmed by hand once. +transform_for() { + case "$1" in + normal) printf 'normal' ;; + left-up) printf '90' ;; + bottom-up) printf '180' ;; + right-up) printf '270' ;; + *) return 1 ;; + esac +} + +current_transform() { + wlr-randr 2>/dev/null | + awk -v o="$OUTPUT" ' + $1 == o { found = 1; next } + /^[^ \t]/ { found = 0 } + found && $1 == "Transform:" { print $2; exit } + ' +} + +applied="$(current_transform)" +[ -n "$applied" ] || { + printf 'auto-rotate: output %s not found\n' "$OUTPUT" >&2 + exit 1 +} +log "output ${OUTPUT}, current transform ${applied}" + +orientation="" # last non-undefined orientation from the sensor +prev_orientation="" # the one before that, exported to hooks +locked=0 + +toggle_lock() { + locked=$((1 - locked)) + [ "$locked" = 1 ] && log "rotation locked at ${applied}" || log "rotation unlocked" +} + +apply() { + local target + target="$(transform_for "$orientation")" || return 0 + [ "$target" = "$applied" ] && return 0 + + if ! wlr-randr --output "$OUTPUT" --transform "$target"; then + # Do not advance `applied` — a failed rotation must not desync our idea + # of the output state from reality. This was rot8's other failure mode. + log "wlr-randr failed, keeping transform as ${applied}" + return 0 + fi + log "${applied} -> ${target} (${orientation})" + applied="$target" + + local hook + for hook in ${HOOKS+"${HOOKS[@]}"}; do + ORIENTATION="$orientation" PREV_ORIENTATION="$prev_orientation" \ + bash -c "$hook" || log "hook failed: ${hook}" + done +} + +reapply() { + applied="$(current_transform)" + log "re-syncing, output is at ${applied}" + [ "$locked" = 0 ] && [ -n "$orientation" ] && apply + return 0 +} + +trap toggle_lock USR1 +trap reapply USR2 + +exec 3< <(stdbuf -oL monitor-sensor 2>&1) +monitor_pid=$! +trap 'kill "$monitor_pid" 2>/dev/null; exit 0' INT TERM EXIT + +while :; do + # A signal interrupts read; that is not an error, so retry rather than exit. + IFS= read -r line <&3 || { + kill -0 "$monitor_pid" 2>/dev/null && continue + break + } + + case "$line" in + *"Accelerometer orientation changed: "*) + new="${line##*: }" + ;; + *"Has accelerometer (orientation: "*) + # The initial state only ever arrives on this banner line; a + # "changed" line is not emitted until the orientation moves. + new="${line#*orientation: }" + new="${new%%,*}" + ;; + *"iio-sensor-proxy appeared"*) + # The proxy restarted; our cached transform may be stale. + reapply + continue + ;; + *"Compass heading changed"*) + # ~1 Hz of noise we have no use for; drop it even when verbose. + continue + ;; + *) + vlog "$line" + continue + ;; + esac + vlog "sensor says ${new}" + + # `undefined` means the device is lying too flat to tell. Hold position. + [ "$new" = undefined ] && continue + [ "$new" = "$orientation" ] && continue + + prev_orientation="$orientation" + orientation="$new" + + [ "$locked" = 1 ] && { + vlog "locked, ignoring ${orientation}" + continue + } + apply +done + +log "monitor-sensor exited" diff --git a/home/fuji/tablet-mode.sh b/home/fuji/tablet-mode.sh new file mode 100644 index 0000000..8d995ef --- /dev/null +++ b/home/fuji/tablet-mode.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# tablet-mode — run commands as the machine is folded into, and out of, a tablet. +# +# sway has `bindswitch tablet:on`, but att_wm has none and river's init is a +# startup script rather than an IPC surface, so there is nothing to bind there. +# Reading SW_TABLET_MODE off evdev is the one mechanism that works in both +# sessions, which is why it is the only one used here — sway's bindswitch is +# left gating rotation and nothing else. +# +# evtest does the reading: without --grab it only listens, so the compositor +# still gets the switch, and its opening dump reports the *current* state. That +# last part is the reason it is evtest and not `libinput debug-events` — a +# session that starts already folded has no transition to react to. + +set -uo pipefail + +DEVICE="${TABLET_MODE_DEVICE:-}" +ON_CMD="" +OFF_CMD="" +VERBOSE=0 + +usage() { + cat <<-EOF + usage: tablet-mode [-d DEVICE] [--on CMD] [--off CMD] [-v] + + -d, --device DEVICE evdev node to watch (default: found by capability) + --on CMD run CMD when the machine enters tablet mode + --off CMD run CMD when it leaves + -v, --verbose log every line evtest prints + + Both commands run through \`bash -c\` with \$TABLET_MODE set to 1 or 0. + EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + -d | --device) + DEVICE="${2:?--device needs a value}" + shift 2 + ;; + --on) + ON_CMD="${2:?--on needs a value}" + shift 2 + ;; + --off) + OFF_CMD="${2:?--off needs a value}" + shift 2 + ;; + -v | --verbose) + VERBOSE=1 + shift + ;; + -h | --help) + usage + exit 0 + ;; + *) + printf 'tablet-mode: unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +command -v evtest >/dev/null || { + printf 'tablet-mode: evtest not found in PATH\n' >&2 + exit 1 +} + +log() { printf '[tablet-mode] %s\n' "$*"; } +vlog() { + [ "$VERBOSE" = 1 ] && log "$@" + return 0 +} + +# Find the switch by capability rather than by name or by-path: on fuji it is an +# intel-hid device that is registered after boot, so neither its event number +# nor its presence at our start time can be counted on. Not finding it is a +# non-zero exit, which is what makes systemd try us again a few seconds later. +# +# In /proc/bus/input/devices each device ends with its capability bitmaps, and +# SW_TABLET_MODE is bit 1 of the switch one. `H:` precedes `B:` in every record, +# so the handler list is always already in hand by the time the bitmap arrives. +find_device() { + local line handlers="" bitmap handler + while IFS= read -r line; do + case "$line" in + "H: Handlers="*) + handlers="${line#H: Handlers=}" + ;; + "B: SW="*) + # A bitmap is hex words, most significant first; bit 1 + # lives in the last of them. + bitmap="${line#B: SW=}" + bitmap="${bitmap##* }" + [ -n "$bitmap" ] || continue + (((0x$bitmap & 2) == 0)) && continue + for handler in $handlers; do + case "$handler" in + event*) + printf '/dev/input/%s' "$handler" + return 0 + ;; + esac + done + ;; + esac + done &2 + exit 1 + } +fi +[ -r "$DEVICE" ] || { + printf 'tablet-mode: cannot read %s\n' "$DEVICE" >&2 + exit 1 +} +log "watching ${DEVICE}" + +state="" # last seen switch state, "" until evtest reports the first one + +apply() { + local cmd + case "$1" in + 1) cmd="$ON_CMD" ;; + 0) cmd="$OFF_CMD" ;; + *) return 0 ;; + esac + [ -n "$cmd" ] || return 0 + TABLET_MODE="$1" bash -c "$cmd" || log "hook failed: ${cmd}" +} + +exec 3< <(stdbuf -oL evtest "$DEVICE" 2>&1) +evtest_pid=$! +# Only kill the child here — an `exit` in an EXIT trap would overwrite the +# status below, and systemd needs to see a failure to restart us. +trap 'kill "$evtest_pid" 2>/dev/null' EXIT +trap 'exit 0' INT TERM + +while IFS= read -r line <&3; do + case "$line" in + # A transition: + # Event: time ..., type 5 (EV_SW), code 1 (SW_TABLET_MODE), value 1 + *"(SW_TABLET_MODE), value "*) + new="${line##*, value }" + ;; + # The opening dump, which is where the state at startup comes from: + # Event code 1 (SW_TABLET_MODE) state 0 + *"(SW_TABLET_MODE) state "*) + new="${line##* state }" + ;; + *) + vlog "$line" + continue + ;; + esac + + [ "$new" = "$state" ] && continue + state="$new" + [ "$state" = 1 ] && log "tablet mode on" || log "tablet mode off" + apply "$state" +done + +log "evtest exited" +exit 1