#!/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: # // optional: # # Subcommands: # list list available desk profiles # current show currently active profile (basename of display.kdl) # switch 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 " >&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 "$@"