#!/usr/bin/env bash # TUI for creating/updating device-specific Hyprland input overrides. # Usage: mk-device-exception.sh set -euo pipefail die() { printf 'error: %s\n' "$*" >&2; exit 1; } [[ $# -eq 1 ]] || die "Usage: ${0##*/} " target="$1" command -v hyprctl &>/dev/null || die "hyprctl not found" command -v dialog &>/dev/null || die "dialog not found" command -v jq &>/dev/null || die "jq not found" # ---------- device selection ---------- devices=$(hyprctl devices -j) entries=$(jq -r ' (.keyboards[]?.name | "keyboard\t" + .), (.mice[]?.name | "mouse\t" + .) ' <<<"$devices") [[ -z "$entries" ]] && die "No keyboard or mouse devices found" if command -v fzf &>/dev/null; then selected=$(printf '%s\n' "$entries" \ | fzf --prompt="Select device: " --height=~10 --with-nth=2.. --delimiter=$'\t') else echo "Select a device:" >&2 IFS=$'\n' select selected in $entries; do [[ -n "$selected" ]] && break; done unset IFS fi [[ -z "$selected" ]] && exit 1 type=${selected%%$'\t'*} name=${selected#*$'\t'} # ---------- parse existing field value from target file ---------- # Returns the Lua literal for the field (e.g. `"de"`, `15`, `false`). get_field() { local field="$1" default="$2" [[ -f "$target" ]] || { printf '%s' "$default"; return; } awk -v dev="$name" -v fld="$field" -v def="$default" ' /^hl\.device\(\{/ { blk=""; in_blk=1; matched=0 } in_blk { blk = blk $0 "\n" } in_blk && /name = / { if ($0 ~ "\"" dev "\"") matched=1 } in_blk && /^\}\)/ { if (matched) { n = split(blk, lines, "\n") for (i = 1; i <= n; i++) { line = lines[i] sub(/^[[:space:]]+/, "", line) # strip indentation if (line ~ "^" fld "[[:space:]]*=") { sub(/^[^=]+=/, "", line) # drop "fieldname = " sub(/^[[:space:]]*/, "", line) # strip leading space sub(/[[:space:]]*,.*$/, "", line) # drop trailing comma and any inline comment print line; exit } } print def; exit } in_blk = 0 } ' "$target" } # Strip surrounding Lua string quotes for display in form ("de" -> de). unquote() { local v="$1"; v="${v#\"}"; v="${v%\"}"; printf '%s' "$v"; } # ---------- show form and collect values ---------- case "$type" in keyboard) v_layout=$(unquote "$(get_field kb_layout '"de"')") v_variant=$(unquote "$(get_field kb_variant '""')") v_model=$(unquote "$(get_field kb_model '""')") v_options=$(unquote "$(get_field kb_options '""')") v_rules=$(unquote "$(get_field kb_rules '""')") v_rate=$(get_field repeat_rate 0) v_delay=$(get_field repeat_delay 0) v_sens=$(get_field sensitivity 0) # dialog writes its TUI to stdout (/dev/tty) and the result to stderr. values=$(dialog \ --title "Keyboard exception: $name" \ --form "" 16 74 8 \ "kb_layout" 1 1 "$v_layout" 1 17 42 0 \ "kb_variant" 2 1 "$v_variant" 2 17 42 0 \ "kb_model" 3 1 "$v_model" 3 17 42 0 \ "kb_options" 4 1 "$v_options" 4 17 42 0 \ "kb_rules" 5 1 "$v_rules" 5 17 42 0 \ "repeat_rate" 6 1 "$v_rate" 6 17 10 0 \ "repeat_delay" 7 1 "$v_delay" 7 17 10 0 \ "sensitivity" 8 1 "$v_sens" 8 17 10 0 \ 2>&1 >/dev/tty) || exit 0 mapfile -t vals <<< "$values" new_block="hl.device({ name = \"$name\", kb_layout = \"${vals[0]:-}\", kb_variant = \"${vals[1]:-}\", kb_model = \"${vals[2]:-}\", kb_options = \"${vals[3]:-}\", kb_rules = \"${vals[4]:-}\", repeat_rate = ${vals[5]:-0}, repeat_delay = ${vals[6]:-0}, sensitivity = ${vals[7]:-0}, })" ;; mouse) v_sens=$(get_field sensitivity 0) v_accel=$(unquote "$(get_field accel_profile '""')") v_left=$(get_field left_handed false) v_natural=$(get_field natural_scroll false) v_scroll=$(unquote "$(get_field scroll_points '""')") values=$(dialog \ --title "Mouse exception: $name" \ --form "" 13 74 5 \ "sensitivity" 1 1 "$v_sens" 1 20 20 0 \ "accel_profile" 2 1 "$v_accel" 2 20 20 0 \ "left_handed" 3 1 "$v_left" 3 20 10 0 \ "natural_scroll" 4 1 "$v_natural" 4 20 10 0 \ "scroll_points" 5 1 "$v_scroll" 5 20 20 0 \ 2>&1 >/dev/tty) || exit 0 mapfile -t vals <<< "$values" new_block="hl.device({ name = \"$name\", sensitivity = ${vals[0]:-0}, accel_profile = \"${vals[1]:-}\", left_handed = ${vals[2]:-false}, natural_scroll = ${vals[3]:-false}, scroll_points = \"${vals[4]:-}\", })" ;; esac # ---------- replace or append in target file ---------- [[ -f "$target" ]] || touch "$target" # Remove the existing block for this device (no-op if not present). tmp=$(mktemp) awk -v dev="$name" ' /^hl\.device\(\{/ { blk = $0 "\n"; in_blk=1; matched=0; next } in_blk { blk = blk $0 "\n" if (/name = / && ($0 ~ "\"" dev "\"")) matched=1 if (/^\}\)/) { if (!matched) printf "%s", blk in_blk=0; blk="" } next } { print } ' "$target" > "$tmp" mv "$tmp" "$target" # Append the new block, separated by a blank line if the file is non-empty. if [[ -s "$target" ]]; then [[ "$(tail -c 1 "$target")" != $'\n' ]] && printf '\n' >> "$target" printf '\n' >> "$target" fi printf '%s\n' "$new_block" >> "$target" printf 'Written to %s\n' "$target"