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:
Gabriel Brown
2026-08-18 00:59:08 -04:00
parent 6377bfb8fd
commit 150f3cdb09
10 changed files with 541 additions and 19 deletions
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Snapshots of the Panama settings store.
#
# The whole desktop configuration is one JSON file, which makes a backup a copy
# and a restore an overwrite. That is worth exposing: the settings app now
# changes real things -- compositor geometry, idle timeouts, the dock -- and
# being able to get back to a known-good state without hunting through git is
# the difference between experimenting freely and being cautious.
#
# panama-settings-backup save snapshot the current settings
# panama-settings-backup list JSON list of snapshots, newest first
# panama-settings-backup restore <name> replace settings with a snapshot
#
# Snapshots are validated as JSON on the way in and on the way out, so a
# truncated file can never be restored over a working configuration.
#
# 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 very file it was about to read.
set -euo pipefail
settings="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
backup_dir="${XDG_STATE_HOME:-$HOME/.local/state}/panama/backups"
keep=15
fail() {
printf '%s\n' "$1" >&2
exit 1
}
case "${1:-list}" in
save)
[[ -r "$settings" ]] || fail "No settings file to back up."
jq -e . "$settings" >/dev/null 2>&1 || fail "The current settings file is not valid JSON."
mkdir -p "$backup_dir"
stamp="$(date +%Y%m%d-%H%M%S%3N)"
cp "$settings" "$backup_dir/settings-$stamp.json"
# Keep the most recent few. A snapshot per change would otherwise grow
# without bound in a directory nobody ever looks at.
ls -1t "$backup_dir"/settings-*.json 2>/dev/null | tail -n +$((keep + 1)) | while read -r old; do
rm -f "$old"
done
printf '{"saved":"settings-%s.json"}\n' "$stamp"
;;
list)
mkdir -p "$backup_dir"
first=true
printf '['
for file in $(ls -1t "$backup_dir"/settings-*.json 2>/dev/null); do
name="$(basename "$file")"
# settings-20260818-004512.json -> 2026-08-18 00:45
raw="${name#settings-}"; raw="${raw%.json}"
pretty="${raw:0:4}-${raw:4:2}-${raw:6:2} ${raw:9:2}:${raw:11:2}:${raw:13:2}"
keys="$(jq -r 'keys | length' "$file" 2>/dev/null || printf 0)"
[[ "$first" == true ]] || printf ','
first=false
printf '{"name":"%s","when":"%s","keys":%s}' "$name" "$pretty" "$keys"
done
printf ']\n'
;;
restore)
name="${2:-}"
[[ -n "$name" ]] || fail "Which snapshot?"
# Only a bare filename from the backup directory, so a caller cannot
# walk out of it with a path.
[[ "$name" =~ ^settings-[0-9]{8}-[0-9]{9}\.json$ ]] || fail "Not a snapshot name."
source_file="$backup_dir/$name"
[[ -r "$source_file" ]] || fail "That snapshot is missing."
jq -e . "$source_file" >/dev/null 2>&1 || fail "That snapshot is not valid JSON."
# Snapshot what is being replaced, so restore is itself undoable.
if [[ -r "$settings" ]] && jq -e . "$settings" >/dev/null 2>&1; then
mkdir -p "$backup_dir"
cp "$settings" "$backup_dir/settings-$(date +%Y%m%d-%H%M%S%3N).json"
fi
mkdir -p "$(dirname "$settings")"
cp "$source_file" "$settings.tmp"
mv "$settings.tmp" "$settings"
printf '{"restored":"%s"}\n' "$name"
;;
*)
fail "usage: panama-settings-backup [save|list|restore <name>]"
;;
esac