#!/usr/bin/env nix-shell
#!nix-shell -i bash -p bash -p coreutils -p jq -p killall -p playerctl -p procps -p sane-open -p sway -p util-linux -p wireplumber
# vim: set filetype=bash :

# 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 tap       -> file browser
#   - volup hold      -> app menu
#   - voldown press   -> keyboard
#   - voldown hold    -> terminal
#   - power x2        -> screenoff
#   - power tap->hold -> kill app
#   - power,volup     -> screen rotate CCW
#   - power,voldown   -> screen rotate CW
#   - power+volup     -> screenshot
#   - power+voldown   -> camera
# - when locked:
#   - volup tap       -> volume up
#   - volup hold      -> media seek forward
#   - voldown tap     -> volume down
#   - voldown hold    -> media seek backward
#   - power tap       -> screen on
#   - power hold      -> play/pause media
# limitations/downsides:
# - voldown hold is over eager: easy to open terminals when phone is slow.
#   - remap to voldown tap->hold ?
#
# EXAMPLE EVENT FIRINGS:
# - double-tap voldown:
#   - voldown_start
#   - voldown_tap_1
#   - voldown_tap_2
# - hold voldown:
#   - voldown_start
#   - voldown_hold_1
#   - voldown_hold_2
#   - voldown_hold_3
# - hold power:
#   - power_hold  (notice: it doesn't fire power_start)
# - double-tap power:
#   - power_tap_1
#   - power_tap_2
# - power tap-then-hold:
#   - power_tap_1
#   - power_tap_1_hold



# increments to use for volume adjustment (in %)
VOL_INCR=5
KEYBOARD="${KEYBOARD:-wvkbd-mobintl}"
CAMERA="${CAMERA:-org.postmarketos.Megapixels.desktop}"

action="$1"

showHelp() {
  echo "usage: sane-input-handler <action>"
  echo ""
  echo "where action is one of:"
  echo "- power_tap_{1,2}"
  echo "- power_hold"
  echo "- power_tap_1_hold"
  echo "- power_and_volup"
  echo "- power_and_voldown"
  echo "- power_then_volup"
  echo "- power_then_voldown"
  echo "- volup_tap_{1,2,3}"
  echo "- volup_hold_{1,2,3}"
  echo "- voldown_tap_{1,2,3}"
  echo "- voldown_hold_{1,2,3}"
  echo "- voldown_start"
}

log() {
  printf "sane-input-handler: %s\n" "$1"
}

## HELPERS

# swaySetOutput true|false
# turns the display on or off
swaySetOutput() {
  swaymsg -- output '*' power "$1"
}
# swaySetTouch enabled|disabled
# turns touch input on or off
swaySetTouch() {
  # XXX(2024/06/09): `type:touch` method is documented, but now silently fails
  # swaymsg -- input type:touch events "$1"

  local inputs=$(swaymsg -t get_inputs --raw | jq '. | map(select(.type == "touch")) | map(.identifier) | join(" ")' --raw-output)
  for id in "${inputs[@]}"; do
    swaymsg -- input "$id" events "$1"
  done
}

# success if all touch inputs have their events enabled
swayGetTouch() {
  swaymsg -t get_inputs --raw \
    | jq --exit-status '. | map(select(.type == "touch")) | all(.libinput.send_events == "enabled")' \
    > /dev/null
}
# success if all outputs have power
swayGetOutput() {
  swaymsg -t get_outputs --raw \
    | jq --exit-status '. | all(.power)' \
    > /dev/null
}

isAllOn() {
  swayGetOutput && swayGetTouch
}

isInhibited() {
  pidof rofi
}

handleWith() {
  local state=
  if [ -n "$_isInhibited" ]; then
    state="inhibited+"
  fi
  if [ -n "$_isAllOn" ]; then
    state="${state}on"
  else
    state="${state}off"
  fi
  log "state=$state action=$action: handleWith: $*"
  "$@"
  exit $?
}


## HANDLERS
ignore() {
  true
}
inhibited() {
  true
}
unmapped() {
  true
}

allOn() {
  swaySetOutput true
  swaySetTouch enabled
}
allOff() {
  swaySetOutput false
  swaySetTouch disabled
}

