Add wallpaper, power, date, accessibility, and real search
Continues the settings expansion toward replacing GNOME Settings for everything Panama actually owns. Wallpaper. A thumbnail grid rather than a path field: the value of this setting is the picture, so typing a path to something you cannot see is the worst version of it. Two things about hyprpaper 0.8 shaped this. Its IPC is much smaller than older documentation suggests -- preload, listloaded, unload, and reload all answer "invalid hyprpaper request", so setting is a single call with no preload. And the "<empty>,<path>" form that used to mean every output is silently ignored, so a wallpaper set that way appears to succeed and never changes; outputs are walked explicitly instead. hyprpaper.conf lives in the repo through the ~/.config/hypr symlink and so cannot hold machine state, which is why the choice lives in the shared settings store and is re-applied at startup. Power & Lock. hypridle has no IPC for reconfiguration and its config is hyprlang rather than the shared JSON, so scripts/panama-idle generates a config from the settings store and restarts the daemon. The generated file lives under XDG_STATE_HOME for the same symlink reason, with a systemd drop-in pointing hypridle at it. Management is a real state and the page says which one you are in rather than showing sliders that quietly do nothing. Zero means never for all three timers, which a naive template would render as "immediately". Date & Time. Deliberately not stored in Panama's settings: the timezone and network time belong to the machine and are shared with sessions that never see this file. Storing a copy would create a second answer to a question the system already answers. Reads and writes timedatectl directly; a cancelled polkit prompt surfaces as an error rather than as a value that appears to have been accepted. Accessibility. Pointer size and text scale have to agree across three consumers with no shared configuration -- the compositor, GTK, and the shell -- so the store is the source of truth and the values are pushed outward to gsettings and hyprctl setcursor. Search now indexes the schema instead of the twelve page labels. "gaps", "wallpaper", and "screenshot" previously found nothing on an app that has all three, which is the clearest way a settings app feels smaller than it is. Shortcuts are indexed by what they do. A contract asserts every non-internal schema label is reachable, so a new setting cannot be added in an undiscoverable state. The GNOME delegation allow-list was widened to the panel names gnome-control-center actually reports; the previous list contained "users", which is not one of them and so opened nothing. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# scripts/panama-idle generates hypridle's configuration from Panama's settings.
|
||||
#
|
||||
# This is the one generator that can cost the user their automatic screen lock,
|
||||
# so the properties that matter are: zero means never (rather than "immediately",
|
||||
# which a naive template would produce), values are clamped even when the
|
||||
# settings file has been hand-edited, and a missing or corrupt file still yields
|
||||
# a working configuration rather than an empty one.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
helper="$repo_dir/config/dot/quickshell/scripts/panama-idle"
|
||||
work="$(mktemp -d /tmp/panama-idle-contract.XXXXXX)"
|
||||
|
||||
fail() {
|
||||
printf 'idle config contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() { rm -rf "$work"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
generated="$work/state/panama/hypridle.conf"
|
||||
|
||||
run_with() {
|
||||
# Isolated config AND state, so this never touches the real generated file
|
||||
# and never restarts the user's hypridle: `apply` only restarts when the
|
||||
# systemd drop-in exists, and it cannot exist under this temporary root.
|
||||
mkdir -p "$work/config/panama"
|
||||
printf '%s' "$1" >"$work/config/panama/settings.json"
|
||||
XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" apply
|
||||
}
|
||||
|
||||
# ── Shipped defaults ─────────────────────────────────────────────────────────
|
||||
run_with '{}'
|
||||
grep -q 'timeout = 300' "$generated" || fail 'default screen blank is not 5 minutes'
|
||||
grep -q 'timeout = 600' "$generated" || fail 'default lock is not 10 minutes'
|
||||
grep -q 'before_sleep_cmd' "$generated" || fail 'lock before sleep missing by default'
|
||||
grep -q 'systemctl suspend' "$generated" && fail 'automatic suspend is on by default'
|
||||
|
||||
# ── Zero means never, not immediately ────────────────────────────────────────
|
||||
run_with '{"screenBlankMinutes":0,"lockMinutes":0,"suspendMinutes":0}'
|
||||
grep -q 'timeout = 0' "$generated" && fail 'a zero timeout was written as an immediate trigger'
|
||||
# `dpms` also appears in general.after_sleep_cmd, which is correct and must
|
||||
# stay; what must not exist is any listener at all.
|
||||
grep -qE '^listener' "$generated" && fail 'a listener was written when everything is set to never'
|
||||
grep -q 'after_sleep_cmd' "$generated" || fail 'the general block was lost when all timers are off'
|
||||
|
||||
# ── Values are honoured ──────────────────────────────────────────────────────
|
||||
run_with '{"screenBlankMinutes":2,"lockMinutes":7,"suspendMinutes":45}'
|
||||
grep -q 'timeout = 120' "$generated" || fail '2 minute blank not honoured'
|
||||
grep -q 'timeout = 420' "$generated" || fail '7 minute lock not honoured'
|
||||
grep -q 'timeout = 2700' "$generated" || fail '45 minute suspend not honoured'
|
||||
grep -q 'systemctl suspend' "$generated" || fail 'suspend listener missing when set'
|
||||
|
||||
# ── lockOnSleep off removes the pre-sleep lock ───────────────────────────────
|
||||
run_with '{"lockOnSleep":false}'
|
||||
grep -q 'before_sleep_cmd' "$generated" && fail 'pre-sleep lock present when disabled'
|
||||
|
||||
# ── Hand-edited nonsense is clamped, not passed through ──────────────────────
|
||||
run_with '{"screenBlankMinutes":99999,"lockMinutes":-40,"suspendMinutes":"soon"}'
|
||||
grep -q 'timeout = 7200' "$generated" || fail 'an above-range blank was not clamped to the maximum'
|
||||
grep -q 'timeout = 0' "$generated" && fail 'a negative lock produced an immediate trigger'
|
||||
grep -q 'systemctl suspend' "$generated" && fail 'a non-numeric suspend produced a listener'
|
||||
|
||||
# ── A corrupt or absent settings file still yields a working config ──────────
|
||||
run_with '{ not json at all'
|
||||
grep -q 'timeout = 300' "$generated" || fail 'a corrupt settings file did not fall back to defaults'
|
||||
rm -f "$work/config/panama/settings.json"
|
||||
XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" apply
|
||||
grep -q 'timeout = 300' "$generated" || fail 'an absent settings file did not fall back to defaults'
|
||||
|
||||
# ── status reports what it generated ─────────────────────────────────────────
|
||||
status="$(XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" status)"
|
||||
jq -e '.managed == false and .blankMinutes == 5 and .lockMinutes == 10' <<<"$status" >/dev/null \
|
||||
|| fail "status did not report the generated values: $status"
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'idle config contract: PASS\n'
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The sidebar search indexes the schema, not the page names.
|
||||
#
|
||||
# Before this, the field filtered twelve page labels, so "gaps", "wallpaper",
|
||||
# and "screenshot" all returned nothing on an app that has all three. That is
|
||||
# the single clearest way a settings app feels smaller than it is, and it
|
||||
# regresses invisibly: nothing breaks, results just quietly stop appearing.
|
||||
#
|
||||
# This pins that every schema entry is reachable by its own label, that
|
||||
# shortcuts are searchable by what they do, and that results route to a page
|
||||
# that actually exists.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
harness="$repo_dir/config/dot/quickshell/settings-search-harness.qml"
|
||||
|
||||
fail() {
|
||||
printf 'settings search contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
qs_for_harness() {
|
||||
qs -p "$harness" "$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
qs_for_harness kill >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
qs_for_harness --daemonize >/dev/null
|
||||
for _ in $(seq 1 40); do
|
||||
qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-search-test$' && break
|
||||
sleep 0.1
|
||||
done
|
||||
qs_for_harness ipc show 2>/dev/null | rg -q '^target settings-search-test$' || fail 'test IPC target did not start'
|
||||
|
||||
find_top() {
|
||||
qs_for_harness ipc call settings-search-test find "$1"
|
||||
}
|
||||
|
||||
# ── An empty query is navigation, not a search ───────────────────────────────
|
||||
[[ "$(find_top '' | jq -r .count)" == "0" ]] || fail 'an empty query returned results'
|
||||
|
||||
# ── Real settings are findable by what they are ──────────────────────────────
|
||||
while IFS='|' read -r query expect_label expect_page; do
|
||||
result="$(find_top "$query")"
|
||||
got_label="$(jq -r .top <<<"$result")"
|
||||
got_page="$(jq -r .topPage <<<"$result")"
|
||||
[[ "$got_label" == "$expect_label" ]] \
|
||||
|| fail "searching '$query' put '$got_label' first, expected '$expect_label'"
|
||||
[[ "$got_page" == "$expect_page" ]] \
|
||||
|| fail "searching '$query' routes to '$got_page', expected '$expect_page'"
|
||||
done <<'CASES'
|
||||
wallpaper|Wallpaper|appearance
|
||||
blur|Blur|appearance
|
||||
timezone|Timezone|datetime
|
||||
repeat delay|Repeat delay|shortcuts
|
||||
CASES
|
||||
|
||||
# ── Shortcuts are searchable by what they do ─────────────────────────────────
|
||||
[[ "$(find_top screenshot | jq -r .topPage)" == "shortcuts" ]] \
|
||||
|| fail 'searching a shortcut description did not route to the shortcuts page'
|
||||
|
||||
# ── Every non-internal schema entry is reachable by its own label ────────────
|
||||
# A setting that cannot be found by typing its name is a setting the user
|
||||
# cannot discover, which is the failure this whole page structure exists to fix.
|
||||
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
|
||||
missing=0
|
||||
while IFS= read -r label; do
|
||||
[[ -n "$label" ]] || continue
|
||||
count="$(find_top "$label" | jq -r .count)"
|
||||
if [[ "$count" == "0" ]]; then
|
||||
printf 'settings search contract: no result for schema label "%s"\n' "$label" >&2
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done < <(python3 - "$schema" <<'EXTRACT'
|
||||
import re, sys
|
||||
# Entries marked `internal` are shell state the user never edits, so they are
|
||||
# deliberately excluded from search. Extract only the user-facing labels.
|
||||
text = open(sys.argv[1]).read()
|
||||
for block in re.findall(r"\{\s*\n\s+key:.*?\n\s{8}\}", text, re.S):
|
||||
if "internal: true" in block:
|
||||
continue
|
||||
match = re.search(r'label: "([^"]+)"', block)
|
||||
if match:
|
||||
print(match.group(1))
|
||||
EXTRACT
|
||||
)
|
||||
[[ "$missing" -eq 0 ]] || fail "$missing schema label(s) are not findable by search"
|
||||
|
||||
# ── Results only ever route to pages the shell can open ──────────────────────
|
||||
allowed="$(grep -oE 'const allowed = \[[^]]*\]' "$repo_dir/config/dot/quickshell/services/ShellState.qml" \
|
||||
| grep -oE '"[a-z-]+"' | tr -d '"' | sort -u)"
|
||||
for query in wallpaper blur timezone screenshot pointer lock volume gaps; do
|
||||
page="$(find_top "$query" | jq -r .topPage)"
|
||||
grep -qx "$page" <<<"$allowed" || fail "search routed '$query' to unknown page '$page'"
|
||||
done
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'settings search contract: PASS\n'
|
||||
Reference in New Issue
Block a user