Files
arthur-os/shell
Ryan Hughes b5f7ba02c2 Unify PillButton, CursorPill, ChoiceButton into qs.Ui.Button + ButtonGroup
The three components were three takes on the same shape \u2014 a clickable
rectangle with text/icon, a hot/hover state, an optional persistent
border, and an optional 'selected' or 'active' highlight. CursorPill
was a 30-line PillButton wrapper that added a hovered() signal;
ChoiceButton was effectively PillButton with selected: bool painting
an accent fill+border.

Collapse them into a single qs.Ui.Button. State flags compose
independently:

  hasCursor / hover         hot fill
  active                    persistent foreground-tint fill
  selected                  accent fill + accent border
  bordered: true            persistent 1px idle border (form primaries)
  focusable: true           Tab focus paints the accent ring
  pressed                   pressed fill

The hovered(bool) signal is now built-in, so CursorPill's wrapper is
unnecessary. ButtonGroup wraps a Row+Repeater for the form-style
'pick one of N' pattern; panel-cursor-driven cases still compose
Buttons directly in a Row with per-instance hasCursor wiring.

Theme tokens move into a new [style] section in shell.toml:

  border-width        = 1
  focus-border-width  = 3
  idle-border-alpha   = 0.4
  hot-fill-alpha      = 0.08
  selected-fill-alpha = 0.18
  pressed-fill-alpha  = 0.22
  focus-fill-alpha    = 0.22

Style.qml parses these out of the same shell.toml [font] / [bar]
already reads, and exposes pre-computed Style.hotFill / selectedFill /
pressedFill / idleBorderColor / selectedAccentFill / borderWidth +
the existing focusBorder* tokens. Themes that don't ship a [style]
section get the previous defaults unchanged.

Dev gallery consolidates three sections (PillButton, CursorPill,
ChoiceButton) into Button + ButtonGroup, with the cursor model
sections renamed accordingly.
2026-05-18 12:09:13 -04:00
..

Omarchy shell

omarchy-shell is a single long-running Quickshell instance that hosts the Omarchy desktop. A supervised user systemd service keeps one shell running per graphical session; everything else — the bar, the bar settings UI, the background switcher, future panels and overlays — runs inside the shell as a plugin.

Hosting everything inside one shell means:

  • shared services and singletons live once, not once per process
  • summoning a panel is an IPC call into a process that is already running, not a fresh quickshell -p ... cold start
  • third-party plugins can be loaded from disk without changing any source code in Omarchy itself

The runtime layout:

shell/
  shell.qml              entry point (ShellRoot)
  shell-defaults.json    canonical out-of-the-box config
  services/
    PluginRegistry.qml   discovers, validates plugins, looks up enabled state in shell.json
    BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
  ui/
    settings/
      DynamicSettingsForm.qml  renders plugin-declared schemas
  plugins/
    bar/                 first-party plugins (see plugins/README.md)
    settings/
    image-picker/
    menu/
    notifications/
    osd/
    polkit/

The plugin discovery path is documented in plugins/README.md.

Plugin manifest

Every plugin ships a manifest.json describing what it is and how the shell should load it. Minimal example:

{
  "schemaVersion": 1,
  "id": "my.org.cool-clock",
  "name": "Cool clock",
  "version": "1.0.0",
  "author": "You",
  "description": "A clock that does cool things",
  "kinds": ["bar-widget"],
  "activation": "on-demand",
  "entryPoints": { "barWidget": "Widget.qml" },
  "barWidget": {
    "displayName": "Cool clock",
    "category": "Time",
    "allowMultiple": false,
    "defaults": { "format": "HH:mm" },
    "schema": [
      { "key": "format", "type": "string", "label": "Format" }
    ]
  }
}

Supported kinds:

Kind What it is
bar-widget A component that the bar can drop into a section
panel A persistent or summoned floating window (e.g. bar settings)
overlay A fullscreen overlay (e.g. background switcher)
menu A summoned menu surface
service A headless singleton, no UI
bar Reserved for the first-party bar host (omarchy.bar). Third-party plugins should ship bar-widgets; they do not replace the host bar.

activation is either persistent (loaded on startup, never unloaded) or on-demand (loaded by shell summon <id> and unloaded by shell hide). Plugins that need to outlive a single summon can set keepLoaded: true (e.g. the image picker keeps its overlay window mounted between summons).

The full schema lives in services/PluginRegistry.qml.

Installing a third-party plugin

  1. Drop the plugin into ~/.config/omarchy/plugins/<plugin-id>/. The directory must contain a manifest.json plus the QML files referenced from its entryPoints.
  2. omarchy-shell shell rescanPlugins.
  3. Enable the plugin with omarchy-shell shell setPluginEnabled <id> true.
  4. If it's a bar-widget, add it to a layout section from the bar editor.

