From d865cb74a15e0a6c4e624e345f15c3c786974847 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Sun, 23 Aug 2026 10:06:38 -0400 Subject: [PATCH] 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 --- .../quickshell/config/DesktopPreferences.qml | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/config/dot/quickshell/config/DesktopPreferences.qml b/config/dot/quickshell/config/DesktopPreferences.qml index fba6f16..a42f17d 100644 --- a/config/dot/quickshell/config/DesktopPreferences.qml +++ b/config/dot/quickshell/config/DesktopPreferences.qml @@ -130,6 +130,14 @@ Singleton { printErrors: false 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() // No file yet is the normal first-run case, not an error. onLoadFailed: root.load() @@ -145,7 +153,27 @@ Singleton { Timer { id: persistTimer 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