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
134 lines
4.4 KiB
QML
134 lines
4.4 KiB
QML
pragma Singleton
|
|
|
|
// System date, time, and timezone.
|
|
//
|
|
// Deliberately NOT backed by the Panama settings store. The timezone and the
|
|
// network-time setting belong to the machine, not to this desktop: they are
|
|
// shared with every other session and with services that never see Panama's
|
|
// JSON. Storing a copy would create a second answer to a question the system
|
|
// already answers, which is the exact failure this settings rewrite exists to
|
|
// remove. So this reads and writes `timedatectl` directly and holds no state of
|
|
// its own beyond what it last observed.
|
|
//
|
|
// Setting the timezone or toggling NTP needs privilege. timedatectl asks
|
|
// polkit, which shows the usual authentication dialog; on refusal the command
|
|
// fails and the error is surfaced rather than the UI pretending it worked.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
property string timezone: ""
|
|
property bool ntpEnabled: false
|
|
property bool ntpSynchronized: false
|
|
property string localTime: ""
|
|
property string universalTime: ""
|
|
property string rtcTime: ""
|
|
property string lastError: ""
|
|
|
|
property var zones: []
|
|
|
|
readonly property bool busy: statusQuery.running || zonesQuery.running || writeRun.running
|
|
|
|
// "America/New_York" -> "New York" for display, keeping the region as a
|
|
// separate field so the list can be grouped and searched sensibly.
|
|
function regionOf(zone: string): string {
|
|
const slash = zone.indexOf("/");
|
|
return slash < 0 ? zone : zone.slice(0, slash);
|
|
}
|
|
|
|
function cityOf(zone: string): string {
|
|
const slash = zone.indexOf("/");
|
|
return (slash < 0 ? zone : zone.slice(slash + 1)).replace(/_/g, " ");
|
|
}
|
|
|
|
Process {
|
|
id: statusQuery
|
|
command: ["timedatectl", "show",
|
|
"-p", "Timezone", "-p", "NTP", "-p", "NTPSynchronized",
|
|
"-p", "TimeUSec", "-p", "RTCTimeUSec"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.parseStatus(this.text)
|
|
}
|
|
onExited: (exitCode, exitStatus) => {
|
|
if (exitCode !== 0)
|
|
root.lastError = "Could not read the system clock settings.";
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: zonesQuery
|
|
command: ["timedatectl", "list-timezones"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
root.zones = this.text.split("\n")
|
|
.map(line => line.trim())
|
|
.filter(line => line.length > 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: writeRun
|
|
onExited: (exitCode, exitStatus) => {
|
|
// A polkit refusal and a bad value both land here. Neither should
|
|
// leave the UI showing a value the system did not take, so the
|
|
// status is re-read either way.
|
|
root.lastError = exitCode === 0
|
|
? ""
|
|
: "The system rejected that change, or authentication was canceled.";
|
|
root.refresh();
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
root.refresh();
|
|
zonesQuery.running = true;
|
|
}
|
|
|
|
function parseStatus(text: string): void {
|
|
for (const line of text.split("\n")) {
|
|
const split = line.indexOf("=");
|
|
if (split < 0)
|
|
continue;
|
|
const key = line.slice(0, split);
|
|
const value = line.slice(split + 1);
|
|
if (key === "Timezone")
|
|
root.timezone = value;
|
|
else if (key === "NTP")
|
|
root.ntpEnabled = value === "yes";
|
|
else if (key === "NTPSynchronized")
|
|
root.ntpSynchronized = value === "yes";
|
|
}
|
|
root.lastError = "";
|
|
}
|
|
|
|
function refresh(): void {
|
|
if (!statusQuery.running)
|
|
statusQuery.running = true;
|
|
}
|
|
|
|
// Only a timezone the system itself listed is ever passed on, so no
|
|
// caller-supplied text reaches the command.
|
|
function setTimezone(zone: string): bool {
|
|
if (root.zones.indexOf(zone) < 0) {
|
|
root.lastError = "That is not a timezone this system recognizes.";
|
|
return false;
|
|
}
|
|
if (writeRun.running)
|
|
return false;
|
|
writeRun.exec(["timedatectl", "set-timezone", zone]);
|
|
return true;
|
|
}
|
|
|
|
function setNtp(enabled: bool): bool {
|
|
if (writeRun.running)
|
|
return false;
|
|
writeRun.exec(["timedatectl", "set-ntp", enabled ? "true" : "false"]);
|
|
return true;
|
|
}
|
|
}
|