toggleKeyboard() {
  local keyboardPid=$(pidof "$KEYBOARD")
  if [ -z "$keyboardPid" ]; then
    log "cannot find $KEYBOARD"
    return
  fi

  for p in $keyboardPid; do
    # `env` so that we get the right `kill` binary instead of bash's builtin
    # `kill` only one keyboard process. in the case of e.g. sandboxing,
    # the keyboard might consist of multiple processes and each one we signal would cause a toggle
    if env kill -s RTMIN+0 "$p"; then
      break
    fi
  done
}

## DISPATCHERS

dispatchDefault() {
  case "$action" in
    "power_tap_2")
      # power twice => screenoff
      handleWith allOff
      ;;
    "power_hold")
      # power twice => toggle media player
      handleWith playerctl play-pause
      ;;

    volup_tap*)
      handleWith wpctl set-volume @DEFAULT_AUDIO_SINK@ "$VOL_INCR"%+
      ;;
    voldown_tap*)
      handleWith wpctl set-volume @DEFAULT_AUDIO_SINK@ "$VOL_INCR"%-
      ;;
  esac
}

dispatchOff() {
  case "$action" in
    "power_tap_1")
      # power once => unlock
      handleWith allOn
      ;;
    "power_tap_1_hold")
      # power tap->hold: escape hatch for when bonsaid locks up
      handleWith systemctl restart bonsaid
      ;;
    volup_hold*)
      handleWith playerctl position 30+
      ;;
    voldown_hold*)
      handleWith playerctl position 10-
      ;;
  esac
}

dispatchOn() {
  case "$action" in
    # power_tap_1: intentional default to no-op (it's important this be unmapped, because events can be misordered with power_tap_1 arriving *after* power_tap_2)
    # power_tap_2: intentional default to screenoff
    "power_tap_1_hold")
      # power tap->hold: kill active window
      # TODO: disable this if locked (with e.g. schlock, swaylock, etc)
      handleWith swaymsg kill
      ;;
    "power_and_volup")
      # power (hold) -> volup: take screenshot
      handleWith sane-open --application sane-screenshot.desktop
      ;;
    "power_and_voldown")
      # power (hold) -> voldown: open camera
      handleWith sane-open --auto-keyboard --application "$CAMERA"
      ;;
    "power_then_volup")
      # power (tap) -> volup: rotate CCW
      handleWith swaymsg -- output '-' transform 90 anticlockwise
      ;;
    "power_then_voldown")
      # power (tap) -> voldown: rotate CW
      handleWith swaymsg -- output '-' transform 90 clockwise
      ;;

    "volup_tap_1")
      # volume up once: filesystem browser
      handleWith sane-open --auto-keyboard --application rofi-filebrowser.desktop
      ;;
    "volup_hold_1")
      # volume up hold: browse files and apps
      # reset fs directory: useful in case you get stuck in broken directory (e.g. one which lacks a `..` entry)
      rm -f ~/.cache/rofi/rofi3.filebrowsercache
      handleWith sane-open --auto-keyboard --application rofi.desktop
      ;;

    "voldown_start")
      # volume down once: toggle keyboard
      handleWith toggleKeyboard
      ;;
    "voldown_hold_1")
      # hold voldown to launch terminal
      # note we already triggered the keyboard; that's fine: usually keyboard + terminal go together :)
      handleWith sane-open --auto-keyboard --application 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
    "power_tap_1_hold")
      # power hold: escape hatch in case rofi has hung
      handleWith killall -9 rofi
      ;;
    *)
      # eat everything else (and let rofi consume it)
      handleWith inhibited
      ;;
  esac
}

dispatchToplevel() {
  _isAllOn="$(isAllOn && echo 1 || true)"

  if [ -z "$_isAllOn" ]; then
    dispatchOff
  else
    _isInhibited="$(isInhibited && echo 1 || true)"
    if [ -n "$_isInhibited" ]; then
      dispatchInhibited
    else
      dispatchOn
    fi
  fi

  dispatchDefault
}

case "$action" in
  (--help)
    showHelp
    exit 0
    ;;
  (*)
    dispatchToplevel
    handleWith unmapped
    ;;
esac

