# Omarchy Menu System Architecture: How the Quickshell-Based UI Works

> Explore the Omarchy menu system architecture. Discover how Quickshell powers a data-driven UI with JSONC, a JavaScript runtime, and dynamic providers for a hot-reloadable desktop menu.

- Repository: [37signals/omarchy](https://github.com/basecamp/omarchy)
- Tags: architecture
- Published: 2026-08-20

---

**Omarchy's menu system is a three-layer, data-driven architecture built on Quickshell that uses JSONC configuration files, a JavaScript runtime model, and dynamic providers to render a hot-reloadable, extensible desktop menu.**

The Omarchy menu system, as implemented in `basecamp/omarchy`, separates static definitions from runtime behavior through a carefully layered design. This architecture enables users to customize their menu without modifying core files or restarting the shell, while maintaining consistent performance through batched guard evaluation and efficient search ranking.

## Three-Layer Architecture

The Omarchy menu architecture consists of tightly coupled layers that handle configuration, extension, and rendering:

| Layer | Responsibility | Primary Source |
|-------|----------------|----------------|
| **Static definition** | JSONC files describing every menu entry (icons, labels, actions, links, providers, aliases, guards) | `default/omarchy/omarchy-menu.jsonc` |
| **User extensions** | Optional overlay that can retitle, re-icon, or add new items without restarting | `~/.config/omarchy/extensions/omarchy-menu.jsonc` (sample in `config/omarchy/extensions/omarchy-menu.jsonc`) |
| **Runtime engine** | JavaScript model that parses JSONC, merges sources, evaluates guards, resolves routes, and drives the QML UI | [`shell/plugins/menu/MenuModel.js`](https://github.com/basecamp/omarchy/blob/main/shell/plugins/menu/MenuModel.js) |

## JSONC Data Format

The Omarchy menu system uses **JSONC** (JSON with Comments) as its configuration language. The parser strips comments via `stripJsonc` before passing to `JSON.parse`.

### Key Format Characteristics

- **Line comments** (`//`) and **trailing commas** are permitted
- **Dotted IDs** implicitly define the tree hierarchy (e.g., `trigger.share.file` is a child of `trigger.share`)
- **Kind inference** determines entry type:
  - `action` field present → leaf that runs a shell command
  - `target` field present → link to another submenu
  - Neither → submenu container

The complete Entry schema is documented in [`docs/menu.md`](https://github.com/basecamp/omarchy/blob/main/docs/menu.md), covering fields like `icon`, `label`, `title`, `action`, `target`, `provider`, `aliases`, `description`, and guard expressions (`when`, `checked`, `disabled`).

## Loading and Merging Process

At startup, the shell reads both JSONC files and watches them for changes. The [`MenuModel.js`](https://github.com/basecamp/omarchy/blob/main/MenuModel.js) implements this through three functions: `parseMenuJsonc` → `normalizeItem` → `mergeMenuSources`.

### Merge Behavior

1. **Default items** are read first from `default/omarchy/omarchy-menu.jsonc`
2. **User items** from the extensions file are overlaid
3. **Existing IDs**: Only declared fields replace defaults; original position is retained
4. **New IDs**: Appended to the menu structure
5. **Synthetic root**: Injected if missing (`mergeMenuSources` ensures `root` exists)

## Dynamic Providers

Static JSONC entries define fixed rows. Submenus can specify `provider: "<name>"` to receive runtime-generated content.

Provider names are declared in `shell/plugins/menu/Menu.qml` within the *providers map*. When a submenu has a provider, the runtime requests rows from that provider, with each row receiving the submenu's ID as `providerMenu`.

The `swapProviderRows` function merges provider output by replacing only rows from that specific provider, leaving static items untouched. This enables live updates to dynamic sections (like plugin lists or window menus) without full re-renders.

## Guard Evaluation System

Menu entries can use `when`, `checked`, or `disabled` fields containing **Bash expressions** that control visibility and state.

### Batched Execution

Rather than forking Bash for each row, the Omarchy menu system batches all guard expressions into a **single Bash script** (`guardScript`). The script reports results in the format `<id>:<w|c|d>:<0|1>` for each guard type:

- `w` = when (visibility)
- `c` = checked
- `d` = disabled

Results are cached and consulted by `isVisible`, `isDisabled`, and `labelFor` to hide rows, display check-marks, or dim entries.

## Routing and Search

The Omarchy menu system implements sophisticated navigation and query matching.

### Route Resolution

`resolveRoute` normalizes input by:
- Converting to lower-case
- Replacing `_` with `-`

The resolver looks up **exact IDs first**, then falls back to **aliases**.

### Search Ranking

Search tokens are built from `label`, leaf ID, and `aliases` (combined as `nameSearchText`). The `matchesQuery` and `searchScore` functions rank results with this priority:

1. Exact matches
2. Prefix matches
3. Description matches
4. Depth and original ordering

## CLI Integration

The `bin/omarchy-menu` wrapper forwards commands to the menu plugin's IPC surface, ensuring consistency between graphical and scripted access:

```bash
omarchy menu                # toggle root menu

omarchy menu toggle system  # open/close a specific route

omarchy menu summon style.theme
omarchy menu refresh        # re-parse JSONC files

```

These commands invoke the same runtime logic used by the QML UI.

## Practical Configuration Examples

### Adding a Custom Entry

Place in `~/.config/omarchy/extensions/omarchy-menu.jsonc`:

```jsonc
{
  // Add a "Launch Calculator" action under the root menu
  "tools.calculator": {
    "icon": "",
    "label": "Calculator",
    "action": "omarchy-launch calculator"
  }
}

```

The dotted ID `tools.calculator` creates a new `tools` submenu (if absent) with a `calculator` leaf. The presence of `action` infers this as an **action** entry.

### Overriding an Existing Entry

Change the icon of the built-in "System" menu:

```jsonc
{
  "system": {
    "icon": "⚙️"
  }
}

```

Reusing the ID `system` overwrites only the `icon` field while preserving the default `action` and children.

### Defining a Provider-Backed Submenu

In the default menu:

```jsonc
{
  "plugins": {
    "label": "Plugins",
    "provider": "plugins"
  }
}

```

The `plugins` submenu receives its rows from the provider named `plugins` declared in `Menu.qml`.

### Using Conditional Visibility

Hide a row when a package is already installed:

```jsonc
{
  "install.git": {
    "icon": "",
    "label": "Install Git",
    "action": "omarchy-pkg-add git",
    "when": "omarchy-pkg-missing git"
  }
}

```

The row appears only if `git` is not installed; the guard is evaluated in the batched guard script.

## Key Source Files

| File | Role |
|------|------|
| `default/omarchy/omarchy-menu.jsonc` | Core static menu definition |
| `config/omarchy/extensions/omarchy-menu.jsonc` | User-level overlay sample |
| [`docs/menu.md`](https://github.com/basecamp/omarchy/blob/main/docs/menu.md) | Complete schema and format specification |
| [`shell/plugins/menu/MenuModel.js`](https://github.com/basecamp/omarchy/blob/main/shell/plugins/menu/MenuModel.js) | JavaScript runtime model (parsing, merging, guards, routing) |
| `shell/plugins/menu/Menu.qml` | QML plugin (UI rendering, providers map) |
| `bin/omarchy-menu` | CLI wrapper for menu commands |

## Summary

- **Omarchy menu architecture** uses three layers: static JSONC, user extensions, and a JavaScript runtime engine
- **JSONC format** supports comments, trailing commas, dotted IDs for hierarchy, and kind inference from field presence
- **Merging rules** overlay user entries onto defaults, with field-level replacement and position preservation
- **Dynamic providers** enable runtime-generated content through the providers map in `Menu.qml`
- **Guard evaluation** batches Bash expressions into a single script for performance, with cached results
- **Search and routing** normalize input, resolve exact IDs and aliases, and rank by match quality and depth
- **CLI wrapper** (`bin/omarchy-menu`) exposes all functionality for scripting and automation

## Frequently Asked Questions

### How do I add a custom menu entry without modifying core files?

Create or edit `~/.config/omarchy/extensions/omarchy-menu.jsonc` and add entries using dotted IDs. The user extension layer overlays your changes onto the default menu without requiring shell restart. Only declared fields replace defaults; unspecified fields are inherited.

### What is the difference between `action`, `target`, and plain entries in Omarchy's menu JSONC?

The Omarchy menu system infers entry type from field presence: entries with an `action` field become executable leaves, entries with `target` become links to other submenus, and entries with neither become submenu containers. This eliminates the need for explicit type declarations.

### How does conditional visibility work in the Omarchy menu?

Entries use `when`, `checked`, and `disabled` fields containing Bash expressions. At runtime, all guards are batched into a single `guardScript` execution to avoid per-row forks. Results are parsed and cached, then consulted by `isVisible`, `isDisabled`, and related functions to control presentation.

### Can I trigger the Omarchy menu from scripts or keyboard shortcuts?

Yes. The `bin/omarchy-menu` CLI provides commands like `omarchy menu` (toggle root), `omarchy menu summon <route>`, and `omarchy menu refresh`. These use the same IPC surface as the graphical UI, ensuring consistent behavior across interaction methods.