#!/bin/sh


# Zerocat Coreboot Machines --- Create very satisfying free software devices.
#
# Copyright (C) 2020, 2021, 2022  Kai Mertens <kmx@posteo.net>
#
# This file is part of Zerocat Coreboot Machines.
#
# Zerocat Coreboot Machines is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Zerocat Coreboot Machines is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Zerocat Coreboot Machines.  If not, see <http://www.gnu.org/licenses/>.


# Purpose
# =======
#
# Control Wacom Tablet Devices


# Usage
# =====
#
#     ./wacom-tool.sh --usage


# Devices Names
# =============
#
# Screen = Display [+ Tablet]
# Tablet = Pen_stylus + Pen_eraser [+ Finger_touch]


# Rotation
# ========
#
# The script specifies rotations by numbers:
#
# rotation | script | xinput | xrandr    | xsetwacom
# ---------|--------|--------|-----------|----------
# 0°       | 0      | 0      | normal    | none
# 90°      | 1      | 1      | right     | cw
# 180°     | 2      | 3      | inverted  | half
# 270°     | 3      | 2      | left      | ccw


# check interpreter
if [ -z $BASH_VERSION ]; then
  echo "Wrong interpreter, please use bash (https://www.gnu.org/software/bash)."
  exit 1
fi

# $1: rotation value, script style
# $2: rotation value, xinput style (name reference)
_rot_script2xinput() {
  declare -n rot_xinput=$2
  case "$1" in
    ('0')
      rot_xinput='0'
      ;;
    ('1')
      rot_xinput='1'
      ;;
    ('2')
      rot_xinput='3'
      ;;
    ('3')
      rot_xinput='2'
      ;;
    (*)
      go_exit "internal error: $FUNCNAME()"
      ;;
  esac
}

# $1: rotation value, script style
# $2: rotation value, xrandr style (name reference)
_rot_script2xrandr() {
  declare -n rot_xrandr=$2
  case "$1" in
    ('0')
      rot_xrandr='normal'
      ;;
    ('1')
      rot_xrandr='right'
      ;;
    ('2')
      rot_xrandr='inverted'
      ;;
    ('3')
      rot_xrandr='left'
      ;;
    (*)
      go_exit "internal error: $FUNCNAME()"
      ;;
  esac
}

# $1: rotation value, script style
# $2: rotation value, xsetwacom style (name reference)
_rot_script2xsetwacom() {
  declare -n rot_xsetwacom=$2
  case "$1" in
    ('0')
      rot_xsetwacom='none'
      ;;
    ('1')
      rot_xsetwacom='cw'
      ;;
    ('2')
      rot_xsetwacom='half'
      ;;
    ('3')
      rot_xsetwacom='ccw'
      ;;
    (*)
      go_exit "internal error: $FUNCNAME()"
      ;;
  esac
}

# $1: rotation value, xrandr style
# $2: rotation value, script style (name reference)
_rot_xrandr2script() {
  declare -n rot_script=$2
  case "${1,,}" in
    'normal')
      rot_script='0'
      ;;
    'right')
      rot_script='1'
      ;;
    'inverted')
      rot_script='2'
      ;;
    'left')
      rot_script='3'
      ;;
    *)
      go_exit "internal error: $FUNCNAME()"
      ;;
  esac
  return
}

# $1: X device ID
# $2: X device name (name reference)
_id2name() {
  declare -n name=$2
  case "$1" in
    ("$id_pen_stylus")
      name="$pen_stylus"
    ;;
    ("$id_pen_eraser")
      name="$pen_eraser"
    ;;
    ("$id_finger_touch")
      name="$finger_touch"
    ;;
    (*)
      go_exit "internal error: $FUNCNAME()"
      ;;
  esac
  return
}

# $1: retrieved display rotation, script style (name reference)
retrieve_display_rotation() {
  declare -n rot_display=$1
  declare rot_xrandr=
  declare rot_script=

  rot_xrandr=$(\
$BIN_XRANDR -q --verbose \
| $BIN_EGREP "LVDS. connected" \
| $BIN_SED -r -e 's#(^.*)\) (.*) \((.*$)#\2#;' -\
)

  _rot_xrandr2script "$rot_xrandr" rot_script
  rot_display="$rot_script"
  return
}

