Bring docs/ up to date and cover the undocumented subsystems

Every reference doc was audited claim-by-claim against the code.
file-layout and omarchy-shell were the most decayed (renamed commands,
the etc/ overrides source split, dead IPC entry points and example keys);
update-process lagged the recent pipeline changes and gains a channels
section; theming and audio-tuning were accurate but thin around their
lifecycles.

New reference docs for the subsystems that had none: the menu system,
the CLI router, the notification daemon, and the non-acceptance test
architecture. AGENTS.md links the two of those agents will need most.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-15 16:21:25 +02:00
co-authored by Claude Fable 5
parent b68c22208a
commit f4189398bc
10 changed files with 785 additions and 83 deletions
+5 -2
View File
@@ -90,7 +90,8 @@ Exceptions are allowed for migration and package-helper scripts where the helper
# Menu
- The menu definition lives in `default/omarchy/omarchy-menu.jsonc`.
- The menu definition lives in `default/omarchy/omarchy-menu.jsonc`;
[`docs/menu.md`](docs/menu.md) covers the schema, guards, and providers.
- Do not add `aliases` to new menu entries. Aliases are reserved for
established alternate names users already type, kept for compatibility.
@@ -102,7 +103,9 @@ Exceptions are allowed for migration and package-helper scripts where the helper
# Tests
Run focused automated tests for the area you changed. Current test entry points:
Run focused automated tests for the area you changed;
[`docs/testing.md`](docs/testing.md) covers how the suites are shaped. Current
test entry points:
- `./test/all` - aggregate runner for CLI and shell tests; it intentionally does not run graphical acceptance tests
- `./test/cli` - CLI routing, command metadata, theme helpers, and safe dispatch coverage
+19 -2
View File
@@ -12,7 +12,9 @@ default/audio/tunings/<vendor>-<model>/
```
`on` renders the graph into `~/.config/pipewire/omarchy-speaker-tuning.conf.d/`
and runs it as its own PipeWire client via `omarchy-speaker-tuning.service`, rather than
(plus the shared host config `omarchy-speaker-tuning.conf`, from
`default/audio/filter-chain-host.conf`) and runs it as its own PipeWire client via
`omarchy-speaker-tuning.service`, rather than
loading it into the audio daemon. The daemon only reads its own config at startup,
so a daemon-loaded tuning could only be switched by restarting PipeWire — which
drops every PulseAudio client's connection, and applications that do not reconnect
@@ -40,6 +42,13 @@ omarchy audio tuning status # installed? in use? what matches?
`match` and `fronted-sink` are also accepted; they exist for the install hooks
and the sink-listing scripts rather than for daily use.
`on` verifies its own work: it waits for the tuning sink to appear and confirms
its output linked to the expected physical sink, and uninstalls everything again
on either failure — so a broken tuning cannot be left half-applied. It also
switches the default sink to the tuning and moves existing app streams onto it
(`off` reverses both). When everything is already installed and linked, `on` is
a no-op; pass `--force` when iterating on a tuning in place.
## Adding a tuning
Add a directory with a `tuning.conf` and a `filter-chain.conf`. No new command is
@@ -59,8 +68,16 @@ been validated on:
match_sku=("0DB9" "0DBA") # XPS 14 and XPS 16
```
Only the first *defined* key is consulted, in the order `match_command`,
`match_sku`, `match_dmi` — keys below a defined one are ignored, and a tuning
defining none never matches.
`sink_pattern` is required whichever method you use, since the graph's target sink
is substituted from it.
is substituted from it. The graph must also keep two fixed properties: the sink
node is named `omarchy_speaker_tuning` (the install checks for that exact name,
so renaming it silently fails verification), and its output stream sets
`node.dont-fallback` alongside `node.dont-move`, so WirePlumber cannot link it
somewhere else when the target sink is absent at host start.
Gate narrowly and widen as models are validated; a tuning aimed at the wrong
drivers can sound worse than none and can stress them. When a tuning covers a model
+130
View File
@@ -0,0 +1,130 @@
# The Omarchy CLI router
`bin/omarchy` maps spaced commands onto the flat `bin/omarchy-*` namespace:
`omarchy theme set foo` becomes `exec bin/omarchy-theme-set foo`. There is no
registry to maintain — every executable `bin/omarchy-*` file is a command, and
its filename is its default route. Metadata comments in the file header refine
how it presents and routes; the keys are documented in
[`agents/skills/command-metadata.md`](../agents/skills/command-metadata.md).
This document covers what that guide does not: how resolution and dispatch
actually work.
## How a binary becomes routes
The stem after `omarchy-` splits at the first hyphen: `omarchy-theme-set` gets
group `theme` and name `set`, with remaining hyphens becoming spaces
(`omarchy-hw-asus-rog` → group `hw`, name `asus rog`). A single-segment stem
(`omarchy-update`) is the root command of its own group, with an empty name.
Every command registers two routes: the canonical route `omarchy <group>
<name>` after metadata overrides, and the filename route with *all* hyphens
turned to spaces. When metadata moves nothing, they are the same route. When it
does, both keep working — `# omarchy:name=gaming xbox-cloud` on
`omarchy-install-gaming-xbox-cloud` keeps the hyphen inside the name, so
`omarchy install gaming xbox-cloud` is canonical while the filename route
`omarchy install gaming xbox cloud` still resolves. An explicitly *empty* `#
omarchy:name=` makes a command the root of its group: `omarchy-menu-share` sets
`group=share` and an empty name, so its canonical route is `omarchy share`
while `omarchy menu share` remains as the filename route. Alias routes register
the same way but are flagged, so listings show them as aliases rather than
commands.
Two routes claiming different binaries is a collision: the first registration
wins, dispatch is unaffected, and the conflict is recorded for `omarchy
commands --check` to report. Hidden commands (`# omarchy:hidden=true`) still
register and dispatch normally — hiding only removes them from listings, which
is how install-time plumbing like `omarchy apply hardware` stays callable
without being browsable.
Metadata is read from the comment header only: the first 80 lines, stopping at
the first non-comment line, so metadata-shaped comments after code never take
effect. Malformed `omarchy:` lines and unknown keys are ignored rather than
fatal — a typo degrades a command to its filename route instead of breaking the
router. The first plain comment line doubles as a fallback summary, and a
command with no comments at all still gets a generated one, though `--check`
demands the explicit form (below).
## Dispatch
Resolution is longest-prefix: the router tries the full argument list as a
route, then drops trailing words until something matches. Whatever it drops is
passed to the binary as arguments. This runs in two passes.
The fast path joins argument prefixes with hyphens and checks for an
executable file: `omarchy theme set foo` probes `omarchy-theme-set-foo`, then
`omarchy-theme-set`, which exists — resolved without reading a single metadata
header. This exists because plain dispatch is the hot path: parsing the
headers of several hundred binaries on every invocation is measurable
latency (`omarchy dev benchmark cli` tracks it), and a filename probe is a few
stat calls. Metadata loads lazily, only for the resolved command when help is
needed.
When no filename matches — metadata-moved routes like `omarchy share`, and
aliases like `omarchy screenshot` — the router falls back to loading all
metadata and resolving against the registered route table, with the same
longest-prefix rule.
Both paths intercept `--help`/`-h` *anywhere* in the leftover arguments, not
just the first one. Resolution can succeed with unresolved words still ahead of
the flag — `omarchy update aur --help` resolves `update` with leftovers `aur
--help` — and checking only the first leftover once let that invocation start a
real update. A `--` ends the scan: everything after it belongs to the command,
so `omarchy foo run -- --help` forwards the flag. `--json` alongside `--help`
switches the help output to the command's JSON record; `--json` alone is just
an argument for the command.
Bare invocations are also guarded. If a command declares required arguments
(its `args` metadata, minus `[bracketed]` optional parts, is non-empty) and
none were given, the router shows help instead of executing — `omarchy theme
set` prints usage rather than running an interactive setter. A bare group name
with child commands shows the group help.
Dispatch is `exec`: the router process is replaced, the binary sees only the
leftover arguments, and the exit code is the binary's own. The router itself
exits 127 for unknown routes or missing binaries.
When nothing resolves, the router tries a prefix listing — `omarchy hw asus`
prints every command whose usage starts with that prefix — and otherwise
errors with a "did you mean" suggestion (a known route extending the first
word) and a pointer to `omarchy commands --all`.
## Groups and the top-level listing
Group help is synthesized from metadata, not written anywhere. A command
belongs to a group when either its metadata group or its filename group
matches, listed under the route that fits the group being viewed: `omarchy
menu --help` shows `omarchy-menu-share` as `omarchy menu share`, while its
canonical `omarchy share` stands alone. On the fast path, group help loads
only that group's filename-prefixed binaries rather than everything.
The top-level `omarchy` listing is driven entirely by the hand-curated
`GROUP_DESCRIPTIONS` table in `bin/omarchy`, which also titles each group's
help. An entry there advertises the group even when every command in it is
hidden — which is exactly why `apply` and `provision` have none: they route,
but a listing entry would put install-time plumbing back in front of users
(see the Command Naming section of `AGENTS.md`). Adding a browsable command
group means adding its `GROUP_DESCRIPTIONS` entry; adding hidden plumbing
means deliberately not doing so.
## Introspection
`omarchy commands` prints every non-hidden command with its summary, plus an
alias table. `--all` includes hidden commands, `--markdown` emits a table, and
`--json` emits full records: route, binary, group, name, summary, flags, args,
examples, aliases, `filename_route`, and `routes` (the union of everything
that resolves to the binary). Per-command JSON comes from `omarchy <route>
--help --json`.
`omarchy commands --check` is the metadata lint, run by `test/cli`. It fails
on:
- route collisions between binaries
- a missing explicit `# omarchy:summary=` — a plain-comment fallback renders
in help but does not satisfy the check
- invalid boolean metadata: `hidden` and `requires-sudo` must be `true` or
omitted, never `false`
- a registered command whose binary is missing or not executable
When debugging a routing surprise, `omarchy <route> --help` shows the resolved
binary and, when it differs, the filename route; `omarchy commands --all
--json` shows every route the router knows.
+80 -46
View File
@@ -5,8 +5,8 @@ system.
## Mental model
Two Arch packages are built from this one repo (PKGBUILDs live in
`omarchy-pkgs/pkgbuilds/`):
Two Arch packages are built from this one repo (PKGBUILDs live in the
separate `omarchy-pkgs` repository, under `pkgbuilds/`):
- **`omarchy`** — runtime binaries (`bin/`, including `bin/omarchy-dev-*`),
install/finalize scripts (`install/`), migrations, themes, and the
@@ -22,19 +22,24 @@ Two Arch packages are built from this one repo (PKGBUILDs live in
(`omarchy-debug`, `omarchy-debug-idle`, `omarchy-upload-log`) needed by
the live ISO env.
Two other packages live in `omarchy-pkgs/` but stand alone:
Two other packages live in `omarchy-pkgs` but stand alone:
`omarchy-keyring` (GPG keys for pacman) and `omarchy-nvim` (the Neovim
setup; independently seeds `/etc/skel`).
Some trees ship in neither package and exist only in the repo: `manual/`
(user manual chapters), `agents/skills/` (contributor task guides), `docs/`,
`test/`, and `plans/`.
Three layers populate `$HOME`:
1. **Seed**`omarchy-settings` ships static defaults to `/etc/skel/`.
Arch's `useradd -m` copies that tree into a new user's `$HOME` at user
creation. This is the only mechanism that touches a brand-new user's home
for these files.
2. **Finalize**`omarchy-finalize-user` runs once per user and handles the
things `/etc/skel` can't do because they need `$HOME` expansion, the live
`$OMARCHY_PATH`, or runtime detection of system state.
2. **Finalize**`omarchy-provision-user` (routed as `omarchy finalize
user`) runs once per user and handles the things `/etc/skel` can't do
because they need `$HOME` expansion, the live `$OMARCHY_PATH`, or runtime
detection of system state.
3. **Resync** — `omarchy-reinstall-configs` is the explicit, destructive
command for an existing user to clobber their configs back to shipped
defaults.
@@ -42,6 +47,13 @@ Three layers populate `$HOME`:
`/etc/skel` only fires at user creation. Existing users picking up new
defaults must use the resync command.
Deferred-provisioning installs (`omarchy-apply-system --defer-provisioning`)
create no user at all: the ISO leaves `/var/lib/omarchy/provisioning/pending`
behind, which arms `omarchy-provision-owner.service` (shipped from
`install/provisioning/`, alongside the factory-reset finish unit and
`setup-form.sh`). On first boot `bin/omarchy-provision-owner` creates the
user on tty1 and runs the finalize step itself.
Current generated theme state lives under
`~/.local/state/omarchy/current/`. Keep `~/.config/omarchy/` for files a user
may intentionally version in a dotfile manager, such as user themes, hooks,
@@ -82,7 +94,12 @@ applications/icons/* ──► omarchy-settings /usr/share/icons/h
etc/** ──► omarchy-settings /etc/** (drop-ins we own outright)
├─ mkinitcpio.conf.d/{omarchy_hooks,thunderbolt_module}.conf
─ limine-entry-tool.d/{omarchy-defaults,omarchy-uki}.conf
─ limine-entry-tool.d/{omarchy-defaults,omarchy-uki}.conf
├─ NetworkManager/, sudoers.d/, sysctl.d/, tmpfiles.d/,
│ profile.d/omarchy.sh, … (a summary — `ls etc/` for the full ~17-entry tree)
└─ security/faillock.conf, nsswitch.conf,
cups/cups-browsed.conf, plymouth/plymouthd.conf /usr/share/omarchy/etc-overrides/
→ /etc/* (post_install cp -f, see below)
default/limine/limine.conf ──► omarchy-settings /usr/share/omarchy/default/limine/limine.conf
default/limine/default.conf ──► omarchy-settings /usr/share/omarchy/default/limine/default.conf
@@ -95,7 +112,8 @@ default/** ──► omarchy-settings /usr/share/omarchy
│ (sourced by every shell/session entry point; see "Env bootstrap")
├─ bashrc /usr/share/omarchy/etc-overrides/dot.bashrc
│ → /etc/skel/.bashrc (post_install cp -f)
├─ hypr/toggles/flags.lua /etc/skel/.local/state/omarchy/toggles/hypr/
├─ hypr/toggles/*.lua (flags,
│ single-window-aspect-ratio, window-no-gaps) /etc/skel/.local/state/omarchy/toggles/hypr/
├─ nautilus-python/extensions/*.py /etc/skel/.local/share/nautilus-python/extensions/
├─ tensaku/state.toml /etc/skel/.local/state/tensaku/state.toml
├─ uwsm/env.d/10-omarchy /usr/share/uwsm/env.d/
@@ -104,18 +122,16 @@ default/** ──► omarchy-settings /usr/share/omarchy
│ + symlink /etc/fonts/conf.d/50-omarchy.conf
├─ xdg-terminal-exec/*.list /usr/share/xdg-terminal-exec/
├─ applications/mimeapps.list /usr/share/applications/mimeapps.list
├─ systemd/user/*.{service,path} /usr/lib/systemd/user/
├─ systemd/user/*.service /usr/lib/systemd/user/
├─ systemd/user/app.slice.d/10-oomd.conf /usr/lib/systemd/user/app.slice.d/
├─ systemd/system-sleep/unmount-fuse /usr/lib/systemd/system-sleep/
├─ systemd/system-sleep/{force-igpu,
│ keyboard-backlight,unmount-fuse} /usr/lib/systemd/system-sleep/
├─ systemd/zram-generator.conf.d/90-omarchy.conf /usr/lib/systemd/zram-generator.conf.d/
├─ fonts/omarchy/omarchy.ttf /usr/share/fonts/omarchy/
├─ sddm/omarchy/ /usr/share/sddm/themes/omarchy/
├─ sddm/hyprland.lua /usr/share/sddm/hyprland.lua
├─ wayland-sessions/omarchy.desktop /usr/local/share/wayland-sessions/
─ plymouth/ /usr/share/plymouth/themes/omarchy/
└─ security/faillock, nsswitch, cups-browsed,
plymouthd.conf, os-release /usr/share/omarchy/etc-overrides/
→ /etc/* (post_install cp -f, see below)
─ plymouth/ /usr/share/plymouth/themes/omarchy/
logo.{txt,svg}, icon.{txt,png} ──► omarchy-settings /usr/share/omarchy/ (resync source)
/usr/share/pixmaps/omarchy.png
@@ -126,9 +142,10 @@ logo.{txt,svg}, icon.{txt,png} ──► omarchy-settings /usr/share/omarchy
### Why `etc-overrides/` exists
Some files under `/etc/` (`.bashrc` in `/etc/skel`, `nsswitch.conf`,
`security/faillock.conf`, `cups/cups-browsed.conf`, `plymouth/plymouthd.conf`,
`os-release`) are owned by upstream Arch packages, so we can't install over
them via pacman without a file conflict. Instead they ship at
`security/faillock.conf`, `cups/cups-browsed.conf`, `plymouth/plymouthd.conf`)
are owned by upstream Arch packages, so we can't install over them via pacman
without a file conflict. Instead their sources (under `etc/` in the repo;
`.bashrc` from `default/bashrc`) ship at
`/usr/share/omarchy/etc-overrides/` and the `omarchy-settings` `post_install`
/ `post_upgrade` scriptlet `cp -f`'s them into place.
@@ -146,6 +163,10 @@ Single source of truth for `OMARCHY_PATH` and dev-link-aware `PATH`. It:
- Prepends `$OMARCHY_PATH/bin` to `PATH` **only when** `OMARCHY_PATH` is
not `/usr/share/omarchy`. On a production install the binaries are
already on `PATH` as `/usr/bin/omarchy-*` via the `omarchy` package.
- Appends `~/.local/share/mise/shims` and `~/.local/bin` so login shells and
the uwsm session find mise-managed tools — kept in sync with the PAM `PATH`
line written by `install/config/ssh-command-path.sh`, which covers SSH
commands that run no shell setup at all.
Sourced by every entry point that needs the env set:
@@ -171,15 +192,17 @@ yet and silently runs the packaged copy of one it has. The drop-in is validated
with `visudo -c` before install and removed by `omarchy-dev-unlink`; unlike
`/etc/omarchy.conf`, it takes effect without a reboot.
## Runtime finalization (`omarchy-finalize-user`)
## Runtime finalization (`omarchy-provision-user`)
Runs once per user. It does **not** copy `~/.config/**`, `~/.bashrc`,
`flags.lua`, or the nautilus extensions — `/etc/skel` already seeded those.
It only does the things `/etc/skel` can't:
- Skill symlinks `~/.{agents,claude,codex,pi/agent}/skills/omarchy`
`$OMARCHY_PATH/default/agents/skills/omarchy`. Symlinks (not copies) so
`omarchy dev link` against a dev checkout repoints them correctly.
- Skill symlinks `~/.{agents,claude,codex,pi/agent}/skills/<name>`
`$OMARCHY_PATH/default/agents/skills/<name>`, looping over every skill
directory there (currently `omarchy` and `diagnose-crash`) so new skills
need no edit. Symlinks (not copies) so `omarchy dev link` against a dev
checkout repoints them correctly.
- `xdg-user-dirs-update` (Templates/Public/Desktop folded back into `$HOME`)
and `~/.config/gtk-3.0/bookmarks` (needs `$HOME` expansion).
- Hyprland's package-owned default input reads `XKBLAYOUT` / `XKBVARIANT`
@@ -187,17 +210,19 @@ It only does the things `/etc/skel` can't:
- `xdg-settings set default-web-browser chromium.desktop` and
`xdg-mime default HEY.desktop x-scheme-handler/mailto` (XDG-aware paths).
- `omarchy-refresh-applications` (composes generated `.desktop` launchers).
- Sources `install/user/all.sh` — theme, git, mise, keyring, per-user
hardware quirks (asus mic/mixer, framework f13 audio, …).
- Sources `install/user/all.sh` — theme, chromium, git, xcompose, mise,
keyring, per-user hardware quirks (asus mic/mixer, framework f13 audio, …).
- On `--first-install`, marks every shipped user migration as already applied
for the freshly-created user.
Idempotency marker: `~/.local/state/omarchy/done/finalize-user`, managed
by `omarchy-done`.
The ISO calls it as `omarchy-finalize-user --force --first-install` in the
The ISO calls it as `omarchy-provision-user --force --first-install` in the
target chroot as the install user, after `omarchy-apply-system` has finished
the root-side work.
the root-side work. `omarchy-provision-owner` makes the same call (with
`OMARCHY_SETUP_CONTEXT=provision-owner`) when it creates the user during
deferred first-boot provisioning.
## Migrations (`omarchy-migrate`)
@@ -213,9 +238,10 @@ machine-wide repairs should no-op when another user already applied them.
Each graphical user has `omarchy-migrate-notify.service`, started once per login
through `WantedBy=graphical-session.target` and ordered after that target so
notification actions can safely launch through UWSM. The package also ships
`omarchy-update-user-notify.service` as a symlink onto it, so users enabled
under the old unit name keep working before they reach migration `1785095882`.
notification actions can safely launch through UWSM. The `omarchy-pkgs`
PKGBUILD has shipped `omarchy-update-user-notify.service` as a symlink onto
it, so users enabled under the old unit name keep working before they reach
migration `1785095882`.
It runs `omarchy-migrate-notify` as
that user, which checks `omarchy-migrate --pending`. If this user has missing
migration state, it shows a notification that opens a terminal for
@@ -234,27 +260,31 @@ transaction in the already-visible update terminal, then runs
## First-run (`omarchy-provision-first-run`)
Runs once on first interactive login, after the user manager is live. Used
for steps that need a running graphical session and/or a working user
systemd instance:
Runs once on first interactive login, after the user manager is live. It
first runs `omarchy-provision-user || true` so finalize catches up if it
never ran, then handles the steps that need a running graphical session
and/or a working user systemd instance:
- `omarchy-hook-install post-update install-voxtype.hook` — register the
Voxtype post-update hook.
- `install/user/first-run/enable-user-units.sh``systemctl --user enable`
the shipped user units (`bt-agent`, `omarchy-sleep-lock`,
`omarchy-recover-internal-monitor`, `omarchy-migrate-notify.service`,
`omarchy-fcitx5.service`).
- `omarchy-hook-install post-update` for the three shipped hooks
(`install-voxtype.hook`, `setup-fingerprint.hook`, `setup-agent.hook`).
- `install/user/first-run/enable-user-units.sh` — daemon-reload, then
`systemctl --user enable --now` the shipped user units (`bt-agent`,
`omarchy-sleep-lock`, `omarchy-recover-internal-monitor`,
`omarchy-migrate-notify.service`, `omarchy-fcitx5.service`,
`omarchy-crash-watch.service`) so they run in the first session too.
Done here, not at finalize, because
the user manager isn't reachable from the ISO chroot; `ConditionPath*`
in the unit files keeps services inert when they don't apply.
- `install/user/first-run/gnome-theme.sh`,
`install/user/first-run/gtk-primary-paste.sh` — GNOME/GTK settings that
need the dconf daemon.
- `install/user/first-run/audio-tuning.sh` — apply speaker tuning.
- `install/user/first-run/welcome.sh` — keybindings toast that greets the
first login and opens the cheatsheet when clicked.
- `install/user/first-run/wifi.sh` — Wi-Fi/update toasts (waits for a live
notification server before firing, then waits detached on `nm-online` so the
update prompt only lands once there is a connection).
first login and opens the cheatsheet when clicked. The caller runs
`omarchy-notification-wait` once before this and the Wi-Fi step, so both
toasts land on a live notification server.
- `install/user/first-run/wifi.sh` — Wi-Fi/update toasts (waits detached on
`nm-online` so the update prompt only lands once there is a connection).
The entire sequence has one idempotency marker:
`~/.local/state/omarchy/done/first-run-user`, managed by `omarchy-done`.
@@ -274,8 +304,8 @@ the legacy finalization marker from `~/.local/state/omarchy/` into `done/`.
finalization. It sources:
- `install/config/all.sh` — theme links, lockout limits, lockscreen PAM,
powerprofilesctl shebang fix, docker setup, Snapper retention, locate
index tuning, service enablement, firewall.
powerprofilesctl shebang fix, SSH command path and keepalive, docker setup,
Snapper retention, locate index tuning, service enablement, firewall.
- `install/hardware/all.sh` via `omarchy-apply-hardware` — vendor- and
device-specific kernel modules, udev rules, microcode, wireless regdom,
ASUS / Framework / Intel / Apple / Lenovo quirks.
@@ -285,6 +315,10 @@ finalization. It sources:
Logging goes to `/var/log/omarchy-install.log` via
`install/helpers/logging.sh`.
The package lists the ISO pacstraps live at `install/omarchy-base.packages`
and `install/omarchy-other.packages`; the ISO builder also reads them when
constructing its offline mirror.
## Explicit resync (`omarchy-reinstall-configs`)
When an existing user wants to reset to shipped defaults:
@@ -310,10 +344,10 @@ return to the packaged default.
| --- | --- |
| Default file at `~/.config/foo/` | `config/foo/` |
| `/etc/` drop-in we own outright | `etc/` |
| `/etc/` file owned by an upstream package | `default/`, then add to `etc-overrides` in `omarchy-settings` PKGBUILD + scriptlet |
| Package-owned system file (e.g. systemd user service/path in `/usr/lib`) | `default/`, document the mapping in `default/package-defaults.tsv`, then add the `install -Dm644` line in `omarchy-settings` PKGBUILD |
| `/etc/` file owned by an upstream package | `etc/` (see `etc/security/faillock.conf`), then add to `etc-overrides` in `omarchy-settings` PKGBUILD + scriptlet |
| Package-owned system file (e.g. systemd user service in `/usr/lib`) | `default/`, then add the `install -Dm644` line in `omarchy-settings` PKGBUILD |
| Per-user file that's static but lives outside `~/.config` | `default/`, then add `install -Dm644 ... $pkgdir/etc/skel/...` in `omarchy-settings` PKGBUILD |
| Runtime tweak that needs `$HOME` or live system state | extend `omarchy-finalize-user`, or add a per-user leaf under `install/user/` and wire into `install/user/all.sh` |
| Runtime tweak that needs `$HOME` or live system state | extend `omarchy-provision-user`, or add a per-user leaf under `install/user/` and wire into `install/user/all.sh` |
| One-time root-side setup step | `install/config/*.sh` or `install/hardware/*.sh`, wire into `install/config/all.sh` or `install/hardware/all.sh` |
| One-time fix for existing installs | `migrations/<unix-timestamp>.sh` |
| Package-owned path something else may already write | Prefer a path nothing else writes, such as a vendor drop-in under `/usr/lib`. Otherwise the `--overwrite` entry in `bin/omarchy-update-system-pkgs` has to ship a release before the file |
+167
View File
@@ -0,0 +1,167 @@
# The Omarchy menu
The menu is the `omarchy.menu` plugin of the Quickshell desktop, with its
content defined as data in `default/omarchy/omarchy-menu.jsonc` (read at
runtime from `$OMARCHY_PATH`) and overlaid by the user's
`~/.config/omarchy/extensions/omarchy-menu.jsonc`. The shell parses both files
at startup and watches them for changes, so the keybind → IPC → visible path
never shells out to parse anything, and edits to either file take effect
without restarting the shell. Rendering and behavior live in
`shell/plugins/menu/Menu.qml`; the pure logic lives in
`shell/plugins/menu/MenuModel.js`, which is plain JavaScript that Node can
also load — the shell tests in `test/shell.d/menu-test.sh` and
`menu-guards-test.sh` exercise it directly.
JSONC here means JSON plus comments and trailing commas, stripped by the
parser rather than a real JSONC grammar: only whole-line `//` comments are
removed, so an inline trailing comment breaks the parse. A file that fails to
parse contributes no entries — a broken user extension silently drops every
user entry while the shipped menu keeps working.
## Entry schema
Entries are object keys. The dotted id is the tree: `trigger.share.file` is a
child of `trigger.share`, and an id with no dot sits on the root menu. There
is no separate parent field to keep in sync — where an entry appears follows
from what it is called (an explicit `parent` is accepted but nothing shipped
uses one).
Kind is inferred rather than declared: an entry with `action` is an action,
one with `target` is a link to another submenu, and anything else is a
submenu. The fields:
| Field | Meaning |
|---|---|
| `icon` | Glyph in the icon column (usually Nerd Font) |
| `iconFont` | Font family for the glyph when it differs from the menu font — how the private `omarchy` font's brand glyphs render |
| `label` | Visible row title; defaults to the id |
| `title` | Header text when the submenu is open; defaults to `label`. Lets a row read "Browser" under Defaults while the open menu says "Default Browser" |
| `action` | Shell command to run, detached, when selected |
| `target` | Existing submenu id to open; makes the row a link |
| `provider` | Runtime row source for this submenu (see Providers) |
| `aliases` | Alternate `omarchy menu summon <name>` routes; also searchable |
| `description` | Subtitle shown while searching, and extra search text matched by whole word |
| `when` / `checked` / `disabled` | Shell conditions (see Guards) |
Do not add `aliases` to new entries. They are reserved for established
alternate names users already type (`power-menu`, `settings`), kept for
compatibility — see `AGENTS.md`. Search does not need them: labels, the last
id segment, and descriptions are all searchable.
## Load and merge
`mergeMenuSources` overlays user entries on the defaults per key: reusing a
shipped id replaces only the fields you declare, so an extension can retitle
or re-icon a row without re-declaring its action, and an overridden entry
keeps its original position in the list. New ids append. A `root` entry is
injected if neither file declares one.
The sample extension at `config/omarchy/extensions/omarchy-menu.jsonc`
(refreshed into `~/.config/`) documents the format in its header and ships
only comments, so the default state adds nothing.
## Guards
`when`, `checked`, and `disabled` are bash conditions. The shell never
evaluates them on the open path: all guards in the menu are batched into a
single bash process per (re)load and per open, reporting `<id>:<w|c|d>:<0|1>`
lines. The menu opens immediately on the previous evaluation's answers, so
the batch's runtime is exactly how long a row can contradict the state it
describes — which is why the batch works hard to be fast:
- Package and command presence (`omarchy-pkg-present` and friends) are
answered in-process from one `pacman -Q` snapshot instead of a fork per
row. The snapshot resolves provides too, so gvim answers for vim.
- Commands that several rows read a value from — every Defaults > Browser row
compares against `$(omarchy-default-browser)` — run once, with the captured
answer substituted into each expression. The reader list is
`GUARD_READERS` in `MenuModel.js`; a new `$(omarchy-...)` reader used by
more than one row must be added there, and `menu-guards-test.sh` fails the
build if it is not.
The three guards differ in what failure means:
- `when` hides the row when it fails. A submenu whose visible descendants
are all hidden disappears with them (provider-backed submenus stay, since
their rows load on demand).
- `checked` appends ✓ when it succeeds — the "this is the current choice"
marker on defaults, DNS, channel rows.
- `disabled` keeps the row listed but dims it, marks it ✓, and makes it
unselectable: cursor, pointer, and Enter all step over it, and search omits
it. The Install submenus use it so software already on the machine reads as
installed rather than vanishing from the list it was installed from — the
list stays a catalog of what Omarchy can install. Since a dimmed row means
"you already have this", it earns the same ✓ as `checked` does elsewhere.
Install rows should therefore carry `disabled:` with the presence check, not
`when:`; Remove rows are the opposite, hiding via `when:` what is not there
to remove. `menu-test.sh` enforces the Install side of this convention.
## Providers
A submenu with `provider: "name"` gets its rows at runtime instead of from
JSONC. The names are defined by the shell, not the menu file — an extension
can point a submenu at an existing provider but cannot declare a new one:
- `apps` is QML-native: rows come from the shared AppLibrary (desktop
entries), carrying image icons, launch feedback, and uninstall support like
the launcher. App rows are searchable by their desktop Keywords but never
routable, so an installed app cannot capture a menu route (htop ships
`Keywords=system;...`, and SUPER+ESCAPE must still open the system menu).
- `fonts` and `power-profiles` are bash one-liners in the `providers` map in
`Menu.qml`. The contract is one tab-delimited line per row:
`label\tvalue\tcurrent`. The row whose value equals `current` gets the ✓
icon, and selection runs the spec's `actionFor(value)`. Row ids are
`<menuId>.<slugify(value)>`, with a `-` appended on collision so two values
that slugify alike cannot silently drop a row.
A provider marked `volatile` re-runs every time its submenu is entered — a
font installed since the shell started shows up without a restart — but not
on search keystrokes, which would restart the same enumeration per key.
`swapProviderRows` in `MenuModel.js` merges the results: rows carry the id of
the submenu that produced them, so a provider that runs again drops its
previous batch without disturbing static children declared in JSONC. Both it
and the app merge return fresh item maps for the caller to assign in one go —
writing into a map held by a QML `var` property occasionally loses the write,
which used to duplicate launcher rows. Never mutate `root.items` in place.
Adding a provider means adding an entry to the `providers` map in `Menu.qml`
(script, icon, `actionFor`, optionally `volatile`) and pointing a submenu at
it with `provider:`.
## Driving the menu from the CLI
`bin/omarchy-menu` is a thin wrapper over the standard plugin IPC surface:
```bash
omarchy menu # toggle the root menu
omarchy menu toggle system # open at a route, or close if already open
omarchy menu summon style.theme # always open (no close-if-visible)
omarchy menu close
omarchy menu refresh # re-parse the JSONC files
omarchy menu ping
```
A route is an item id or a declared alias, case-insensitive, with
underscores normalized to dashes. An exact id beats any alias; empty input,
`go`, and `menu` mean root; an unknown string falls through as a literal id
so a misspelling still attempts to open that id. Summoning a route that
resolves to an action — an alias for a leaf, like `screenrecord-stop` — runs
the action directly instead of opening an action with no children, and a
link is followed to its target. The default Hyprland bindings in
`default/hypr/bindings/utilities.lua` all go through this surface
(SUPER+SPACE toggles root, SUPER+ESCAPE the system menu, and so on).
## Select and input modes
The same plugin doubles as the system's dmenu. `omarchy-menu-select` and
`omarchy-menu-input` summon it with a `mode: select` or `mode: input`
payload, then block on a tempfile handshake: the shell writes the selection
to `selectionFile` and touches `doneFile`, and cancellation (empty
selection) exits 1. A select option is `label`, `glyph\tlabel`, or
`glyph\tlabel\tsubtext` — the glyph shows but never returns, the subtext
renders under the label, filters with it, and comes back as
`label\tsubtext` so callers with same-named rows get a stable key. This is
how the pickers behind menu actions (`omarchy-menu-plugin`,
`omarchy-menu-timezone`, ...) present lists without owning any UI.
+143
View File
@@ -0,0 +1,143 @@
# Notifications
The shell is the notification daemon: `shell/plugins/notifications/Service.qml`
hosts a Quickshell `NotificationServer` that claims `org.freedesktop.Notifications`
on the session bus. There is no dunst or mako — anything that speaks the
freedesktop notification protocol (notify-send, libnotify apps, Chromium web
apps) lands in the shell, which renders it as a toast card stacked in the
top-right corner. The pure decision logic lives in `NotificationLogic.js`,
which is also loadable from Node so `test/shell.d/` can exercise it without
a compositor.
The end-user view (hotkey notices for time, battery, weather) is in
`manual/10-notices.md`; this document is the system shape behind it.
## Toast lifecycle
A toast lives on screen for at least 5s (low), 8s (normal), or forever
(critical), stretched up to 30s if the sender asked for a longer
`expire_timeout`. Hovering pauses the countdown, and a content update restarts
it — new text deserves a full look. Left-click invokes the default action,
right-click or the hover-revealed close button dismisses.
Every on-screen popup is mirrored to its own file under
`~/.local/state/omarchy/notifications/` (one JSON line per file, named
`<timestamp>-<id>.json`), so live toasts survive the shell restart that
`omarchy-update` performs. When a toast leaves the screen — expiry, dismissal,
or click — its file moves into `notifications/history/`, trimmed to the newest
ten. That directory *is* the history: `showHistory` replays exactly what has
been moved in there. Referenced avatars/images are copied into
`notifications/images/`, because senders delete their originals on close.
`replaces_id` updates never produce a second notification signal: the server
writes new content onto the object the service already holds, so the service
watches the object's property-change signals and rewrites the row and its file
in place, under the popup's original file identity. Restored rows carry ids
from a dead server generation (ids restart from 1 each shell process), so
they are keyed by timestamp+id and never matched against live objects — a
fresh notification reusing an old id must not dismiss or replace them.
## Silencing
Do-not-disturb is a single boolean, persisted as the `dnd` key in
`~/.local/state/omarchy/notifications.json` and toggled via shell IPC
(`omarchy-shell notifications toggleDnd` / `setDnd` / `dndState`).
`omarchy-toggle-notification-silencing` wraps the toggle and refreshes the
bar's `omarchy.indicators` widget, whose Dnd indicator binds directly to the
service's `doNotDisturb` property.
Two kinds of notification punch through DND, chosen to be intentional and
rare:
- `app_name` = `omarchy-action` — Omarchy's own user-action confirmation
toasts ("Theme changed"). The user just did something; their feedback shows.
- urgency critical *and* `app_name` = `notify-send` — bare-CLI emergency
alerts. Critical alone is not enough, because chat apps abuse it to force
visibility; they set `app_name` to their brand, which fails this rule.
A silenced notification that anyone might look back at is written straight
into history — "what did I miss while silenced" is what history is for.
Ephemeral ones (the freedesktop `transient` hint, or an `app_name` of
`notify-send`/`omarchy-action`) are dropped entirely.
## The sender contract
`bin/omarchy-notification-send` is the one way Omarchy code sends
notifications — never raw `notify-send`. It translates its flags into
notify-send arguments and passes any unrecognized options through:
| Flag | Becomes | Meaning |
|---|---|---|
| `-g` / `--glyph` | `--hint=string:omarchy-glyph:` | Nerd Font glyph for the icon slot when no image icon resolves |
| `--exec` | `--hint=string:omarchy-exec:` | shell command the card runs when clicked |
| `--image` | `--hint=string:image-path:` | the standard freedesktop image hint |
| `--app-name` | `-a` | defaults to `omarchy-action` |
| `-u` / `--urgency` | `-u` | defaults to `low` |
The defaults are the point: an unadorned `omarchy-notification-send "Done"`
is a low-urgency user-action toast that pops through DND and is treated as
ephemeral noise when silenced.
`--exec` is deliberately not a libnotify action. An action keeps the sender
blocked waiting for `ActionInvoked`, and dies unanswered whenever the shell
restarts underneath it — the installer toasts restart the shell as their
first act. Carrying the command as a hint means the shell executes the click
itself (detached, so the command outlives the shell process) from the copy it
keeps with the popup, which the persistence files preserve: a restored toast
clicks through exactly like a live one, and oneshot senders can exit
immediately. For third-party clients the click falls back to the libnotify
`default` action while the sender is alive, then to focusing the sender's
window by class via `omarchy-hyprland-focus-app` — chat apps rarely register
an action and just expect click-to-jump.
## Helper commands
- `omarchy-notification-wait [timeout]` — polls until the shell answers IPC
*and* has claimed the bus name. Anything sending near session start or a
shell restart uses it, or the toast is sent into the void.
- `omarchy-notification-dismiss <summary>` — dismiss by summary substring,
used by the first-run toasts once their action has been clicked.
- `omarchy-notification-time` / `-battery` — the hotkey notices: one-line
low-urgency glyph toasts wrapping `date` and `omarchy-battery-status`.
- `omarchy-notification-weather` — despite the name, not a sender: it toggles
the `omarchy.weather` shell panel.
Keybindings live in `default/hypr/bindings/utilities.lua`: `Super+comma`
variants map to the IPC methods `dismissOne`, `dismissAll`, `invokeLast`,
`showHistory`, and the silencing toggle.
## How subsystems plug in
Everything goes through the same sender contract, so the pieces are small:
- **Low battery** — `omarchy-battery-low` sends a critical toast and runs the
`battery-low` hook.
- **Crash capture** — `omarchy-crash-watch` follows the systemd-coredump
journal stream and announces each crashed program (deduped per minute) as a
critical toast whose `--exec` runs `omarchy-agent-crash`. It waits for the
server first: a shell crash takes the notification server down with it, and
that crash is the one most worth reporting.
- **Pending migrations** — `omarchy-migrate-notify` (from its user service
after `graphical-session.target`) waits for the server, then sends a
critical toast whose click opens a terminal running `omarchy-migrate`,
falling back to printing in the terminal if the hand-off fails.
## Reminders
Reminders ride on notifications rather than being their own daemon.
`bin/omarchy-reminder <minutes> [message]` creates a transient systemd user
timer via `systemd-run --user --collect --on-active=<minutes>m` under the
unit name `omarchy-reminder-<minutes>m-<epoch>`; the timer's payload sends the
reminder toast, deletes its message file, and refreshes the bar indicator.
Custom messages are stashed in `$XDG_RUNTIME_DIR/omarchy-reminders/<unit>.message`
since a unit name cannot carry arbitrary text. `--collect` means fired timers
leave nothing behind.
The state therefore lives entirely in systemd: `show` and `clear` enumerate
`systemctl --user list-timers "omarchy-reminder-*.timer"``show` as a
summary toast, `show --json` as the JSON the bar's Reminder indicator polls
(refreshed by the same `omarchy-shell -q omarchy.indicators refresh` call the
timers and mutations make). `omarchy-reminder -i` summons the
`omarchy.reminders` overlay (`shell/plugins/reminders/ReminderFlow.qml`), a
two-step minutes/message prompt that shells back out to `omarchy-reminder` to
do the setting.
+51 -23
View File
@@ -2,9 +2,12 @@
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. IPC is the canonical way for CLIs
to talk to a running shell `omarchy-shell-ipc` auto-starts it on
first call.
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
@@ -34,9 +37,14 @@ first call.
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
`keepLoaded: true` to survive between summons. First-party services are
loaded at startup.
Panels, overlays, and menus are loaded when summoned. Plugins can set the
top-level manifest key `keepLoaded: true` to survive between summons.
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).
@@ -76,20 +84,25 @@ 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 prompt when run bare in a terminal and run unattended when given
arguments — add `--yes` to skip every prompt (the path for scripts and agents).
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 and can be moved with `omarchy bar move`; enabling a full bar
replaces the one in use.
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 plus extra targets registered by
individual plugins (`bar`, `image-selector`, …).
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 |
|---------------------------------------|---------------------------------|
@@ -97,17 +110,24 @@ individual plugins (`bar`, `image-selector`, …).
| `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.
`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
@@ -122,8 +142,7 @@ individual plugins (`bar`, `image-selector`, …).
"id": "omarchy.bar",
"position": "top",
"transparent": false,
"centerAnchor": "calendar",
"fontFamily": "JetBrainsMono Nerd Font",
"centerAnchor": "omarchy.clock",
"layout": {
"left": [ { "id": "omarchy.menu" } ],
"center": [ { "id": "omarchy.clock", "format": "HH:mm" } ],
@@ -146,10 +165,9 @@ Rules:
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`, …).
The migration rewrites older ids such as `Clock` and `AudioPanel` forward.
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. `allowMultiple: true` in the manifest permits multiple instances.
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.
@@ -157,6 +175,12 @@ Rules:
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,
@@ -165,20 +189,23 @@ 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.
`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 two singletons in
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.launcher.*`, `Color.imagePicker.*`, `Color.polkit.*`,
`Color.lock.*`). Clipboard and emojis share `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
@@ -371,5 +398,6 @@ 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)`.
`run(cmd)`, `showTooltip(t, s)` / `hideTooltip(t)`,
`requestPopout(o)` / `releasePopout(o)`. To shell-quote arguments for
`run`, use `Util.shellQuote(v)` from `qs.Commons`.
+138
View File
@@ -0,0 +1,138 @@
# Testing
How the non-graphical test suites are organized: what each runner owns, the
protocol test files speak, and the conventions that keep them runnable on any
machine — including headless CI sandboxes with no compositor. The graphical
acceptance suite is a separate thing that drives a live session in a disposable
VM; see [`agents/skills/acceptance-tests.md`](../agents/skills/acceptance-tests.md).
## Suite map
`./test/all` runs both suites below and keeps going when one fails, so a single
failure cannot hide the other suite behind it. It reports the failed suites at
the end and exits non-zero.
- **`./test/cli`** — one big script, one suite. It owns the CLI router: help
and group rendering, route resolution, aliases, hidden commands, and the
guarantee that a trailing `--help` never executes the target. It also owns
the metadata lint — every `omarchy-*` executable under `bin/` is checked for a
`# omarchy:summary=` header and against removed or redundant fields — plus
the theme pipeline: template rendering (`omarchy-theme-set-templates`,
`omarchy-theme-color`, `omarchy-theme-osc`), the theme sync commands
(tmux, GNOME, VS Code, Pi, Claude) run against stub binaries and a fake
`$HOME`, and the theme-state migrations.
- **`./test/shell`** — runs every `test/shell.d/*-test.sh` (except
`base-test.sh` itself). Each file is an independent suite covering one area:
a shell plugin, a `bin/` command, a config invariant, a migration. This is
where new tests go.
- **Acceptance** — everything that needs a real desktop doing real things.
Deliberately excluded from `./test/all`; it runs in a VM, not the
development session.
A new shell test only needs the right name: drop `<area>-test.sh` into
`test/shell.d/` and `./test/shell` picks it up automatically. Shared fixtures
live under `test/shell.d/fixtures/`.
## The base-test.sh contract
Every shell test starts the same way:
```bash
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
```
`base-test.sh` refuses to be executed directly — it is a library. It discovers
the repo root from its own location and exports it as `ROOT`, so tests
reference files as `$ROOT/bin/...` and never depend on the caller's working
directory or an installed Omarchy.
Assertions are TAP-flavored and blunt:
- `pass "description"` prints `ok - description`.
- `fail "description" [detail]` prints the optional detail and
`not ok - description` to stderr, then **exits the file**. There is no
counting or continuing within a file: the first failed assertion ends it,
which keeps later assertions from reporting against state the failure
already invalidated.
- `require_command <cmd>` fails the file when a needed tool is absent.
The runner compensates for that early exit: `./test/shell` continues past a
failing file and summarizes the failures at the end. Aborting the whole run at
the first bad file once let a single packaging failure mask 114 of 134 files.
Failure granularity is therefore per file inside a run, per assertion inside a
file.
## Compositor-dependent tests
Some tests launch Quickshell or query Hyprland, but the suite must stay green
on headless machines. `require_compositor "description"` handles this: when no
compositor answers it prints `ok - no Wayland compositor; skipping ...` and
exits 0 — a skip is a passing test — and otherwise returns so the file
proceeds.
The probe is more than an environment check, because `WAYLAND_DISPLAY` only
proves the variable was inherited. Sandboxes pass the environment through
while blocking `$XDG_RUNTIME_DIR`, so Quickshell clears a bare variable check
and then aborts inside QGuiApplication — a core dump per launch where a skip
belonged. So `compositor_reachable` checks the socket actually exists, then
asks Hyprland itself (`hyprctl -j monitors`, retried, and only when
`HYPRLAND_INSTANCE_SIGNATURE` makes it askable), since a compositor that died
mid-session leaves its socket behind. When the compositor is reachable,
`require_compositor` also sets `ulimit -c 0`: Quickshell leaves through
`qFatal()` if its connection drops mid-run, and the test should fail without
writing a core dump as debris.
Gate only what needs gating — put `require_compositor` in files whose runtime
half needs a live session, and keep static analysis of the same area in code
that runs unconditionally before or beside it.
## Unit-testing shell JavaScript from bash
The Quickshell plugins keep their logic in plain `.js` modules
(`shell/plugins/menu/MenuModel.js`, `bar/BarModel.js`, ...) that end in a
guarded `if (typeof module !== "undefined") module.exports = {...}` block. QML
imports them directly and ignores the guard; Node loads them as CommonJS. That
dual citizenship is what makes the shell's model logic unit-testable without a
compositor.
`run_node_test` is the bridge: it prepends a JS prelude to a heredoc and pipes
the result into `node`. The prelude mirrors the bash assertion protocol
(`pass`, `fail`, `assert`, `assertEqual`, `assertDeepEqual` — same
`ok`/`not ok` lines, same exit-on-first-failure) and provides `root` (from the
exported `ROOT`), `path`, and `requireFromRoot(relativePath)`:
```bash
run_node_test <<'JS'
const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
const parsed = menu.parseMenuJsonc('{ "items": { "root": { "label": "Go" }, }, }')
assertEqual(parsed.length, 1, 'menu parses JSONC with trailing commas')
JS
```
Roughly a quarter of the shell test files use this to test parsing, merging,
and layout logic as pure functions, reserving compositor-gated tests for what
only a live session can prove.
## Conventions worth copying
- **Stub the world, run the real code.** Tests build a scratch `bin/` of stub
executables (`sudo`, `tmux`, `gsettings`, helper commands) that log their
arguments to a file, prepend it to `PATH`, and then run the real script
under test. Assertions grep the call log and the files the script wrote.
- **Fake `$HOME`, real `$OMARCHY_PATH`.** Anything touching user state runs
with `HOME` pointed at a `mktemp -d` directory (cleaned up via
`trap ... EXIT`) and `OMARCHY_PATH="$ROOT"`, so tests exercise the checkout
without touching the developer's machine.
- **Migrations run directly.** A migration test builds the legacy state in a
fake `$HOME`, runs `bash -euo pipefail "$ROOT/migrations/<ts>.sh"`, and
asserts the resulting state — including running it twice to prove
idempotence, and once against non-legacy state to prove it leaves user
customization alone.
- **Assert the invariant, not the snapshot.** Config tests pin the property a
test is named for (this widget stays adjacent to that one) rather than whole
structures, so unrelated churn does not fail them.
+22 -5
View File
@@ -6,6 +6,13 @@ Omarchy themes live under `themes/<name>/` in the source tree (installed at
`colors.toml`; Omarchy generates the active theme files from
`default/themed/*.tpl` when `omarchy-theme-set <name>` runs.
Beyond `colors.toml` and hand-written config overrides, a theme can ship
`backgrounds/` (users overlay their own via
`~/.config/omarchy/backgrounds/<name>/`; the active image is the
`~/.local/state/omarchy/current/background` symlink), `preview.png` and
`preview-unlock.png` for the theme switcher, `icons.theme`, `keyboard.rgb`,
`unlock.png`, and a `light.mode` marker file.
## Theme activation flow
`omarchy-theme-set <name>` builds a clean staging directory at
@@ -28,6 +35,14 @@ User templates in `~/.config/omarchy/themed/*.tpl` are processed before the
built-in templates. If a user template has the same output filename as a
built-in template, the built-in output is skipped.
After activation, `omarchy-theme-set` fires the `theme-set` hook
(`~/.config/omarchy/hooks/theme-set*`, theme name in `$1`) and dispatches a
parallel retint of running apps — terminals, Hyprland, btop, browser, editors,
and the rest of the `post_theme_commands` list in `bin/omarchy-theme-set`.
Making a new app follow theme changes means adding its restart/retint command
to that list. Runs serialize on a `flock`, so scripted theme changes queue
instead of racing.
## `colors.toml`
`colors.toml` provides the palette keys used by templates. Keys are grouped
@@ -64,7 +79,8 @@ shell palette is loaded from:
`color4`
- `muted` — de-emphasized elements (comments, placeholders, dividers); also
serves as ANSI `color8`
- `urgent` / `red` / `color1`
- `red` / `color1` — populate the shell's urgent role; there is no `urgent`
palette key (one defined in `colors.toml` is ignored)
Themes and user templates using the legacy short names remain supported.
Canonical names take precedence when both forms are defined, and resolved
@@ -304,10 +320,11 @@ BorderSurface {
}
```
Use `Border.surfaceSpec(section, token, fallbackColor, fallbackWidth)` for
shell theme tokens, `Border.controlSpec(state, foreground, accent)` for shared
controls, and `Border.flat(color, width)` for a deliberate local border that
should not be overridden by the active theme. `Color.<section>.border` is the
Use `Border.surfaceSpec(section, token, fallbackColor, fallbackWidth, alphaKey)`
for shell theme tokens (the optional `alphaKey` names the alpha token, e.g.
`"border-alpha"`), `Border.controlSpec(state, foreground, accent, urgent)` for
shared controls, and `Border.flat(color, width)` for a deliberate local border
that should not be overridden by the active theme. `Color.<section>.border` is the
flat first-stop color for consumers that cannot render full border specs.
## Hyprland templates
+30 -5
View File
@@ -82,7 +82,7 @@ Omarchy update command, the hook exits non-zero with `AbortOnFail`, which stops
the transaction before packages are changed.
`omarchy-update-system-pkgs`, `omarchy-refresh-pacman`, `omarchy-reinstall-pkgs`,
and the v4 upgrader run pacman through:
`omarchy-channel-set`, and the v4 upgrader run pacman through:
```bash
env OMARCHY_UPDATE_PACMAN=1 pacman ...
@@ -116,9 +116,14 @@ omarchy-update
├─ omarchy-update-lock
│ └─ acquire the update lock and run omarchy-update inside it
├─ omarchy-update-requires-free-space
│ └─ check free space on / and warn below the configured threshold
│ └─ abort below the configured free-space threshold on /
├─ confirm unless -y
├─ create snapper snapshot, if snapper is installed
├─ omarchy-update-pkg-prune
│ └─ trim the pacman cache to two versions per package, deliberately
│ before the snapshot since the cache lives on the snapshotted subvolume
├─ create snapper snapshot (skipped silently without snapper; snapper
│ installed but unconfigured fails the snapshot loudly, pointing at
│ install/config/snapper.sh, and the update continues without one)
├─ omarchy-update-stay-awake start
├─ run package updates, migrations, hooks, and log analysis
├─ omarchy-update-status
@@ -132,6 +137,9 @@ Important behavior:
- In dev-link mode, `omarchy update` fast-forwards the active checkout from its
configured upstream before changing system packages or running migrations.
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` — a promise not to ask anything.
Steps that would prompt (orphan removal, conflict handoff) report and skip
instead of blocking.
- The free-space requirement uses a 10 GiB threshold and stops the update before
confirmation when it is not met. If free space cannot be determined, the
check is silently skipped. Set `OMARCHY_UPDATE_FORCE=1` to bypass the check.
@@ -235,6 +243,20 @@ Exit codes:
The widget runs this check on shell startup and every six hours. Clicking the
update icon launches `omarchy-update` in a floating terminal.
## Channels and versions
Updates install whatever the active channel points at. `omarchy-channel-set
<stable|rc|edge|dev>` switches channels: the three package channels select
which pacman repo the mirrorlist points at (and swap between the `omarchy` and
`omarchy-dev` packages through a guard-allowed pacman run), while `dev` links
the runtime to a git checkout via the dev-link mechanism, after which
`omarchy update` fast-forwards that checkout instead of upgrading a package.
There is no version file at runtime. `omarchy-version` derives the version from
`pacman -Q` on whichever package is installed, or reports `dev (<hash>)` for a
linked checkout, and `omarchy-version-channel` sniffs the mirrorlist and
pacman.conf to answer which channel is active.
## Update-related binaries
This inventory is intentionally opinionated. Some commands are useful as stable
@@ -250,14 +272,17 @@ scripts.
| `omarchy-update-confirm` | Gum confirmation copy for `omarchy update`. | **Question.** Could be inlined into `omarchy-update`; separate file only helps keep copy isolated. |
| `omarchy-update-dev` | Fast-forwards the active dev-linked checkout from its configured upstream; no-ops for package-backed installs. | **Keep.** Runs before package updates so a checkout conflict stops the update before system mutation. |
| `omarchy-update-keyring` | Ensures Omarchy keyring and Arch keyring are current before the main transaction. | **Keep, but review.** It uses targeted `pacman -Sy` for keyring bootstrapping; acceptable for this special case but should remain tightly scoped. |
| `omarchy-update-system-pkgs` | Runs `sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm` with targeted transition `--overwrite` entries so the ALPM guard allows the transaction and early package-layout conflicts are handled. | **Keep for now.** Small leaf command, clear/testable. |
| `omarchy-update-system-pkgs` | Runs `sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm` with `--overwrite '/usr/share/omarchy/*'`, capturing stderr to a report file; on failure it execs `omarchy-update-system-pkgs-when-conflicted`. | **Keep for now.** Small leaf command, clear/testable. |
| `omarchy-update-system-pkgs-when-conflicted` | Hidden conflict handler: quarantines unowned conflicting files under `/var/lib/omarchy/replaced`, retries the upgrade once, restores files the upgrade didn't claim, and hands package-vs-package conflicts to an interactive pacman run (never under `-y`). | **Keep internal/hidden.** Keeps conflict recovery out of the happy path. |
| `omarchy-update-pkg-prune` | Trims the pacman cache to two versions per package (`paccache -rk2`) before the snapshot, keeping the offline downgrade path while capping snapshot growth. | **Keep internal/hidden.** |
| `omarchy-update-requires-free-space` | Aborts the update below a 10 GiB free-space threshold on `/`; silently skipped when free space cannot be determined; `OMARCHY_UPDATE_FORCE=1` bypasses. | **Keep internal/hidden.** |
| `omarchy-migrate` | Public migration command. Waits for pacman, then runs all pending migrations for the current user. Supports `--pending`. | **Keep.** This replaces the discarded `omarchy-update-user-finalize` name and no longer needs `--force`. |
| `omarchy-update-pacman-guard` | ALPM pre-transaction guard that aborts direct `pacman -Syu` style upgrades unless Omarchy set `OMARCHY_UPDATE_PACMAN=1` or the user explicitly set `OMARCHY_ALLOW_DIRECT_PACMAN=1`. | **Keep internal/hidden.** This is what nudges users back to `omarchy update`. |
| `omarchy-migrate-notify` | Internal login-time notification helper. Uses `omarchy-migrate --pending` and shows a notification only when this user has pending migrations. | **Keep internal/hidden.** Clear name now that the public command is `omarchy-migrate`. |
| `omarchy-update-user-notify` | Hidden compatibility wrapper for `omarchy-migrate-notify`. | **Temporary.** Keep only for old callers. |
| `omarchy-update-available` | Update checker for shell widget and post-update refresh. | **Keep.** Could eventually be renamed `omarchy-update-check`, but current name matches widget semantics. |
| `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. |
| `omarchy-update-mise` | Runs `mise up` for mise-managed tools. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
| `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. |
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
| `omarchy-update-restart` | Prompts for reboot after kernel/Hyprland updates, restarts components with `restart-*-required` markers, and always restarts the shell. | **Keep.** Important final step; may eventually include service-restart checks. |