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:
+90
@@ -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
|
||||
Reference in New Issue
Block a user