rotate_screen_cw() {
  echo "$BASH_SOURCE: $FUNCNAME(): rotate screen clock-wise ..."
  declare rot_display=
  retrieve_display_rotation rot_display
  case "$rot_display" in
    ('0')
      rot_display='1'
      ;;
    ('1')
      rot_display='2'
      ;;
    ('2')
      rot_display='3'
      ;;
    ('3')
      rot_display='0'
      ;;
    (*)
      go_exit "internal error: $FUNCNAME()"
  esac
  set_display_rotation "$rot_display"
  set_tablet_rotation "$rot_display"
  rot_screen="$rot_display"
  echo "$BASH_SOURCE: $FUNCNAME(): ... screen clock-wise rotated."
  return
}

# $1: new rotation value, script style
set_display_rotation() {
  declare newrot=
  _rot_script2xrandr "$1" newrot
  $BIN_XRANDR -o "$newrot" ||
    go_exit "internal error: $FUNCNAME()"
  return
}

# $1: new rotation value, script style
set_tablet_rotation () {
  declare -r property='Wacom Rotation'
  declare newrot=

  _rot_script2xinput "$1" newrot

  [ "$id_pen_stylus" ] && {
    $BIN_XINPUT --set-prop "$id_pen_stylus" "$property" "$newrot" ||
      go_exit "internal error: $FUNCNAME()"
  }
  [ "$id_pen_eraser" ] && {
    $BIN_XINPUT --set-prop "$id_pen_eraser" "$property" "$newrot" ||
      go_exit "internal error: $FUNCNAME()"
  }
  [ "$id_finger_touch" ] && {
    $BIN_XINPUT --set-prop "$id_finger_touch" "$property" "$newrot" ||
      go_exit "internal error: $FUNCNAME()"
  }
  return
}

sync_rotation() {
  declare rot_display=
  retrieve_display_rotation rot_display
  [ "$rot_display" != "$rot_screen" ] && {
    set_tablet_rotation "$rot_display"
    # update stored screen rotation
    rot_screen="$rot_display"
  }
  return
}

# $1: loop resolution in seconds, i.e. 3
sync_rotation_loop() {
  echo "$BASH_SOURCE: $FUNCNAME(): sync-rotation-loop activated ..."
  echo "  Infinite loop is active, resolution: $1s"
  echo "  Hit <Ctrl><C> to abort the loop."

  trap '
    echo
    echo "$BASH_SOURCE: $FUNCNAME(): ... sync-rotation-loop deactivated by keyboard input."
    break
    ' SIGINT   # what sigspec is related to ERR??
  while [[ 1 ]]; do
    sync_rotation
    $BIN_SLEEP $1
  done
  trap - SIGINT

  return
}

list_devices() {
  echo "$BASH_SOURCE: $FUNCNAME(): list tablet’s devices ..."
  if [ $id_pen_stylus ]; then
    echo -e "  Pen stylus:\t$pen_stylus (ID: $id_pen_stylus)"
  else
    echo -e "  Pen stylus:\tno device"
  fi
  if [ $id_pen_eraser ]; then
    echo -e "  Pen eraser:\t$pen_eraser (ID: $id_pen_eraser)"
  else
    echo -e "  Pen eraser:\tno device"
  fi
  if [ $id_finger_touch ]; then
    echo -e "  Finger touch:\t$finger_touch (ID: $id_finger_touch)"
  else
    echo -e "  Finger touch:\tno device"
  fi
  echo "$BASH_SOURCE: $FUNCNAME(): ... tablet’s devices listed."
  return
}

# $1: optional message
go_exit() {
  echo "$BASH_SOURCE: $FUNCNAME(): error${1:+:} $1"
  exit 1
}

