Finish the wonderland: System told truthfully, in eight tabs instead of ten

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 23:31:52 -04:00
parent 9ffaf45a4d
commit be0e55214b
57 changed files with 5040 additions and 925 deletions
+102
View File
@@ -24,15 +24,44 @@ Singleton {
property string timezone: ""
property bool ntpEnabled: false
property bool ntpSynchronized: false
// Whether timedatectl has answered even once. The status query is async, so
// for the first moment of the shell's life `ntpEnabled` is false because
// nothing has looked, not because network time is off -- and setTime's
// whole job is to refuse while it is on. Treating "not looked yet" as "off"
// is the same wrong answer that looks fine as everywhere else in Panama.
property bool statusRead: false
// What timedatectl last said the three clocks read. localTime is what the
// Clock card shows and what seeds the manual-set field, so it is kept
// ticking (see clockTick below) rather than frozen at the last scan;
// universalTime and rtcTime are facts, shown as read.
property string localTime: ""
property string universalTime: ""
property string rtcTime: ""
property string lastError: ""
// Milliseconds between this shell's own clock and the one timedatectl
// reported. Normally zero -- both read the same system clock -- but
// deriving the displayed time from the system's own answer rather than
// from QML's assumption means a clock that moves under us shows the move.
property real systemOffsetMs: 0
// Set by the page while the Clock card is on screen. The tick is a local
// recomputation, never another timedatectl call: a process per second to
// learn a value the local clock already tracks exactly would be a
// remarkable amount of work for a second hand.
property bool tracking: false
property var zones: []
readonly property bool busy: statusQuery.running || zonesQuery.running || writeRun.running
// The shape `setTime` accepts, and the shape the field should be seeded
// with. Seconds are optional because "set it to 9:30" is a whole request.
readonly property var timePattern:
new RegExp("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}(?::\\d{2})?$")
// "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 {
@@ -102,10 +131,83 @@ Singleton {
root.ntpEnabled = value === "yes";
else if (key === "NTPSynchronized")
root.ntpSynchronized = value === "yes";
else if (key === "TimeUSec")
root.anchorClock(value);
else if (key === "RTCTimeUSec")
root.rtcTime = root.tidyStamp(value);
}
root.statusRead = true;
root.lastError = "";
}
// timedatectl prints "Mon 2026-08-24 21:22:55 EDT". The weekday is a
// duplicate of the date and the zone is already its own row, so what is
// left is the part a clock actually shows.
function tidyStamp(value: string): string {
const match = String(value).match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
return match ? match[1] : String(value).trim();
}
function anchorClock(value: string): void {
const stamp = root.tidyStamp(value);
// Parsed as local wall time, which is what timedatectl printed. A
// stamp this cannot read leaves the offset alone rather than jumping
// the displayed clock by whatever the misparse happened to produce.
const parsed = Date.parse(stamp.replace(" ", "T"));
root.systemOffsetMs = Number.isFinite(parsed) ? parsed - Date.now() : 0;
root.tickClock();
}
function tickClock(): void {
const now = new Date(Date.now() + root.systemOffsetMs);
root.localTime = Qt.formatDateTime(now, "yyyy-MM-dd HH:mm:ss");
// toISOString is already UTC, which is the whole question here; doing
// the offset arithmetic by hand is a way to get it wrong twice a year.
root.universalTime = now.toISOString().slice(0, 19).replace("T", " ") + " UTC";
}
Timer {
id: clockTick
interval: 1000
repeat: true
running: root.tracking
onTriggered: root.tickClock()
}
// Setting the clock by hand, which is only a coherent request while
// network time is off. With NTP on, timedatectl refuses outright and a
// toggle-then-set from the page would race the daemon putting the time
// back -- so the refusal happens here, where it can be explained, rather
// than as an opaque failure from a command the user did not type.
function setTime(iso: string): bool {
if (!root.statusRead) {
root.lastError = "The clock settings have not been read yet.";
root.refresh();
return false;
}
if (root.ntpEnabled) {
root.lastError = "Turn off network time before setting the clock by hand.";
return false;
}
const stamp = String(iso).trim();
if (!root.timePattern.test(stamp)) {
root.lastError = "Enter the time as YYYY-MM-DD HH:MM, with optional seconds.";
return false;
}
// Shape is not enough: "2026-13-45 99:99" matches the pattern and is
// not a moment. Round-tripping through Date is what rejects it.
const parsed = new Date(stamp.replace(" ", "T"));
if (!Number.isFinite(parsed.getTime())
|| Qt.formatDateTime(parsed, "yyyy-MM-dd HH:mm") !== stamp.slice(0, 16)) {
root.lastError = "That is not a real date and time.";
return false;
}
if (writeRun.running)
return false;
writeRun.exec(["timedatectl", "set-time", stamp]);
return true;
}
function refresh(): void {
if (!statusQuery.running)
statusQuery.running = true;