First-party plugins under shell/plugins/ are discovered the same way and cannot be disabled.

IPC contract

The shell exposes a single shell IPC target plus whatever extra targets individual plugins register (e.g. the bar's bar target for refresh hooks, the image picker's image-selector target). omarchy-menu uses the shell target to summon the first-party omarchy.menu plugin instead of running a separate Quickshell instance.

Method Returns Effect
ping ok health check
summon <id> <payloadJson> ok / unknown load + open a panel/overlay plugin
hide <id> close a previously-summoned plugin
toggle <id> <payloadJson> summon if closed, hide if open
rescanPlugins re-walk plugin dirs and pick up new/changed manifests
setPluginEnabled <id> <enabled> flip the persisted enabled bit (see note)
listPlugins JSON every discovered plugin (id, name, kinds, enabled)

Direct invocation:

quickshell ipc -p $OMARCHY_PATH/shell call shell ping

The omarchy-shell.service user unit starts the shell for the graphical session and restarts it if it exits. Use omarchy-restart-shell to reload the long-running shell process.

A convenience wrapper, omarchy-shell, forwards IPC calls to the running service. It does not start the shell; the systemd unit owns the shell lifecycle.

omarchy-shell shell ping
omarchy-shell shell summon omarchy.settings "{}"
omarchy-shell shell listPlugins
omarchy-shell shell rescanPlugins

Note on setPluginEnabled: the enabled argument is a string. Only the literal "true" enables the plugin; every other value (including "True", "1", "yes", or omitted) disables it. This keeps the IPC surface type-stable across QML's string-only IPC arguments.

Persisted state

There is one user config file. Everything that distinguishes your customization from the shipped defaults lives in it.

Path Owner Purpose
~/.config/omarchy/shell.json the shell full layout + per-entry settings + enabled plugin list
~/.config/omarchy/plugins/<id>/ user drop-in third-party plugin source files

The shell-defaults.json bundled with the shell describes the fresh-install state. When the user has no shell.json, the shell uses the defaults verbatim. Once the user customizes anything, shell.json becomes the authoritative file — we do not deep-merge defaults back in. Pressing Reset bar to defaults in omarchy launch bar settings rewrites the bar subtree from the current shell-defaults.json.

shell.json shape

{
  "version": 1,
  "bar": {
    "position": "top",
    "transparent": false,
    "centerAnchor": "calendar",
    "fontFamily": "JetBrainsMono Nerd Font",
    "layout": {
      "left":   [ { "id": "omarchy" }, { "id": "workspaces" } ],
      "center": [ { "id": "calendar", "format": "HH:mm" } ],
      "right": [
        { "id": "audioPanel" }
      ]
    }
  },
  "plugins": [
    { "id": "omarchy.settings" },
    { "id": "omarchy.image-picker" }
  ]
}

Storage rules

  1. Every plugin instance is one entry. Either in bar.layout.<section> for bar widgets, or in plugins[] for panels, overlays, services, menus, and anything else non-bar.
  2. Settings are inline on the entry. No config: sub-object, no separate per-plugin settings file, no merge layers. The fields on each entry are the values the plugin sees.
  3. Enabled ⇔ present. A plugin is enabled iff its id appears somewhere in shell.json. For bar widgets, the bar settings UI adds/removes layout entries; other plugin kinds are enabled with the shell IPC.
  4. Multiple instances are allowed when a manifest sets allowMultiple: true. Each instance is independent — e.g. two clocks in different timezones are just two {"id":"calendar", "timezone": ...} entries with their own values.
  5. version: 1 is required at the top level. The shell will fall back to defaults rather than load an unknown version.

Implementation history

Built up in phases on this branch:

  • Phase 1 — omarchy-shell phase 1: host the existing bar in a single shell
  • Phase 2 — omarchy-shell phase 2: plugin registry and bar widget registry
  • Phase 3 — omarchy-shell phase 3: fold bar-settings into the shell as a panel plugin
  • Phase 4 — omarchy-shell phase 4: absorb background-switcher as a plugin
  • Phase 5 — omarchy-shell phase 5: docs, cleanup, and migration crumbs
  • Phase 6 — omarchy-shell phase 6: reviewer cleanup (path traversal, collision, races)
  • Phase 7 — omarchy-shell phase 7: replace socket with IpcHandler, rename to image-picker
  • Phase 8a — omarchy-shell phase 8a: unified shell.json with inline plugin settings

Shared services and Pipewire/UPower/Hyprland consolidation are explicitly out of scope here and deferred to a follow-up after a review pass.