Files
OmarchyClockBar/Config.qml
T
HarveySaaymanandClaude Opus 5 b3123ecb3b Seconds per clock, and type that scales with the bar height
Seconds move from one bar-wide switch to a per-clock setting. A clock's own
`showSeconds` wins, the top-level one is the default for clocks with no
opinion, and the centre clock gets `local.showSeconds` since it has no entry in
`clocks[]`. An explicit false on a clock beats a true default, so the check is
for the property's presence rather than its truthiness.

The tick rate follows what is actually displayed: `anyShowsSeconds()` decides
between a per-second and a per-minute SystemClock, so a bar showing no seconds
does not wake the shell sixty times as often to redraw an identical minute.

`barSize` is now editable from the popup, and everything drawn on the bar is
sized against the theme's own bar height rather than pinned to the theme's
12px type. A taller bar carries proportionally larger text, gaps and rule; at
the theme's own height the scale is exactly 1, so nothing moves. Heights are
clamped to 12-120 rather than rejected, so a value the editor can produce can
never be one the config layer silently discards.

Fixes a latent bug this surfaced: updateRow() rebuilt a draft row from a fixed
pair of fields, so editing a clock's label would have thrown away its own
seconds setting. It now copies every key the row had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MhxtL7WzL5XmuTd8GHbcTY
2026-09-04 18:02:36 +02:00

175 lines
6.2 KiB
QML

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,
// The bar-wide default for seconds. Any clock, and the centre clock via
// `local`, may override it.
showSeconds: false,
local: { 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, "")
var clock = { label: label || tz, timezone: tz }
// Carried through only when the clock actually sets it, so a clock
// with no opinion keeps inheriting the bar-wide default rather than
// being frozen to whatever it happened to be when this was written.
if (typeof entry.showSeconds === "boolean") clock.showSeconds = entry.showSeconds
clocks.push(clock)
}
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
if (raw.local && typeof raw.local === "object" && typeof raw.local.showSeconds === "boolean")
out.local.showSeconds = raw.local.showSeconds
// 0 means "follow the theme". The ceiling is a guard against a typo
// turning the bar into a full-screen panel with no way back to the
// editor that would undo it.
var size = Number(raw.barSize)
if (isFinite(size) && size > 0) out.barSize = Math.min(120, Math.max(12, Math.round(size)))
else if (isFinite(size)) out.barSize = 0
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() })
}