Files
Panama/config/dot/quickshell/config/DesktopPreferences.qml
T
Gabriel Brown d96863b687 Convert British spellings to American across the repo
colour -> color, behaviour -> behavior, centre -> center, favourite ->
favorite, and about twenty other pairs, applied consistently across
comments, docs, error/UI copy, and a handful of QML identifiers that
used the British spelling as their actual name: SystemSettings'
serialiseValue/serialiseTable/normaliseGradient, Displays'
normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's
serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's
_normalise helper, and ShortcutCapture's cancelled signal (with its
onCancelled handler in ShortcutsPage.qml). Every call site and the two
tests that assert on the literal source text (settings-ownership and
settings-backup-live contracts) were updated in lockstep.

Left untouched: config/dot/espanso/match/packages/misspell-en/ is a
vendored third-party autocorrect dictionary -- its entries are typo
corrections, not our prose, and rewriting them would fight the
package's own purpose (and any future re-sync from upstream).

The already-American `favorites` property (Home page pinned
accessories) was never actually misspelled -- only nearby comments and
error strings said "favourites" -- so no data migration was needed
there.

Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
2026-08-19 08:07:55 -04:00

178 lines
6.8 KiB
QML

pragma Singleton
// ─────────────────────────────────────────────────────────────────────────────
// Durable user choices, derived entirely from config/PreferenceSchema.qml.
//
// This object owns reading, validating, and writing. It knows nothing about
// which settings exist -- that is the schema's job -- so adding a setting never
// requires touching this file. That is the point: the previous implementation
// restated every key four times (a property alias, a JSON adapter property, a
// change handler, and a line in reset), and each omission failed silently.
//
// Reads go through get(), writes through set(). Typed accessors for the values
// the shell reads on every frame live in Settings.qml, which remains the stable
// public surface.
//
// The store lives at ~/.config/panama/settings.json rather than inside
// Quickshell's per-shell state directory, because the Hyprland Lua config reads
// the same file (see config/dot/hypr/prefs.lua) and because a user should be
// able to back it up, diff it, or keep it in a dotfiles repo.
// ─────────────────────────────────────────────────────────────────────────────
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string path: (Quickshell.env("XDG_CONFIG_HOME") || `${Quickshell.env("HOME")}/.config`) + "/panama/settings.json"
// Bumped on every accepted change. get() reads it so bindings built on get()
// have something to invalidate; a bare function call would otherwise capture
// no dependency and every reader would silently go stale.
property int revision: 0
// Everything currently on disk, including keys this build does not know
// about. Unknown keys are carried through untouched so that rolling back to
// an older Panama does not discard a newer version's settings.
property var values: ({})
property bool loaded: false
function get(key: string): var {
root.revision;
const stored = root.values[key];
if (stored === undefined)
return PreferenceSchema.defaultFor(key);
const coerced = PreferenceSchema.coerce(key, stored);
return coerced === undefined ? PreferenceSchema.defaultFor(key) : coerced;
}
// Returns false when the key is unknown or the value cannot be represented,
// so a caller can surface the rejection instead of assuming it took.
function set(key: string, value: var): bool {
if (!PreferenceSchema.has(key))
return false;
const coerced = PreferenceSchema.coerce(key, value);
if (coerced === undefined)
return false;
if (root.values[key] === coerced)
return true;
// Reassign rather than mutate: QML does not notify on in-place changes
// to a var property's contents.
const next = Object.assign({}, root.values);
next[key] = coerced;
root.values = next;
root.revision++;
persistTimer.restart();
return true;
}
// Restores every schema default in one write. Complete by construction --
// there is no hand-maintained list to fall out of sync with the schema.
function resetDesktopDefaults(): void {
const next = Object.assign({}, root.values, PreferenceSchema.defaults());
root.values = next;
root.revision++;
persistTimer.restart();
}
// Re-read the file from disk. Used after something outside the shell has
// rewritten it -- restoring a snapshot, or a hand edit -- so the running
// desktop reflects the new contents without waiting for the next change.
function reload(): void {
preferencesFile.reload();
root.load();
}
function load(): void {
let parsed = {};
try {
const text = preferencesFile.text();
if (text && text.trim().length > 0)
parsed = JSON.parse(text);
} catch (error) {
// A corrupt file must not cost the user a working desktop. Fall
// back to shipped defaults and let the next write replace it.
parsed = {};
}
const raw = (parsed && typeof parsed === "object") ? parsed : {};
// Upgrade before anything reads a value. A stored key the current
// schema no longer recognizes is carried through untouched and silently
// stops taking effect, so the conversion has to happen here rather than
// being noticed later by whoever owns that setting.
const result = Migrations.apply(raw);
root.values = result.values;
if (result.migrated)
console.info("Settings migrated from version", result.from, "to", result.to + ":",
result.applied.join("; "));
// Write whenever the version moved, which includes stamping a file
// written before versioning existed. Left unwritten, the stamp lives
// only in memory and is redone on every launch, and a migration that is
// not idempotent would compound.
if (result.changed)
persistTimer.restart();
root.revision++;
root.loaded = true;
}
FileView {
id: preferencesFile
path: root.path
blockLoading: true
printErrors: false
atomicWrites: true
onLoaded: root.load()
// No file yet is the normal first-run case, not an error.
onLoadFailed: root.load()
}
Component.onCompleted: {
migration.adopt();
root.load();
}
// Coalesce a burst of changes into one atomic write. Writing on every
// assignment races a second assignment and can reload the older value.
Timer {
id: persistTimer
interval: 0
onTriggered: preferencesFile.setText(JSON.stringify(root.values, null, 2) + "\n")
}
// One-time move from the pre-Stage-1 location inside Quickshell's state
// directory. Reads the old file only when the new one does not exist yet, so
// it can never overwrite newer settings, and never deletes the original.
QtObject {
id: migration
function adopt(): void {
if (preferencesFile.text())
return;
try {
const legacy = legacyFile.text();
if (legacy && legacy.trim().length > 0)
preferencesFile.setText(legacy);
} catch (error) {
// Nothing to migrate.
}
}
}
FileView {
id: legacyFile
path: Quickshell.stateDir + "/panama-settings.json"
blockLoading: true
printErrors: false
}
}