import QtQuick import Quickshell import Quickshell.Io import "ClockModel.js" as Model // Owns ~/.config/omarchy/clockbar.json: reads it, watches it, writes it back // when the editor saves. // // The file is the source of truth in both directions — the editor writes it // and the watcher picks the change straight back up, so a hand-edit and an // edit through the popup are the same operation as far as the bar is // concerned. That is what makes the two config routes impossible to // desynchronise. QtObject { id: root readonly property string home: Quickshell.env("HOME") readonly property string path: home + "/.config/omarchy/clockbar.json" property var config: defaults() property bool loaded: false // Set when the file on disk is present but unparseable. The bar keeps // rendering the last good config and says so, rather than emptying itself // and losing the user's clocks behind a stray comma. property string parseError: "" signal changed() function defaults() { return { version: 1, clocks: [ { label: "LA", timezone: "America/Los_Angeles" }, { label: "NYC", timezone: "America/New_York" }, { label: "SG", timezone: "Asia/Singapore" }, { label: "AUS", timezone: "Australia/Sydney" }, { label: "NZ", timezone: "Pacific/Auckland" } ], workingHours: { start: 8, end: 18, workdays: [1, 2, 3, 4, 5] }, dimOutsideWorkingHours: true, showSeconds: false, barSize: 0 } } // Every field is coerced rather than trusted, so a half-finished hand-edit // degrades to the default for that one key instead of breaking the bar. function normalize(raw) { var base = defaults() if (!raw || typeof raw !== "object") return base var out = base if (Array.isArray(raw.clocks)) { var clocks = [] for (var i = 0; i < raw.clocks.length; i++) { var entry = raw.clocks[i] if (!entry || typeof entry !== "object") continue var tz = String(entry.timezone || "").replace(/^\s+|\s+$/g, "") if (!tz) continue var label = String(entry.label === undefined || entry.label === null ? "" : entry.label).replace(/^\s+|\s+$/g, "") clocks.push({ label: label || tz, timezone: tz }) } out.clocks = clocks } var wh = raw.workingHours if (wh && typeof wh === "object") { var start = Number(wh.start) var end = Number(wh.end) if (isFinite(start) && start >= 0 && start <= 24) out.workingHours.start = start if (isFinite(end) && end >= 0 && end <= 24) out.workingHours.end = end if (Array.isArray(wh.workdays)) { var days = [] for (var d = 0; d < wh.workdays.length; d++) { var n = Number(wh.workdays[d]) if (isFinite(n) && n >= 0 && n <= 6 && days.indexOf(n) === -1) days.push(n) } if (days.length) out.workingHours.workdays = days } } if (typeof raw.dimOutsideWorkingHours === "boolean") out.dimOutsideWorkingHours = raw.dimOutsideWorkingHours if (typeof raw.showSeconds === "boolean") out.showSeconds = raw.showSeconds var size = Number(raw.barSize) if (isFinite(size) && size >= 0 && size <= 200) out.barSize = size return out } // Applying what was read. This path NEVER writes. // // FileView can emit `loaded` more than once while starting up, and the early // emission can carry empty text before the read has actually landed. Seeding // defaults from that and saving them destroyed the real file — the user's // clocks were replaced by the stock five on the next shell restart. So an // empty or unparseable read now leaves both the file and the last good // config alone, and only a confirmed missing file seeds anything. function apply(raw) { var text = String(raw || "").replace(/^\s+|\s+$/g, "") if (!text) { root.loaded = true return } var parsed try { parsed = JSON.parse(text) } catch (e) { root.parseError = String(e.message || e) console.warn("clockbar: " + root.path + " is not valid JSON, keeping the last good config: " + root.parseError) root.loaded = true return } root.parseError = "" root.config = normalize(parsed) root.loaded = true root.changed() } // The one place defaults are written, and only when the file genuinely is // not there. A permissions error or an unknown failure must not be answered // by overwriting whatever is on disk. function seed() { root.config = defaults() root.parseError = "" root.loaded = true save(root.config) } function save(next) { var clean = normalize(next) root.config = clean file.setText(JSON.stringify(clean, null, 2) + "\n") root.parseError = "" root.changed() } property FileView file: FileView { path: root.path watchChanges: true atomicWrites: true printErrors: false onLoaded: root.apply(text()) onLoadFailed: function (error) { if (error === FileViewError.FileNotFound) { // First run. Seed the file so the user has something to edit and so // the watcher has a real file to follow from here on. root.seed() return } console.warn("clockbar: could not read " + root.path + ": " + FileViewError.toString(error)) root.loaded = true } onFileChanged: reload() } Component.onCompleted: Qt.callLater(function () { root.file.reload() }) }