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
122 lines
5.5 KiB
QML
122 lines
5.5 KiB
QML
pragma Singleton
|
|
|
|
// Versioned upgrades for the settings file.
|
|
//
|
|
// The schema is the single source of truth for what a setting IS, but it cannot
|
|
// describe what a setting USED to be. Renaming a key, changing its units, or
|
|
// splitting one setting into two all leave a stored value that the new schema
|
|
// does not recognize -- and an unrecognized key is silently carried through
|
|
// untouched, so the user's choice simply stops taking effect with nothing to
|
|
// say why. That is the failure this exists to prevent.
|
|
//
|
|
// HOW IT WORKS
|
|
//
|
|
// settings.json carries a schemaVersion. On load, every migration with a
|
|
// version ABOVE the stored one runs in order, then the file is stamped with
|
|
// `current`. A file with no schemaVersion at all is a file written before this
|
|
// existed; it is stamped at `baseline` and NOT migrated, because those
|
|
// migrations were never written for it.
|
|
//
|
|
// WRITING ONE
|
|
//
|
|
// { version: 2, describe: "rename dockDelay to dockHideDelayMs",
|
|
// migrate: values => { ... return values; } }
|
|
//
|
|
// Rules that make this safe to run against a real user's file:
|
|
//
|
|
// * migrate() receives the whole values object and returns it. Mutating and
|
|
// returning the same object is fine.
|
|
// * NEVER delete a key you are not replacing. Unknown keys are deliberately
|
|
// preserved so that rolling back to an older Panama does not discard a
|
|
// newer version's settings, and a migration is the one place that promise
|
|
// could quietly be broken.
|
|
// * A migration must tolerate its input being absent or the wrong type. It
|
|
// runs against files written by every previous version, including ones
|
|
// that were hand-edited.
|
|
// * Migrations never run twice: the stored version only moves forward.
|
|
|
|
import QtQuick
|
|
|
|
QtObject {
|
|
id: root
|
|
|
|
// What a file written today is stamped with. Bump this when adding a
|
|
// migration, to the version of the migration you added.
|
|
readonly property int current: 1
|
|
|
|
// Files predating versioning are stamped here without being migrated.
|
|
readonly property int baseline: 1
|
|
|
|
readonly property string versionKey: "schemaVersion"
|
|
|
|
// Ordered by version. Empty is the correct state until the first breaking
|
|
// schema change -- this exists so that change is a routine edit rather than
|
|
// an emergency.
|
|
readonly property var steps: []
|
|
|
|
function storedVersion(values: var): int {
|
|
const raw = values ? values[root.versionKey] : undefined;
|
|
return (typeof raw === "number" && isFinite(raw)) ? Math.floor(raw) : 0;
|
|
}
|
|
|
|
// Returns { values, migrated, changed, from, to, applied }.
|
|
//
|
|
// `applied` names each step that ran, so the caller can log something
|
|
// meaningful rather than "settings changed somehow". `changed` is the one
|
|
// the caller should write on: stamping a pre-versioning file changes it
|
|
// without running any step, and left unwritten the stamp would live only in
|
|
// memory and be redone on every launch.
|
|
function apply(values: var): var {
|
|
return root.applyWith(values, root.steps, root.current, root.baseline);
|
|
}
|
|
|
|
// The same logic with the step list injected, so the machinery can be
|
|
// tested against fixture migrations. The real list is empty until the first
|
|
// breaking schema change, and a mechanism that has never run against a
|
|
// failing step is not one to find out about during an upgrade.
|
|
function applyWith(values: var, steps: var, current: int, baseline: int): var {
|
|
const safe = (values && typeof values === "object") ? values : {};
|
|
const from = root.storedVersion(safe);
|
|
|
|
// No version: written before versioning existed. Stamp it and stop.
|
|
// Running the migration list against it would apply upgrades designed
|
|
// for schemas this file never had.
|
|
if (from === 0) {
|
|
safe[root.versionKey] = baseline;
|
|
return { values: safe, migrated: false, changed: true, from: 0, to: baseline, applied: [] };
|
|
}
|
|
|
|
// A file from a NEWER Panama. Left completely alone: downgrading its
|
|
// keys is not something this can do correctly, and unknown keys are
|
|
// already preserved, so the older build simply ignores what it does not
|
|
// understand.
|
|
if (from > current)
|
|
return { values: safe, migrated: false, changed: false, from: from, to: from, applied: [] };
|
|
|
|
const applied = [];
|
|
let working = safe;
|
|
for (const step of steps) {
|
|
if (step.version <= from || step.version > current)
|
|
continue;
|
|
try {
|
|
const result = step.migrate(working);
|
|
if (result && typeof result === "object")
|
|
working = result;
|
|
applied.push(step.version + ": " + step.describe);
|
|
} catch (error) {
|
|
// One bad migration must not cost the user every setting. Stop
|
|
// at the last good version so the next launch retries from
|
|
// here rather than skipping the failed step forever.
|
|
console.warn("Migrations: step", step.version, "failed:", error);
|
|
working[root.versionKey] = step.version - 1;
|
|
return { values: working, migrated: applied.length > 0, changed: applied.length > 0,
|
|
from: from, to: step.version - 1, applied: applied };
|
|
}
|
|
}
|
|
|
|
working[root.versionKey] = current;
|
|
return { values: working, migrated: applied.length > 0, changed: from !== current,
|
|
from: from, to: current, applied: applied };
|
|
}
|
|
}
|