- desk.sh: list/current/switch/cycle/match/capture over cfg/desk-*.kdl profiles; apply = relink display.kdl, niri validate (rollback on fail), load-config-file, noctalia notification; match scores // requires: lines (EDID long names) against live niri msg outputs - desk-autoswitch.sh: watch niri msg --json event-stream for OutputsChanged, debounced desk.sh match --apply, reconnect loop - blankpad-desk-autoswitch.service: ConditionHost=blankpad user unit - gen-keybinds.sh: keybinds.kdl -> markdown cheat sheet (sections from // --- headers, hotkey-overlay-title preferred over action) - tests/: 27 bats cases, all green (desk 16, gen-keybinds 6, niri-config 5 incl. real niri validate on the assembled tree with each desk profile active)
224 lines
6.5 KiB
Bash
Executable File
224 lines
6.5 KiB
Bash
Executable File
#!/usr/bin/env bats
|
|
# tests/desk.bats — black-box tests for scripts/desk.sh
|
|
#
|
|
# desk.sh contract (see script header): subcommands list/current/switch/
|
|
# cycle/match/capture driven by env overrides DESK_CFG_DIR, DESK_NIRI,
|
|
# DESK_NOTIFY. Everything observable is via stdout/stderr/exit status,
|
|
# the display.kdl symlink, and the stub invocation log.
|
|
|
|
setup() {
|
|
TEST_TMP="$(mktemp -d)"
|
|
CFG="$TEST_TMP/cfg"
|
|
LOG="$TEST_TMP/stub.log"
|
|
mkdir -p "$CFG"
|
|
|
|
# canned `niri msg outputs` payload (live niri human format)
|
|
cat > "$TEST_TMP/outputs.txt" <<'EOF'
|
|
Output "Sceptre Tech Inc Sceptre M34 Unknown" (DP-2)
|
|
Current mode: 3440x1440 @ 239.999 Hz (current)
|
|
Output "Lenovo Group Limited 0x40A9 Unknown" (eDP-1)
|
|
Current mode: 1920x1080 @ 60.001 Hz (current, preferred)
|
|
EOF
|
|
|
|
# stub niri: logs every invocation, serves canned outputs,
|
|
# validate exit code controlled by $STUB_VALIDATE (default 0)
|
|
cat > "$TEST_TMP/niri" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
echo "niri $*" >> "$STUB_LOG"
|
|
case "$*" in
|
|
"msg outputs") cat "$STUB_OUTPUTS" ;;
|
|
"validate") exit "${STUB_VALIDATE:-0}" ;;
|
|
"msg action load-config-file") exit 0 ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
EOF
|
|
chmod +x "$TEST_TMP/niri"
|
|
|
|
# stub notifier: records what it was shown
|
|
cat > "$TEST_TMP/notify" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
echo "notify $*" >> "$STUB_LOG"
|
|
EOF
|
|
chmod +x "$TEST_TMP/notify"
|
|
|
|
export STUB_LOG="$LOG"
|
|
export STUB_OUTPUTS="$TEST_TMP/outputs.txt"
|
|
export DESK_CFG_DIR="$CFG"
|
|
export DESK_NIRI="$TEST_TMP/niri"
|
|
export DESK_NOTIFY="$TEST_TMP/notify"
|
|
|
|
DESK="$BATS_TEST_DIRNAME/../scripts/desk.sh"
|
|
}
|
|
|
|
teardown() {
|
|
rm -rf "$TEST_TMP"
|
|
}
|
|
|
|
make_profiles() {
|
|
cat > "$CFG/desk-1-home.kdl" <<'EOF'
|
|
// requires: Sceptre Tech Inc Sceptre M34 Unknown
|
|
// requires: Lenovo Group Limited 0x40A9 Unknown
|
|
output "Sceptre Tech Inc Sceptre M34 Unknown" { scale 1 }
|
|
EOF
|
|
cat > "$CFG/desk-2-portable.kdl" <<'EOF'
|
|
// requires: Sceptre Tech Inc Sceptre M34 Unknown
|
|
// requires: PM1601 Portable Monitor Unknown
|
|
output "Sceptre Tech Inc Sceptre M34 Unknown" { scale 1 }
|
|
EOF
|
|
# non-profile files must be ignored
|
|
echo 'include "display.kdl"' > "$CFG/config.kdl"
|
|
echo 'output "X" {}' > "$CFG/keybinds.kdl"
|
|
}
|
|
|
|
@test "list: shows desk profiles only, sorted" {
|
|
make_profiles
|
|
run "$DESK" list
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "$(printf 'desk-1-home\ndesk-2-portable')" ]
|
|
}
|
|
|
|
@test "list: no profiles yields empty output" {
|
|
run "$DESK" list
|
|
[ "$status" -eq 0 ]
|
|
[ -z "$output" ]
|
|
}
|
|
|
|
@test "current: reports none without symlink, profile with symlink" {
|
|
make_profiles
|
|
run "$DESK" current
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "none" ]
|
|
|
|
ln -sfn desk-1-home.kdl "$CFG/display.kdl"
|
|
run "$DESK" current
|
|
[ "$output" = "desk-1-home" ]
|
|
}
|
|
|
|
@test "switch: relinks, validates, reloads, notifies" {
|
|
make_profiles
|
|
run "$DESK" switch desk-1-home
|
|
[[ "$output" == *"switched to desk-1-home"* ]]
|
|
[ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-1-home.kdl" ]
|
|
|
|
run cat "$LOG"
|
|
[[ "$output" == *"niri validate"* ]]
|
|
[[ "$output" == *"niri msg action load-config-file"* ]]
|
|
[[ "$output" == *"notify Desk switched to desk-1-home"* ]]
|
|
}
|
|
|
|
@test "switch: accepts .kdl suffix" {
|
|
make_profiles
|
|
run "$DESK" switch desk-2-portable.kdl
|
|
[ "$status" -eq 0 ]
|
|
[ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-2-portable.kdl" ]
|
|
}
|
|
|
|
@test "switch: unknown profile fails without touching the link" {
|
|
make_profiles
|
|
ln -sfn desk-1-home.kdl "$CFG/display.kdl"
|
|
run "$DESK" switch nonexistent
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"no such profile"* ]]
|
|
[ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-1-home.kdl" ]
|
|
# validate must never have run for a missing profile
|
|
run cat "$LOG"
|
|
[[ "$output" != *"niri validate"* ]]
|
|
}
|
|
|
|
@test "switch: failed validate rolls the link back to the previous profile" {
|
|
make_profiles
|
|
ln -sfn desk-1-home.kdl "$CFG/display.kdl"
|
|
export STUB_VALIDATE=1
|
|
run "$DESK" switch desk-2-portable
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"validate failed"* ]]
|
|
[ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-1-home.kdl" ]
|
|
# no reload, no notification after a failed validate
|
|
[[ "$output" != *"load-config-file"* ]]
|
|
run cat "$LOG"
|
|
[[ "$output" != *"notify"* ]]
|
|
}
|
|
|
|
@test "switch: failed validate with no previous profile removes the link" {
|
|
make_profiles
|
|
export STUB_VALIDATE=1
|
|
run "$DESK" switch desk-1-home
|
|
[ "$status" -ne 0 ]
|
|
[ ! -L "$CFG/display.kdl" ]
|
|
}
|
|
|
|
@test "match: picks the profile whose requires are all connected" {
|
|
make_profiles
|
|
run "$DESK" match
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "desk-1-home" ]
|
|
# dry run: must not touch the symlink
|
|
[ ! -L "$CFG/display.kdl" ]
|
|
}
|
|
|
|
@test "match --apply: applies the winner" {
|
|
make_profiles
|
|
run "$DESK" match --apply
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"switched to desk-1-home"* ]]
|
|
[ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-1-home.kdl" ]
|
|
}
|
|
|
|
@test "match: no matching profile exits nonzero and keeps the link" {
|
|
make_profiles
|
|
ln -sfn desk-2-portable.kdl "$CFG/display.kdl"
|
|
cat > "$TEST_TMP/outputs.txt" <<'EOF'
|
|
Output "Some Other Monitor 0x1234 Unknown" (DP-9)
|
|
EOF
|
|
run "$DESK" match
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"no desk profile matches"* ]]
|
|
[ "$(basename "$(readlink "$CFG/display.kdl")")" = "desk-2-portable.kdl" ]
|
|
}
|
|
|
|
@test "match: prefers the profile with more matched requires" {
|
|
# both profiles match the connected set; desk-1 requires 2, desk-2 requires 1
|
|
cat > "$CFG/desk-1-home.kdl" <<'EOF'
|
|
// requires: Sceptre Tech Inc Sceptre M34 Unknown
|
|
// requires: Lenovo Group Limited 0x40A9 Unknown
|
|
EOF
|
|
cat > "$CFG/desk-9-guest.kdl" <<'EOF'
|
|
// requires: Sceptre Tech Inc Sceptre M34 Unknown
|
|
EOF
|
|
run "$DESK" match
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "desk-1-home" ]
|
|
}
|
|
|
|
@test "cycle: advances to the next profile and wraps around" {
|
|
make_profiles
|
|
ln -sfn desk-1-home.kdl "$CFG/display.kdl"
|
|
run "$DESK" cycle
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"switched to desk-2-portable"* ]]
|
|
|
|
run "$DESK" cycle
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"switched to desk-1-home"* ]]
|
|
}
|
|
|
|
@test "cycle: with no active profile applies the first" {
|
|
make_profiles
|
|
run "$DESK" cycle
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"switched to desk-1-home"* ]]
|
|
}
|
|
|
|
@test "cycle: no profiles at all fails cleanly" {
|
|
run "$DESK" cycle
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"no desk profiles"* ]]
|
|
}
|
|
|
|
@test "capture: prints requires lines for connected outputs" {
|
|
run "$DESK" capture
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"// requires: Sceptre Tech Inc Sceptre M34 Unknown"* ]]
|
|
[[ "$output" == *"// requires: Lenovo Group Limited 0x40A9 Unknown"* ]]
|
|
}
|