#!/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 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 ]" ;; esac