#!/bin/bash
# Console handler for the Apple keyboard's brightness keys.
#
# In X/Wayland the desktop grabs these keys itself. On a bare TTY nothing
# does, so F1/F2 are dead. This watches libinput and drives the sysfs
# backlights directly.
#
# F1/F2 move the screen and the keyboard backlight together.
#   F1 at the lowest level  -> everything fully off (connector powered down)
#   F2 from off             -> back on at the lowest level, nothing brighter
#
# Stepping is geometric, not linear - see STEPS below.
#
# Ported from the Fedora Asahi install on nvme0n1p8.

PANEL=apple-panel-bl
KBD=kbd_backlight

# Lowest level each device actually honours.
# The DCP clamps the panel at 4: writing 0,1,2,3,4 all read back as
# actual_brightness 4, and 5 is the first value that reads back as itself.
# So 4 is the true bottom - anything below it is a lie.
PANEL_MIN=${PANEL_MIN:-4}
KBD_MIN=${KBD_MIN:-1}

# Presses to get from minimum to maximum.
STEPS=${STEPS:-10}

get()  { brightnessctl -m -d "$1" get; }
max()  { brightnessctl -m -d "$1" max; }
set_() { brightnessctl -q -d "$1" set "$2"; }

# Perceived brightness is roughly logarithmic, so equal *ratios* feel like
# equal steps - not equal amounts. A linear 10%-of-max step is a huge jump
# at the bottom of the range and invisible at the top.
#
# So each press multiplies/divides by a fixed ratio, chosen per device so
# that min -> max always takes STEPS presses:
#     ratio = (max/min) ^ (1/STEPS)
# Panel  (4..420):  ratio 1.59    Keyboard (1..255): ratio 1.74
# Returned as an integer percent so the shell can do it with integer maths.
ratio_pct() {
    awk -v mx="$(max "$1")" -v mn="$2" -v n="$STEPS" \
        'BEGIN { printf "%d", 100 * exp(log(mx/mn)/n) + 0.5 }'
}
PANEL_RATIO=$(ratio_pct "$PANEL" "$PANEL_MIN")
KBD_RATIO=$(ratio_pct "$KBD" "$KBD_MIN")

# The backlight sysfs file CANNOT turn this panel off - see PANEL_MIN above.
# The only thing that genuinely kills it is powering the connector down,
# which a console blank does via DRM DPMS.
# /dev/tty0 is whichever VC is currently active. The ioctl needs the console
# on STDIN, not stdout - redirecting only stdout gives ENOTTY.
vc_blank()   { TERM=linux setterm --blank force < /dev/tty0 > /dev/tty0 2>/dev/null; }
vc_unblank() { TERM=linux setterm --blank poke  < /dev/tty0 > /dev/tty0 2>/dev/null; }

is_off() { [ "$(get "$PANEL")" -eq 0 ]; }

blank_all() {
    set_ "$PANEL" 0     # reads back as 4 in hardware, but marks the off state
    set_ "$KBD"   0     # and keeps it as dim as possible if a stray key
    vc_blank            # wakes the VC. this is what actually powers it off.
}

wake() {
    vc_unblank
    set_ "$PANEL" "$PANEL_MIN"
    set_ "$KBD"   "$KBD_MIN"
}

# Multiply or divide one device by its ratio, clamped to [min, max].
# Never crosses into 0 - going off is blank_all's job, not a rounding
# accident.
nudge() {
    local dev=$1 dir=$2 min=$3 ratio=$4 cur new hi
    cur=$(get "$dev"); hi=$(max "$dev")
    [ "$cur" -eq 0 ] && [ "$dir" = "-" ] && return   # kbd already off, leave it
    if [ "$dir" = "+" ]; then
        new=$(( cur * ratio / 100 ))
        [ "$new" -le "$cur" ] && new=$(( cur + 1 ))  # integer rounding stalls
    else
        new=$(( cur * 100 / ratio ))
        [ "$new" -ge "$cur" ] && new=$(( cur - 1 ))  # ...at low values
    fi
    [ "$new" -lt "$min" ] && new=$min
    [ "$new" -gt "$hi" ]  && new=$hi
    set_ "$dev" "$new"
}

