Finish the wonderland: System told truthfully, in eight tabs instead of ten

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 23:31:52 -04:00
parent 9ffaf45a4d
commit be0e55214b
57 changed files with 5040 additions and 925 deletions
@@ -39,6 +39,13 @@ KEEP = 15
SNAPSHOT_RE = re.compile(r"^settings-[0-9]{8}-[0-9]{9}\.json$")
ENTITY_RE = re.compile(r"^light\.[a-z0-9_]+$")
# A label somebody types, kept inside the envelope rather than in the filename.
# The name on disk stays the timestamp SNAPSHOT_RE describes: it is what
# orders the list, what prune and restore match against, and what confines a
# restore to this directory. A user-supplied filename would put all three of
# those in the caller's hands.
LABEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 ._-]{0,79}$")
class BackupError(RuntimeError):
pass
@@ -210,12 +217,27 @@ def clean_transaction_artifacts() -> None:
fsync_directory(TRANSACTION_PARENT)
# Quickshell's FileView writes atomically through QSaveFile, which stages at
# "<name>.XXXXXX" -- no leading dot, six random characters -- and renames. A
# shell killed mid-write leaves that file behind forever: nothing ever looks at
# it again, and ~/.config/panama slowly fills with half-remembered copies of
# the settings store. Panama's own writer uses the dotted prefixes above, so
# this pattern is only ever somebody else's leftover.
QSAVEFILE_RE = re.compile(r"^(settings\.json|panama-home\.json)\.[A-Za-z0-9]{6}$")
# A write in flight looks exactly like a leaked one. Only files older than this
# are swept, which is several orders of magnitude longer than a settings write
# takes and short enough that nobody accumulates them.
STALE_AFTER_SECONDS = 3600
def clean_stale_atomic_files() -> None:
locations = (
(SETTINGS.parent, (".settings.json.",)),
(HOME_STATE.parent, (".panama-home.json.",)),
(BACKUP_DIR, (".settings-",)),
)
now = time.time()
for directory, prefixes in locations:
if not directory.exists():
continue
@@ -223,8 +245,15 @@ def clean_stale_atomic_files() -> None:
fail(f"{directory} is not a safe directory.")
changed = False
for child in directory.iterdir():
if not any(child.name.startswith(prefix) for prefix in prefixes):
continue
owned = any(child.name.startswith(prefix) for prefix in prefixes)
if not owned:
if QSAVEFILE_RE.fullmatch(child.name) is None:
continue
try:
if now - child.stat().st_mtime < STALE_AFTER_SECONDS:
continue
except OSError:
continue
# Only Panama's hidden atomic-write names are eligible. A matching
# directory is unexpected and is never recursively removed.
if child.is_dir() and not child.is_symlink():
@@ -359,7 +388,23 @@ def prune_snapshots() -> None:
durable_remove(old)
def save_snapshot(*, require_any: bool, validate: bool) -> Path | None:
def sanitize_label(raw: str) -> str:
"""A typed name, reduced to what can safely sit in a JSON envelope.
Collapsed rather than refused: somebody who typed two spaces or a trailing
one meant the obvious thing, and losing their snapshot over it would be
absurd. Anything still outside the charset after that is refused, because
at that point they typed something this does not mean to carry.
"""
collapsed = " ".join(raw.split())
if not collapsed:
return ""
if LABEL_RE.fullmatch(collapsed) is None:
fail("That name cannot be used.")
return collapsed
def save_snapshot(*, require_any: bool, validate: bool, label: str = "") -> Path | None:
try:
desktop_present, desktop = current_store(
SETTINGS, "The current settings file"
@@ -386,6 +431,11 @@ def save_snapshot(*, require_any: bool, validate: bool) -> Path | None:
envelope["desktop"]["data"] = desktop
if home_present:
envelope["home"]["data"] = home
# Absent rather than empty when there is no label, so a snapshot taken
# automatically before a risky action is distinguishable from one somebody
# deliberately named "".
if label:
envelope["label"] = label
destination = next_snapshot_path()
atomic_write_json(destination, envelope)
@@ -515,6 +565,37 @@ def command_save(arguments: list[str]) -> None:
print(json.dumps({"saved": destination.name}, separators=(",", ":")))
def command_create(arguments: list[str]) -> None:
"""save, with a name on it.
Kept as its own verb rather than an optional argument to `save`: `save`
already takes live Home state as its first argument, and overloading that
position by type is how a Home payload eventually gets read as a label.
"""
label = sanitize_label(arguments[0]) if arguments else ""
if len(arguments) > 1:
write_live_home(arguments[1])
destination = save_snapshot(require_any=True, validate=True, label=label)
assert destination is not None
print(json.dumps({"saved": destination.name, "label": label},
separators=(",", ":")))
def command_delete(arguments: list[str]) -> None:
"""Remove one snapshot, named the way restore names one.
Goes through snapshot_source, which is what confines the name to this
directory -- the same gate a restore passes. A delete that resolved paths
its own way would be a second boundary to keep correct, and the weaker of
the two is the one that gets used.
"""
if not arguments:
fail("Which snapshot?")
source = snapshot_source(arguments[0])
durable_remove(source)
print(json.dumps({"deleted": source.name}, separators=(",", ":")))
def snapshot_files() -> list[Path]:
ensure_directory(BACKUP_DIR)
return sorted(
@@ -533,6 +614,7 @@ def snapshot_files() -> list[Path]:
def command_list() -> None:
output: list[dict[str, Any]] = []
for path in snapshot_files():
label = ""
try:
value = read_json(path, "A snapshot")
if is_v2_envelope(value):
@@ -540,6 +622,9 @@ def command_list() -> None:
keys = len(desktop["data"]) if desktop["present"] else 0
else:
keys = len(value)
raw_label = value.get("label")
if isinstance(raw_label, str) and LABEL_RE.fullmatch(raw_label):
label = raw_label
except BackupError:
keys = 0
raw = path.name.removeprefix("settings-").removesuffix(".json")
@@ -547,7 +632,14 @@ def command_list() -> None:
f"{raw[0:4]}-{raw[4:6]}-{raw[6:8]} "
f"{raw[9:11]}:{raw[11:13]}:{raw[13:15]}"
)
output.append({"name": path.name, "when": pretty, "keys": keys})
# Size on disk, so the page can say what fifteen snapshots actually
# cost rather than leaving it as an unbounded mystery.
try:
size = path.stat().st_size
except OSError:
size = 0
output.append({"name": path.name, "when": pretty, "keys": keys,
"bytes": size, "label": label})
print(json.dumps(output, separators=(",", ":")))
@@ -628,12 +720,17 @@ def main() -> None:
arguments = sys.argv[2:]
if command == "save":
command_save(arguments)
elif command == "create":
command_create(arguments)
elif command == "list":
command_list()
elif command == "restore":
command_restore(arguments)
elif command == "delete":
command_delete(arguments)
else:
fail("usage: panama-settings-backup [save|list|restore <name>]")
fail("usage: panama-settings-backup "
"[save|create [name]|list|restore <name>|delete <name>]")
if __name__ == "__main__":