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() } }