How the Omarchy Plugin System Works: MenuModel Architecture Explained
The Omarchy plugin system uses a lightweight JavaScript module pattern centered around MenuModel in shell/plugins/menu/MenuModel.js, where plugins export data models and use swapProviderRows() to inject or replace menu entries without mutating global state.
Omarchy's extensibility is built around a lightweight plugin pattern that lives in the shell/plugins/ tree. Each plugin is a plain JavaScript module that exports a model describing the data it contributes, such as panel widgets, services, or menu items. According to the omacom/omarchy source code, this architecture keeps the core thin while enabling deterministic ordering and reactive UI updates through Quickshell's runtime.
Core Architecture of the Omarchy Plugin System
The system revolves around the MenuModel class, which acts as the central registry for all menu-related data. Unlike monolithic plugin systems, Omarchy employs a functional approach where plugins provide data structures rather than executing side effects directly.
The MenuModel Foundation
At the heart of the system sits shell/plugins/menu/MenuModel.js. This file parses static JSONC menu definitions and merges them with user-provided overrides. The model maintains two critical collections: items (the data objects) and itemOrder (the display sequence). Because these are plain JavaScript objects, the QML UI automatically reacts to updates without requiring manual refreshes.
The Registration Contract
Every plugin adheres to a simple contract: they expose a rows array containing objects with id, label, icon, and optional providerMenu fields. When loaded by the Quickshell runtime, plugins invoke core helper functions to publish their rows into the global state. This design avoids in-place mutations that Quickshell's engine might lose track of, ensuring type safety and predictable behavior.
Key Helper Functions in MenuModel.js
The MenuModel exposes two primary functions that plugins use to integrate with the system. These functions handle the complex logic of merging and updating while preserving existing state.
swapProviderRows for Dynamic Updates
The swapProviderRows(items, itemOrder, menuId, rows) function, implemented in lines 136‑165 of shell/plugins/menu/MenuModel.js, replaces rows for a specific provider while leaving all other menu entries untouched. This enables hot-reloading scenarios where enabling a plugin immediately updates the menu without losing static entries or user customizations.
The function signature accepts the current state (items and itemOrder), a menuId identifying the provider, and the new rows array. It returns updated collections that the plugin then publishes back to the model.
mergeMenuSources for Configuration Merging
The mergeMenuSources(defaultItems, userItems) function, found in lines 66‑95 of MenuModel.js, combines built-in menu JSONC with user customizations from ~/.config/omarchy/menu.jsonc. This merge algorithm guarantees a root entry exists and preserves the intended order while allowing users to add aliases or modify existing items.
Plugin Types in the Omarchy Ecosystem
The Omarchy plugin system supports several categories of extensions, each following the same register-merge-publish flow but serving different UI purposes.
Panel Widgets
shell/plugins/bar/BarModel.js demonstrates how bar-panel plugins register widgets for the top bar. These plugins provide tray icons, keyboard layout indicators, and system monitors by exporting rows that the bar component renders as QML items.
Service Models
Service-oriented plugins like shell/plugins/services/nightlight/NightlightModel.js expose state and actions for background services. These models typically include toggle actions and status indicators that appear in system menus, using the same swapProviderRows mechanism to update the UI when service states change.
OSD and Notifications
The shell/plugins/osd/OsdModel.js and shell/plugins/notifications/NotificationLogic.js files illustrate non-menu plugins that handle on-screen displays and notification data. While these don't target the menu system directly, they use the same model-export pattern to communicate with the Quickshell runtime.
Building a Custom Omarchy Plugin
Creating a plugin requires implementing the registration contract and calling the appropriate helpers. Here is a complete example of a panel plugin that contributes menu rows:
/* Example: a simple panel plugin (e.g., "example/Model.js") */
import { swapProviderRows } from '../../plugins/menu/MenuModel.js'
// Define rows the plugin contributes
const rows = [
{
id: 'example.toggle',
label: 'Example Toggle',
kind: 'action',
action: 'omarchy-toggle-example',
icon: 'mdi-lightbulb',
providerMenu: 'example'
}
]
// Insert or replace the rows for this provider
export function register () {
const { items, itemOrder } = MenuModel.current()
const { items: newItems, itemOrder: newOrder } =
swapProviderRows(items, itemOrder, 'example', rows)
// Publish the updated model back to the UI
MenuModel.update(newItems, newOrder)
}
Users can customize how plugins appear by overriding options in ~/.config/omarchy/menu.jsonc:
{
"items": {
"power": { "aliases": ["shutdown", "restart"] }
}
}
The mergeMenuSources function automatically integrates these user aliases with the plugin's default configuration.
Summary
- The Omarchy plugin system centers on
shell/plugins/menu/MenuModel.js, which provides the core registration and merging logic. - Plugins use
swapProviderRows()(lines 136‑165) to dynamically update menu content without affecting other providers. - User customizations merge with defaults via
mergeMenuSources()(lines 66‑95), supporting JSONC configuration files. - All plugins follow a register-merge-publish flow, exporting JavaScript models that Quickshell evaluates at runtime.
- The architecture supports panel widgets, service models, and OSD components through the same lightweight contract.
Frequently Asked Questions
How does Omarchy handle plugin hot-reloading without losing menu state?
Omarchy uses the swapProviderRows function to replace only the rows belonging to the specific provider being reloaded. This function, defined in shell/plugins/menu/MenuModel.js at lines 136‑165, creates new items and itemOrder collections rather than mutating existing arrays in place, ensuring Quickshell's reactive engine detects the changes while preserving entries from other plugins.
What file structure is required for a new Omarchy plugin?
A plugin must reside in the shell/plugins/ directory as a plain JavaScript module. The file should export a model—typically named Model or <Name>Model—and implement a register() function that calls swapProviderRows with appropriately formatted row objects. Panel-specific plugins often follow the shell/plugins/panels/<name>/Model.js convention.
Can users override plugin-provided menu entries?
Yes. Users can create or modify ~/.config/omarchy/menu.jsonc to add aliases, change labels, or reorder items. The mergeMenuSources function in MenuModel.js (lines 66‑95) automatically merges these user configurations with the default menu definition, giving user settings precedence while maintaining the plugin's core functionality.
What is the difference between items and itemOrder in the MenuModel?
The items object stores the actual data for each menu entry keyed by ID, while itemOrder is an array defining the display sequence. This separation allows plugins to update metadata in items without affecting ordering, or reorder entries via itemOrder without touching the underlying data structures. Both collections are plain JavaScript objects that the QML UI observes for changes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →