diff --git a/config/omarchy/shell.json b/config/omarchy/shell.json index e6202da85d9..f56086985d8 100644 --- a/config/omarchy/shell.json +++ b/config/omarchy/shell.json @@ -8,6 +8,7 @@ "position": "top", "transparent": false, "centerAnchor": "omarchy.clock", + "trayMenuMaxHeight": 420, "layout": { "left": [ { diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index d62ff7436df..831723bb8fe 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -135,6 +135,7 @@ string on a miss. "position": "top", "transparent": false, "centerAnchor": "omarchy.clock", + "trayMenuMaxHeight": 420, "layout": { "left": [ { "id": "omarchy.menu" } ], "center": [ { "id": "omarchy.clock", "format": "HH:mm" } ], @@ -163,6 +164,11 @@ Rules: 7. `idle.screensaver` and `idle.lock` are seconds since user idle began. 8. `version: 1` is required. +Set `bar.trayMenuMaxHeight` to a positive integer to control the tray menu's +preferred maximum height in theme-scaled units. The default is `420`. Invalid +or missing values use the default, and the popup still clamps to the usable +monitor area. + `config/omarchy/shell.json` describes the fresh-install state. When no user `shell.json` exists, defaults are used verbatim. Once the user customizes, `shell.json` is canonical — there is no deep-merge. diff --git a/manual/05-the-top-bar.md b/manual/05-the-top-bar.md index 09a028986f4..9e121638924 100644 --- a/manual/05-the-top-bar.md +++ b/manual/05-the-top-bar.md @@ -125,6 +125,12 @@ All of it is stored in `~/.config/omarchy/shell.json`, under the `bar` key. Here } ``` +The tray menu defaults to a maximum height of `420` theme-scaled units. To +make a tall tray menu show more entries before scrolling, add +`"trayMenuMaxHeight": 600` under `bar` in `~/.config/omarchy/shell.json`. +The value must be a positive integer; missing or invalid values use the +default, and the menu still stays within the available monitor space. + Every widget is one entry in one of the three layout arrays, and its settings sit inline on that entry — there's no separate settings file and no `config` sub-object. The clock's `format`, `formatAlt` (what right-click cycles to), and `verticalFormat` all live right there on `{ "id": "omarchy.clock" }`. `centerAnchor` names the one center widget that gets pinned to the exact center of the screen, with the others flanking it. That's how the clock stays dead center even as the weather and update badge come and go. Set it to an empty string and the center list is just centered as a group instead. diff --git a/shell/README.md b/shell/README.md index ca07b520ae4..53d78c0d059 100644 --- a/shell/README.md +++ b/shell/README.md @@ -244,6 +244,7 @@ becomes the authoritative file — we do **not** deep-merge defaults back in. "position": "top", "transparent": false, "centerAnchor": "omarchy.clock", + "trayMenuMaxHeight": 420, "layout": { "left": [ { "id": "omarchy.menu" }, { "id": "omarchy.workspaces" } ], "center": [ { "id": "omarchy.clock", "format": "HH:mm" } ], diff --git a/shell/plugins/bar/widgets/Tray.qml b/shell/plugins/bar/widgets/Tray.qml index 650358c130f..d05605e7878 100644 --- a/shell/plugins/bar/widgets/Tray.qml +++ b/shell/plugins/bar/widgets/Tray.qml @@ -18,6 +18,10 @@ BarWidget { property var activeTrayAnchor: null readonly property color foreground: bar ? bar.foreground : Color.foreground readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family + readonly property int trayMenuMaxHeight: { + var value = bar && bar.barConfig ? bar.barConfig.trayMenuMaxHeight : undefined + return typeof value === "number" && isFinite(value) && value > 0 && Math.floor(value) === value ? value : 420 + } readonly property var pinnedIds: settings.pinned instanceof Array ? settings.pinned : [] readonly property var hiddenIds: settings.hidden instanceof Array ? settings.hidden : [] readonly property var pinnedItems: bucket("pinned") @@ -534,7 +538,7 @@ BarWidget { padding: Style.space(8) borderColor: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.45) contentWidth: trayMenuPopup.fittedContentWidth(Style.space(232)) - contentHeight: trayMenuPopup.fittedContentHeight(menuHeaderHeight + trayMenuColumn.implicitHeight, Style.space(420)) + contentHeight: trayMenuPopup.fittedContentHeight(menuHeaderHeight + trayMenuColumn.implicitHeight, Style.space(root.trayMenuMaxHeight)) // Column skips invisible children but keeps reporting their height, so // read the header's extent through its own visibility. diff --git a/test/shell.d/bar-test.sh b/test/shell.d/bar-test.sh index 0b06258a4ca..0decc4a3c3b 100644 --- a/test/shell.d/bar-test.sh +++ b/test/shell.d/bar-test.sh @@ -34,6 +34,7 @@ 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 traySource = fs.readFileSync(root + '/shell/plugins/bar/widgets/Tray.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') @@ -286,6 +287,18 @@ assert( assertEqual(bar.normalizePosition('left'), 'left', 'bar accepts valid positions') assertEqual(bar.normalizePosition('sideways'), 'top', 'bar defaults invalid positions') +assert( + /readonly property int trayMenuMaxHeight: \{[\s\S]*?typeof value === "number" && isFinite\(value\) && value > 0 && Math\.floor\(value\) === value \? value : 420/.test(traySource), + 'tray menu height falls back for missing and invalid values' +) +assert( + /var value = bar && bar\.barConfig \? bar\.barConfig\.trayMenuMaxHeight : undefined/.test(traySource), + 'tray menu height reads the bar shell configuration' +) +assert( + /fittedContentHeight\(menuHeaderHeight \+ trayMenuColumn\.implicitHeight, Style\.space\(root\.trayMenuMaxHeight\)\)/.test(traySource), + 'tray popup uses the configured height cap' +) 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') diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 355ca26b9e0..10cc709e932 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -16,6 +16,9 @@ pass "default shell.json is valid JSON" jq -e '.version == 1 and (.bar.layout.left | type == "array") and (.bar.layout.center | type == "array") and (.bar.layout.right | type == "array")' "$ROOT/config/omarchy/shell.json" >/dev/null pass "default shell.json has versioned bar layout" +jq -e '(.bar.trayMenuMaxHeight | type) == "number" and .bar.trayMenuMaxHeight > 0 and (.bar.trayMenuMaxHeight | floor) == .bar.trayMenuMaxHeight' "$ROOT/config/omarchy/shell.json" >/dev/null +pass "default shell.json has a positive integer tray menu height" + # Pinning the whole row made this fail every time an unrelated widget moved, # so assert the adjacency the name is about and let the rest of the row change. jq -e '