diff --git a/AGENTS.md b/AGENTS.md index 26a7aada..5713b35a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/docs/audio-tuning.md b/docs/audio-tuning.md index 8f24917a..d5c39c07 100644 --- a/docs/audio-tuning.md +++ b/docs/audio-tuning.md @@ -12,7 +12,9 @@ default/audio/tunings/-/ ``` `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 diff --git a/docs/cli-router.md b/docs/cli-router.md new file mode 100644 index 00000000..12003328 --- /dev/null +++ b/docs/cli-router.md @@ -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 +` 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 +--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 --help` shows the resolved +binary and, when it differs, the filename route; `omarchy commands --all +--json` shows every route the router knows. diff --git a/docs/file-layout.md b/docs/file-layout.md index ce7ec0ec..2a9d965b 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -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/` → + `$OMARCHY_PATH/default/agents/skills/`, 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/.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 | diff --git a/docs/menu.md b/docs/menu.md new file mode 100644 index 00000000..480d128c --- /dev/null +++ b/docs/menu.md @@ -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 ` 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 `::<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 + `.`, 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. diff --git a/docs/notifications.md b/docs/notifications.md new file mode 100644 index 00000000..c2eba6d9 --- /dev/null +++ b/docs/notifications.md @@ -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 +`-.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 ` — 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 [message]` creates a transient systemd user +timer via `systemd-run --user --collect --on-active=m` under the +unit name `omarchy-reminder-m-`; 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/.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. diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index a1349ae4..a7c1b389 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -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//`, run `omarchy-shell shell rescanPlugins`, then `omarchy plugin enable `. 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 ` | load + open a plugin | | `hide ` | close a previously-summoned | | `toggle ` | summon if closed, hide if open | +| `togglePanelAt
` | toggle the panel at a bar position | | `call ` | call an already-loaded plugin | | `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code | | `reloadConfig` | reload shell.json | +| `applyTheme ` | push theme colors + shell.toml | | `toggleBarTransparency` | flip the bar background between solid and transparent | | `setPluginEnabled <"true"\|…>` | flip enabled bit (`ok` / `unknown`) | | `enablePlugin ` | enable and place in one mutation | +| `putBarWidget ` | place a widget only where absent (`omarchy bar put`) | | `moveBarWidget ` | move a configured widget | | `setBarWidget ` | 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//colors.toml` and surface roles + sizing in `themes//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.
.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`. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..3928ed2b --- /dev/null +++ b/docs/testing.md @@ -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 `-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 ` 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/.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. diff --git a/docs/theming.md b/docs/theming.md index f21db05f..fb270ce0 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -6,6 +6,13 @@ Omarchy themes live under `themes//` in the source tree (installed at `colors.toml`; Omarchy generates the active theme files from `default/themed/*.tpl` when `omarchy-theme-set ` runs. +Beyond `colors.toml` and hand-written config overrides, a theme can ship +`backgrounds/` (users overlay their own via +`~/.config/omarchy/backgrounds//`; 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 ` 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.
.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.
.border` is the flat first-stop color for consumers that cannot render full border specs. ## Hyprland templates diff --git a/docs/update-process.md b/docs/update-process.md index 2d89e05d..bec3350d 100644 --- a/docs/update-process.md +++ b/docs/update-process.md @@ -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 +` 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 ()` 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. |