diff --git a/config/dot/quickshell/services/SettingsBackup.qml b/config/dot/quickshell/services/SettingsBackup.qml index 985b4dc..6d04241 100644 --- a/config/dot/quickshell/services/SettingsBackup.qml +++ b/config/dot/quickshell/services/SettingsBackup.qml @@ -181,7 +181,24 @@ Singleton { } } - Component.onCompleted: root.refresh() + Component.onCompleted: { + root.refresh(); + + // Restoring defaults is the only irreversible action Panama offers, and + // it lives in SystemSettings -- which must not reference this singleton, + // since this one already references it. So the capability is PUSHED + // there rather than pulled from here. + // + // Returns false when a snapshot is already running rather than queueing: + // the caller is about to wipe the stores, and a snapshot landing after + // that would record the wiped state as if it were the user's. + SystemSettings.takeSafetySnapshot = function() { + if (actionRun.running) + return false; + root.save(); + return true; + }; + } function refresh(): void { if (!listQuery.running) @@ -195,6 +212,7 @@ Singleton { actionRun.exec([root.helperPath, "save", root.serializeHomeState()]); } + function serializeHomeState(): string { const current = root.readHomeState(); const favorites = []; diff --git a/config/dot/quickshell/services/SystemSettings.qml b/config/dot/quickshell/services/SystemSettings.qml index b511e3d..a778514 100644 --- a/config/dot/quickshell/services/SystemSettings.qml +++ b/config/dot/quickshell/services/SystemSettings.qml @@ -38,6 +38,13 @@ Singleton { property var displayBusy: function() { return Displays.busy || Displays.awaitingConfirmation; } property var readDisplays: function() { return DesktopPreferences.get("displays"); } property var protectDisplays: function(value) { return DesktopPreferences.set("displays", value); } + + // Installed by SettingsBackup at startup. A default no-op rather than a + // direct reference, because SettingsBackup already references this + // singleton and a mutual reference between two singletons is an + // initialisation-order problem waiting to happen. Tests override it the + // same way they override the seams above. + property var takeSafetySnapshot: function() { return false; } property var setDisplayBlocked: function(blocked) { Displays.externalChangeBlocked = blocked; } property var reloadKeybinds: function() { Keybinds.applyReload(); } property var keybindsReloading: function() { return Keybinds.reloading; } @@ -509,6 +516,17 @@ Singleton { return false; } + // Snapshot before wiping. Restoring defaults clears every preference + // and the Home accessory store, and there is no undo for it anywhere in + // the app -- so the one automatic snapshot Panama takes is the one taken + // immediately before the only irreversible action it offers. + // + // Deliberately not fatal if it fails: a user who asked to reset should + // get their reset, and a snapshot that could not be written is reported + // rather than allowed to block the thing they asked for. + if (!root.takeSafetySnapshot()) + console.warn("SystemSettings: could not snapshot before restoring defaults"); + root.setDisplayBlocked(true); DesktopPreferences.resetDesktopDefaults(); diff --git a/config/dot/quickshell/settings-system-harness.qml b/config/dot/quickshell/settings-system-harness.qml index a500fc6..c8579dd 100644 --- a/config/dot/quickshell/settings-system-harness.qml +++ b/config/dot/quickshell/settings-system-harness.qml @@ -43,6 +43,12 @@ ShellRoot { root.recordReset("display.block:" + blocked); root.displayBlocked = blocked; }; + // Recorded so the contract can prove the snapshot happens BEFORE the + // stores are cleared, not merely that it happens. + SystemSettings.takeSafetySnapshot = function() { + root.recordReset("snapshot"); + return true; + }; SystemSettings.reloadKeybinds = function() { root.recordReset("keybinds.reload"); }; SystemSettings.keybindsReloading = function() { return false; }; SystemSettings.applyWallpaper = function(path) { root.recordReset("wallpaper.set:" + path); }; diff --git a/tests/quickshell/settings-commit-reset-contract.sh b/tests/quickshell/settings-commit-reset-contract.sh index 67e8510..d3a6b41 100755 --- a/tests/quickshell/settings-commit-reset-contract.sh +++ b/tests/quickshell/settings-commit-reset-contract.sh @@ -136,7 +136,14 @@ jq -e '.count == 1 and .initialized == true' <<<"$home_before" >/dev/null \ sleep 0.6 reset_state="$(qs_for_harness ipc call settings-system-test resetState)" +# The snapshot must come FIRST. Restoring defaults is the only irreversible +# action Panama offers, and a snapshot taken after the stores were cleared would +# faithfully record the wiped state as if it were the user's. +jq -e '.calls[0] == "snapshot"' <<<"$reset_state" >/dev/null \ + || fail "reset did not snapshot before wiping the stores: $reset_state" + jq -e '.calls == [ + "snapshot", "display.block:true", "keybinds.reload", "wallpaper.set:",