How to Create a New Bar Widget Plugin for the Omarchy Shell

Create a new bar widget plugin by adding a QML file that inherits from BarWidget, an optional JavaScript model for business logic, and a manifest file that registers the widget with the shell.

The Omarchy shell (from the omacom/omarchy repository) renders its status bar through a Quickshell plugin system where each component is a self-contained QML module. To create a new bar widget plugin, you implement a declarative UI component, bundle it with a JSON manifest, and reference it in your user configuration.

Overview of the Bar Widget Architecture

Omarchy’s status bar is implemented as the omarchy.bar Quickshell plugin. Each widget is a QML component backed by an optional JavaScript model, discovered at runtime through a *.manifest.json file. The loading logic resides in shell/plugins/bar/BarModel.js, which resolves widget IDs from shell.json into filesystem paths and instantiates them as children of the bar container.

The creation process follows four mandatory steps:

  1. Create a QML widget file (MyWidget.qml) under shell/plugins/bar/widgets/
  2. Optionally add a JavaScript model (MyWidgetModel.js) for non-UI logic
  3. Create a manifest (MyWidget.manifest.json) declaring the widget’s ID and entry point
  4. Enable the plugin and add it to the bar layout in ~/.config/omarchy/shell.json

Step 1: Create the QML Widget File

Create a new file under shell/plugins/bar/widgets/. The root element must be BarWidget, a base type defined in shell/plugins/bar/Bar.qml that injects required properties including bar, moduleName, and settings.

// shell/plugins/bar/widgets/MyWidget.qml
import QtQuick
import Quickshell
import qs.Commons
import "MyWidgetModel.js" as MyWidgetModel

BarWidget {
    id: root
    moduleName: "omarchy.my-widget"

    property int counter: 0

    WidgetButton {
        anchors.fill: parent
        bar: root.bar
        text: "MY"
        tooltipText: "Clicked " + root.counter + " times"
        onPressed: root.counter++
    }
}

Why BarWidget? All first-party widgets inherit from this base class. It provides the standardized interface that BarModel.js expects when constructing the layout tree, ensuring your widget receives the correct shell context and settings object.

Step 2: Add the JavaScript Model (Optional)

If your widget interacts with system state or performs complex data processing, isolate that logic in a sibling JavaScript file. Import it into your QML using the standard QtQuick module import syntax.

// shell/plugins/bar/widgets/MyWidgetModel.js
function formatNumber(n) {
    return String(n).padStart(2, "0");
}

module.exports = { formatNumber };

Reference the model in your QML:

import "MyWidgetModel.js" as MyWidgetModel

// Inside a component:
text: MyWidgetModel.formatNumber(root.counter)

This pattern keeps UI declaration separate from business logic, matching the architecture used by built-in widgets like KeyboardLayout (see KeyboardLayoutModel.js for a production example).

Step 3: Register the Widget with a Manifest

Every widget requires a manifest file that declares its kind (always "bar-widget" for bar components) and its entry QML file.

Create MyWidget.manifest.json in the same directory as your QML file:

{
  "id": "omarchy.my-widget",
  "kind": "bar-widget",
  "entry": "MyWidget.qml"
}

Critical constraints:

  • The id must be globally unique across the Omarchy ecosystem (use reverse-domain notation).
  • The entry path is relative to the manifest’s directory.
  • Do not edit the bar’s own manifest.json; the shell discovers your widget automatically when the manifest is present in shell/plugins/bar/widgets/.

Step 4: Enable and Configure the Widget

Enable the plugin using the Omarchy CLI:

omarchy plugin enable omarchy.my-widget

Then define its position in the bar by editing ~/.config/omarchy/shell.json:

{
  "version": 1,
  "bar": {
    "layout": {
      "right": [
        { "id": "omarchy.tray" },
        { "id": "omarchy.my-widget" }
      ]
    }
  }
}

The bar monitors this configuration file in real time; the widget appears immediately upon saving. Alternatively, use the CLI to automate placement:

omarchy bar move right omarchy.my-widget

How the Bar Loads Custom Widgets

When the shell initializes, BarModel.js reads the bar.layout object and constructs a layout tree. For each entry, the model invokes customModulePath(entry, home, configDir) to resolve the widget’s filesystem location. For first-party widgets with manifests, this resolves to widgets/MyWidget.qml.

Key functions in shell/plugins/bar/BarModel.js governing this behavior:

  • customModuleType(entry) – Determines whether a layout entry references a command or a QML module.
  • customModulePath(entry, home, configDir) – Resolves the absolute path to the widget’s QML file based on the manifest ID.
  • inlineSettingsDelta(current, next) – Optimizes live configuration updates, allowing the bar to update widget properties without rebuilding the entire layout.

Complete Minimal Widget Example

The following file tree represents a deployable widget:


shell/
└─ plugins/
   └─ bar/
      └─ widgets/
         ├─ MyWidget.qml
         ├─ MyWidget.manifest.json
         └─ MyWidgetModel.js

MyWidget.qml:

import QtQuick
import Quickshell
import qs.Commons
import "MyWidgetModel.js" as MyWidgetModel

BarWidget {
    id: root
    moduleName: "omarchy.my-widget"
    property int counter: 0

    WidgetButton {
        anchors.fill: parent
        bar: root.bar
        text: "MY"
        tooltipText: "Clicked " + root.counter + " times"
        onPressed: root.counter = MyWidgetModel.double(root.counter)
    }
}

MyWidget.manifest.json:

{
  "id": "omarchy.my-widget",
  "kind": "bar-widget",
  "entry": "MyWidget.qml"
}

MyWidgetModel.js:

function double(n) { return n * 2; }
module.exports = { double };

Install the widget by copying these files to shell/plugins/bar/widgets/, then enable and position it using the commands in Step 4.

Summary

  • Inherit from BarWidget in shell/plugins/bar/Bar.qml to receive required shell context and properties.
  • Isolate logic in an optional JavaScript model imported into your QML.
  • Declare the manifest with a unique ID, kind "bar-widget", and relative entry path.
  • Enable via CLI with omarchy plugin enable <id> and position via shell.json or omarchy bar move.
  • Loader resolution happens in BarModel.js through customModulePath(), supporting live reloads without shell restart.

Frequently Asked Questions

What is the required directory structure for a new bar widget?

Place all widget files under shell/plugins/bar/widgets/ within the Omarchy repository or your local configuration directory. The QML file, manifest, and optional JavaScript model must share the same base filename (e.g., Clock.qml, Clock.manifest.json, ClockModel.js) to ensure the loader in BarModel.js resolves dependencies correctly.

Can I update a widgets code without restarting the Omarchy shell?

Yes. The bar configuration is monitored in real time. Changes to ~/.config/omarchy/shell.json apply instantly. However, modifications to the QML or JavaScript source files typically require a shell restart unless the widget explicitly implements hot-reloading logic using Quickshell’s dynamic instantiation APIs.

What is the difference between kind: "bar-widget" and custom command modules?

The "bar-widget" kind indicates a native QML component that inherits from BarWidget and is discovered via a manifest file. Custom command modules (referenced directly in shell.json without a manifest) execute external processes or load arbitrary QML files that do not necessarily inherit from the standard base class, bypassing the manifest discovery system in BarModel.js.

How do I access user settings from within my widget?

The BarWidget base class automatically injects a settings object into your QML context. Access it via root.settings (where root is your widget’s ID). This object contains the merged configuration from shell.json and responds to live updates processed by inlineSettingsDelta() in BarModel.js.

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 →