Claiming the shared suppression flag before showing meant the outgoing panel's close cleared it again, leaving the incoming panel open with the indicators still revealed. Guarding the clear instead only moved the problem: handing off to a panel that does not manage the flag left it stuck on, and the center indicators stopped revealing on hover for good. Claim it after the handoff instead. The panel taking over always wins, and a handoff to a panel that knows nothing about the flag still leaves it cleared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
162 lines
12 KiB
Bash
Executable File
162 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
|
|
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')
|
|
const widgetSource = fs.readFileSync(root + '/shell/plugins/panels/clock/BarWidget.qml', 'utf8')
|
|
|
|
// ---- week start resolution
|
|
assertEqual(calendar.normalizedWeekStart('monday', 0), 1, 'calendar reads a named week start')
|
|
assertEqual(calendar.normalizedWeekStart('SUN', 1), 0, 'calendar reads an abbreviated week start')
|
|
assertEqual(calendar.normalizedWeekStart(6, 1), 6, 'calendar reads a numeric week start')
|
|
assertEqual(calendar.normalizedWeekStart(null, 0), 0, 'calendar falls back to the locale first day')
|
|
assertEqual(calendar.normalizedWeekStart('garbage', 0), 0, 'calendar falls back when the setting is unparseable')
|
|
assertEqual(calendar.normalizedWeekStart(undefined, undefined), 1, 'calendar defaults to Monday without a locale')
|
|
|
|
assertEqual(calendar.weekStartSettingName(0), 'sunday', 'calendar persists Sunday by name')
|
|
assertEqual(calendar.weekStartSettingName(1), 'monday', 'calendar persists Monday by name')
|
|
|
|
assertEqual(calendar.toggledWeekStart(1), 0, 'calendar toggles Monday to Sunday')
|
|
assertEqual(calendar.toggledWeekStart(0), 1, 'calendar toggles Sunday to Monday')
|
|
assertEqual(calendar.toggledWeekStart(6), 1, 'calendar toggles an exotic week start to Monday')
|
|
|
|
assertDeepEqual(calendar.weekdayOrder(1), [1, 2, 3, 4, 5, 6, 0], 'calendar orders weekdays from Monday')
|
|
assertDeepEqual(calendar.weekdayOrder(0), [0, 1, 2, 3, 4, 5, 6], 'calendar orders weekdays from Sunday')
|
|
|
|
// ---- ISO week numbers, matching the clock widget's 'ww' token
|
|
assertEqual(calendar.isoWeek(2026, 0, 1), 1, 'calendar numbers a Thursday January 1 as week 1')
|
|
assertEqual(calendar.isoWeek(2021, 0, 1), 53, 'calendar numbers early January into the previous ISO year')
|
|
assertEqual(calendar.isoWeek(2026, 11, 31), 53, 'calendar numbers the last week of a long year')
|
|
assertEqual(calendar.isoWeek(2026, 6, 26), 30, 'calendar numbers a midsummer Sunday')
|
|
|
|
// ---- day-of-year arithmetic behind the hero stats
|
|
assertEqual(calendar.dayOfYear(2026, 6, 26), 207, 'calendar counts the day of the year')
|
|
assertEqual(calendar.yearProgressPercent(2026, 0, 1), 0, 'calendar starts the year at zero percent done')
|
|
assertEqual(calendar.yearProgressPercent(2026, 6, 26), 56, 'calendar reports the share of the year behind you')
|
|
assertEqual(calendar.yearProgressPercent(2026, 11, 31), 100, 'calendar finishes the year at a hundred percent')
|
|
assertEqual(calendar.yearProgressPercent(2024, 11, 31), 100, 'calendar finishes a leap year too')
|
|
assertEqual(calendar.daysInYear(2024), 366, 'calendar knows leap years are longer')
|
|
assertEqual(calendar.daysInYear(2026), 365, 'calendar knows common years')
|
|
|
|
// ---- month grid
|
|
const july = calendar.monthGrid(2026, 6, 1, '2026-07-26')
|
|
assertEqual(july.length, 6, 'calendar always draws six week rows')
|
|
assert(july.every(week => week.days.length === 7), 'calendar always draws seven day columns')
|
|
assertDeepEqual(july.map(week => week.week), [27, 28, 29, 30, 31, 32], 'calendar numbers every row by ISO week')
|
|
assertDeepEqual(july[0].days.map(day => day.day), [29, 30, 1, 2, 3, 4, 5], 'calendar pads the first row with the previous month')
|
|
assertDeepEqual(july[0].days.map(day => day.inMonth), [false, false, true, true, true, true, true], 'calendar marks padding days as outside the month')
|
|
assertDeepEqual(july[0].days.map(day => day.weekend), [false, false, false, false, false, true, true], 'calendar marks Saturday and Sunday as the weekend')
|
|
|
|
assertDeepEqual(
|
|
july.flatMap(week => week.days).filter(day => day.today).map(day => day.key),
|
|
['2026-07-26'],
|
|
'calendar marks exactly one cell as today'
|
|
)
|
|
assert(july.flatMap(week => week.days).every(day => day.cursor === undefined), 'calendar grid carries no selection state')
|
|
|
|
// Both conventions land on the same ISO week numbers, because every row is
|
|
// numbered by the ISO week owning its Thursday.
|
|
const julySunday = calendar.monthGrid(2026, 6, 0, '')
|
|
assertDeepEqual(julySunday[0].days.map(day => day.day), [28, 29, 30, 1, 2, 3, 4], 'calendar shifts the grid for a Sunday week start')
|
|
assertDeepEqual(julySunday.map(week => week.week), [27, 28, 29, 30, 31, 32], 'calendar keeps ISO week numbers across week starts')
|
|
|
|
const januarySunday = calendar.monthGrid(2021, 0, 0, '')
|
|
assertEqual(januarySunday[0].week, 53, 'calendar carries the previous ISO year into a straddling first row')
|
|
|
|
// ---- stepping
|
|
assertDeepEqual(calendar.stepMonth(2026, 0, 1), { year: 2026, month: 1 }, 'calendar steps to the next month')
|
|
assertDeepEqual(calendar.stepMonth(2026, 0, -1), { year: 2025, month: 11 }, 'calendar steps back across the new year')
|
|
assertDeepEqual(calendar.stepMonth(2026, 11, 1), { year: 2027, month: 0 }, 'calendar steps forward across the new year')
|
|
assertDeepEqual(calendar.stepMonth(2026, 6, 12), { year: 2027, month: 6 }, 'calendar steps a whole year at a time')
|
|
|
|
assertEqual(calendar.dateKey(2026, 0, 5), '2026-01-05', 'calendar zero-pads date keys')
|
|
assertEqual(calendar.keyForDate(new Date(2026, 6, 26)), '2026-07-26', 'calendar keys a Date the same way')
|
|
|
|
// ---- bar label format ring
|
|
// The order must not depend on which entry is current: cycling writes the
|
|
// result back to shell.json, so a self-reordering ring would ping-pong.
|
|
const ring = calendar.clockFormatRing('dddd HH:mm', "d MMMM 'W'ww yyyy", calendar.clockFormats(false))
|
|
assertDeepEqual(ring, calendar.clockFormats(false), 'clock rings the stock presets in their own order')
|
|
assertEqual(new Set(ring).size, ring.length, 'clock never offers the same format twice')
|
|
|
|
assertDeepEqual(
|
|
calendar.clockFormatRing("d MMMM 'W'ww yyyy", 'dddd HH:mm', calendar.clockFormats(false)),
|
|
ring,
|
|
'clock keeps the ring order steady as the current format moves through it'
|
|
)
|
|
assertDeepEqual(
|
|
calendar.clockFormatRing('HH:mm:ss', '', ['HH:mm', 'dddd HH:mm']),
|
|
['HH:mm', 'dddd HH:mm', 'HH:mm:ss'],
|
|
'clock appends a hand-written format to the end of the ring'
|
|
)
|
|
assertDeepEqual(calendar.clockFormatRing('', '', []), ['HH:mm'], 'clock keeps a usable format when nothing is configured')
|
|
|
|
assertEqual(calendar.nextClockFormat(ring, ring[0]), ring[1], 'clock steps to the next format')
|
|
assertEqual(calendar.nextClockFormat(ring, ring[ring.length - 1]), ring[0], 'clock wraps the format ring')
|
|
assertEqual(calendar.nextClockFormat(ring, 'HH:mm:ss'), ring[0], 'clock starts at the top from a format outside the ring')
|
|
assertEqual(calendar.clockFormats(true)[0], 'HH\n\u2014\nmm', 'clock keeps stacked formats for vertical bars')
|
|
assertEqual(calendar.isoWeekLiteral(2026, 0, 5), '02', 'clock zero-pads the ISO week token')
|
|
|
|
// ---- 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')
|
|
assert(/manageIpc: false/.test(panelSource), 'calendar panel leaves the IPC target to the bar widget')
|
|
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(/setting\("weekStartDay", null\)/.test(panelSource) && /entry\.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')
|
|
assert(!/property bool onToday/.test(panelSource) && !/root\.onToday/.test(panelSource), 'calendar panel avoids the on-prefixed property name QML reads as a signal handler')
|
|
assert(/readonly property bool viewingCurrentMonth:/.test(panelSource), 'calendar panel tracks whether the current month is on screen')
|
|
assert(!/MouseArea/.test(panelSource.slice(panelSource.indexOf('model: modelData.days'), panelSource.indexOf('// Hairline'))), 'calendar day cells are not selectable')
|
|
assert(/yearDone: Model\.yearProgress\(today\./.test(panelSource), 'calendar year bar stays pinned to today while months are stepped')
|
|
assert(/Qt\.callLater\(function\(\) \{\s*\n\s*if \(root\.opened\) setCenterHoverRevealSuppressed\(true\)/.test(panelSource), 'calendar claims the shared hover-reveal flag after the popout handoff, so the panel taking over wins')
|
|
assert(/function close\(\) \{\s*\n\s*setCenterHoverRevealSuppressed\(false\)/.test(panelSource), 'calendar always releases the shared hover-reveal flag on close')
|
|
assert(/width: Math\.max\(calendarScroll\.width, gridColumn\.width\)/.test(panelSource), 'calendar scrolls rather than clipping the grid on a narrow popup')
|
|
assert(/enabled: !root\.viewingCurrentMonth/.test(panelSource) && /onClicked: root\.goToToday\(\)/.test(panelSource), 'calendar hero returns to today once the view has stepped away')
|
|
assert(!/clampMonth/.test(panelSource), 'calendar steps freely into future months')
|
|
assert(/Qt\.formatDate\(root\.today, "MMMM d"\)/.test(panelSource), 'calendar hero spells out today')
|
|
assert(/id: yearLabel/.test(panelSource) && /root\.yearDone/.test(panelSource), 'calendar panel shows the year progress bar')
|
|
|
|
assert(/source: Qt\.resolvedUrl\("Panel\.qml"\)/.test(widgetSource), 'clock widget hosts the calendar panel')
|
|
assert(/readonly property bool opened:/.test(widgetSource), 'clock widget exposes the panel open state to shell routing')
|
|
assert(/Qt\.RightButton\) root\.cycleFormat\(\)/.test(widgetSource), 'clock right click cycles the label format')
|
|
assert(/readonly property string activeFormat: configuredFormat/.test(widgetSource), 'clock shows the format it has stored')
|
|
assert(/entry\[vertical \? "verticalFormat" : "format"\] = next/.test(widgetSource) && /updateEntryInline/.test(widgetSource), 'clock writes a cycled format back to shell.json')
|
|
assert(!/formatIndex/.test(widgetSource), 'clock keeps no session-only format position')
|
|
assert(/else root\.togglePanel\(\)/.test(widgetSource), 'clock left click reveals the calendar')
|
|
assert(/omarchy-menu-timezone/.test(widgetSource), 'clock keeps the timezone picker on middle click')
|
|
|
|
// The bar identifies a panel by the widget in its slot, so the nested panel
|
|
// has to present the host widget rather than itself.
|
|
assert(/owner: root\.barIdentity/.test(panelSource), 'calendar panel gives the bar its host widget as popout identity')
|
|
assert(/switchPanelFrom\(root\.barIdentity, direction\)/.test(panelSource), 'calendar panel switches panels as its host widget')
|
|
assert(/hostWidget" in target\) target\.hostWidget = root/.test(widgetSource), 'clock widget injects itself as the panel host')
|
|
assert(/readonly property bool popoutSwitchClosing:/.test(widgetSource) && /function closeForPopoutSwitch\(\)/.test(widgetSource), 'clock widget forwards the popout-switch handshake')
|
|
assert(/openPanelIndicatorWidth: button\.labelWidth/.test(widgetSource), 'clock sizes the open-panel dot to its label')
|
|
assert(/openPanelIndicatorHeight: Math\.max/.test(widgetSource), 'clock sizes the vertical open-panel dot to one stacked line')
|
|
const barSource = fs.readFileSync(root + '/shell/plugins/bar/Bar.qml', 'utf8')
|
|
assert(/openPanelIndicatorWidth/.test(barSource) && /openPanelIndicatorHeight/.test(barSource), 'bar honours a widget-supplied open-panel dot size on both axes')
|
|
assert(/height: root\.vertical \? slot\.panelIndicatorExtent : Style\.space\(2\)/.test(barSource), 'bar sizes the vertical dot from the same hint as the horizontal one')
|
|
assert(/readonly property real labelWidth:/.test(fs.readFileSync(root + '/shell/Ui/WidgetButton.qml', 'utf8')), 'widget buttons expose their painted label width')
|
|
|
|
// A horizontal wheel reports angleDelta.y === 0; without the guard every one
|
|
// of them would read as a forward step.
|
|
assert(/if \(event\.angleDelta\.y === 0\) return/.test(panelSource), 'calendar ignores wheel events with no vertical delta')
|
|
JS
|
|
|
|
shell_json=$(cd "$ROOT" && jq -r '[.bar.layout.center[].id] | join(",")' config/omarchy/shell.json)
|
|
[[ $shell_json == *"omarchy.clock"* ]] || fail "default bar layout includes the clock widget" "center: $shell_json"
|
|
[[ $shell_json != *"omarchy.calendar"* ]] || fail "clock hosts the calendar instead of a second bar pill" "center: $shell_json"
|
|
pass "default bar layout includes the clock widget"
|
|
|
|
grep -q 'o.bind("SUPER + CTRL + ALT + D", "Calendar", "omarchy-shell shell toggle omarchy.clock")' \
|
|
"$ROOT/default/hypr/bindings/utilities.lua" ||
|
|
fail "SUPER+CTRL+ALT+D toggles the calendar panel"
|
|
pass "SUPER+CTRL+ALT+D toggles the calendar panel"
|