# $1: new orientation, script style
set_screen_rotation() {
  echo "$BASH_SOURCE: $FUNCNAME(): set screen rotation ..."

  declare -r property='Wacom Rotation'
  declare newrot=

  # reset display orientation
  _rot_script2xrandr "$1" newrot
  $BIN_XRANDR -o "$newrot"

  # reset tablet rotation
  _rot_script2xinput "$1" newrot
  [ "$id_pen_stylus" ] &&
    $BIN_XINPUT --set-prop "$id_pen_stylus" "$property" "$newrot"
  [ "$id_pen_eraser" ] &&
    $BIN_XINPUT --set-prop "$id_pen_eraser" "$property" "$newrot"
  [ "$id_finger_touch" ] &&
    $BIN_XINPUT --set-prop "$id_finger_touch" "$property" "$newrot"

  echo "$BASH_SOURCE: $FUNCNAME(): ... screen rotation set."
}

# $1: device_id, i.e.$id_pen_stylus, $id_pen_eraser, $id_finger_touch
calibrate_area() {
  if [ "$1" ]; then
    declare devname=
    declare output=
    declare values=
    declare min_x=
    declare max_x=
    declare min_y=
    declare max_y=
    declare MinX=
    declare MaxX=
    declare MinY=
    declare MaxY=

    _id2name "$1" devname

    echo "$BASH_SOURCE: $FUNCNAME(): calibrate area for “$devname” (ID: $1) ..."
    echo -n "  Hit <Enter> to continue: " && read -s && echo

    output="$($BIN_XINPUT_CALIBRATOR --device $1 --output-type xorg.conf.d)"

    values="$(echo "$output" | $BIN_SED -r -e '/^[ \t]*current calibration values:/!d; s/^[ \t]*//;')"
    min_x="$(echo "$values" | $BIN_SED -r -e 's/(.*)(min_x\=)(.*)(,.*)(,.*)/\3/;')"
    max_x="$(echo "$values" | $BIN_SED -r -e 's/(.*)(max_x\=)(.*)( and.*)/\3/;')"
    min_y="$(echo "$values" | $BIN_SED -r -e 's/(.*)(min_y\=)(.*)(,.*)/\3/;')"
    max_y="$(echo "$values" | $BIN_SED -r -e 's/(.*)(max_y\=)(.*)$/\3/;')"
    echo -e "  current calibration:\tMinX=$min_x  MinY=$min_y  MaxX=$max_x  MaxY=$max_y"

    MinX="$(echo "$output" | $BIN_SED -r -e '/MinX/!d;s/(.*\")(.*)(\".*$)/\2/;' -)"
    MaxX="$(echo "$output" | $BIN_SED -r -e '/MaxX/!d;s/(.*\")(.*)(\".*$)/\2/;' -)"
    MinY="$(echo "$output" | $BIN_SED -r -e '/MinY/!d;s/(.*\")(.*)(\".*$)/\2/;' -)"
    MaxY="$(echo "$output" | $BIN_SED -r -e '/MaxY/!d;s/(.*\")(.*)(\".*$)/\2/;' -)"
    echo -e "  new calibration:\tMinX=$MinX  MinY=$MinY  MaxX=$MaxX  MaxY=$MaxY"

    set_area "$1" "$MinX" "$MaxX" "$MinY" "$MaxY"

# These permanent settings doesn't seem to work, don't display them...
    #~ echo
    #~ echo "In order to make the calibration permanent,"
    #~ echo "$(echo "$output" | $BIN_SED -r -e '/^  copy/!d;s/^  //;s/$/:/;' -)"
    #~ echo "-----8<---------------------------------------------------------->8-----"
    #~ echo "$(echo "$output" | $BIN_SED -r -e '/^Section/,/^EndSection/!d;s/\!\!.*\!\!/'"$devname"'/;' -)"
    #~ echo "-----8<---------------------------------------------------------->8-----"
    #~ echo

    echo "$BASH_SOURCE: $FUNCNAME(): ... area coordinates have been calibrated."
  else
    go_exit "internal error: $FUNCNAME(): no ID"
  fi
  return
}

