Restoring defaults clears every preference and the Home accessory store, and nothing in the app can undo it. Snapshots existed but were entirely manual, so the one irreversible action Panama offers was also the one with no safety net. It now snapshots first. Not fatal if that fails: someone who asked to reset gets their reset, and a snapshot that could not be written is reported rather than allowed to block what they asked for. The wiring is inverted deliberately. SettingsBackup already references SystemSettings, so referencing it back would make two singletons depend on each other, which is an initialisation-order problem waiting to happen. Instead SystemSettings exposes a seam defaulting to a no-op and SettingsBackup installs itself into it at startup -- the same shape as the seams the reset path already uses for test isolation. The contract asserts the snapshot is FIRST in the call sequence, not merely present. A snapshot taken after the stores were cleared would faithfully record the wiped state as the user's own, which is worse than no snapshot: it looks like a safety net and is a copy of the damage. Verified against both mutations -- removing the snapshot, and moving it after the wipe. One trap, hit for the third time today: QML allows only one Component.onCompleted per object, and SettingsBackup already had one. Adding a second does not fail locally -- it poisons the entire services module, so every singleton reports "Type X unavailable" and the real error is the last line of a forty-line cascade. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
140 lines
5.3 KiB
QML
140 lines
5.3 KiB
QML
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
|
|
import qs.config
|
|
import qs.services
|
|
|
|
ShellRoot {
|
|
id: root
|
|
|
|
property var resetCalls: []
|
|
property var appliedBatches: []
|
|
property bool displayBlocked: false
|
|
|
|
function recordReset(name: string): void {
|
|
const next = root.resetCalls.slice();
|
|
next.push(name);
|
|
root.resetCalls = next;
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
// Keep compositor verification entirely inside the isolated harness.
|
|
// Production applyOptions is covered separately by the Hyprland write
|
|
// contract; this seam proves commit/reset routing without changing the
|
|
// desktop that is running the test.
|
|
if (Quickshell.env("PANAMA_SETTINGS_TEST_ISOLATE_COMPOSITOR") === "1") {
|
|
SystemSettings.compositorApplyOverride = function(requested) {
|
|
const batches = root.appliedBatches.slice();
|
|
batches.push(requested);
|
|
root.appliedBatches = batches;
|
|
for (const key in requested)
|
|
DesktopPreferences.set(key, requested[key]);
|
|
return true;
|
|
};
|
|
}
|
|
SystemSettings.displayBusy = function() { return false; };
|
|
SystemSettings.readDisplays = function() { return DesktopPreferences.get("displays"); };
|
|
SystemSettings.protectDisplays = function(value) {
|
|
root.recordReset("display.protect");
|
|
return DesktopPreferences.set("displays", value);
|
|
};
|
|
SystemSettings.setDisplayBlocked = function(blocked) {
|
|
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); };
|
|
SystemSettings.regenerateLock = function() { root.recordReset("lock.regenerate"); };
|
|
SystemSettings.lockBusy = function() { return false; };
|
|
}
|
|
|
|
IpcHandler {
|
|
target: "settings-system-test"
|
|
|
|
function refresh(): void { SystemSettings.refresh(); }
|
|
|
|
// One batch, matching how the shell applies persisted policy. Three
|
|
// separate setters would be refused as overlapping writes, since each
|
|
// one is only complete after its value has been read back.
|
|
function apply(autoHdr: bool, vrr: int, directScanout: int): void {
|
|
SystemSettings.applyOptions({
|
|
autoHdr: autoHdr,
|
|
vrrPolicy: vrr,
|
|
directScanoutPolicy: directScanout
|
|
});
|
|
}
|
|
|
|
// Applies an arbitrary batch of schema keys, so the contract can cover
|
|
// every getoption answer shape -- int, bool, float, str, and the css
|
|
// box that gaps read back as -- not just the display policies.
|
|
function applyJson(payload: string): bool {
|
|
return SystemSettings.applyOptions(JSON.parse(payload));
|
|
}
|
|
|
|
// The routing entry point the settings rows use: a compositor-backed
|
|
// key must be applied and verified before it is stored, a local one is
|
|
// written directly. The contract checks both halves.
|
|
function commit(key: string, payload: string): bool {
|
|
return SystemSettings.commitPreference(key, JSON.parse(payload));
|
|
}
|
|
|
|
function stored(key: string): string {
|
|
return JSON.stringify(DesktopPreferences.get(key));
|
|
}
|
|
|
|
function seedHome(): void {
|
|
HomePreferences.favorites = [{ id: "light.contract_probe", alias: "Probe" }];
|
|
HomePreferences.initialized = true;
|
|
}
|
|
|
|
function homeState(): string {
|
|
return JSON.stringify({
|
|
count: HomePreferences.favorites.length,
|
|
initialized: HomePreferences.initialized
|
|
});
|
|
}
|
|
|
|
function restoreDefaults(): bool {
|
|
root.resetCalls = [];
|
|
return SystemSettings.restoreDefaults();
|
|
}
|
|
|
|
function resetState(): string {
|
|
return JSON.stringify({
|
|
calls: root.resetCalls,
|
|
displayBlocked: root.displayBlocked,
|
|
appliedBatches: root.appliedBatches
|
|
});
|
|
}
|
|
|
|
function applyState(): string {
|
|
return JSON.stringify(root.appliedBatches);
|
|
}
|
|
|
|
function panelAllowed(panel: string): bool {
|
|
return SystemSettings.isGnomePanelAllowed(panel);
|
|
}
|
|
|
|
function status(): string {
|
|
return JSON.stringify({
|
|
monitorName: SystemSettings.monitorName,
|
|
monitorDescription: SystemSettings.monitorDescription,
|
|
width: SystemSettings.monitorWidth,
|
|
height: SystemSettings.monitorHeight,
|
|
scale: SystemSettings.monitorScale,
|
|
format: SystemSettings.monitorFormat,
|
|
busy: SystemSettings.busy,
|
|
lastError: SystemSettings.lastError
|
|
});
|
|
}
|
|
}
|
|
}
|