Finding a zone meant already knowing its name, which is the wrong way round when the thing you know is the country -- Ghana's zone is named after Accra, and nothing in the picker said so. Zones now carry the countries they cover, drawn from tzdata's own tables: zone1970.tab and zone.tab for the zone-to-country-code rows, iso3166.tab for the names. The picker already matched on description as well as label, so putting the countries there makes them searchable and shows them in the list. Those tables only name about 420 of the ~600 zones; the rest are backward-compatibility aliases like America/Buenos_Aires and US/Pacific, which tzdata ships as hard links to their canonical zone. Matching an alias to its canonical name by inode lifts coverage to 553, and the 45 left are the Etc/GMT pseudo-zones, which have no country by definition. The inherited countries are the union of every zone sharing the inode, not the first one found: those siblings are one clock under several countries' names, and each table names a different subset, so first-match made Africa/Asmera findable as Ethiopia but not as Eritrea. Joining is done in ClockModel against a fixture, so it does not depend on the host's tzdata version; the shell side only reads the files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MhxtL7WzL5XmuTd8GHbcTY
121 lines
4.5 KiB
QML
121 lines
4.5 KiB
QML
import QtQuick
|
|
import Quickshell.Io
|
|
import "ClockModel.js" as Model
|
|
|
|
// Supplies the two things QML cannot work out for itself: the current UTC
|
|
// offset of each configured zone, and the list of zones the picker can offer.
|
|
//
|
|
// Qt's QML engine ships no `Intl`, so there is no in-process way to render an
|
|
// IANA zone. Rather than bundle a copy of the tz database, this asks the
|
|
// system one through `date`, which keeps the bar correct across DST changes
|
|
// and tzdata updates without any code of ours needing to know the rules.
|
|
//
|
|
// The probe is cheap (one short-lived shell, a few `date` calls) and its
|
|
// result stays valid until a transition, so it runs on a slow timer instead of
|
|
// on every tick — the clocks themselves are advanced by arithmetic in between.
|
|
QtObject {
|
|
id: root
|
|
|
|
// zone name -> "+0200". A zone absent from here failed to resolve and is
|
|
// rendered as unknown rather than being silently shown as UTC.
|
|
property var offsets: ({})
|
|
property string localOffset: ""
|
|
// [{ zone, countries }] for the picker: the zone name plus the countries it
|
|
// covers, so it can be found by country as well as by name.
|
|
property var allZones: []
|
|
property bool ready: false
|
|
|
|
property var zones: []
|
|
|
|
signal updated()
|
|
|
|
// Skipping zones tzdata does not have keeps a typo out of the output
|
|
// entirely, which is what lets the model tell "unknown zone" apart from
|
|
// "genuinely UTC". Zone names arrive as argv, never interpolated into the
|
|
// script, and the model has already filtered them to the tzdata name shape.
|
|
readonly property string probeScript:
|
|
'printf "%s\\t%s\\n" LOCAL "$(date +%z)"\n' +
|
|
'for z in "$@"; do\n' +
|
|
' [ -e "/usr/share/zoneinfo/$z" ] || continue\n' +
|
|
' printf "%s\\t%s\\n" "$z" "$(TZ="$z" date +%z)"\n' +
|
|
'done'
|
|
|
|
// A probe requested while one is in flight is remembered, not dropped.
|
|
// Config changes arrive as a burst of binding updates, so the request that
|
|
// actually carries the newly added zone is usually not the first one — and
|
|
// dropping it left that clock reading "--:--" until the ten-minute timer
|
|
// came round.
|
|
property bool probePending: false
|
|
|
|
function probe() {
|
|
if (probeProc.running) {
|
|
root.probePending = true
|
|
return
|
|
}
|
|
root.probePending = false
|
|
probeProc.command = ["sh", "-c", root.probeScript, "sh"].concat(root.zones)
|
|
probeProc.running = true
|
|
}
|
|
|
|
onZonesChanged: probe()
|
|
|
|
property Process probeProc: Process {
|
|
// Deferred so `running` has settled to false before the next probe starts.
|
|
onExited: if (root.probePending) Qt.callLater(root.probe)
|
|
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: {
|
|
var report = Model.parseOffsetReport(text)
|
|
var local = report["LOCAL"]
|
|
delete report["LOCAL"]
|
|
if (local) root.localOffset = local
|
|
root.offsets = report
|
|
root.ready = true
|
|
root.updated()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Emits tzdata's own country tables alongside the zone list, tagged into
|
|
// sections. The shell only reads files; the joining is done in ClockModel so
|
|
// it can be tested. `right/` and `posix/` are pruned because they duplicate
|
|
// every zone under a different inode, which would double the output and
|
|
// break the alias-by-inode match.
|
|
readonly property string catalogScript:
|
|
'printf "@ISO\n"\n' +
|
|
'cat /usr/share/zoneinfo/iso3166.tab 2>/dev/null\n' +
|
|
'printf "@ZONE\n"\n' +
|
|
'cat /usr/share/zoneinfo/zone1970.tab /usr/share/zoneinfo/zone.tab 2>/dev/null\n' +
|
|
'printf "@INODE\n"\n' +
|
|
'find /usr/share/zoneinfo -type f -not -path "*/right/*" -not -path "*/posix/*" ' +
|
|
'-printf "%i\\t%P\\n" 2>/dev/null\n' +
|
|
'printf "@LIST\n"\n' +
|
|
'timedatectl list-timezones 2>/dev/null || ' +
|
|
'find /usr/share/zoneinfo -type f -not -path "*/right/*" -not -path "*/posix/*" ' +
|
|
'-printf "%P\\n" 2>/dev/null | grep -E "^[A-Z][A-Za-z_+-]*/" | sort'
|
|
|
|
property Process zoneListProc: Process {
|
|
command: ["sh", "-c", root.catalogScript]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: root.allZones = Model.parseZoneCatalog(text)
|
|
}
|
|
}
|
|
|
|
// DST transitions land on the hour, so a re-probe shortly after every hour
|
|
// boundary catches one within a minute; the ten-minute tick is the safety
|
|
// net for tzdata updates and suspend/resume.
|
|
property Timer refreshTimer: Timer {
|
|
interval: 10 * 60 * 1000
|
|
repeat: true
|
|
running: true
|
|
onTriggered: root.probe()
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
zoneListProc.running = true
|
|
probe()
|
|
}
|
|
}
|