# $1: device_id
# $2: min_x
# $3: max_x
# $4: min_y
# $5: max_y
set_area() {
  # Calibrate Wacom Devices
  #   Order of coordinates with xinput: MinX, MinY, MaxX, MaxY
  # Invoke this function if tablet settings require correction.
  # Unsure about how to make these seetings permanent for login,
  # wake from suspend and wake from hibernation.

  declare devname=
  declare -r property='Wacom Tablet Area'

  echo "$BASH_SOURCE: $FUNCNAME(): set area coordinates ..."
  _id2name "$1" devname
  echo -e "  Device:\t$devname"
  echo -e "  Area:\t\tMinX=$2  MinY=$4  MaxX=$3  MaxY=$5"

  # Pen stylus
  # Pen eraser
  # Finger touch
  if [ "$1" ]; then
    $BIN_XINPUT --set-prop "$1" "$property" "$2", "$4", "$3", "$5" ||
      go_exit "internal error: $FUNCNAME()"
    echo "$BASH_SOURCE: $FUNCNAME(): ... area coordinates have been set."
  else
    go_exit "internal error: $FUNCNAME()"
  fi
  return
}

reset_pressure_threshold() {
  # Values have been manually queried with "xinput".
  # Pressure Thresholds are using default values.
  #
  # Unsure about how to make these seetings permanent for login, wake from suspend
  # and wake from hibernation. Invoke this script manually if tablet settings
  # require correction.

  # Calibrate Wacom Devices (using default values):
  # · Pen stylus 'Wacom Pressure Threshold' 27
  # · Pen eraser 'Wacom Pressure Threshold' 27
  # · Finger touch 'Wacom Pressure Threshold' 0

  echo "$BASH_SOURCE: $FUNCNAME(): reset pressure thresholds ..."

  declare -r property='Wacom Pressure Threshold'

  # Pen stylus
  [ "$id_pen_stylus" ] && {
    $BIN_XINPUT --set-prop "$id_pen_stylus" "$property" '27'
  }
  # Pen eraser
  [ "$id_pen_eraser" ] && {
    $BIN_XINPUT --set-prop "$id_pen_eraser" "$property" '27'
  }
  # Finger touch
  [ "$id_finger_touch" ] && {
    $BIN_XINPUT --set-prop "$id_finger_touch" "$property" '0'
  }

  echo "$BASH_SOURCE: $FUNCNAME(): ... pressure thresholds have been reset."
  return
}

# $1: device id
toggle_device() {
  if [ "$1" ]; then
    declare device=
    declare -r property='Device Enabled'
    declare stat=
    declare toggle=

    _id2name "$1" device
    echo "$BASH_SOURCE: $FUNCNAME(): toggle status for “$device” ..."

    # get current status
    stat=$($BIN_XINPUT --list-props "$1" | $BIN_SED -r -e '/'"$property"'/!d; s/.*:[ \t]//;')

    # get new status
    case "$stat" in
      ('0')
        toggle='1'
        ;;
      ('1')
        toggle='0'
        ;;
      (*)
        go_exit "internal error: $FUNCNAME()"
        ;;
    esac

    # set new status
    $BIN_XINPUT --set-prop "$1" "$property" "$toggle" ||
      go_exit "internal error: $FUNCNAME()"

    echo "$BASH_SOURCE: $FUNCNAME(): ... status toggled: $stat -> $toggle"
  else
    go_exit "internal error: $FUNCNAME()"
  fi
  return
}

