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
178 lines
6.8 KiB
QML
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
|
|
}
|
|
}
|