panama-settings-backup already snapshots this machine so it can be put back exactly as it was, arrangement and all. This is the other thing: an export meant to travel, carrying the preferences that describe taste rather than hardware. The export is an allow-list read from the preference schema rather than a deny-list of things to remove. A key added later that happens to hold a token cannot leak into a file somebody emails to themselves; being wrong in this direction loses a setting, being wrong the other way publishes a secret. It earned that immediately -- this machine's store holds an orphaned shadowOffset from a setting that no longer exists anywhere in the source, and it was left behind without anyone having to know about it. Three settings stay: the display arrangement, which is keyed by output names that mean nothing elsewhere; the last page opened, which is session noise; and schemaVersion, which belongs to the store rather than to a person. Import is a merge, so settings a file does not mention are left alone, and it is idempotent. Two bugs made and caught here, in opposite directions. Validation missed 36 settings because "real" was spelled "float" and enums fell through entirely, so an out-of-range or nonsense value would have been written straight into the store. Correcting that then broke numeric enums -- vrrPolicy is an enum of 0..3 and the options were read with a regex that only matched quoted values, so those settings had no known choices, were declared unverifiable and were refused: valid settings dropped silently in transit. The contract could not see the second one. It checked only that bad values are refused, and when numeric enums were unreadable they never reached the bundle at all, so every "did it arrive" assertion was satisfied by their absence. It now requires the export to carry what it should as well as withhold what it should not, and was verified to fail in both directions. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
76 lines
2.7 KiB
QML
76 lines
2.7 KiB
QML
pragma Singleton
|
|
|
|
// Carrying settings to another machine.
|
|
//
|
|
// Distinct from SettingsBackup, which snapshots this machine so it can be put
|
|
// back exactly as it was. This produces something meant to travel: the
|
|
// preferences that describe taste rather than hardware.
|
|
//
|
|
// The helper exports by allow-list from the preference schema, so a key added
|
|
// later that happens to hold a token cannot leak into a file somebody emails to
|
|
// themselves, and validates everything again on arrival.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-settings-sync"
|
|
|
|
// Where an export lands by default. Somewhere a person will find it again.
|
|
readonly property string defaultPath:
|
|
(Quickshell.env("HOME") ?? "") + "/panama-settings.json"
|
|
|
|
property string lastError: ""
|
|
property string lastAction: ""
|
|
property int carried: 0
|
|
property int applied: 0
|
|
property var left: []
|
|
property var changes: []
|
|
property var skipped: []
|
|
property string exportedFrom: ""
|
|
property bool previewed: false
|
|
|
|
readonly property bool busy: worker.running
|
|
|
|
function run(action: string, path: string): void {
|
|
if (worker.running)
|
|
return;
|
|
root.lastError = "";
|
|
root.lastAction = action;
|
|
worker.command = [root.helperPath, action, path];
|
|
worker.running = true;
|
|
}
|
|
|
|
function exportTo(path: string): void { root.run("export", path); }
|
|
function preview(path: string): void { root.run("preview", path); }
|
|
function importFrom(path: string): void { root.run("import", path); }
|
|
|
|
function absorb(text: string): void {
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
root.lastError = String(parsed.error ?? "");
|
|
root.carried = Number(parsed.carried ?? 0);
|
|
root.applied = Number(parsed.applied ?? 0);
|
|
root.left = Array.isArray(parsed.left) ? parsed.left : [];
|
|
root.changes = Array.isArray(parsed.changes) ? parsed.changes : [];
|
|
root.skipped = Array.isArray(parsed.skipped) ? parsed.skipped : [];
|
|
root.exportedFrom = String(parsed.exportedFrom ?? "");
|
|
root.previewed = root.lastAction === "preview" && root.lastError === "";
|
|
} catch (error) {
|
|
root.lastError = "Could not read the result.";
|
|
console.warn("SettingsSync: could not parse helper output:", error);
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: worker
|
|
stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
|
|
stderr: StdioCollector {
|
|
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
|
|
}
|
|
}
|
|
}
|