Files
Panama/config/dot/quickshell/services/SettingsSync.qml
T
Gabriel Brown de45f205ad Carry settings between machines by allow-list, not by stripping
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
2026-08-20 11:02:42 -04:00

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()
}
}
}