#!/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"