Files
omarchy/docs/omarchy-shell.md
T
d3d23fddde Honor keepLoaded for services during plugin hot-reload (#9485)
* Honor keepLoaded for services during plugin hot-reload

Plugin reload destroyed every service, including omarchy.lock, which drops the ext-session-lock client while Hyprland still holds the lock and surfaces the crashed-lockscreen fallback.

* Prove keepLoaded service survival with a fixture service

A fresh lock service also reports an empty lastEventAt, so comparing it
across the rescan passed whether or not the instance survived. A fixture
keepLoaded service whose in-memory marker is set before the rescan and
read back after can only pass when the same instance is still mounted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop kept services whose plugin no longer declares a service

The _syncServices cleanup only asked whether the plugin was still
installed and enabled, so a kept service whose plugin dropped its
service kind or entry point kept running as a zombie until shell
restart. Apply the same eligibility checks used at creation, and hand
kept instances the refreshed manifest after a rescan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Cover omarchy.media in keepLoaded expectations; note kept services reload on restart

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 12:33:44 +02:00

402 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# omarchy-shell
A single long-running [Quickshell](https://quickshell.org/) instance
that hosts the Omarchy desktop. The bar, panels, overlays, menus, and
services all run inside as plugins. Hyprland autostart launches the
shell via `omarchy-launch-shell`; restart it with `omarchy-restart-shell`.
IPC is the canonical way for CLIs to talk to a running shell —
`omarchy-shell` forwards a call and fails when the shell is not running
(`-q` makes it quiet best-effort; `OMARCHY_SHELL_IPC_TIMEOUT` bounds the
wait).
## Plugin manifest
```json
{
"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"],
"entryPoints": { "barWidget": "Widget.qml" }
}
```
`kinds` (a manifest may declare more than one):
| Kind | What it is |
|--------------|---------------------------------------------|
| `bar-widget` | Component the active bar drops into a section |
| `bar` | Full bar option that can replace `omarchy.bar` |
| `panel` | Floating window (e.g. OSD) |
| `overlay` | Fullscreen overlay (e.g. background picker) |
| `menu` | Summoned menu surface |
| `service` | Headless singleton, no UI |
Only one full bar option is active at a time. The built-in `omarchy.bar` is
used when `bar.id` is omitted or when a selected third-party bar cannot load.
Panels, overlays, and menus are loaded when summoned. Plugins can set the top-level manifest key `keepLoaded: true` to survive between summons, and to keep a service mounted across plugin hot-reload (so `omarchy.lock` is not destroyed while Hyprland still holds the session lock). First-party services are loaded at startup.
Entry points are QML `Item`s. Panel, overlay, and menu entry points expose
`open(payloadJson)` and `close()` for summon/hide; on load the host injects
`omarchyPath`, `shell`, `manifest`, and the registries (`pluginRegistry` /
`barWidgetRegistry`) as properties.
Full schema: [`shell/services/PluginRegistry.qml`](../shell/services/PluginRegistry.qml).
## Installing a third-party plugin
A plugin is a **git repo** with a `manifest.json` at its root. Adding one
clones it straight into `~/.config/omarchy/plugins/<id>/`; updating is a
fast-forward pull:
```bash
omarchy plugin add https://github.com/acme/omarchy-weather.git
omarchy plugin update # fetches, shows a diff, fast-forwards
omarchy plugin remove acme.weather
```
**Setup › Plugins** offers Enable, Disable, Add, Clone, and Remove. Enable and
Disable include built-ins as well as installed plugins. Clone is limited to
built-ins, while Remove is limited to installed plugins since a built-in has
no checkout to delete. Add, Clone, and Remove open a terminal so their warning,
editor, confirmation, and output stay visible.
Cloning `omarchy.clock`, for example, creates and switches to
`~/.config/omarchy/plugins/<username>.clock/` (e.g. `dhh.clock`), names it
`My Clock`, and preserves the built-in IPC identity so existing shortcuts keep
working. The username prefix keeps a shared clone from colliding with anyone
else's. Saving files in any installed plugin reloads its code automatically,
and removing an active clone switches back to its built-in source.
For a bar widget, on and off means its place in the bar. Everything else is
loaded by default when it is built in, so `shell.json` records only the
deviation: a third-party plugin you added under `plugins[]`, a built-in you
switched off under `disabledPlugins[]`. A full bar has no off state: enabling
one replaces the active bar, and it is therefore never offered under Disable.
Bar widgets may set `barWidget.defaultSection` to `left`, `center`, or `right`;
widgets that omit it default to `center`.
Plugins run as **unsandboxed code** inside `omarchy-shell`. Adding warns you
before cloning, plugins land disabled so you can review the code before
`omarchy plugin enable`, and updates show a diff before touching anything.
Commands confirm in a terminal even when given arguments; without one they
refuse rather than guess. Add `--yes` to skip every prompt (the path for
scripts and agents).
You can still install by hand: drop a plugin into
`~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then
`omarchy plugin enable <id>`. A bar widget starts in its declared default
section; enabling a full bar replaces the one in use. `omarchy bar` drives the
bar from the CLI — `use | reset | defaults | position | transparent | put |
move | set`, with placement flags such as `--section` and `--index`.
The lower-level IPC methods remain available through `omarchy-shell shell ...`.
## IPC
The shell exposes a `shell` target (the host also registers
`image-selector`) plus targets registered by individual plugins, named
for the plugin rather than for where it appears: `background`, `osd`,
`media`, `notifications`, and per-widget targets such as `omarchy.clock`
or `omarchy.power`. There is no `bar` target.
| Method | Effect |
|---------------------------------------|---------------------------------|
| `ping` | health check |
| `summon <id> <payloadJson>` | load + open a plugin |
| `hide <id>` | close a previously-summoned |
| `toggle <id> <payloadJson>` | summon if closed, hide if open |
| `togglePanelAt <section> <index>` | toggle the panel at a bar position |
| `call <id> <method> <arg>` | call an already-loaded plugin |
| `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code |
| `reloadConfig` | reload shell.json |
| `applyTheme <colorsB64> <shellB64>` | push theme colors + shell.toml |
| `toggleBarTransparency` | flip the bar background between solid and transparent |
| `setPluginEnabled <id> <"true"\|…>` | flip enabled bit (`ok` / `unknown`) |
| `enablePlugin <id> <placementJson>` | enable and place in one mutation |
| `putBarWidget <id> <placementJson>` | place a widget only where absent (`omarchy bar put`) |
| `moveBarWidget <id> <placementJson>` | move a configured widget |
| `setBarWidget <id> <key> <valueJson> <selectorJson>` | set an inline widget option |
| `listPlugins` | JSON of every discovered plugin |
| `listShellConfig` | effective shell.json as JSON |
| `debugBarGeometry` | bar geometry dump for debugging |
`setPluginEnabled` takes a string; only literal `"true"` enables. Methods
answer on stdout with exit 0 — `ok` on success, `unknown` or an error
string on a miss.
## shell.json
```json
{
"version": 1,
"idle": {
"screensaver": 150,
"lock": 300
},
"bar": {
"id": "omarchy.bar",
"position": "top",
"transparent": false,
"centerAnchor": "omarchy.clock",
"layout": {
"left": [ { "id": "omarchy.menu" } ],
"center": [ { "id": "omarchy.clock", "format": "HH:mm" } ],
"right": [ { "id": "omarchy.audio" } ]
}
},
"plugins": [
{ "id": "community.weather-extra" }
]
}
```
Rules:
1. The active bar option is `bar.id`. Omit it or set it to `omarchy.bar` for
the built-in bar; set it to a plugin whose manifest declares `kind: "bar"`
to replace the full bar.
2. Every plugin instance is one entry — `bar.layout.<section>` for
bar widgets, `plugins[]` for everything else.
3. Settings are inline on the entry. No `config:` sub-object, no
merge layers.
4. Built-in bar widget ids are namespaced (`omarchy.clock`, `omarchy.audio`, …).
5. Third-party enabled ⇔ present; for full bar options that means `bar.id`.
First-party non-bar plugins are enabled unless listed in `disabledPlugins[]`.
6. `barWidget.allowMultiple: true` in the manifest permits multiple instances.
7. `idle.screensaver` and `idle.lock` are seconds since user idle began.
8. `version: 1` is required.
`config/omarchy/shell.json` describes the fresh-install state. When no
user `shell.json` exists, defaults are used verbatim. Once the user
customizes, `shell.json` is canonical — there is no deep-merge.
`shell.json` is shell configuration; theme tokens live in `shell.toml`
(next section). Both are current — they answer different questions. A
machine-level `~/.config/omarchy/shell.toml` is watched live by the
shell and its keys win over the active theme's `shell.toml`, so
overrides like `omarchy display text size` survive theme switches.
## Theme tokens
See [`theming.md`](theming.md) for the full theme/template workflow,
including generated `*.tpl` files, gradient helpers, and shell border syntax.
Themes ship colors in `themes/<name>/colors.toml` and surface roles +
sizing in `themes/<name>/shell.toml`. Defaults are generated from
`default/themed/shell.toml.tpl`; a theme may also drop a hand-written
`shell.toml` next to its `colors.toml` to replace the generated file,
or override a single section with `shell.<section>.toml`, merged in by
`omarchy-theme-set-templates` (see [`theming.md`](theming.md)).
`colors.toml` uses `foreground` and `background` for the foundational
text/background palette, exposed to QML as `Color.foreground` and
`Color.background`.
The shell exposes these tokens to QML via three singletons in
`qs.Commons`:
- `Color` — palette (`foreground`, `background`, `accent`, `urgent`)
and per-surface roles (`Color.bar.*`, `Color.popups.*`,
`Color.tooltip.*`, `Color.notifications.*`, `Color.menu.*`,
`Color.polkit.*`, `Color.lock.*`, `Color.imagePicker.*`). Clipboard
and emojis share `Color.menu.*`; the `[launcher]` section is consumed
by the launcher outside shell QML.
- `Style` — structural tokens (`cornerRadius`), shared interactive
state tokens/helpers, spacing (`Style.spacing.*` / `Style.space(px)`),
the type scale (`Style.font.*`), and bar dimensions
(`Style.bar.sizeHorizontal` / `Style.bar.sizeVertical`).
- `Border` — border-spec helpers for QML surfaces. Use with
`BorderSurface` from `qs.Ui` when a border should honor shell theme
gradients or per-side widths. `Color.<section>.border` is only the
flat-color fallback for code that cannot render a real border.
### Interactive states
`[controls]` standardizes reusable control chrome (buttons, dropdowns,
tab strips, etc.) around four states: `normal`, `hover-cursor`, `focus`,
and `selected`. State colors and border tokens accept palette roles
(`foreground`, `accent`, `urgent`, `background`) or hex strings; border
values may also be gradients. Fill alpha applies to the state color;
border alpha applies to the state's border token.
Surfaces like `[menu]`, `[launcher]`, and `[image-picker]` define
their own `selected-*` tokens and do **not** inherit from `[controls]`.
`[controls]` only governs the shared button/dropdown chrome.
| State | Color token | Fill alpha | Border token | Border width | Border alpha |
|-------|-------------|------------|--------------|--------------|--------------|
| Normal idle chrome | `normal-color` | `normal-fill-alpha` | `normal-border` | `normal-border-width` | `normal-border-alpha` |
| Hover / keyboard cursor | `hover-cursor-color` | `hover-cursor-fill-alpha` | `hover-cursor-border` | `hover-cursor-border-width` | `hover-cursor-border-alpha` |
| Qt activeFocus | `focus-color` | `focus-fill-alpha` | `focus-border` | `focus-border-width` | `focus-border-alpha` |
| Persistent selected/current | `selected-color` | `selected-fill-alpha` | `selected-border` | `selected-border-width` | `selected-border-alpha` |
The template ships `focus-*` adjacent to `hover-cursor-*` with the same
values so mouse hover, keyboard cursor, and tab focus read identically.
Themes that want focus to stand out override the `focus-*` keys.
Border widths are the theme-level on/off switches for state borders; set
a width to `0` to keep the fill while removing that state border. The
default keeps selected borders off globally (`selected-border-width =
0`); explicitly bordered controls keep their normal border when selected.
```toml
[controls]
# Accent-tinted cursor/focus, foreground-tinted selected state.
hover-cursor-color = "accent"
focus-color = "accent"
selected-color = "foreground"
focus-border = "rgba(33ccffee) rgba(00ff99ee) 45deg"
# Keep selected fills but remove selected-state borders.
selected-border-width = 0
```
Momentary fills use `pressed-fill-alpha` for button press feedback and
`selection-fill-alpha` for text selection. Themes may also provide
`pressed-color` or `selection-color`; they fall back to hover-cursor and
foreground respectively.
The section was previously named `[style]`. Hand-written theme
`shell.toml` files using the old name still apply — the parser accepts
both `[controls]` and `[style]`.
### Spacing
`[spacing] scale` multiplies the shell's shared margins, gaps, padding,
control sizes, and panel dimensions. The default is `1.0`; values above
`1.0` create more breathing room while values below `1.0` make controls
dense. By default `scale-with-font = true`, so increasing `[font]
base-size` also scales buttons, popup widths, row heights, and panel
padding proportionally.
```toml
[spacing]
scale = 1.15
scale-with-font = true # grow controls and panels with [font] base-size
```
QML components should prefer semantic tokens where possible:
| Token | Default use |
|-------|-------------|
| `Style.spacing.controlPaddingX` / `controlPaddingY` | Button and tooltip padding |
| `Style.spacing.inputPaddingY` | Text-field vertical padding |
| `Style.spacing.controlHeight` / `popupRowHeight` | Dropdown and number-field row heights |
| `Style.spacing.dropdownWidth` / `searchableDropdownWidth` / `numberFieldWidth` | Default field widths |
| `Style.spacing.searchablePopupMinHeight` | Minimum searchable dropdown popup height |
| `Style.spacing.controlGap` | Gap between icon and label inside controls |
| `Style.spacing.labelGap` | Label-to-control and compact list gaps |
| `Style.spacing.rowGap` / `rowPaddingX` | Form rows and list row content |
| `Style.spacing.panelGap` / `panelPadding` | Panel section spacing and interior padding |
| `Style.spacing.popupPadding` | Popout interior padding |
Popout placement deliberately follows Hyprland's `general:gaps_out`
(`Style.gapsOut`) so panels align with tiled windows. Use a theme's
`hyprland.lua` to change that outer alignment; `[spacing]` controls the
interior breathing room.
For one-off proportional constants, use `Style.space(px)` to preserve the
old default at scale `1.0` and `base-size = 12` while still responding to
the theme scale and font scale. Use `Style.spaceReal(px)` only for
fractional geometry that should not be rounded, such as bar widget text
margins. Themes can override any semantic token directly in `[spacing]`,
e.g.:
```toml
[spacing]
scale = 1.0
panel-padding = 22
row-gap = 10
```
### Typography
`[font] base-size` is the rem root for the scale. Every
`Style.font.<token>` derives from it via a fixed multiplier, so
bumping `base-size` rescales the whole shell proportionally:
| Token | Multiplier | Default |
|-----------------------|------------|---------|
| `Style.font.caption` | 0.833 | 10 |
| `Style.font.bodySmall` | 0.917 | 11 |
| `Style.font.body` | 1.0 | 12 |
| `Style.font.subtitle` | 1.083 | 13 |
| `Style.font.title` | 1.167 | 14 |
| `Style.font.heading` | 1.333 | 16 |
| `Style.font.display` | 2.0 | 24 |
| `Style.font.displayLarge` | 2.333 | 28 |
| `Style.font.iconSmall` | bodySmall | 11 |
| `Style.font.icon` | title | 14 |
| `Style.font.iconLarge` | 1.5 | 18 |
A theme can either scale everything by tweaking `base-size`:
```toml
[font]
base-size = 13 # roomier
```
…or pin individual tokens for stylistic emphasis without affecting
the rest of the scale:
```toml
[font]
base-size = 12
heading = 20
display-large = 36
```
Recognized override keys: `base-size`, `caption`, `body-small`,
`body`, `subtitle`, `title`, `heading`, `display`, `display-large`,
`icon-small`, `icon`, `icon-large`.
`base-size` has no upper clamp; the shell only floors it at **1px** to
avoid nonsensical zero/negative sizes. Per-token overrides aren't clamped
either. The shell font family is the fontconfig `monospace` alias —
themes don't set it, the user does via `omarchy font set <name>`.
### Bar size
`[bar] size-horizontal` / `size-vertical` set the cross-axis dimension
of top/bottom and left/right bars respectively, measured at the default
12px font base. By default `scale-with-font = true`, so increasing
`[font] base-size` also increases the bar's cross-axis size:
```toml
[bar]
scale-with-font = true
size-horizontal = 26 # top/bottom bar height at base-size 12
size-vertical = 28 # left/right bar width at base-size 12
```
Set `scale-with-font = false` to keep those bar sizes as fixed pixels.
## Custom bar modules
If a full plugin is overkill, declare a one-off module inline in
`bar.layout.<section>`:
```json
{ "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status",
"interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" }
```
Output is plain text or Waybar-style JSON (`{ "text": ..., "tooltip": ..., "class": ... }`).
For a custom QML widget:
```json
{ "id": "gpu", "type": "qml" }
```
Then `~/.config/omarchy/bar/modules/gpu.qml` (or set `source` to point
elsewhere). The module is an `Item` and receives `bar`, `moduleName`,
`settings` properties. `bar` exposes `foreground` / `background` /
`urgent` / `fontFamily` / `position` / `vertical` / `barSize`, plus
`run(cmd)`, `showTooltip(t, s)` / `hideTooltip(t)`,
`requestPopout(o)` / `releasePopout(o)`. To shell-quote arguments for
`run`, use `Util.shellQuote(v)` from `qs.Commons`.