Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
5.6 KiB
Omarchy Shell Development
Read this before editing the Quickshell desktop under shell/.
The Quickshell desktop runs as a single long-running process out of
shell/. Hyprland autostart launches it directly with quickshell -n -p;
do not start additional standalone Quickshell instances for individual
components.
Run omarchy-restart-shell after making changes to QML files.
Plugin contract
- First-party plugins live directly under
shell/plugins/or one category level deeper, such asshell/plugins/panels/weather/. First-party bar-only widgets may use adjacent*.manifest.jsonfiles. Third-party plugins live at~/.config/omarchy/plugins/<id>/with amanifest.jsonat the root. - Every plugin manifest declares
schemaVersion,id,name,version,kinds, andentryPoints. Seedocs/omarchy-shell.mdandshell/services/PluginRegistry.qmlfor the current contract; fields such asactivationare optional. - Entry-point QML files are
Items (notShellRoot), and accept the shell-injected propertiesomarchyPath,shell,manifest, andpluginRegistry/barWidgetRegistryas appropriate. First-party plugins receive the host objects. Third-party plugins receive capability-scoped facades: 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; full-bar plugins additionally receive 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 must be stamped from trusted first-party manifests, and third-party registry views and bar configuration must be detached snapshots rather than shared objects. These facades reduce accidental authority but are not a same-process QML sandbox: a visual bar widget can walk its parent hierarchy to ordinary host objects. Authentication services must therefore remain outside bothShellRoot._servicesand the host QObject tree. Do not expose authentication services through new third-party-facing properties. - Panel / overlay / menu plugins must expose
open(payloadJson)andclose()lifecycle methods forshell summonandshell hide.
IPC
bin/omarchy-shellis the canonical IPC entry point. It forwards to the running shell and does not start it. Prefer it over re-implementing direct Quickshell socket calls in every CLI.- The
shellIPC target exposes lifecycle and configuration methods includingping,summon,hide,toggle,call,rescanPlugins,reloadConfig,setPluginEnabled, andlistPlugins.shell.qmlalso registersimage-selector, which drives theomarchy.image-pickerpanel. - Individual plugins register their own IPC targets, named for the plugin rather
than for where they appear: the background switcher registers
background, and bar widgets register one target each —omarchy.indicators,omarchy.system-update,omarchy.clock. There is nobartarget.
Keeping state current without polling
The shell runs all day on battery, and a widget is built once per monitor, so a Timer that starts a Process costs that process times the number of monitors, every tick, forever. Don't poll for state that only changes through known events. Pick the first of these that fits:
- A native Quickshell service that signals on change:
UPowerandPowerProfiles(Quickshell.Services.UPower),Hyprlandand each workspace'slastIpcObject,Pipewire,Mpris,SystemTray,Notifications. - An event stream the shell already runs, such as the Hyprland IPC events or the long-running
pactl subscribeinCommons/UntypedInput.qml. Extend it rather than starting a second one. - A file watch:
FileViewwithwatchChanges, or the directory watch inplugins/bar/Bar.qml, when the state lives in a file the user can read. - A push from the command that changes the state. If only Omarchy commands change it, have them call
omarchy-shell -q <target> refreshafter the change, the wayomarchy-reminderandomarchy-sudo-passwordlessdo. When the change happens later, without a command running (an expiry, say), schedule the refresh with a transientsystemd-run --user --on-active=…timer. A root-owned timer cannot reach the user's shell.
Polling is the last resort, kept for state with no change signal at all (a remote API, say). Then gate the timer on the panel being open where you can, pick the longest interval the UI can tolerate, and put the poll in one shared service rather than in a per-monitor widget. Never poll a privileged probe: every sudo -n can write to the auth log.
Editing widget files with glyphs
Widget files in shell/plugins/bar/widgets/ contain Nerd Font glyphs as raw
unicode characters. Agent file-editing tools can strip multi-byte codepoints
in some positions — do not rewrite widget files wholesale through those
tools. For glyph fixes, make a targeted edit with the surrounding context, or
use a Python script that inserts codepoints via chr(0xXXXXX).
Palette references
Import qs.Commons as Commons and use Commons.Color for the palette singleton, including Connections.target. Qt 6.12 introduces a Color type in QtQuick that shadows unqualified palette references. This applies to first-party components and third-party plugin QML alike. Keep the unqualified qs.Commons import when the file also uses Style, Util, or Border.