Files
omarchycn/docs/menu.md
T
David Heinemeier HanssonandClaude Fable 5 f4189398bc Bring docs/ up to date and cover the undocumented subsystems
Every reference doc was audited claim-by-claim against the code.
file-layout and omarchy-shell were the most decayed (renamed commands,
the etc/ overrides source split, dead IPC entry points and example keys);
update-process lagged the recent pipeline changes and gains a channels
section; theming and audio-tuning were accurate but thin around their
lifecycles.

New reference docs for the subsystems that had none: the menu system,
the CLI router, the notification daemon, and the non-acceptance test
architecture. AGENTS.md links the two of those agents will need most.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 16:21:25 +02:00

168 lines
8.9 KiB
Markdown

# The Omarchy menu
The menu is the `omarchy.menu` plugin of the Quickshell desktop, with its
content defined as data in `default/omarchy/omarchy-menu.jsonc` (read at
runtime from `$OMARCHY_PATH`) and overlaid by the user's
`~/.config/omarchy/extensions/omarchy-menu.jsonc`. The shell parses both files
at startup and watches them for changes, so the keybind → IPC → visible path
never shells out to parse anything, and edits to either file take effect
without restarting the shell. Rendering and behavior live in
`shell/plugins/menu/Menu.qml`; the pure logic lives in
`shell/plugins/menu/MenuModel.js`, which is plain JavaScript that Node can
also load — the shell tests in `test/shell.d/menu-test.sh` and
`menu-guards-test.sh` exercise it directly.
JSONC here means JSON plus comments and trailing commas, stripped by the
parser rather than a real JSONC grammar: only whole-line `//` comments are
removed, so an inline trailing comment breaks the parse. A file that fails to
parse contributes no entries — a broken user extension silently drops every
user entry while the shipped menu keeps working.
## Entry schema
Entries are object keys. The dotted id is the tree: `trigger.share.file` is a
child of `trigger.share`, and an id with no dot sits on the root menu. There
is no separate parent field to keep in sync — where an entry appears follows
from what it is called (an explicit `parent` is accepted but nothing shipped
uses one).
Kind is inferred rather than declared: an entry with `action` is an action,
one with `target` is a link to another submenu, and anything else is a
submenu. The fields:
| Field | Meaning |
|---|---|
| `icon` | Glyph in the icon column (usually Nerd Font) |
| `iconFont` | Font family for the glyph when it differs from the menu font — how the private `omarchy` font's brand glyphs render |
| `label` | Visible row title; defaults to the id |
| `title` | Header text when the submenu is open; defaults to `label`. Lets a row read "Browser" under Defaults while the open menu says "Default Browser" |
| `action` | Shell command to run, detached, when selected |
| `target` | Existing submenu id to open; makes the row a link |
| `provider` | Runtime row source for this submenu (see Providers) |
| `aliases` | Alternate `omarchy menu summon <name>` routes; also searchable |
| `description` | Subtitle shown while searching, and extra search text matched by whole word |
| `when` / `checked` / `disabled` | Shell conditions (see Guards) |
Do not add `aliases` to new entries. They are reserved for established
alternate names users already type (`power-menu`, `settings`), kept for
compatibility — see `AGENTS.md`. Search does not need them: labels, the last
id segment, and descriptions are all searchable.
## Load and merge
`mergeMenuSources` overlays user entries on the defaults per key: reusing a
shipped id replaces only the fields you declare, so an extension can retitle
or re-icon a row without re-declaring its action, and an overridden entry
keeps its original position in the list. New ids append. A `root` entry is
injected if neither file declares one.
The sample extension at `config/omarchy/extensions/omarchy-menu.jsonc`
(refreshed into `~/.config/`) documents the format in its header and ships
only comments, so the default state adds nothing.
## Guards
`when`, `checked`, and `disabled` are bash conditions. The shell never
evaluates them on the open path: all guards in the menu are batched into a
single bash process per (re)load and per open, reporting `<id>:<w|c|d>:<0|1>`
lines. The menu opens immediately on the previous evaluation's answers, so
the batch's runtime is exactly how long a row can contradict the state it
describes — which is why the batch works hard to be fast:
- Package and command presence (`omarchy-pkg-present` and friends) are
answered in-process from one `pacman -Q` snapshot instead of a fork per
row. The snapshot resolves provides too, so gvim answers for vim.
- Commands that several rows read a value from — every Defaults > Browser row
compares against `$(omarchy-default-browser)` — run once, with the captured
answer substituted into each expression. The reader list is
`GUARD_READERS` in `MenuModel.js`; a new `$(omarchy-...)` reader used by
more than one row must be added there, and `menu-guards-test.sh` fails the
build if it is not.
The three guards differ in what failure means:
- `when` hides the row when it fails. A submenu whose visible descendants
are all hidden disappears with them (provider-backed submenus stay, since
their rows load on demand).
- `checked` appends ✓ when it succeeds — the "this is the current choice"
marker on defaults, DNS, channel rows.
- `disabled` keeps the row listed but dims it, marks it ✓, and makes it
unselectable: cursor, pointer, and Enter all step over it, and search omits
it. The Install submenus use it so software already on the machine reads as
installed rather than vanishing from the list it was installed from — the
list stays a catalog of what Omarchy can install. Since a dimmed row means
"you already have this", it earns the same ✓ as `checked` does elsewhere.
Install rows should therefore carry `disabled:` with the presence check, not
`when:`; Remove rows are the opposite, hiding via `when:` what is not there
to remove. `menu-test.sh` enforces the Install side of this convention.
## Providers
A submenu with `provider: "name"` gets its rows at runtime instead of from
JSONC. The names are defined by the shell, not the menu file — an extension
can point a submenu at an existing provider but cannot declare a new one:
- `apps` is QML-native: rows come from the shared AppLibrary (desktop
entries), carrying image icons, launch feedback, and uninstall support like
the launcher. App rows are searchable by their desktop Keywords but never
routable, so an installed app cannot capture a menu route (htop ships
`Keywords=system;...`, and SUPER+ESCAPE must still open the system menu).
- `fonts` and `power-profiles` are bash one-liners in the `providers` map in
`Menu.qml`. The contract is one tab-delimited line per row:
`label\tvalue\tcurrent`. The row whose value equals `current` gets the ✓
icon, and selection runs the spec's `actionFor(value)`. Row ids are
`<menuId>.<slugify(value)>`, with a `-` appended on collision so two values
that slugify alike cannot silently drop a row.
A provider marked `volatile` re-runs every time its submenu is entered — a
font installed since the shell started shows up without a restart — but not
on search keystrokes, which would restart the same enumeration per key.
`swapProviderRows` in `MenuModel.js` merges the results: rows carry the id of
the submenu that produced them, so a provider that runs again drops its
previous batch without disturbing static children declared in JSONC. Both it
and the app merge return fresh item maps for the caller to assign in one go —
writing into a map held by a QML `var` property occasionally loses the write,
which used to duplicate launcher rows. Never mutate `root.items` in place.
Adding a provider means adding an entry to the `providers` map in `Menu.qml`
(script, icon, `actionFor`, optionally `volatile`) and pointing a submenu at
it with `provider:`.
## Driving the menu from the CLI
`bin/omarchy-menu` is a thin wrapper over the standard plugin IPC surface:
```bash
omarchy menu # toggle the root menu
omarchy menu toggle system # open at a route, or close if already open
omarchy menu summon style.theme # always open (no close-if-visible)
omarchy menu close
omarchy menu refresh # re-parse the JSONC files
omarchy menu ping
```
A route is an item id or a declared alias, case-insensitive, with
underscores normalized to dashes. An exact id beats any alias; empty input,
`go`, and `menu` mean root; an unknown string falls through as a literal id
so a misspelling still attempts to open that id. Summoning a route that
resolves to an action — an alias for a leaf, like `screenrecord-stop` — runs
the action directly instead of opening an action with no children, and a
link is followed to its target. The default Hyprland bindings in
`default/hypr/bindings/utilities.lua` all go through this surface
(SUPER+SPACE toggles root, SUPER+ESCAPE the system menu, and so on).
## Select and input modes
The same plugin doubles as the system's dmenu. `omarchy-menu-select` and
`omarchy-menu-input` summon it with a `mode: select` or `mode: input`
payload, then block on a tempfile handshake: the shell writes the selection
to `selectionFile` and touches `doneFile`, and cancellation (empty
selection) exits 1. A select option is `label`, `glyph\tlabel`, or
`glyph\tlabel\tsubtext` — the glyph shows but never returns, the subtext
renders under the label, filters with it, and comes back as
`label\tsubtext` so callers with same-named rows get a stable key. This is
how the pickers behind menu actions (`omarchy-menu-plugin`,
`omarchy-menu-timezone`, ...) present lists without owning any UI.