#!/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" if [ "$state" = 1 ]; then log "tablet mode on"; else log "tablet mode off"; fi apply "$state" done log "evtest exited" exit 1