Dotfiles/desktopenvs/hyprlua/hypr-usr/mk-device-exception.sh

172 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# TUI for creating/updating device-specific Hyprland input overrides.
# Usage: mk-device-exception.sh <target-lua-file>
set -euo pipefail
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
[[ $# -eq 1 ]] || die "Usage: ${0##*/} <target-lua-file>"
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"