PopupCard gains a triggerMode property — "click" stays as the default (focus-grab dismissal), "hover" skips the grab so the cursor can move freely between the trigger and the card. PopupCard also exposes a containsMouse alias for widgets that want to coordinate hover state. Weather widget now shows on hover and hides 220ms after the cursor leaves both the trigger and the popup. The redundant tooltip is gone — the popup is the detail view. Click is still wired for refresh (middle) and notification dump (right).
Omarchy shell
omarchy-shell is a single long-running Quickshell
instance that hosts the Omarchy desktop. Hyprland autostarts one shell per
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 in this branch:
default/quickshell/omarchy-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)
bar-settings/
image-picker/
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 |
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
- Drop the plugin into
~/.config/omarchy/plugins/<plugin-id>/. The directory must contain amanifest.jsonplus the QML files referenced from itsentryPoints. omarchy-shell-ipc shell rescanPlugins— or open the Plugin Manager tab inomarchy launch bar-settingsand click Rescan.- Enable the plugin (Plugin Manager Enable toggle, or
omarchy-shell-ipc shell setPluginEnabled <id> true). - If it's a
bar-widget, add it to a layout section from the bar editor.
First-party plugins under default/quickshell/omarchy-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 background switcher's image-selector target).
| 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/default/quickshell/omarchy-shell call shell ping
A convenience wrapper, omarchy-shell-ipc,
starts the shell if it is not already running, then forwards a call. It
is the canonical way for other Omarchy CLIs to talk to the shell.
omarchy-shell-ipc shell ping
omarchy-shell-ipc shell summon omarchy.bar-settings "{}"
omarchy-shell-ipc shell listPlugins
omarchy-shell-ipc 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 to defaults in omarchy launch bar-settings
rewrites shell.json from the current shell-defaults.json.
shell.json shape
{
"version": 1,
"bar": {
"position": "top",
"centerAnchor": "calendar",
"fontFamily": "JetBrainsMono Nerd Font",
"layout": {
"left": [ { "id": "omarchy" }, { "id": "workspaces" } ],
"center": [ { "id": "calendar", "format": "HH:mm" } ],
"right": [
{ "id": "audioPanel" },
{ "id": "controlCenter" },
{ "id": "powerMenu" }
]
}
},
"plugins": [
{ "id": "omarchy.bar-settings" },
{ "id": "omarchy.image-picker" }
]
}
Storage rules
- Every plugin instance is one entry. Either in
bar.layout.<section>for bar widgets, or inplugins[]for panels, overlays, services, menus, and anything else non-bar. - 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. - Enabled ⇔ present. A plugin is enabled iff its id appears somewhere in shell.json. To disable, remove it. (The bar settings UI does both.)
- 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. version: 1is 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.