- niri 26.04 has no hotplug event: OutputConfigChanged is only an ACK (niri-ipc). Watch WorkspacesChanged, diff sorted output set, apply on change. Proven live: eDP-1 off/on -> 3 output-set transitions, each matched + applied + reloaded. - unit: %h/.local/bin/desk-autoswitch.sh, quoted Environment, drop User/Group (user unit); add desk-autoswitch.sh bin symlink - desk.sh: silence validate/load-config/notify stdout - deploy.sh: deploy repo symlinks (find -type l), so .local/bin links land - docs: event semantics corrected (displays diagram, log)
167 lines
4.4 KiB
Bash
Executable File
167 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scripts/desk.sh — Desk profile management for niri (blankpad)
|
|
#
|
|
# Desk profiles are cfg/desk-*.kdl files. The active profile is whatever
|
|
# file cfg/display.kdl symlinks to. Profiles declare the monitors they
|
|
# need via comment lines:
|
|
#
|
|
# // requires: <niri output long name>
|
|
# // optional: <free text>
|
|
#
|
|
# Subcommands:
|
|
# list list available desk profiles
|
|
# current show currently active profile (basename of display.kdl)
|
|
# switch <name> apply a specific profile (name or *.kdl)
|
|
# cycle apply the next profile (wraps)
|
|
# match [--apply] pick the profile matching connected outputs
|
|
# capture print `// requires:` lines for current outputs
|
|
#
|
|
# Environment overrides (for tests and deploys):
|
|
# DESK_CFG_DIR config dir (default ~/.config/niri/cfg)
|
|
# DESK_NIRI niri binary (default niri)
|
|
# DESK_NOTIFY notification command prefix (default "noctalia msg notification-show")
|
|
|
|
set -euo pipefail
|
|
|
|
CFG_DIR="${DESK_CFG_DIR:-$HOME/.config/niri/cfg}"
|
|
NIRI="${DESK_NIRI:-niri}"
|
|
NOTIFY="${DESK_NOTIFY:-noctalia msg notification-show}"
|
|
|
|
usage() {
|
|
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
|
}
|
|
|
|
# Profile names (basenames without .kdl), sorted.
|
|
profiles() {
|
|
local f
|
|
for f in "$CFG_DIR"/desk-*.kdl; do
|
|
[ -e "$f" ] || continue
|
|
basename "$f" .kdl
|
|
done | sort
|
|
}
|
|
|
|
# Active profile name, or "none".
|
|
current() {
|
|
if [ -L "$CFG_DIR/display.kdl" ]; then
|
|
basename "$(readlink -f "$CFG_DIR/display.kdl")" .kdl
|
|
else
|
|
echo none
|
|
fi
|
|
}
|
|
|
|
# Connected output long names, one per line.
|
|
outputs() {
|
|
"$NIRI" msg outputs 2>/dev/null | grep -oP 'Output "\K[^"]+' || true
|
|
}
|
|
|
|
# requires lines of a profile, one per line.
|
|
requires() {
|
|
grep -oP '^//\s*requires:\s*\K.+' "$CFG_DIR/$1.kdl" 2>/dev/null || true
|
|
}
|
|
|
|
notify() {
|
|
# shellcheck disable=SC2086 # word splitting of the prefix is intended
|
|
$NOTIFY "Desk" "$1" >/dev/null 2>/dev/null || true
|
|
}
|
|
|
|
# Apply a profile: relink, validate live config, roll back on failure,
|
|
# then reload + notify.
|
|
apply_profile() {
|
|
local name="$1"
|
|
local file="$CFG_DIR/$name.kdl"
|
|
local link="$CFG_DIR/display.kdl"
|
|
|
|
[ -f "$file" ] || { echo "error: no such profile: $name" >&2; return 1; }
|
|
|
|
local old=""
|
|
[ -L "$link" ] && old="$(readlink "$link")"
|
|
|
|
ln -sfn "$name.kdl" "$link"
|
|
|
|
if ! "$NIRI" validate >/dev/null 2>/dev/null; then
|
|
if [ -n "$old" ]; then ln -sfn "$old" "$link"; else rm -f "$link"; fi
|
|
echo "error: niri validate failed; kept $( [ -n "$old" ] && basename "$old" .kdl || echo 'no' ) profile" >&2
|
|
return 1
|
|
fi
|
|
|
|
"$NIRI" msg action load-config-file >/dev/null 2>/dev/null || true
|
|
notify "switched to $name"
|
|
echo "switched to $name"
|
|
}
|
|
|
|
cmd_switch() {
|
|
local target="${1:-}"
|
|
[ -n "$target" ] || { echo "usage: desk.sh switch <profile>" >&2; return 1; }
|
|
target="${target%.kdl}"
|
|
apply_profile "$target"
|
|
}
|
|
|
|
cmd_match() {
|
|
local apply="${1:-}"
|
|
local connected best="" best_n=0
|
|
connected="$(outputs)"
|
|
|
|
local name req count
|
|
while IFS= read -r name; do
|
|
[ -n "$name" ] || continue
|
|
count=0
|
|
local ok=1
|
|
while IFS= read -r req; do
|
|
[ -n "$req" ] || continue
|
|
if grep -qxF "$req" <<<"$connected"; then
|
|
count=$((count + 1))
|
|
else
|
|
ok=0
|
|
break
|
|
fi
|
|
done < <(requires "$name")
|
|
# requires=0 profiles never match; ties go to more requires, then alphabetical
|
|
[ "$ok" -eq 1 ] && [ "$count" -gt 0 ] && [ "$count" -gt "$best_n" ] && { best="$name"; best_n=$count; }
|
|
done < <(profiles)
|
|
|
|
if [ -z "$best" ]; then
|
|
echo "no desk profile matches connected outputs" >&2
|
|
return 2
|
|
fi
|
|
if [ "$apply" = "--apply" ]; then
|
|
apply_profile "$best"
|
|
else
|
|
echo "$best"
|
|
fi
|
|
}
|
|
|
|
cmd_cycle() {
|
|
local list cur next
|
|
list="$(profiles)"
|
|
[ -n "$list" ] || { echo "no desk profiles found in $CFG_DIR" >&2; return 1; }
|
|
cur="$(current)"
|
|
next="$(awk -v cur="$cur" '
|
|
{ a[NR]=$0 }
|
|
END { for (i=1;i<=NR;i++) if (a[i]==cur) { print a[(i%NR)+1]; exit }
|
|
print a[1] }' <<<"$list")"
|
|
apply_profile "$next"
|
|
}
|
|
|
|
cmd_capture() {
|
|
local n
|
|
n="$(outputs | wc -l)"
|
|
[ "$n" -gt 0 ] || { echo "error: no connected outputs (is niri running?)" >&2; return 1; }
|
|
echo "// paste into the profile:"
|
|
outputs | sed 's/^/\/\/ requires: /'
|
|
}
|
|
|
|
main() {
|
|
case "${1:-}" in
|
|
list) profiles ;;
|
|
current) current ;;
|
|
switch) shift; cmd_switch "$@" ;;
|
|
cycle) cmd_cycle ;;
|
|
match) shift; cmd_match "${1:-}" ;;
|
|
capture) cmd_capture ;;
|
|
-h|--help) usage ;;
|
|
*) usage >&2; exit 1 ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|