Make the Dock editable and add settings snapshots
The Dock's pinned applications were a sixteen-entry literal in Settings.qml, so changing what sits in the Dock meant editing QML. They are now an ordered list in the shared store, with move up, move down, unpin, and a filtered picker for adding installed applications. Keeping them in the shared store rather than a file of their own means they are covered by Restore defaults like everything else. This needed a "json" schema type for values the schema stores and resets but does not validate field by field. It exists so structured settings can live in the one file rather than growing a fourth preference store; the owning service validates the contents. Snapshots make the settings app safe to experiment with. The whole configuration is one file, so a backup is a copy and a restore is an overwrite, and restoring snapshots what it replaces so it is itself undoable. A snapshot is validated as JSON before it can be restored over a working configuration, and a name that is not a plain snapshot filename from the backup directory is refused. Snapshot names carry milliseconds. At one-second resolution a save followed promptly by a restore produced the same filename twice, and the restore's own safety snapshot overwrote the file it was about to read -- found by the contract, which restores immediately after saving. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Snapshots of the settings store.
|
||||
#
|
||||
# The restore path overwrites the file the whole desktop reads, so the
|
||||
# properties that matter are: a corrupt snapshot is never restored over a
|
||||
# working configuration, a restore snapshots what it replaces so it is itself
|
||||
# undoable, and a snapshot name cannot be used to reach a file outside the
|
||||
# backup directory.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
helper="$repo_dir/config/dot/quickshell/scripts/panama-settings-backup"
|
||||
work="$(mktemp -d /tmp/panama-backup-contract.XXXXXX)"
|
||||
|
||||
fail() {
|
||||
printf 'settings backup contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
cleanup() { rm -rf "$work"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
settings="$work/config/panama/settings.json"
|
||||
backups="$work/state/panama/backups"
|
||||
mkdir -p "$(dirname "$settings")"
|
||||
|
||||
run() { XDG_CONFIG_HOME="$work/config" XDG_STATE_HOME="$work/state" "$helper" "$@"; }
|
||||
|
||||
# ── Nothing to back up ───────────────────────────────────────────────────────
|
||||
run save >/dev/null 2>&1 && fail 'backing up a missing settings file reported success'
|
||||
[[ "$(run list)" == "[]" ]] || fail 'an empty backup directory did not list as empty'
|
||||
|
||||
# ── A snapshot round-trips ───────────────────────────────────────────────────
|
||||
printf '{"gapsOut":24,"windowRounding":6}' >"$settings"
|
||||
run save >/dev/null || fail 'save failed on a valid settings file'
|
||||
name="$(run list | jq -r '.[0].name')"
|
||||
[[ "$name" =~ ^settings-[0-9]{8}-[0-9]{9}\.json$ ]] || fail "unexpected snapshot name: $name"
|
||||
[[ "$(run list | jq -r '.[0].keys')" == "2" ]] || fail 'snapshot key count is wrong'
|
||||
|
||||
printf '{"gapsOut":99}' >"$settings"
|
||||
run restore "$name" >/dev/null || fail 'restore failed'
|
||||
[[ "$(jq -r .gapsOut "$settings")" == "24" ]] || fail 'restore did not bring back the snapshot contents'
|
||||
[[ "$(jq -r .windowRounding "$settings")" == "6" ]] || fail 'restore lost a key'
|
||||
|
||||
# ── Restoring snapshots what it replaced, so it is undoable ──────────────────
|
||||
count="$(run list | jq 'length')"
|
||||
[[ "$count" -ge 2 ]] || fail "restore did not snapshot the replaced settings (only $count snapshots)"
|
||||
|
||||
# ── A corrupt snapshot is refused ────────────────────────────────────────────
|
||||
bad="settings-19990101-000000000.json"
|
||||
mkdir -p "$backups"
|
||||
printf '{ truncated' >"$backups/$bad"
|
||||
run restore "$bad" >/dev/null 2>&1 && fail 'a corrupt snapshot was restored'
|
||||
[[ "$(jq -r .gapsOut "$settings")" == "24" ]] || fail 'a refused restore still damaged the settings file'
|
||||
|
||||
# ── A snapshot cannot name a path outside the backup directory ───────────────
|
||||
printf '{"pwned":true}' >"$work/outside.json"
|
||||
run restore "../../outside.json" >/dev/null 2>&1 && fail 'a traversing snapshot name was accepted'
|
||||
run restore "/etc/passwd" >/dev/null 2>&1 && fail 'an absolute snapshot path was accepted'
|
||||
jq -e 'has("pwned") | not' "$settings" >/dev/null || fail 'a file outside the backup directory was restored'
|
||||
|
||||
# ── A snapshot that is not listed is refused ─────────────────────────────────
|
||||
run restore "settings-20000101-000000000.json" >/dev/null 2>&1 && fail 'a missing snapshot was reported restored'
|
||||
|
||||
# ── Snapshots are capped ─────────────────────────────────────────────────────
|
||||
for _ in $(seq 1 20); do
|
||||
printf '{"n":%s}' "$RANDOM" >"$settings"
|
||||
run save >/dev/null
|
||||
done
|
||||
kept="$(run list | jq 'length')"
|
||||
[[ "$kept" -le 15 ]] || fail "snapshots are not capped: $kept kept"
|
||||
[[ "$kept" -ge 10 ]] || fail "snapshot pruning was too aggressive: only $kept kept"
|
||||
|
||||
trap - EXIT
|
||||
cleanup
|
||||
printf 'settings backup contract: PASS\n'
|
||||
Reference in New Issue
Block a user