# $@: argument list
parse_cmdline() {
  echo "$BASH_SOURCE: $FUNCNAME(): parsing command line ..."

  declare -i n=
  declare opt=

  while [ $# -gt 0 ]; do
    opt=${1:?${FG_RED}missing parameter${FG_DEFAULT}}
    n=1
    opt="${opt,,}"
    case "$opt" in
      ('-u'|'--usage'|'-h'|'--help')
        usage
        ;;
      ('-rcw'|'--rotate-cw')
        rotate_screen_cw
        ;;
      ('-rr'|'--reset-rotation')
        set_screen_rotation '0'
        ;;
      ('-ld'|'--list-devices')
        list_devices
        ;;
      ('-ts'|'--toggle-stylus')
        if [ "$id_pen_stylus" ]; then
          toggle_device "$id_pen_stylus"
        else
          echo -e "  $opt:\tno such device"
        fi
        ;;
      ('-te'|'--toggle-eraser')
        if [ "$id_pen_eraser" ]; then
          toggle_device "$id_pen_eraser"
        else
          echo -e "  $opt:\tno such device"
        fi
        ;;
      ('-tt'|'--toggle-touch')
        if [ "$id_finger_touch" ]; then
          toggle_device "$id_finger_touch"
        else
          echo -e "  $opt:\tno such device"
        fi
        ;;
      ('-c'|'--calibrate')
        if [ "$id_pen_stylus" -o "$id_pen_eraser" -o "$id_finger_touch" ]; then
          [ "$id_pen_stylus" ] &&
            calibrate_area "$id_pen_stylus"
          [ "$id_pen_eraser" ] &&
            calibrate_area "$id_pen_eraser"
          [ "$id_finger_touch" ] &&
            calibrate_area "$id_finger_touch"
        else
          echo -e "  $opt:\tno device to calibrate"
        fi
        ;;
      ('-cs'|'--calibrate-stylus')
        if [ "$id_pen_stylus" ]; then
          calibrate_area "$id_pen_stylus"
        else
          echo -e "  $opt:\tno device to calibrate"
        fi
        ;;
      ('-ce'|'--calibrate-eraser')
        if [ "$id_pen_eraser" ]; then
          calibrate_area "$id_pen_eraser"
        else
          echo -e "  $opt:\tno device to calibrate"
        fi
        ;;
      ('-ct'|'--calibrate-touch')
        if [ "$id_finger_touch" ]; then
          calibrate_area "$id_finger_touch"
        else
          echo -e "  $opt:\tno device to calibrate"
        fi
        ;;
      ('-sr'|'--sync-rotation')
        if [ "$id_pen_stylus" -o "$id_pen_eraser" -o "$id_finger_touch" ]; then
          echo "$BASH_SOURCE: $FUNCNAME(): synchronizing tablet rotation ..."
          sync_rotation
          echo "... tablet rotation synchronized."
        else
          echo -e "  $opt:\tno device to synchronize"
        fi
        ;;
      ('-srl'|'--sync-rotation-loop')
        if [ "$id_pen_stylus" -o "$id_pen_eraser" -o "$id_finger_touch" ]; then
          sync_rotation_loop 3
        else
          echo -e "  $opt:\tno device to synchronize"
        fi
        ;;
      ('-rpt'|'--reset-pressure-threshold')
        if [ "$id_pen_stylus" -o "$id_pen_eraser" -o "$id_finger_touch" ]; then
          reset_pressure_threshold
        else
          echo -e "  $opt:\tno such device"
        fi
        ;;
      (*)
        echo -e "  $opt:\tunknown option"
        ;;
    esac
    shift $n
  done
  if [ $# -ne 0 ]; then
    echo "$BASH_SOURCE: $FUNCNAME(): ... $# parameters were skipped."
  else
    echo "$BASH_SOURCE: $FUNCNAME(): ... command line parsed."
  fi
}

usage() {
  echo "$BASH_SOURCE: $FUNCNAME(): list usage information ..."
  $BIN_CAT<<EOF

Name:
  $BASH_SOURCE – control your Wacom Tablet, keep it in sync with your laptop’s display

Usage:
  $BASH_SOURCE [<Options>]

Options:
  Help:
    -h   |--help
    -u   |--usage

  Screen (Display + Tablet):
    -rcw |--rotate-cw
    -rr  |--reset-rotation

  Tablet:
    -ld  |--list-devices
    -ts  |--toggle-stylus
    -te  |--toggle-eraser
    -tt  |--toggle-touch
    -c   |--calibrate
    -cs  |--calibrate-stylus
    -ce  |--calibrate-eraser
    -ct  |--calibrate-touch
    -sr  |--sync-rotation
    -srl |--sync-rotation-loop
    -rpt |--reset-pressure-threshold

Terms:
  Let’s use ‘screen’ as a term for the compound device made of ‘display’ and ‘tablet’.
  The tablet might have multiple Wacom Input Devices, as like...
  · ‘Pen stylus’ – sometimes shorted to just ‘stylus’
  · ‘Pen eraser’ – sometimes shorted to just ‘eraser’
  · ‘Finger touch’ – sometimes shorted to just ‘touch’

License:
  For Copyright and License Note, see header within source file.

EOF
  echo "$BASH_SOURCE: $FUNCNAME(): ... usage information listed."
}

main() {
  # hello
  echo "$BASH_SOURCE: $FUNCNAME(): starting ..."

  # whereis
  declare -r BIN_WHEREIS='/usr/bin/whereis'
  [ -x $BIN_WHEREIS ] || go_exit "$BIN_WHEREIS"

  # tools
  declare -r BIN_CAT="$(command -v cat)"
  declare -r BIN_SED="$(command -v sed)"
  declare -r BIN_EGREP="$(command -v egrep)"
  declare -r BIN_SLEEP="$(command -v sleep)"
  declare -r BIN_XRANDR="$(command -v xrandr)"
  declare -r BIN_XINPUT="$(command -v xinput)"
  declare -r BIN_XINPUT_CALIBRATOR="$(command -v xinput_calibrator)"
  [ -x $BIN_CAT ] || go_exit 'not available: $BIN_CAT'
  [ -x $BIN_SED ] || go_exit 'not available: $BIN_SED'
  [ -x $BIN_EGREP ] || go_exit 'not available: $BIN_EGREP'
  [ -x $BIN_SLEEP ] || go_exit 'not available: $BIN_SLEEP'
  [ -x $BIN_XRANDR ] || go_exit 'not available: $BIN_XRANDR'
  [ -x $BIN_XINPUT ] || go_exit 'not available: $BIN_XINPUT'
  [ -x $BIN_XINPUT_CALIBRATOR ] || go_exit 'not available: $BIN_XINPUT_CALIBRATOR'

  # IDs of Tablet’s Devices
  declare -r id_pen_stylus=$($BIN_XINPUT --list | $BIN_SED -r -e '/Pen stylus/!d; s/(^.*id=)([0-9][0-9])(.*$)/\2/;' -)
  declare -r id_pen_eraser=$($BIN_XINPUT --list | $BIN_SED -r -e '/Pen eraser/!d; s/(^.*id=)([0-9][0-9])(.*$)/\2/;' -)
  declare -r id_finger_touch=$($BIN_XINPUT --list | $BIN_SED -r -e '/Finger touch/!d; s/(^.*id=)([0-9][0-9])(.*$)/\2/;' -)

  # Names of Tablet’s Devices
  declare -r pen_stylus=$($BIN_XINPUT --list | $BIN_SED -r -e '/Pen stylus/!d; s/(^[^W]*)(.*)(id=.*)/\2/;' -e 's/[ \t]*$//;' -)
  declare -r pen_eraser=$($BIN_XINPUT --list | $BIN_SED -r -e '/Pen eraser/!d; s/(^[^W]*)(.*)(id=.*)/\2/;' -e 's/[ \t]*$//;' -)
  declare -r finger_touch=$($BIN_XINPUT --list | $BIN_SED -r -e '/Finger touch/!d; s/(^[^W]*)(.*)(id=.*)/\2/;' -e 's/[ \t]*$//;' -)

  # variables
  declare rot_screen=

  # parse command line options
  if [ $# -gt 0 ]; then
    parse_cmdline "$@"
  else
    usage
  fi

  # return
  echo "$BASH_SOURCE: $FUNCNAME(): ... done."
  return
}

main "$@"
exit 0
