#!/usr/bin/env bash # Two behaviors the settings rows depend on: # # commitPreference(key, value) # One entry point for every row. A compositor-backed key must reach Hyprland # and be confirmed before it is stored; a local key is written directly. # Rows bind a schema key and call this, so they never need to know which # kind they hold -- and a row must not be able to store a value the # compositor rejected. # # restoreDefaults() # Panama keeps user state in more than one file. Resetting only the schema # store would leave a customized Home accessory arrangement in place while # claiming to have restored Panama's defaults. That is worse than having no # reset at all, because it is silent. set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" harness="$repo_dir/config/dot/quickshell/settings-system-harness.qml" system_settings="$repo_dir/config/dot/quickshell/services/SystemSettings.qml" wallpaper_service="$repo_dir/config/dot/quickshell/services/Wallpaper.qml" # Preferences are committed to $XDG_CONFIG_HOME, and the Home store lives under # $XDG_STATE_HOME. Both are isolated so this contract cannot touch the real # desktop's settings. The harness replaces the compositor write seam as well, # so interruption cannot leave the daily desktop modified. config_home="$(mktemp -d /tmp/panama-commit-config.XXXXXX)" state_home="$(mktemp -d /tmp/panama-commit-state.XXXXXX)" fail() { printf 'settings commit/reset contract: %s\n' "$1" >&2 exit 1 } rg -Fq 'HomePreferences.resetHomeDefaults();' "$system_settings" \ || fail 'restoreDefaults does not use the durable Home reset boundary' if rg -q 'HomePreferences\.(favorites|initialized)\s*=' "$system_settings"; then fail 'restoreDefaults mutates Home aliases instead of using resetHomeDefaults' fi rg -Fq 'DesktopPreferences.resetDesktopDefaults();' "$system_settings" \ || fail 'restoreDefaults does not clear confirmed display layout fields' rg -Fq 'Keybinds.applyReload();' "$system_settings" \ || fail 'restoreDefaults does not replay shipped keybindings' rg -Fq 'root.applyWallpaper(String(DesktopPreferences.get("wallpaperPath") ?? ""));' "$system_settings" \ || fail 'restoreDefaults does not visibly reapply the shipped wallpaper' rg -Fq 'LockScreen.regenerate();' "$system_settings" \ || fail 'restoreDefaults does not regenerate the shipped lock screen' rg -Fq 'const effectivePath = path === "" ? root.shippedPath : path;' "$wallpaper_service" \ || fail 'clearing wallpaper preference leaves the old image visible' rg -Fq 'storedPath: rawGlobal === root.shippedPath ? "" : rawGlobal' "$wallpaper_service" \ || fail 'the shipped wallpaper cannot remain represented by the default empty preference' qs_for_harness() { XDG_CONFIG_HOME="$config_home" XDG_STATE_HOME="$state_home" \ PANAMA_SETTINGS_TEST_ISOLATE_COMPOSITOR=1 qs -p "$harness" "$@" } restore() { qs_for_harness kill >/dev/null 2>&1 || true rm -rf "$config_home" "$state_home" } trap restore EXIT # This contract shares its harness file with settings-hyprland-write-contract, # and Quickshell identifies an instance by config path -- so if that run's # instance is still alive, the IPC wait below is satisfied by ITS target. That # direction is the dangerous one: this contract believes the compositor seam is # stubbed, so it would happily drive the DAILY DESKTOP's real compositor while # reporting isolation. Refuse to start rather than find out. harness_instances() { # rg -c prints nothing when there are no matches, so an unguarded # substitution yields "" rather than "0". local count count="$(qs list 2>/dev/null | rg -c "^ Config path: $harness\$" || true)" printf '%s' "${count:-0}" } for _ in $(seq 1 50); do [[ "$(harness_instances)" == "0" ]] && break sleep 0.1 done [[ "$(harness_instances)" == "0" ]] \ || fail 'another instance of the settings harness is still running -- this contract would drive it instead of its own isolated one, and that instance may be writing to the real compositor' XDG_CONFIG_HOME="$config_home" XDG_STATE_HOME="$state_home" \ PANAMA_SETTINGS_TEST_ISOLATE_COMPOSITOR=1 qs -p "$harness" --daemonize >/dev/null for _ in $(seq 1 40); do qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-system-test$' && break sleep 0.1 done qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-system-test$' || fail 'test IPC target did not start' # ── A local key is stored directly ─────────────────────────────────────────── [[ "$(qs_for_harness ipc call settings-system-test commit showSeconds false)" == "true" ]] \ || fail 'commitPreference refused a local key' [[ "$(qs_for_harness ipc call settings-system-test stored showSeconds)" == "false" ]] \ || fail 'a local key was not stored' # ── A compositor key reaches the verified apply boundary, then is stored ──── target_rounding=11 [[ "$(qs_for_harness ipc call settings-system-test commit windowRounding "$target_rounding")" == "true" ]] \ || fail 'commitPreference refused a compositor key' apply_state="$(qs_for_harness ipc call settings-system-test applyState)" jq -e '.[-1].windowRounding == 11' <<<"$apply_state" >/dev/null \ || fail "a compositor-backed commit did not reach the apply boundary: $apply_state" [[ "$(qs_for_harness ipc call settings-system-test stored windowRounding)" == "$target_rounding" ]] \ || fail 'a verified compositor commit was not stored' # ── A value the schema rejects is never stored ─────────────────────────────── before="$(qs_for_harness ipc call settings-system-test stored windowRounding)" [[ "$(qs_for_harness ipc call settings-system-test commit windowRounding 9999)" == "true" ]] \ || fail 'an out-of-range value should be clamped by the schema, not refused outright' [[ "$(qs_for_harness ipc call settings-system-test stored windowRounding)" != "9999" ]] \ || fail 'an out-of-range value was stored unclamped' [[ "$(qs_for_harness ipc call settings-system-test commit __not_a_setting__ 1)" == "false" ]] \ || fail 'commitPreference accepted a key outside the schema' # ── Reset spans every store, not just the schema one ───────────────────────── qs_for_harness ipc call settings-system-test seedHome >/dev/null qs_for_harness ipc call settings-system-test commit dockHideDelayMs 900 >/dev/null display_fixture='{"DP-2":{"mode":"4500x3000@60","scale":1.5,"transform":0,"x":0,"y":0,"primary":true},"HDMI-A-1":{"mode":"2560x1440@60","scale":1,"transform":0,"x":3000,"y":0,"primary":false}}' [[ "$(qs_for_harness ipc call settings-system-test commit displays "$display_fixture")" == "true" ]] \ || fail 'the protected display fixture did not apply' sleep 0.4 home_before="$(qs_for_harness ipc call settings-system-test homeState)" jq -e '.count == 1 and .initialized == true' <<<"$home_before" >/dev/null \ || fail "the Home fixture did not apply: $home_before" [[ "$(qs_for_harness ipc call settings-system-test stored dockHideDelayMs)" == "900" ]] \ || fail 'the dock fixture did not apply' [[ "$(qs_for_harness ipc call settings-system-test restoreDefaults)" == "true" ]] \ || fail 'restoreDefaults refused a safe reset' sleep 0.6 reset_state="$(qs_for_harness ipc call settings-system-test resetState)" jq -e '.calls == [ "display.block:true", "keybinds.reload", "wallpaper.set:", "lock.regenerate", "display.block:false" ] and .displayBlocked == false' <<<"$reset_state" >/dev/null \ || fail "reset did not safely replay non-reactive state: $reset_state" [[ "$(qs_for_harness ipc call settings-system-test stored dockHideDelayMs)" == "250" ]] \ || fail 'reset did not restore a schema default' for default_case in \ 'wallpaperMode|"single"' \ 'wallpaperSlideshowPaths|[]' \ 'wallpaperIntervalMinutes|30' \ 'wallpaperShuffle|true' \ 'wallpaperPerMonitor|{}' \ 'lockBackgroundMode|"screenshot"' \ 'lockBlurLevel|3' \ 'lockShowClock|true' \ 'lockShowDate|true' \ 'lockShowUser|true' \ 'lockFadeOnEmpty|false'; do IFS='|' read -r key expected <<<"$default_case" [[ "$(qs_for_harness ipc call settings-system-test stored "$key")" == "$expected" ]] \ || fail "reset did not restore $key to $expected" done [[ "$(qs_for_harness ipc call settings-system-test stored displays | jq -cS .)" == '{}' ]] \ || fail 'reset retained confirmed arrangement fields instead of returning startup to shipped placement' home_after="$(qs_for_harness ipc call settings-system-test homeState)" jq -e '.count == 0 and .initialized == false' <<<"$home_after" >/dev/null \ || fail "reset left the Home accessory store customized: $home_after" # Resetting a stored value does not itself apply compositor policy, so the last # isolated batch must contain the shipped default. jq -e '.appliedBatches[-1].windowRounding == 18' <<<"$reset_state" >/dev/null \ || fail "reset did not re-apply the compositor default: $reset_state" trap - EXIT restore printf 'settings commit/reset contract: PASS\n'