Files
Panama/config/dot/quickshell/config/Migrations.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

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 };
}
}