down() {
    is_off && return                       # already off
    if [ "$(get "$PANEL")" -le "$PANEL_MIN" ]; then
        blank_all                          # at the bottom -> all the way off
    else
        nudge "$PANEL" - "$PANEL_MIN" "$PANEL_RATIO"
        nudge "$KBD"   - "$KBD_MIN"   "$KBD_RATIO"
    fi
}

up() {
    if is_off; then
        wake                               # on at the lowest level, no brighter
    else
        nudge "$PANEL" + "$PANEL_MIN" "$PANEL_RATIO"
        nudge "$KBD"   + "$KBD_MIN"   "$KBD_RATIO"
    fi
}

kbd_toggle() {
    if [ "$(get "$KBD")" -gt 0 ]; then set_ "$KBD" 0; else set_ "$KBD" 50%; fi
}

# --- device selection -----------------------------------------------------
#
# By default libinput watches every device on the seat. That includes the
# trackpad, and every pointer motion then becomes a line this loop has to
# read and pattern-match - measured at ~7.5us per event, so roughly 0.075%
# of a core at a 100Hz report rate, for events that can never match.
#
# Only the keyboard carries the keys we handle, so ask libinput for just the
# devices that actually advertise them. Idle cost is nil either way; this is
# about not doing pointless work whenever the trackpad moves.

# Test one keycode in a device's sysfs capability bitmap.
# The bitmap prints 64-bit words most-significant first, so the LAST field is
# word 0. Pull out the single hex digit holding the bit rather than parsing
# the word as an integer - awk uses doubles and would lose the low bits.
has_keycode() {
    awk -v code="$2" '{
        word = int(code / 64); bit = code % 64
        idx = NF - word
        if (idx < 1) exit 1
        s = $idx
        pos = length(s) - int(bit / 4)
        if (pos < 1) exit 1
        v = index("0123456789abcdef", tolower(substr(s, pos, 1))) - 1
        exit (int(v / (2 ^ (bit % 4))) % 2) ? 0 : 1
    }' "$1/device/capabilities/key" 2>/dev/null
}

# KEY_BRIGHTNESSUP (225) or KEY_KBDILLUMUP (230) - a device with either is
# worth listening to.
find_devices() {
    local ev
    for ev in /sys/class/input/event*; do
        [ -r "$ev/device/capabilities/key" ] || continue
        if has_keycode "$ev" 225 || has_keycode "$ev" 230; then
            printf '%s\n' "/dev/input/${ev##*/}"
        fi
    done
}

# DEVICES may be set explicitly to skip detection entirely.
if [ -n "${DEVICES:-}" ]; then
    read -r -a devices <<< "$DEVICES"
else
    mapfile -t devices < <(find_devices)
fi

args=()
for d in "${devices[@]}"; do args+=(--device "$d"); done

if [ ${#args[@]} -eq 0 ]; then
    # Nothing advertises the keys. Fall back to watching everything rather
    # than sitting there doing nothing - noisier, but it still works.
    echo "brightness-keyd: no device advertises brightness keys, watching all" >&2
    args=(--udev seat0)
fi

echo "brightness-keyd: listening on ${devices[*]:-all devices}" \
     "(panel ratio ${PANEL_RATIO}%, kbd ratio ${KBD_RATIO}%)" >&2

stdbuf -oL libinput debug-events "${args[@]}" | while IFS= read -r line; do
    case "$line" in
        *KEY_BRIGHTNESSUP*pressed*)   up ;;
        *KEY_BRIGHTNESSDOWN*pressed*) down ;;
        *KEY_KBDILLUMUP*pressed*)     nudge "$KBD" + "$KBD_MIN" "$KBD_RATIO" ;;
        *KEY_KBDILLUMDOWN*pressed*)   nudge "$KBD" - "$KBD_MIN" "$KBD_RATIO" ;;
        *KEY_KBDILLUMTOGGLE*pressed*) kbd_toggle ;;
    esac
done
