Files
omarchycn/test/shell.d/bar-test.sh
T
David Heinemeier HanssonandClaude Fable 5 f54edbeba8 Expose the shell's remaining UI-only toggles over IPC
Every toggle you could only reach by mouse or panel hotkey is now
scriptable through omarchy-shell:

- omarchy.bluetooth toggleBluetooth — the radio (was right-click / B)
- omarchy.network toggleNetwork — Wi-Fi on/off (was W in the panel)
- omarchy.tailscale toggleTailscale — the connection as one call,
  matching the widget's right-click, instead of separate up/down
- omarchy.clock toggleWeekStart — calendar week start (was W)
- shell toggleBarTransparency — the bar background (was manage UI only)

Bluetooth and network follow the power/monitor pattern: manageIpc: false
so the panel owns the target's single IpcHandler and can extend it past
the inherited lifecycle methods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 15:44:07 -05:00

189 lines
9.3 KiB
Bash

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
if perl -0ne 'exit(/drag\s*\.\s*target\s*:\s*[^;]*\bslot\b/s ? 0 : 1)' "$ROOT/shell/plugins/bar/Bar.qml"; then
fail "bar module dragging must not mutate ModuleSlot positions"
fi
pass "bar module dragging leaves layout-managed slots in place"
if rg -q 'barMoveSettling|barMoveSettleTimer' "$ROOT/shell/plugins/bar/Bar.qml"; then
fail "bar move outline must clear when the pointer is released"
fi
pass "bar move outline has no post-release settling state"
run_node_test <<'JS'
const fs = require('fs')
const bar = requireFromRoot('shell/plugins/bar/BarModel.js')
const barSource = fs.readFileSync(root + '/shell/plugins/bar/Bar.qml', 'utf8')
const shellSource = fs.readFileSync(root + '/shell/shell.qml', 'utf8')
assert(/function toggleBarTransparency\(\): string \{[\s\S]*?shell\.bar\.toggleTransparency\(\)/.test(shellSource), 'shell exposes the bar transparency toggle over IPC')
// The center section declares two arrangements and shows one; the hidden one
// must not build its modules or every center widget exists twice.
const moduleList = barSource.slice(barSource.indexOf('component ModuleList'), barSource.indexOf('component ModuleSlot'))
assert(
/active: visible && entries\.length > 0/.test(moduleList),
'bar builds only the module list it is showing'
)
// A center module is mounted twice — drawn copy plus zero-size placeholder —
// and the order they register in is not stable across a live reconfiguration,
// so panel routing has to pick the one that is actually on screen.
const drawn = { moduleName: 'omarchy.clock', visible: true, width: 28, height: 81 }
const placeholder = { moduleName: 'omarchy.clock', visible: false, width: 0, height: 0 }
assertEqual(bar.isDrawnSlot(drawn), true, 'bar recognises a drawn slot')
assertEqual(bar.isDrawnSlot(placeholder), false, 'bar recognises a layout placeholder')
assertEqual(bar.pickDrawnSlot([placeholder, drawn]), drawn, 'bar picks the drawn slot when the placeholder registers first')
assertEqual(bar.pickDrawnSlot([drawn, placeholder]), drawn, 'bar picks the drawn slot when it registers first')
assertEqual(bar.pickDrawnSlot([placeholder]), placeholder, 'bar falls back to the placeholder when nothing is drawn')
assertEqual(bar.pickDrawnSlot([]), null, 'bar reports no slot when there are none')
assertEqual(bar.pickDrawnSlot(null), null, 'bar tolerates a missing slot list')
assert(
/BarModel\.pickDrawnSlot\(candidates\)/.test(barSource),
'bar routes panels through the drawn-slot picker'
)
const clockSlot = { id: 'clock' }
const traySlot = { id: 'tray' }
const horizontalTargets = [
{ slot: clockSlot, x: 100, y: 0, width: 100, height: 26 },
{ slot: traySlot, x: 500, y: 0, width: 50, height: 26 }
]
assertDeepEqual(
bar.nearestDropTarget(horizontalTargets, { x: 240, y: 13 }, false),
{ slot: clockSlot, after: true },
'bar resolves free space beside a widget to its nearest insertion edge'
)
assertDeepEqual(
bar.nearestDropTarget(horizontalTargets, { x: 460, y: 13 }, false),
{ slot: traySlot, after: false },
'bar resolves free space before a widget to its nearest insertion edge'
)
assertDeepEqual(
bar.nearestDropTarget(horizontalTargets, { x: 125, y: 13 }, false),
{ slot: clockSlot, after: false },
'bar resolves the first half of a widget before it'
)
assertDeepEqual(
bar.nearestDropTarget(horizontalTargets, { x: 175, y: 13 }, false),
{ slot: clockSlot, after: true },
'bar resolves the second half of a widget after it'
)
assertDeepEqual(
bar.nearestDropTarget([
{ slot: clockSlot, x: 0, y: 100, width: 26, height: 80 }
], { x: 13, y: 220 }, true),
{ slot: clockSlot, after: true },
'vertical bars resolve free space along their vertical axis'
)
assertEqual(bar.nearestDropTarget([], { x: 10, y: 10 }, false), null, 'bar reports no insertion edge without targets')
assert(
/contentItem\.mapFromItem\(null, scenePoint\.x, scenePoint\.y\)[\s\S]*?return null/.test(barSource),
'bar rejects free-space drops after the pointer leaves the bar'
)
assert(
/BarModel\.nearestDropTarget\(candidates, scenePoint, root\.vertical\)/.test(barSource),
'bar uses nearest insertion targeting for widget and free-space drops'
)
assert(
/component DragGhostPanel:[\s\S]*?readonly property var targetRect: root\.barDragTargetGeometry[\s\S]*?color: Color\.accent/.test(barSource),
'bar draws the insertion marker above the bar in the drag overlay'
)
// The open-panel mark sits on the module's desktop-facing edge at every
// position: under a top bar, over a bottom one, inward from left and right.
const indicator = barSource.slice(barSource.indexOf('id: openPanelIndicator'), barSource.indexOf('id: openPanelIndicator') + 1600)
assert(
/x: root\.vertical\s*\n\s*\? \(root\.position === "left" \? parent\.width - width - inset : inset\)/.test(indicator),
'bar pins the open-panel mark to the desktop-facing edge on vertical bars'
)
assert(
/root\.position === "top" \? parent\.height - height - inset : inset/.test(indicator),
'bar pins the open-panel mark to the desktop-facing edge on horizontal bars'
)
assert(
/key in activeItem/.test(barSource),
'bar asks whether a widget declares an indicator hint before reading it'
)
assert(
/width: root\.vertical \? Style\.space\(2\) : slot\.panelIndicatorExtent/.test(indicator) &&
/height: root\.vertical \? slot\.panelIndicatorExtent : Style\.space\(2\)/.test(indicator),
'bar sizes the open-panel mark from the same content hint on both axes'
)
assertEqual(bar.normalizePosition('left'), 'left', 'bar accepts valid positions')
assertEqual(bar.normalizePosition('sideways'), 'top', 'bar defaults invalid positions')
assertDeepEqual(bar.entrySettings({ id: 'omarchy.clock', format: 'HH:mm' }), { format: 'HH:mm' }, 'bar extracts entry settings')
assertEqual(bar.entryId({ id: 'omarchy.clock' }), 'omarchy.clock', 'bar extracts object entry ids')
assertEqual(bar.entryId('omarchy.clock'), 'omarchy.clock', 'bar extracts string entry ids')
const entries = [{ id: 'a' }, { id: 'omarchy.tray' }, { id: 'b' }]
assertDeepEqual(bar.pinTrayToInner(entries, 'left').map(bar.entryId), ['a', 'b', 'omarchy.tray'], 'bar pins tray to left inner edge')
assertDeepEqual(bar.pinTrayToInner(entries, 'right').map(bar.entryId), ['omarchy.tray', 'a', 'b'], 'bar pins tray to right inner edge')
// A settings-only shell.json write must patch the live bar, not rebuild it:
// the module Repeaters recreate every widget when their array model changes.
const settingsLayout = { left: [{ id: 'omarchy.power' }], center: [{ id: 'omarchy.clock', format: 'HH:mm' }], right: [] }
assertDeepEqual(
bar.inlineSettingsDelta(settingsLayout, { left: [{ id: 'omarchy.power', showPercentage: true }], center: [{ id: 'omarchy.clock', format: 'HH:mm' }], right: [] }),
[{ region: 'left', index: 0, entry: { id: 'omarchy.power', showPercentage: true } }],
'bar reports a settings-only change as an inline delta'
)
assertDeepEqual(
bar.inlineSettingsDelta(settingsLayout, JSON.parse(JSON.stringify(settingsLayout))),
[],
'bar reports an unchanged layout as an empty delta'
)
assertEqual(
bar.inlineSettingsDelta(settingsLayout, { left: [{ id: 'omarchy.clock', format: 'HH:mm' }], center: [{ id: 'omarchy.power' }], right: [] }),
null,
'bar treats reordered entries as structural'
)
assertEqual(
bar.inlineSettingsDelta(settingsLayout, { left: [{ id: 'omarchy.power' }, { id: 'omarchy.battery' }], center: settingsLayout.center, right: [] }),
null,
'bar treats added entries as structural'
)
assertEqual(
bar.inlineSettingsDelta(
{ left: [{ id: 'local.status', exec: 'date' }], center: [], right: [] },
{ left: [{ id: 'local.status', exec: 'uptime' }], center: [], right: [] }
),
null,
'bar rebuilds for custom modules, which read their entry directly'
)
assertEqual(
bar.inlineSettingsDelta(
{ left: [{ id: 'x' }], center: [], right: [{ id: 'x' }] },
{ left: [{ id: 'x', a: 1 }], center: [], right: [{ id: 'x' }] }
),
null,
'bar rebuilds when a changed id appears more than once in the layout'
)
assert(
/BarModel\.inlineSettingsDelta\(layoutConfig, next\)/.test(barSource),
'bar consults the inline settings delta before rebuilding the layout'
)
assertEqual(bar.moduleString({ id: 'custom', label: 42 }, 'label', 'fallback'), '42', 'bar stringifies module settings')
assertEqual(bar.entryIndex(entries, 'b'), 2, 'bar finds entry indexes')
assertDeepEqual(bar.entriesBefore(entries, 'b').map(bar.entryId), ['a', 'omarchy.tray'], 'bar returns entries before target')
assertDeepEqual(bar.entriesAfter(entries, 'a').map(bar.entryId), ['omarchy.tray', 'b'], 'bar returns entries after target')
assertEqual(bar.expandPath('~/module.qml', '/home/dhh'), '/home/dhh/module.qml', 'bar expands tilde paths')
assertEqual(bar.expandPath('$HOME/module.qml', '/home/dhh'), '/home/dhh/module.qml', 'bar expands HOME paths')
assert(bar.customModuleSafeName('local.weather'), 'bar accepts safe custom module names')
assert(!bar.customModuleSafeName('../escape'), 'bar rejects path traversal custom module names')
assertEqual(bar.customModuleType({ id: 'custom', exec: 'date' }), 'command', 'bar infers command custom modules')
assertEqual(bar.customModuleType({ id: 'custom', source: '~/Custom.qml' }), 'qml', 'bar infers qml custom modules')
assertEqual(
bar.customModulePath({ id: 'local.weather' }, '/home/dhh', '/home/dhh/.config/omarchy'),
'/home/dhh/.config/omarchy/bar/modules/local.weather.qml',
'bar builds default custom module paths'
)
JS