10 KiB
omarchy-shell
A single long-running Quickshell instance
that hosts the Omarchy desktop. The bar, panels, overlays, menus, and
services all run inside as plugins. IPC is the canonical way for CLIs
to talk to a running shell — omarchy-shell-ipc auto-starts it on
first call.
Plugin manifest
{
"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" }
}
kinds (a manifest may declare more than one):
| Kind | What it is |
|---|---|
bar-widget |
Component the bar drops into a section |
panel |
Floating window (e.g. bar settings) |
overlay |
Fullscreen overlay (e.g. background picker) |
menu |
Summoned menu surface |
service |
Headless singleton, no UI |
activation is persistent (loaded at startup) or on-demand
(loaded by shell summon, unloaded by shell hide). On-demand
plugins can set keepLoaded: true to survive between summons.
Full schema: shell/services/PluginRegistry.qml.
Installing a third-party plugin
- Drop into
~/.config/omarchy/plugins/<id>/with amanifest.jsonplus the QML referenced fromentryPoints. omarchy-shell-ipc shell rescanPluginsomarchy-shell-ipc shell setPluginEnabled <id> true- Bar widgets also need adding to a section via bar settings.
IPC
The shell exposes a shell target plus extra targets registered by
individual plugins (bar, image-selector, …).
| 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 |
rescanPlugins |
re-walk plugin dirs |
setPluginEnabled <id> <"true"|…> |
flip enabled bit |
listPlugins |
JSON of every discovered plugin |
setPluginEnabled takes a string; only literal "true" enables.
shell.json
{
"version": 1,
"bar": {
"position": "top",
"transparent": false,
"centerAnchor": "calendar",
"fontFamily": "JetBrainsMono Nerd Font",
"layout": {
"left": [ { "id": "omarchy" } ],
"center": [ { "id": "calendar", "format": "HH:mm" } ],
"right": [ { "id": "audioPanel" } ]
}
},
"plugins": [
{ "id": "community.weather-extra" }
]
}
Rules:
- Every plugin instance is one entry —
bar.layout.<section>for bar widgets,plugins[]for everything else. - Settings are inline on the entry. No
config:sub-object, no merge layers. - Enabled ⇔ present.
allowMultiple: truein the manifest permits multiple instances.version: 1is required.
shell-defaults.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.
Theme tokens
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 override individual keys.
The shell exposes these tokens to QML via two singletons in
qs.Commons:
Color— palette (foreground,background,accent,urgent) and per-surface roles (Color.bar.*,Color.popups.*,Color.notifications.*,Color.menu.*,Color.imagePicker.*).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).
Interactive states
[style] standardizes reusable control chrome around four states:
normal, hover-cursor, selected, and focus. State colors can be
palette roles (foreground, accent, urgent, background) or hex
strings. Fill/border alphas are applied to that state's color.
Focus inherits hover-cursor by default so mouse hover, the panel
keyboard cursor, and Qt activeFocus read as the same state. Use the
literal value "hover-cursor" on focus-* tokens to keep that
inheritance; set an explicit color/number only when a theme intentionally
wants focus to differ.
| State | Color token | Fill alpha | Border width | Border alpha |
|---|---|---|---|---|
| Normal idle chrome | normal-color |
normal-fill-alpha |
normal-border-width |
normal-border-alpha |
| Hover / keyboard cursor | hover-cursor-color |
hover-cursor-fill-alpha |
hover-cursor-border-width |
hover-cursor-border-alpha |
| Persistent selected/current | selected-color |
selected-fill-alpha |
selected-border-width |
selected-border-alpha |
| Qt activeFocus | focus-color |
focus-fill-alpha |
focus-border-width |
focus-border-alpha |
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 theme keeps selected borders off globally (selected-border-width = 0); explicitly bordered controls can still keep their normal border
when selected.
[style]
# Accent-tinted cursor/focus, foreground-tinted selected state.
hover-cursor-color = "accent"
focus-color = "hover-cursor"
selected-color = "foreground"
# 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.
Spacing
[spacing] scale multiplies the shell's shared margins, gaps, and
padding. The default is 1.0; values above 1.0 create more breathing
room while values below 1.0 make controls denser.
[spacing]
scale = 1.15 # roomier panels and controls
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 while still responding to the theme 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.:
[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:
[font]
base-size = 13 # roomier
…or pin individual tokens for stylistic emphasis without affecting the rest of the scale:
[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 is clamped to 11..13 because some row heights and the bar
cross-axis size remain fixed; per-token overrides aren't clamped. 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 (in px):
[bar]
size-horizontal = 26 # top/bottom bar height
size-vertical = 28 # left/right bar width
Custom bar modules
If a full plugin is overkill, declare a one-off module inline in
bar.layout.<section>:
{ "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:
{ "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), shellQuote(v), showTooltip(t, s) / hideTooltip(t),
requestPopout(o) / releasePopout(o).