diff --git a/shell/plugins/panels/clock/BarWidget.qml b/shell/plugins/panels/clock/BarWidget.qml index 92f0ae56..e33486e5 100644 --- a/shell/plugins/panels/clock/BarWidget.qml +++ b/shell/plugins/panels/clock/BarWidget.qml @@ -28,6 +28,10 @@ BarWidget { // What the bar shows is what shell.json stores, so a cycled format is the // format from then on rather than something that reverts on restart. readonly property string activeFormat: configuredFormat + + // A seconds label needs the clock to tick sixty times as often, and a + // repaint a second is a price only the formats that print seconds pay. + readonly property bool showsSeconds: Model.clockNeedsSeconds(activeFormat) readonly property string displayText: formatted(displayDate) readonly property var verticalLines: displayText.split("\n") @@ -111,7 +115,7 @@ BarWidget { SystemClock { id: clock - precision: SystemClock.Minutes + precision: root.showsSeconds ? SystemClock.Seconds : SystemClock.Minutes onDateChanged: root.displayDate = date } diff --git a/shell/plugins/panels/clock/Model.js b/shell/plugins/panels/clock/Model.js index 9607a4a6..58d767f1 100644 --- a/shell/plugins/panels/clock/Model.js +++ b/shell/plugins/panels/clock/Model.js @@ -21,6 +21,8 @@ var WEEKDAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "fr var CLOCK_FORMATS = [ "dddd HH:mm", "dddd h:mm AP", + "dddd HH:mm:ss", + "dddd h:mm:ss AP", "HH:mm", "h:mm AP", "ddd d MMM HH:mm", @@ -39,6 +41,15 @@ var VERTICAL_CLOCK_FORMATS = [ "HH\nmm" ] +// Whether a format prints seconds, so the widget can tick once a second only +// for the formats that show them. Quoted literals go first: the s in a 'Sat' +// is text rather than a token, and an opening quote with no closing one runs +// to the end of the format the way Qt reads it. +function clockNeedsSeconds(format) { + var text = String(format === undefined || format === null ? "" : format) + return /s/.test(text.replace(/'[^']*'?/g, "")) +} + function clockFormats(vertical) { return vertical ? VERTICAL_CLOCK_FORMATS.slice() : CLOCK_FORMATS.slice() } @@ -289,6 +300,7 @@ if (typeof module !== "undefined") { monthGrid: monthGrid, stepMonth: stepMonth, clockFormats: clockFormats, + clockNeedsSeconds: clockNeedsSeconds, clockFormatRing: clockFormatRing, nextClockFormat: nextClockFormat, isoWeekLiteral: isoWeekLiteral diff --git a/test/shell.d/clock-test.sh b/test/shell.d/clock-test.sh index 582f7736..e10d62f1 100755 --- a/test/shell.d/clock-test.sh +++ b/test/shell.d/clock-test.sh @@ -8,7 +8,10 @@ run_node_test <<'JS' const fs = require('fs') const calendar = requireFromRoot('shell/plugins/panels/clock/Model.js') const panelSource = fs.readFileSync(root + '/shell/plugins/panels/clock/Panel.qml', 'utf8') +// Comments stripped: a wiring assertion that a commented-out line can satisfy +// passes while the widget is broken. const widgetSource = fs.readFileSync(root + '/shell/plugins/panels/clock/BarWidget.qml', 'utf8') + .replace(/^\s*\/\/.*$/gm, '') // ---- week start resolution assertEqual(calendar.normalizedWeekStart('monday', 0), 1, 'calendar reads a named week start') @@ -145,6 +148,7 @@ assertDeepEqual( calendar.clockFormats(false), [ 'dddd HH:mm', 'dddd h:mm AP', + 'dddd HH:mm:ss', 'dddd h:mm:ss AP', 'HH:mm', 'h:mm AP', 'ddd d MMM HH:mm', 'ddd d MMM h:mm AP', "d MMMM 'W'ww yyyy", @@ -166,6 +170,23 @@ assertEqual( ) assertEqual(calendar.isoWeekLiteral(2026, 0, 5), '02', 'clock zero-pads the ISO week token') +// ---- seconds detection, which decides how often the widget's clock ticks +assert(calendar.clockNeedsSeconds('dddd HH:mm:ss'), 'clock sees seconds in the live preset') +assert(calendar.clockNeedsSeconds('h:mm:ss AP'), 'clock sees seconds in an AM/PM format') +assert(!calendar.clockNeedsSeconds('dddd HH:mm'), 'clock sees no seconds in a minute format') +assert(!calendar.clockNeedsSeconds("d MMMM 'W'ww yyyy"), 'clock sees no seconds in the long date format') +assert(!calendar.clockNeedsSeconds("dd\nMMM\n'W'ww\n''yy"), 'clock sees no seconds in the stacked date format') +assert(!calendar.clockNeedsSeconds("HH:mm 'since'"), 'clock reads an s inside a quoted literal as text') +// Qt reads an opening quote with no closing one as a literal running to the end +// of the format, so "HH:mm 'sec" prints "09:39 sec" and never a second count. +assert(!calendar.clockNeedsSeconds("HH:mm 'sec"), 'clock reads an unterminated literal as text to the end') +assert(!calendar.clockNeedsSeconds("HH:mm's"), 'clock reads a trailing unterminated literal as text') +// The doubled quote is a literal apostrophe rather than an empty literal, so +// the s after it is still the seconds token: Qt prints "'23". +assert(calendar.clockNeedsSeconds("''s"), 'clock still sees seconds after an escaped apostrophe') +assert(!calendar.clockNeedsSeconds(''), 'clock sees no seconds in an empty format') +assert(!calendar.clockNeedsSeconds(null), 'clock sees no seconds in a missing format') + // ---- widget wiring assert(/moduleName: "omarchy\.clock"/.test(panelSource), 'calendar panel declares its module name') assert(/ipcTarget: "omarchy\.clock"/.test(panelSource), 'calendar panel registers its IPC target') @@ -173,6 +194,12 @@ assert(/manageIpc: false/.test(panelSource), 'calendar panel leaves the IPC targ assert(/anchorItem: root\.anchorItem/.test(panelSource), 'calendar panel anchors to the host widget button') assert(/function toggleWeekStart\(\)/.test(panelSource), 'calendar panel exposes a week start toggle') assert(/function toggleWeekStart\(\): void \{ root\.toggleWeekStart\(\) \}/.test(widgetSource), 'clock exposes the week start toggle over IPC') +assert(/precision: root\.showsSeconds \? SystemClock\.Seconds : SystemClock\.Minutes/.test(widgetSource), 'clock ticks per second only for a format that prints seconds') +// Both halves of the tick, because either one alone can be true while the +// label sits frozen: the precision must follow the format, and every tick +// must reach the label. +assert(/showsSeconds: Model\.clockNeedsSeconds\(activeFormat\)/.test(widgetSource), 'clock decides its tick rate from the format it is showing') +assert(/onDateChanged: root\.displayDate = date/.test(widgetSource), 'clock repaints the label on every tick') assert(/setting\("weekStartDay", null\)/.test(panelSource) && /persistSettings\(\{ weekStartDay:/.test(panelSource), 'calendar reads and writes the week start as weekStartDay') assert(/updateEntryInline/.test(panelSource), 'calendar panel persists the week start to shell.json') assert(/function moveMonth\(delta\)/.test(panelSource), 'calendar panel steps between months')