#!/usr/bin/env bash set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" fail() { printf 'settings pages contract: %s\n' "$1" >&2 exit 1 } pages=(Home Displays Connectivity Sound Notifications ScreenIntelligence Health About) for page in "${pages[@]}"; do page_file="$repo_dir/config/dot/quickshell/modules/settings/${page}Page.qml" [[ -f "$page_file" ]] || fail "${page}Page.qml is missing" root_type="$(awk ' /^import / { next } /^[[:space:]]*\/\// { next } /^[[:space:]]*$/ { next } match($0, /^[[:space:]]*([A-Za-z][A-Za-z0-9]*)[[:space:]]*\{/, found) { print found[1] exit } ' "$page_file")" [[ "$root_type" == "SettingsPage" ]] \ || fail "${page}Page.qml root is ${root_type:-unknown}, expected SettingsPage" ! rg -q '^[[:space:]]*Flickable[[:space:]]*\{' "$page_file" \ || fail "${page}Page.qml still copies the page Flickable scaffold" done require_row() { local file="$1" local row_type="$2" local setting="$3" python3 - "$file" "$row_type" "$setting" <<'PY' || \ fail "$(basename "$file") is missing $row_type for $setting" import re import sys text = open(sys.argv[1], encoding="utf-8").read() row_type = re.escape(sys.argv[2]) setting = re.escape(sys.argv[3]) pattern = rf"{row_type}\s*\{{(?:(?!\n\s*[A-Z][A-Za-z0-9]*\s*\{{).)*?setting\s*:\s*\"{setting}\"" raise SystemExit(0 if re.search(pattern, text, re.S) else 1) PY } home_page="$repo_dir/config/dot/quickshell/modules/settings/HomePage.qml" require_row "$home_page" ChoiceRow temperatureUnit require_row "$home_page" SliderRow weatherRefreshMinutes require_row "$home_page" SliderRow vitalsIntervalMs notifications_page="$repo_dir/config/dot/quickshell/modules/settings/NotificationsPage.qml" for setting in notificationTimeoutMs notificationTimeoutCriticalMs notificationHistoryLimit maxVisibleToasts; do require_row "$notifications_page" SliderRow "$setting" done python3 - "$notifications_page" <<'PY' || fail 'critical notification timeout does not render zero as Never' import re import sys text = open(sys.argv[1], encoding="utf-8").read() block = re.search( r'SliderRow\s*\{(?:(?!\n\s*[A-Z][A-Za-z0-9]*\s*\{).)*?' r'setting\s*:\s*"notificationTimeoutCriticalMs"(?P.*?)\n\s*\}', text, re.S, ) raise SystemExit(0 if block and re.search(r'zeroLabel\s*:\s*"Never"', block.group(0)) else 1) PY intelligence_page="$repo_dir/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml" require_row "$intelligence_page" ChoiceRow screenshotDir require_row "$intelligence_page" ChoiceRow recordingDir require_row "$intelligence_page" ChoiceRow recorderArgs # The source-only contract is safe during a shared Quickshell quiet window. # The existing compositor integration checks remain available explicitly. if [[ "${PANAMA_SETTINGS_STATIC_ONLY:-0}" == "1" ]]; then printf 'settings pages contract: PASS (static)\n' exit 0 fi state_home="$(mktemp -d /tmp/panama-settings-pages-state.XXXXXX)" source_config_path="$repo_dir/config/dot/quickshell" config_path="$state_home/quickshell" harness="$config_path/settings-pages-harness.qml" test_bin="$state_home/bin" shell_log="$state_home/quickshell.log" production_config_path="$HOME/.config/quickshell/shell.qml" harness_pid="" harness_shell_id="" production_before="" cleanup_bootstrap() { rm -rf "$state_home" } trap cleanup_bootstrap EXIT mkdir -p "$test_bin" cp -a "$source_config_path" "$config_path" python3 - "$config_path/shell.qml" "$harness" "$$" <<'PY' import sys source_path, harness_path, identity = sys.argv[1:] source = open(source_path, encoding="utf-8").read() needle = "ShellRoot {\n" replacement = ( needle + f' readonly property string settingsPagesHarnessIdentity: "settings-pages-contract-{identity}"\n' ) if source.count(needle) != 1: raise SystemExit("shell.qml does not have exactly one ShellRoot") with open(harness_path, "w", encoding="utf-8") as handle: handle.write(source.replace(needle, replacement, 1)) PY cat >"$config_path/scripts/panama-home-assistant" <<'EOF' #!/usr/bin/env bash set -euo pipefail case "${1:-}" in catalog) printf '%s\n' '{"ok":false,"error":"test-helper"}' ;; toggle|brightness) printf '%s\n' '{"ok":true}' ;; esac EOF chmod +x "$config_path/scripts/panama-home-assistant" cat >"$test_bin/hyprctl" <<'EOF' #!/usr/bin/env bash set -euo pipefail if [[ "${1:-}" == "-j" && "${2:-}" == "monitors" ]]; then printf '%s\n' '[{"focused":true,"name":"TEST-1","description":"Settings contract","width":1920,"height":1080,"refreshRate":60,"scale":1,"currentFormat":"XRGB8888","colorManagementPreset":"srgb","vrr":false}]' exit 0 fi if [[ "${1:-}" == "keyword" ]]; then exit 0 fi exec /usr/sbin/hyprctl "$@" EOF chmod +x "$test_bin/hyprctl" cat >"$test_bin/flatpak" <<'EOF' #!/usr/bin/env bash set -euo pipefail if [[ "${1:-}" == "info" ]]; then exit 1 fi exit 97 EOF chmod +x "$test_bin/flatpak" qs_for_test() { if [[ "${1:-}" == "ipc" && "$harness_pid" =~ ^[0-9]+$ ]]; then PATH="$test_bin:$PATH" XDG_STATE_HOME="$state_home" \ qs -p "$harness" ipc --pid "$harness_pid" "${@:2}" else PATH="$test_bin:$PATH" XDG_STATE_HOME="$state_home" \ qs -p "$harness" "$@" fi } instances_for_path() { local expected_path="$1" listing listing="$(qs list --all 2>/dev/null)" || return 1 awk -v expected="$expected_path" ' /^Instance / { pid = ""; shell_id = "" } /^[[:space:]]*Process ID:/ { pid = $3 } /^[[:space:]]*Shell ID:/ { shell_id = $3 } /^[[:space:]]*Config path:/ { path = $0 sub(/^[[:space:]]*Config path: /, "", path) if (path == expected && pid ~ /^[0-9]+$/ && shell_id != "") print pid "|" shell_id } ' <<<"$listing" } harness_identity_matches() { local current [[ "$harness_pid" =~ ^[0-9]+$ && -n "$harness_shell_id" ]] || return 1 current="$(instances_for_path "$harness")" || return 1 grep -Fxq "$harness_pid|$harness_shell_id" <<<"$current" } production_is_preserved() { local current record pid shell_id current="$(instances_for_path "$production_config_path")" || return 1 while IFS='|' read -r pid shell_id; do [[ -n "$pid" ]] || continue kill -0 "$pid" >/dev/null 2>&1 || return 1 record="$pid|$shell_id" grep -Fxq "$record" <<<"$current" || return 1 done <<<"$production_before" } stop_harness() { local remaining if harness_identity_matches; then kill "$harness_pid" >/dev/null 2>&1 || true for _ in $(seq 1 80); do ! kill -0 "$harness_pid" >/dev/null 2>&1 && break sleep 0.05 done if kill -0 "$harness_pid" >/dev/null 2>&1 && harness_identity_matches; then kill -KILL "$harness_pid" >/dev/null 2>&1 || true for _ in $(seq 1 20); do ! kill -0 "$harness_pid" >/dev/null 2>&1 && break sleep 0.05 done fi fi remaining="$(instances_for_path "$harness")" || return 1 harness_pid="" harness_shell_id="" [[ -z "$remaining" ]] } cleanup() { local cleanup_ok=0 stop_harness || cleanup_ok=1 production_is_preserved || cleanup_ok=1 if (( cleanup_ok == 0 )); then rm -rf "$state_home" else printf 'settings pages contract: isolated harness cleanup failed; retained %s\n' \ "$state_home" >&2 fi return "$cleanup_ok" } trap cleanup EXIT start_test_shell() { local harness_instances production_pid production_shell_id harness_instances="$(instances_for_path "$harness")" \ || fail 'could not inspect Quickshell instances before starting the runtime harness' [[ -z "$harness_instances" ]] \ || fail 'an unexpected process already uses the runtime harness path' for _attempt in 1 2; do qs_for_test --daemonize >"$shell_log" 2>&1 for _ in $(seq 1 80); do harness_instances="$(instances_for_path "$harness")" \ || fail 'could not inspect the runtime harness instance' [[ -n "$harness_instances" ]] && break sleep 0.05 done if [[ "$(wc -l <<<"$harness_instances")" == 1 && -n "$harness_instances" ]]; then IFS='|' read -r harness_pid harness_shell_id <<<"$harness_instances" [[ "$harness_pid" =~ ^[0-9]+$ ]] \ || fail 'runtime harness did not expose a numeric PID' while IFS='|' read -r production_pid production_shell_id; do [[ -n "$production_pid" ]] || continue [[ "$harness_shell_id" != "$production_shell_id" ]] || { stop_harness fail 'runtime harness shares a Shell ID with production' } done <<<"$production_before" production_is_preserved || { stop_harness fail 'production changed before isolated page routing began' } for _ in $(seq 1 80); do if qs_for_test ipc show 2>/dev/null | rg '^target settings$' >/dev/null; then return fi sleep 0.1 done fi stop_harness || fail 'failed runtime harness attempt did not stop cleanly' done sed -n '1,200p' "$shell_log" >&2 fail 'isolated branch shell did not start' } production_before="$(instances_for_path "$production_config_path")" \ || fail 'could not list Quickshell instances for the production baseline' production_is_preserved || fail 'could not capture a stable production instance set' start_test_shell qs_for_test ipc call home-assistant fixture ready >/dev/null shell_pid="$harness_pid" [[ "$shell_pid" =~ ^[0-9]+$ ]] || fail 'could not identify the branch shell process' pages=(home appearance displays connectivity home-phone desktop sound notifications screen-intelligence shortcuts services about) for page in "${pages[@]}"; do qs_for_test ipc call settings page "$page" >/dev/null for _ in $(seq 1 20); do [[ "$(qs_for_test ipc call settings status | jq -r .page)" == "$page" ]] && break sleep 0.1 done [[ "$(qs_for_test ipc call settings status | jq -r .page)" == "$page" ]] || fail "$page did not route" /usr/sbin/hyprctl -j clients | jq -e --argjson pid "$shell_pid" \ '[.[] | select(.pid == $pid and .title == "Settings" and .floating == false)] | length == 1' >/dev/null \ || fail "$page created a missing, floating, or duplicate Settings window" done qs_for_test ipc call settings page '__unsupported__' >/dev/null [[ "$(qs_for_test ipc call settings status | jq -r .page)" == "home" ]] || fail 'unsupported page did not fall back to Home' /usr/sbin/hyprctl -j binds | jq -e '.[] | select(.description == "Settings" and .key == "I" and .modmask == 64)' >/dev/null \ || fail 'Super+I is not registered as Panama Settings' /usr/sbin/hyprctl -j binds | jq -e '.[] | select(.description == "Screen Intelligence" and .key == "S" and .modmask == 65)' >/dev/null \ || fail 'Super+Shift+S is not registered as Screen Intelligence' desktop_file="$HOME/.local/share/applications/panama-settings.desktop" [[ -L "$desktop_file" || -f "$desktop_file" ]] || fail 'searchable desktop entry is not installed' desktop-file-validate "$desktop_file" >/dev/null || fail 'desktop entry is invalid' gnome_desktop_file="$HOME/.local/share/applications/gnome-settings-hyprland.desktop" [[ -L "$gnome_desktop_file" || -f "$gnome_desktop_file" ]] || fail 'searchable GNOME Settings fallback is not installed' desktop-file-validate "$gnome_desktop_file" >/dev/null || fail 'GNOME Settings fallback entry is invalid' intelligence_desktop_file="$HOME/.local/share/applications/panama-screen-intelligence.desktop" [[ -L "$intelligence_desktop_file" || -f "$intelligence_desktop_file" ]] || fail 'Screen Intelligence desktop entry is not installed' desktop-file-validate "$intelligence_desktop_file" >/dev/null || fail 'Screen Intelligence desktop entry is invalid' trap - EXIT cleanup || fail 'runtime harness did not stop without disturbing production' [[ ! -e "$state_home" ]] || fail 'temporary Settings state was not removed after shell exit' production_pids="$(cut -d'|' -f1 <<<"$production_before" | paste -sd, -)" [[ -n "$production_pids" ]] || production_pids="none" printf 'settings pages contract: PASS (production PIDs preserved: %s)\n' "$production_pids"