Screenshots and recordings offered three folders to choose between, and three
guesses cannot include the folder somebody's other software already writes to --
which is the only folder that matters. This machine has had ~/Pictures/Screenshots
and ~/Videos/Screencasts since long before Panama, and Panama was writing
recordings to a Videos/Recordings it invented. Both are free text now, and the
recording default is the folder that was already there.
Wallpapers were swept from four directories at once, so the distribution's stock
images arrived mixed in with the user's own and there was no way to ask for just
one. Where wallpapers live is something somebody knows about their own machine.
It is a setting, not a search.
All three accept an absolute path as well as one relative to home, which meant
fixing Capture: it prefixed $HOME unconditionally, so naming /mnt/captures would
have written screenshots to ~/mnt/captures and left nobody able to find them.
The generator turned out to skip any entry whose comment sits inside the braces
rather than above them -- it looks for `key:` immediately after `{`. Three
settings were invisible in the reference because of it, one of them dockScreens,
which has never appeared there at all. The staleness contract could not see it
either: regenerating reproduced the same omission, so the copy was current and
incomplete at once. It now counts what was declared against what it could read
and refuses rather than quietly documenting less than exists.
Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
343 lines
13 KiB
Bash
Executable File
343 lines
13 KiB
Bash
Executable File
#!/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
|
|
# vitalsIntervalMs moved to Appearance, beside the toggles it governs. It sat
|
|
# on Home while showCpu/showMemory/showGpu sat on Appearance -- one concept
|
|
# across two pages, which the ownership rule forbids and which made a search
|
|
# for it open a page that did not contain it.
|
|
appearance_page="$repo_dir/config/dot/quickshell/modules/settings/AppearancePage.qml"
|
|
require_row "$appearance_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<tail>.*?)\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"
|
|
# Free text, not a choice. Three preset folders could not include the one the
|
|
# rest of somebody's software already writes to, which is the only folder that
|
|
# matters -- so these became editable and the row type changed with them.
|
|
require_row "$intelligence_page" TextEntryRow screenshotDir
|
|
require_row "$intelligence_page" TextEntryRow 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"
|