Never let a stale settings copy erase a saved choice

The preferences store was loaded once at shell startup, and every save wrote
the whole in-memory copy back. Anything that reached the file after startup --
a hand edit, a script, another shell instance flushing during a session
handoff -- was erased by the next unrelated save. That is how a display scale
set to 1.5 kept coming back as 2: the entry was written, then silently
clobbered, and the catch-all auto rule filled the gap.

Two halves, either alone insufficient. The store now watches its file and
adopts outside writes instead of holding a stale copy over them; and a save
merges with what is on disk rather than overwriting it -- keys this shell has
set win, keys it has never seen survive. A setting changed anywhere now
persists until it is changed again, which is the only behavior a settings
file is entitled to have.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
This commit is contained in:
Gabriel Brown
2026-08-23 10:06:38 -04:00
parent a95c480c6a
commit d865cb74a1
@@ -130,6 +130,14 @@ Singleton {
printErrors: false printErrors: false
atomicWrites: true atomicWrites: true
// Adopt writes made from outside the shell -- a hand edit, a script,
// a restored snapshot -- instead of holding a stale copy in memory
// and silently erasing them at the next save. A change made anywhere
// must survive everywhere; the shell is the editor, not the owner.
// The shell's own atomic writes land here too and reload as a no-op.
watchChanges: true
onFileChanged: this.reload()
onLoaded: root.load() onLoaded: root.load()
// No file yet is the normal first-run case, not an error. // No file yet is the normal first-run case, not an error.
onLoadFailed: root.load() onLoadFailed: root.load()
@@ -145,7 +153,27 @@ Singleton {
Timer { Timer {
id: persistTimer id: persistTimer
interval: 0 interval: 0
onTriggered: preferencesFile.setText(JSON.stringify(root.values, null, 2) + "\n") // Merge with what is on disk rather than overwriting it. This model
// was loaded at startup; a key written to the file since then -- a
// hand edit, a script, another shell instance during a session
// handoff -- would otherwise be erased by the next unrelated save,
// which is how a setting "changed itself back". Keys this shell has
// set win; keys it has never seen survive.
onTriggered: {
let disk = {};
try {
const text = preferencesFile.text();
if (text && text.trim().length > 0)
disk = JSON.parse(text);
} catch (error) {
// An unreadable file loses the merge, never the write.
}
if (!disk || typeof disk !== "object")
disk = {};
const merged = Object.assign({}, disk, root.values);
root.values = merged;
preferencesFile.setText(JSON.stringify(merged, null, 2) + "\n");
}
} }
// One-time move from the pre-Stage-1 location inside Quickshell's state // One-time move from the pre-Stage-1 location inside Quickshell's state