* Move Elsewhen into Omarchy as omarchy.elsewhen The world clock ships in the shell tree instead of its own package. A migration renames existing bar entries with their settings and removes the retired package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Remove Elsewhen's shelved Earth row, overlap band and sky tint Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tidy Elsewhen's models, data helper and docs, and drop currency Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restructure Elsewhen's panel and globe on the shared shell components Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show Elsewhen's bar globe upright Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Use the shared city search for Elsewhen's globe jump bar Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep legacy Elsewhen entries single and retry offline geocodes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fetch Elsewhen's weather with curl instead of a Python helper Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
21 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. 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
{
"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" },
"barWidget": {
"displayName": "Cool clock",
"category": "Time",
"allowMultiple": false,
"defaultSection": "left",
"defaults": { "format": "HH:mm" },
"schema": [
{ "key": "format", "type": "string", "label": "Format" }
]
}
}
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). The kept service instance is not replaced, so changes to its code take effect on a shell restart. First-party services are loaded at startup.
Entry points are QML Items. 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. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades instead: ordinary plugins may look up and control only their own service and lifecycle, built-in clones retain narrow source-specific configuration and UI compatibility, menu plugins receive an application-library facade, and plugins can read detached scalar bar state. A full-bar plugin additionally receives detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities are stamped from trusted first-party manifests, authentication services are kept out of the host's public service map and QML object tree, and third-party registry/configuration snapshots can be changed only locally without mutating host state. The facades are API boundaries, not same-process QML sandboxes: a visual widget shares the host bar's scene and can walk its parent hierarchy to ordinary host objects. Sensitive state must not rely on the facade alone for isolation.
A third-party replacement bar can render registered widget components, but widgets it hosts receive a service-less entry facade. Allowing the bar to manufacture an own-service facade for an arbitrary widget would also let it retrieve that plugin's live service object. Service-backed third-party widgets therefore retain their full integration only under the trusted built-in bar; a replacement bar may still provide their target-scoped lifecycle and settings operations.
Full schema: 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:
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). The scoped interfaces remove direct access to authentication services and avoid handing generic cross-plugin service factories to replacement bars, but visual plugins can still traverse ordinary objects in their shared QML scene. Plugin code also has the same user-level file and process access as the shell.
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 ....
Elsewhen
Elsewhen (omarchy.elsewhen), the world clock, is a first-party plugin in shell/plugins/panels/elsewhen/; elsewhen.md covers how it works. It shipped as the separate elsewhen package under the id omacom.elsewhen until it moved in. New installs place it immediately before the clock; the placement migration uses omarchy bar put omarchy.elsewhen --before omarchy.clock, which preserves an existing placement and uses Elsewhen's normal right-side placement if the clock is absent. With no shell to ask, as in an update from a TTY, it skips the placement rather than failing the update. A later migration renames existing omacom.elsewhen entries in shell.json, keeping their settings, and removes the retired package and any dev-checkout link to its /usr/share/omarchy path, leaving links and checkouts the user made alone.
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
{
"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:
- The active bar option is
bar.id. Omit it or set it toomarchy.barfor the built-in bar; set it to a plugin whose manifest declareskind: "bar"to replace the full bar. - 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. - Built-in bar widget ids are namespaced (
omarchy.clock,omarchy.audio, …). - Third-party enabled ⇔ present; for full bar options that means
bar.id. First-party non-bar plugins are enabled unless listed indisabledPlugins[]. barWidget.allowMultiple: truein the manifest permits multiple instances.idle.screensaverandidle.lockare seconds since user idle began.version: 1is 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 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).
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 shareColor.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 withBorderSurfacefromqs.Uiwhen a border should honor shell theme gradients or per-side widths.Color.<section>.borderis 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.
[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.
[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.:
[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 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:
[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>:
{ "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), showTooltip(t, s) / hideTooltip(t),
requestPopout(o) / releasePopout(o). To shell-quote arguments for
run, use Util.shellQuote(v) from qs.Commons.