How the Omarchy Plugin System Manages Bar Widgets: Architecture & Registration Flow

The Omarchy plugin system manages bar widgets through a three-stage architecture: plugin manifests declare bar-widget capabilities, PluginRegistry loads and instantiates components, and BarWidgetRegistry maintains a reactive catalogue that the UI queries for dynamic rendering.

Omarchy's desktop shell treats bar widgets as first-class plugins, enabling hot-reloading without requiring a full restart. This article examines the complete lifecycle—from manifest parsing to on-screen rendering—using the actual source code from the basecamp/omarchy repository.

The Bar Widget Lifecycle Overview

Bar widgets in Omarchy follow a strict but flexible registration pattern. Every widget passes through three coordinated systems:

  • Manifest declaration — defines the plugin kind and entry point
  • Component registration — PluginRegistry creates QML components and calls BarWidgetRegistry.register()
  • UI instantiation — the bar queries registered widgets and renders them by section and order

This design decouples plugin discovery from visual presentation, allowing the shell to add, remove, or reorder widgets reactively.

Plugin Manifests: Declaring Bar Widgets

Every bar-widget plugin includes a manifest.json that declares its capabilities. The manifest specifies "bar-widget" as a kind and points to a QML entry point.

Consider the weather panel implementation:

// shell/plugins/panels/weather/manifest.json
{
  "id": "weather",
  "name": "Weather",
  "kinds": ["bar-widget", "panel"],
  "entryPoints": {
    "barWidget": "BarWidget.qml",
    "panel": "Panel.qml"
  },
  "metadata": {
    "defaultSection": "right",
    "order": 50
  }
}

The defaultSection and order metadata fields control placement: left, center, or right bar sections, with lower order values appearing first within each section.

BarWidgetRegistry: The Central Widget Catalogue

The shell/services/BarWidgetRegistry.qml file implements the core registration service. It maintains an internal map of widget IDs to their components and metadata, exposing three primary operations:

Method Purpose
register(id, component, metadata) Adds a widget to the catalogue and increments revision
unregister(id) Removes a widget and triggers change notification
metadataFor(id) Returns placement and ordering information

The registry emits a changed signal whenever its internal state updates. This signal drives reactive UI updates, enabling the bar to respond immediately when plugins are installed, updated, or removed.

The registry is instantiated as a singleton in shell/shell.qml:

// shell/shell.qml
property BarWidgetRegistry barWidgetRegistry: BarWidgetRegistry { }

PluginRegistry: Loading and Registration

The shell/services/PluginRegistry.qml handles manifest discovery and component instantiation. During shell startup, it:

  1. Walks plugin directories ($HOME/.config/omarchy/plugins plus built-in plugins)
  2. Parses each manifest.json
  3. For manifests containing "bar-widget" in their kinds array, creates the component and registers it:
// Conceptual flow from PluginRegistry.qml
var manifest = JSON.parse(manifestFile.read());
if (manifest.kinds.includes("bar-widget")) {
    var entryPath = pluginDir + "/" + manifest.entryPoints.barWidget;
    var comp = Qt.createComponent(entryPath);
    BarWidgetRegistry.register(manifest.id, comp, manifest.metadata);
}

This registration call bridges static plugin description and dynamic UI availability.

Bar Widget Components: The Contract

Individual bar widgets inherit from a shared base contract. Examining shell/plugins/panels/weather/BarWidget.qml reveals the standard API:

// shell/plugins/panels/weather/BarWidget.qml
BarWidget {
    id: root
    
    // Required properties injected by the panel system
    property var bar
    property var settings
    property Item anchorItem
    
    // State exposure for routing
    readonly property bool opened: panelLoader.item?.opened ?? false
    
    function injectPanel(panelComponent) {
        panelLoader.sourceComponent = panelComponent;
        panelLoader.item.bar = root.bar;
        panelLoader.item.settings = root.settings;
        panelLoader.item.anchorItem = root;
    }
    
    function open() {
        if (panelLoader.item) panelLoader.item.open();
    }
    
    function close() {
        if (panelLoader.item) panelLoader.item.close();
    }
    
    // Visual content...
}

The injectPanel(), open(), and close() methods enable the shell's summon/hide/toggle IPC commands to control panel visibility. The opened property allows the routing system to determine which panel currently has focus.

Rendering: From Registry to Screen

When drawing the bar, the UI queries BarWidgetRegistry.availableIds(), sorts by metadata.defaultSection and metadata.order, then instantiates each component:

// Conceptual bar rendering logic
Repeater {
    model: BarWidgetRegistry.availableIds().sort((a, b) => {
        var ma = BarWidgetRegistry.metadataFor(a);
        var mb = BarWidgetRegistry.metadataFor(b);
        return ma.order - mb.order;
    })
    
    delegate: Loader {
        sourceComponent: BarWidgetRegistry.componentFor(modelData)
        onLoaded: {
            // Inject bar context and connect signals
        }
    }
}

Because BarWidgetRegistry stores Component objects rather than instances, the bar can create and destroy widget instances on-the-fly. This enables true hot-reloading: updating a plugin's QML file causes the registry to reload the component and emit changed, triggering immediate UI reconstruction.

Key Files and Their Roles

Summary

  • Manifest-driven: Plugins declare bar-widget status via "bar-widget" kind and barWidget entry point
  • Registry-based: BarWidgetRegistry maintains a reactive map of all available widgets, enabling hot-reloading
  • Decoupled loading: PluginRegistry handles discovery and instantiation separately from UI rendering
  • Standardized API: Widgets implement injectPanel(), open(), close(), and opened for shell integration
  • Metadata-controlled: defaultSection and order fields determine visual placement without code changes

Frequently Asked Questions

How does Omarchy achieve hot-reloading of bar widgets without restarting?

The BarWidgetRegistry stores Component objects and emits a changed signal on any registration or unregistration. The bar UI binds to this signal and reconstructs its widget list dynamically. When PluginRegistry detects a file change, it reloads the component and calls register() again, triggering immediate UI updates.

Can a single plugin provide multiple bar widgets?

No, the current architecture assumes one bar widget per plugin manifest. A plugin can expose multiple kinds (such as both "bar-widget" and "panel"), but only one barWidget entry point. To add multiple distinct widgets, create separate plugin directories each with their own manifest.

What happens if two plugins specify the same order value?

The sorting algorithm in the bar UI uses JavaScript's sort(), which is stable. If order values match, plugins maintain their relative positions based on availableIds() return order, typically filesystem or registry insertion sequence. For predictable positioning, assign unique order values spanning a suitable range (e.g., 10, 20, 30).

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →