* Fix unclickable tray submenus by drilling down inside the popup Clicking a tray menu entry that has children was a silent no-op: the row called QsMenuEntry.display(), which renders a *platform* menu, and Quickshell refuses that unless the shell root sets `//@ pragma UseQApplication` -- shell.qml does not. The log shows "Cannot display PlatformMenuEntry as quickshell was not started in QApplication mode" and nothing opens. Apps whose whole menu is submenus, like radiotray-ng's station list, were unusable. Adding the pragma would be the wrong fix: it switches the entire shell from QGuiApplication to QApplication, dragging QtWidgets into the process and changing application-class behavior for the sake of one popup -- which would then render as an unstyled platform menu beside omarchy's own popup styling anyway. Instead, submenus drill down inside the existing popup. A child QsMenuEntry inherits QsMenuHandle, so it can feed a nested QsMenuOpener and render through the same row delegate. Each level keeps its own live opener on a stack -- a child entry is owned by its parent opener's model, so collapsing to a single reassigned opener would destroy the very entry being displayed. A back header row walks out one level; at the root the menu renders exactly as before, and items without a DBusMenu still use the platform fallback. * Destroy submenu openers deepest-first and reset before switching items resetTrayMenu() destroyed openers front-to-back and only cleared submenuStack afterward. A deeper opener's menu entry is owned by its parent's children model, so destroying the parent first could invalidate an entry a still-live child opener referenced. Clear the stack before tearing anything down, then destroy deepest-first so a child is always gone before the parent whose model owns its entry. openTrayMenu() reassigned activeTrayItem before calling resetTrayMenu(). trayMenuOpener.menu binds to activeTrayItem.menu, so that reassignment immediately swaps what the root opener's children expose -- invalidating entries any live submenu opener still referenced, before resetTrayMenu() got a chance to tear them down. Reset first, then switch items. Thanks @Copilot for catching both. * Defer submenu reset until the popup's fade-out actually finishes onTrayMenuOpenChanged reset the submenu stack the instant trayMenuOpen went false, but the popup stays visible for the whole 140ms opacity fade (PopupCard's own visible: open || card.opacity > 0) -- dismissing from a submenu flashed the root menu mid-fade, and could resize or reposition the fading popup if the two have different geometry. Moved the reset to trayMenuPopup's own onVisibleChanged, which only fires once the fade has genuinely completed. Switching to a different tray item is unaffected: openTrayMenu() already resets explicitly before assigning the new item, independent of whether the popup ever dips to invisible (rapid reopen mid-fade never does). Thanks @Copilot for catching this. * Ignore tray menu clicks for a beat after changing submenu level Changing level swaps the Repeater's model, which rebuilds the row delegates synchronously -- a fresh row lands under a cursor that hasn't moved. Submenu clicks used to be silent no-ops, which trained users to click them twice, so that second click now fires whatever entry took the spot. On radiotray-ng that means an accidental station switch. Gate row and back-header clicks for 250ms after each level change. A deliberate follow-up click is slower than that; a double-click is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Pin the submenu back header above the scrolling menu rows The back header lived inside the Flickable's Column, so in a submenu taller than the 420px cap -- exactly the long station list this drill-down exists for -- scrolling down pushed the only way back off screen, with no Escape or right-click alternative. Move it into a pinned Column above the Flickable and account for its height in the popup's contentHeight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Reset the tray menu scroll offset when the drill-down is torn down Flickable keeps its contentY across a model swap whenever the new content is still tall enough to hold it. A menu dismissed while scrolled therefore reopened part-way down with its first entries off screen: reproducible on any tray app whose root menu outgrows the 420px cap, and now reachable on every app once a long submenu has been scrolled. Zero the offset in resetTrayMenu(), which runs both on teardown and before switching items. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Toni Nowak <t.nowak@ai-flow.no> Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Omarchy bar
This is the Quickshell implementation of the Omarchy status bar. It is
shipped as a first-party plugin of omarchy-shell, the
long-running shell host. The bar is mounted at startup and lives inside
the shell for its whole session.
manifest.jsondeclares the plugin (id: omarchy.bar,kind: bar) and points atBar.qmlas the entry point.Bar.qmlis Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly.widgets/holds simple first-party bar widgets with sibling manifests.- Feature plugins such as
../panels/audio/,../panels/network/,../panels/power/, and../agents/provide richer popup bar plugins. - The bar receives its config from the host shell as a
barConfigproperty; the host loads it from~/.config/omarchy/shell.json(orconfig/omarchy/shell.jsonwhen the user has no file). omarchy bar positionupdates only the user shell.json file.
Customizing
The bar config lives under the bar: key of ~/.config/omarchy/shell.json. Out of the box the shell uses config/omarchy/shell.json. Once you customize anything via the bar gestures, omarchy bar ..., or by editing shell.json directly, your file is canonical — there is no deep-merge.
The bar is configured directly on the bar itself: drag empty bar space (or click-and-hold) to move the bar to another screen edge, double-left-click empty center-bar space to toggle transparency, and drag widgets to reorder them. The omarchy bar position, omarchy bar transparent, omarchy bar move, and omarchy bar set commands do the same from scripts. Enable or disable widgets with omarchy plugin enable and omarchy plugin disable (widget ids come from omarchy plugin list).
Example shell.json (bar subtree only shown):
{
"version": 1,
"bar": {
"position": "top",
"transparent": false,
"centerAnchor": "omarchy.clock",
"layout": {
"left": [
{ "id": "omarchy.menu" },
{ "id": "omarchy.spacer", "size": 12 },
{ "id": "omarchy.workspaces" }
],
"center": [
{ "id": "omarchy.media" },
{ "id": "omarchy.clock", "format": "HH:mm" }
],
"right": [
{ "id": "omarchy.audio" },
{ "id": "omarchy.power" }
]
}
}
}
centerAnchor pins one center module to the exact horizontal/vertical center and flanks others around it. Set to an empty string to disable anchoring (the center list is centered as a group).
Module catalogue
First-party interactive widgets
| Name | What it does | Interactions |
|---|---|---|
omarchy.menu |
Omarchy menu launcher | left = menu · right = terminal |
omarchy.workspaces |
Hyprland workspace switcher | left = focus workspace |
omarchy.clock |
Date/time label + popup with a month grid, ISO week numbers, and month stepping | left = popup · right = cycle label format · middle = timezone selector |
omarchy.media |
MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
omarchy.indicators |
Manual state indicators | left = indicator action |
omarchy.system-update |
Available update indicator | left = update |
omarchy.tray |
System tray | hover = reveal drawer · right on chevron = manage |
omarchy.weather |
Weather icon + popup with forecast | left = popup · right = full notification |
omarchy.microphone |
Mic icon + scroll volume | left = mute toggle · middle = audio panel · scroll = source volume |
| omarchy.audio | Volume icon + popup with master slider, output-device picker, per-app mixer | left = popup · right = mute · middle = popup · scroll = volume |
| omarchy.network | Wi-Fi/Ethernet icon + popup with Wi-Fi scan, signal, connect, DNS provider selection | left = popup · right = nmtui |
| omarchy.tailscale | Tailscale status, connection switcher, machine browser, and copy actions | left = popup · right = toggle · middle = refresh |
| omarchy.agents | AI coding agent limits with pace, today, last week, and all-time model breakdown | left = panel · right = refresh · middle = next subscription |
| omarchy.power | Battery/AC icon + popup with battery stats, power profiles, and system info | left = popup · right = toggle percentage |
| omarchy.bluetooth | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI |
| omarchy.monitor | Brightness and laptop display controls | left = popup |
The omarchy.indicators widget loads individual bar indicators from indicators/. Omit items (or set it to an empty array) to show all indicators in the default order, or set items to a subset such as ["Dnd", "Reminder", "NightLight"]. Set alwaysShow to true to keep inactive indicators visible instead of revealing them only on hover. Multiple omarchy.indicators instances are allowed, so different sections can show different subsets.
Orientation
All widgets work in top, bottom, left, and right positions. Popups anchor on the side opposite the bar edge, sliding into the workspace. Vertical bars use 28px width; widgets that show text fall back to compact icon-only forms (e.g. media hides its scrolling label).
Custom user modules
The schema accepts arbitrary module ids that you provide. Set type to command for shell-driven output or qml for a custom QML widget. Both still go under bar.layout.<section> in shell.json.
Command module:
{
"version": 1,
"bar": {
"layout": {
"right": [
{ "id": "omarchy.tray" },
{ "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status", "interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" },
{ "id": "omarchy.audio" }
]
}
}
}
The command may print plain text or Waybar-style JSON, for example:
{"text":"","tooltip":"Work VPN","class":"active"}
QML module:
{
"version": 1,
"bar": {
"layout": {
"right": [
{ "id": "gpu", "type": "qml" },
{ "id": "omarchy.audio" }
]
}
}
}
Then create ~/.config/omarchy/bar/modules/gpu.qml. If you want to store it elsewhere, add a source path.
Custom QML modules should be an Item with implicitWidth and implicitHeight. They may optionally define these properties, which the bar fills after loading:
import QtQuick
Item {
property var bar
property string moduleName
property var settings
implicitWidth: 28
implicitHeight: bar ? bar.barSize : 26
Text {
anchors.centerIn: parent
text: "GPU"
color: bar ? bar.foreground : "white"
font.family: bar ? bar.fontFamily : "monospace"
font.pixelSize: 12
}
MouseArea {
anchors.fill: parent
onClicked: if (bar) bar.run("omarchy-launch-or-focus-tui btop")
}
}
Bar properties available to widgets
Widgets receive bar (the shell root), moduleName (string), and settings (object) injected at load time. The bar exposes:
bar.foreground,bar.background,bar.urgent— theme colors (live-updated)bar.fontFamily— current monospace familybar.position—"top" | "bottom" | "left" | "right"bar.vertical— boolean shortcutbar.barSize— 26 horizontal / 28 verticalbar.run(command)— fire-and-forget bash execbar.shellQuote(value)— safe shell-quote a stringbar.showTooltip(target, text)/bar.hideTooltip(target)— shared tooltip popupbar.requestPopout(owner)/bar.releasePopout(owner)— one-popup-at-a-time coordinator
First-party bar widgets are manifest-backed just like third-party widgets.
Simple widgets carry sibling manifests such as widgets/Workspaces.manifest.json;
richer popup plugins live in feature directories such as ../panels/audio/,
../panels/network/, and ../agents/; and feature plugins such as
omarchy.menu and omarchy.media declare their bar-widget entry points in their own
manifest.json. Bar layout ids are namespaced, e.g. omarchy.audio,
omarchy.network, and omarchy.clock. Older UpperCamelCase ids such as
AudioPanel and Clock are migrated forward; new configs should use the
namespaced ids.
Third-party widgets ship as separate plugins under
~/.config/omarchy/plugins/<plugin-id>/ with their own manifest.json
declaring kinds: ["bar-widget"] and a barWidget entry point. See
../../README.md for the manifest schema. Rescan, enable,
and place third-party plugins with omarchy-shell shell rescanPlugins,
omarchy plugin enable, and omarchy bar move.