Skip to content

quickshell

Recipe card from the charly-check plugin (Commands — runtime CLI verbs).

QUICKSHELL — IPC against a Quickshell desktop shell

Section titled “QUICKSHELL — IPC against a Quickshell desktop shell”

The quickshell: check verb drives the control channel of a Quickshell desktop shell — the IPC surface a desktop’s panels, menus, notifications and overlays are summoned and dismissed through. It is NOT a host charly subcommand: it is a declarative check verb served out-of-process by its plugin, parallel to wl:, cdp: and dbus:.

It is desktop-agnostic. The verb speaks Quickshell, not omarchy: config: names the shell’s config directory and target:/function: name whatever that shell’s QML exposes through an IpcHandler.

Why this is a verb and not a command: step

Section titled “Why this is a verb and not a command: step”

This is the whole justification, and it is worth understanding before authoring:

qs ipc reports failures on STDOUT and exits ZERO.

$ qs ipc -p /usr/share/omarchy/shell call -- shell nosuchfunction; echo "exit=$?"
Function not found.
exit=0

So a command: step asserting “the menu opened” PASSES against a shell with no menu plugin loaded, against a typo in the target, and against an IPC surface that changed underneath it. The step is green and asserts nothing.

quickshell: reads those responses and fails on them. The four it keys on — Target not found., Function not found., and the two argument-count errors — are strings in the quickshell binary itself, not omarchy’s wrapper, which is what keeps the guard desktop-agnostic.

Method Purpose
quickshell: ping liveness — calls the shell’s conventional shell.ping
quickshell: call the generic primitive — invoke function on target

ping is a convenience over call with overridable defaults; everything a desktop exposes is reachable through call.

Field Meaning
config: required — the Quickshell config DIRECTORY (the folder holding shell.qml)
target: the IpcHandler target name; defaults to shell for ping
function: the function to invoke; required for call
args: positional arguments; a JSON payload is just a string argument
wayland_display: override display discovery (see below)

config: is required because qs matches instances BY CONFIG PATH, and a step arriving from outside the session has no ambient one.

# liveness — the scalar shorthand needs the fields, so use the map form
- check: the shell answers a ping
context: [runtime]
eventually: 60s
quickshell:
method: ping
config: /usr/share/omarchy/shell
# every core plugin is loaded, asserted in ONE step
- check: the core shell plugins are loaded
context: [runtime]
quickshell:
method: call
config: /usr/share/omarchy/shell
target: shell
function: listPlugins
stdout:
- contains: omarchy.bar
- contains: omarchy.menu
# summon a surface. EVERY argument the QML function declares is REQUIRED —
# omarchy's is summon(pluginId, payloadJson), so a panel that needs no payload
# still passes one. Pass '{}', not nothing.
- check: the root menu opens
context: [runtime]
stdout:
- matches: '^ok\\s*$'
quickshell:
method: call
config: /usr/share/omarchy/shell
target: shell
function: summon
args: ['omarchy.menu', '{"menu":"root"}']
# a panel with no payload of its own — still two arguments
- check: the weather panel opens
context: [runtime]
stdout:
- matches: '^ok\\s*$'
quickshell:
method: call
config: /usr/share/omarchy/shell
target: shell
function: summon
args: ['omarchy.weather', '{}']

The verb closes the FIRST: qs ipc reports IPC-level failures on stdout while exiting zero, so this plugin classifies “Target not found.”, “Function not found.” and the argument-count errors as failures. Passing one argument to a two-argument function is caught here:

quickshell: shell.summon: Too few arguments provided (2 required but 1 were provided.)

The verb CANNOT close the second, because it is the shell’s own contract rather than Quickshell’s. omarchy’s summon returns the STRING “ok” or “unknown” and exits zero either way — measured on a live guest:

$ qs ipc -n -p /usr/share/omarchy/shell call -- shell summon totally.bogus.plugin '{}'
unknown
exit=0
$ qs ipc -n -p /usr/share/omarchy/shell call -- shell summon omarchy.weather '{}'
ok
exit=0

So exit status proves only that the round-trip happened — NOT that anything opened. A summon step without an stdout: assertion passes against a shell that summoned nothing, which is the failure-open shape this verb exists to remove. Assert the ANSWER on every summon step. Read the target’s QML for the function’s real return before trusting a bare exit code: hide(id) returns void and needs no assertion, summon and call return strings that do.

The display, and why steps over SSH need it

Section titled “The display, and why steps over SSH need it”

qs matches instances by WAYLAND_DISPLAY, and a check step arriving over SSH has none. The verb recovers it from XDG_RUNTIME_DIR automatically — without that it would fail on exactly the guests it is most useful for. Set wayland_display: only to override that discovery.

quickshell: drives the shell; wl: observes the compositor. They answer different questions and the strong assertions use both:

- check: the menu opens # quickshell: DRIVE it
quickshell: {method: call, config: , target: shell, function: summon, args: ['omarchy.menu']}
- check: its surface is mapped # wl: OBSERVE the result
wl: hypr-layers
stdout: [{contains: omarchy-menu}]
- check: and it actually rendered # wl: + OCR: prove it DREW
wl: {method: screenshot, artifact: /tmp/m.png, artifact_contains_text: Apps}

A shell can accept an IPC call and render nothing; a surface can map and draw nothing. See /charly-check:wl for the layer and OCR halves.