Files
omarchy-setup/README.md
T
noconnorandClaude Opus 5.5 87a31858fd Profiles: machines, roles and shared layers
The repo was one machine's capture. It is now a set of profiles built from
layers under profiles/:

  machines/  hp-laptop, macbook-air-m2, coder-desktop
  roles/     developer, devops, ai-agents
  features/  handoff (lan-mouse)
  shared     base, personal, personal-desktop

A profile resolves to its includes (depth first), any --roles, then itself;
for a path several layers provide, the last wins. install.sh --show prints
the layers and which one owns each file.

Hyprland settings no longer share a file: base's hyprland.lua loads
~/.config/hypr/layers/*.lua with Omarchy's own require_all, and bindings.lua
stays stock so nothing binds twice. Verified with Hyprland 0.56.2
--verify-config for all three profiles, plus a broken probe layer to prove
the directory is loaded.

packages.txt gains per-line arch (Arch Linux ARM lacks opencode, codex, mise,
shellcheck) and cmd= (skip when dotfiles' ~/.local/bin already provides the
command), and an [ignore] section that replaces capture.sh's hard-coded list.
Tab-separated records would have collapsed empty fields under read, so
package records use the unit separator.

hp-laptop: re-captured. monitors.lua catch-all scale is now 1, shell.json
has four new plugins, fix-stuck-displays has its connector-name fix,
navbar-cat is gone, plugin-manager and omarchy-phone are pinned, the
herdr-hud working tree is re-diffed and bin/herdr-web rides along. Its
handoff peer is now the Air (192.168.1.49); the old default list was the
HP's own addresses, written from the Air's side.

macbook-air-m2: captured read-only over SSH as it is (no themes, plugins
or bar changes). Its LAN_MOUSE_POINTER_SCALE=0.7 moves from the shared unit
into a lan-mouse.service.d drop-in in its own layer.

coder-desktop: the Omarchy settings covert-cloud's image used to patch in
by hand (Virtual-1 at 1920x1080, no animations, stay-awake, Retro 82 once),
for the image to apply at a pinned commit. Roles reach it per workspace via
install.sh --roles ... --no-clobber.

capture.sh writes each file back into the layer that owns it, and refuses
to run before the first layered install unless --force, since the old file
shapes would land in shared layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153egowV1Xw2RNjZnSyZicc
2026-10-16 16:01:54 -04:00

337 lines
18 KiB
Markdown

# omarchy-setup
Omarchy customizations on top of a stock install, as **profiles**: one per
machine, each built from shared **layers** and generic **roles**.
| Profile | Machine |
|---|---|
| `hp-laptop` | The HP (`omarchy-laptop`): Intel, Anker dock, LG displays. Where this repo started. |
| `macbook-air-m2` | The Air (`omarchy-air`): Asahi Linux on Arch Linux ARM, aarch64. |
| `coder-desktop` | The streamed Coder desktops, built by covert-cloud's image pipeline. |
| Role | Adds |
|---|---|
| `developer` | gh, delta, uv, just, direnv, shfmt/shellcheck, httpie, mise, VS Code |
| `devops` | kubectl, helm, k9s, kubectx, stern, kustomize, talosctl, cilium, tofu, terragrunt, packer, ansible, go-task, yq, sops/age, dig/mtr/nmap/tcpdump |
| `ai-agents` | Claude Code, OpenCode, Codex, Gemini CLI, Copilot CLI |
```bash
git clone https://git.nic-oconnor.com/public/omarchy-setup.git ~/omarchy-setup
cd ~/omarchy-setup
./install.sh --profiles # what exists
./install.sh --profile hp-laptop --show # its layers, and which layer owns each file
./install.sh --dry-run # see what it would change (profile picked by hostname)
./install.sh # do it
./install.sh --roles devops,ai-agents # add roles on top of the profile
```
`install.sh` is idempotent and backs up anything it overwrites as
`<file>.bak.<epoch>`. It records the profile and roles it applied in
`~/.config/omarchy-setup/profile`, so later runs (and `capture.sh`) need no
flags. `capture.sh` runs the other way, pulling live changes back into the
layer that owns each file, so the repo stays the source of truth.
## How profiles are built
Everything lives under `profiles/`. A layer is a directory with a `profile`
file (bash: `description`, `include=(...)`, and for machines `hostnames`,
`arch`, `handoff_peer_ips`) and any of the same pieces this repo always had:
| Path in a layer | Installed to |
|---|---|
| `config/` | `~/.config`, mirrored |
| `themes/` | `~/.config/omarchy/themes` |
| `plugins.txt`, `plugin-patches/` | `~/.config/omarchy/plugins`, pinned; local work replayed |
| `bin/` | `~/.local/bin` |
| `systemd/user/` | user units, plus `<unit>.service.d/*.conf` drop-ins |
| `system/` | `/etc`, mirrored (sudo) |
| `handoff/` | the lan-mouse patch and example configs (`install.sh handoff`) |
| `packages.txt` | `omarchy pkg add` / `omarchy pkg aur add` |
| `setup.sh` | run last, for state outside `~/.config` (markers, gsettings, a first-run theme) |
A profile resolves to an ordered list of layers: its `include`s first (depth
first, each once), then any `--roles`, then the profile itself. **For a path
several layers provide, the last one wins**, so a machine overrides the roles
and shared layers beneath it. `./install.sh --show` prints the result.
| Layer | Used by | What |
|---|---|---|
| `base` | all | `hyprland.lua` with the `hypr/layers/` loader; `bindings.lua` kept at stock |
| `personal` | both laptops | branding, default agent (`claude`), herdr, opencode, voxtype |
| `personal-desktop` | HP | the three hand-built themes, the shell plugins, bar layout, Claude/Herdr bindings |
| `features/handoff` | both laptops | `omarchy-handoff` + patched lan-mouse, its unit and binding |
| `roles/*` | per profile, or `--roles` | packages, and any bindings the role needs |
| `machines/*` | — | displays, input, hardware packages, `/etc` drop-ins, per-machine overrides |
**Hyprland settings never share a file.** `base`'s `hyprland.lua` loads every
`~/.config/hypr/layers/*.lua` in name order (Omarchy's own `require_all`, with
reload), after `monitors`/`input`/`bindings`/`looknfeel`/`autostart`. Each
layer ships its own `NN-<layer>.lua` there — roles 20s, `personal-desktop` 40,
features 50, machines 80 — and `bindings.lua` stays stock so nothing is bound
twice. `install.sh` warns about a file in `hypr/layers/` that no layer of the
current profile provides (a role you dropped).
**Packages** lines are `name [arch...] [cmd=a,b] # why`, in `[repo]`, `[aur]`,
`[optional]` or `[ignore]` sections. An arch limits a line to `x86_64` or
`aarch64` (Arch Linux ARM lacks `opencode`, `openai-codex`, `mise` and
`shellcheck`). `cmd=` skips the package when one of those commands is already
on PATH — on the laptops, `dotfiles`' `bin/` puts most devops and agent CLIs in
`~/.local/bin`, and the Arch package would only duplicate them; on a Coder
desktop, which has no `dotfiles`, the package is the tool. `[ignore]` is for
packages a machine has on purpose but this repo does not manage, so
`capture.sh packages` stays quiet about them.
Roles name the `dotfiles` modules they pair with (`dotfiles=(...)`), and
`install.sh` prints them when `~/.dotfiles` is absent. It never runs
`dotfiles` itself: that needs a 1Password token, which a Coder desktop does
not have.
## Coder desktops
The image is built by `covert-cloud`
(`platform/workspaces/omarchy-desktop/image/`). Its `provision/user.sh` clones
this repo at a **pinned commit** into `~/.local/share/omarchy-setup` and runs
`install.sh --profile coder-desktop`, which bakes in `base`, `developer` and
the `coder-desktop` layer:
- one fixed `Virtual-1` monitor at 1920x1080, GDK scale 1 — **must match**
`selkies.service`'s `--manual-width/--manual-height`; the build fails if the
line is missing;
- no window animations, and GTK reduced motion: the VM has no GPU, so every
frame is drawn on the CPU;
- Omarchy's stay-awake mode: the desktop user has no password, so a lock
screen could never be unlocked; the build checks for the marker;
- Retro 82 as the default theme, set once only, so a theme the developer picks
survives restarts. (The image itself installs the Covert Cloud themes.)
The plumbing stays in `covert-cloud`: Selkies, the Coder agent, the Super key
stand-in, the start page, autologin, and the lockdown (no SSH, no file
transfer, locked password). Nothing from `personal*` or `handoff` goes on a
desktop that other people use.
**Roles per workspace.** The Coder template has a **Roles** parameter
(multi-select: `devops`, `ai-agents`). At every workspace start it runs
`install.sh --profile coder-desktop --roles <chosen> --no-clobber`:
idempotent, so an unchanged choice costs a few seconds, and `--no-clobber`
keeps any file the developer has edited. Deselecting a role stops managing
it but does not uninstall its packages; its `hypr/layers/` file is left in place
and reported.
**Changing what the desktops get** is a commit here plus a pin bump there:
`OMARCHY_SETUP_REF` in `covert-cloud`'s `provision/user.sh` (rebuild the image)
and in the template's `main.tf` (template push; reaches running desktops at
their next start).
## The MacBook Air M2
Captured as it was on 2026-10-16 (Omarchy 4.0.3rc4), read-only over SSH: no
hand-built themes, no shell plugins, stock bar and terminal fonts, so it
includes `personal` but not `personal-desktop`. Add that to its `include=` to
give it the HP's desktop. What is its own:
- `monitors.lua`: monitor scale 1.33333, GDK scale 1, no named outputs.
- `input.lua`: stock gestures (no three-finger workspace swipe), and the
measured note on why `sensitivity`/`accel_profile` rules are inert for the
lan-mouse virtual pointer.
- `/etc/modprobe.d`: `hid_apple fnmode=1` (the built-in keyboard; the HP uses 2
for an external Apple keyboard) and `appledrm show_notch=1`.
- `lan-mouse.service.d/pointer-scale.conf`: `LAN_MOUSE_POINTER_SCALE=0.7`, for
the Air's 140 logical ppi against the LG's ~96.
- Its own `herdr/config.toml` (no agent-row customization yet), `opencode.json`
(`"theme": "system"`) and `voxtype/config.toml`.
- The Asahi platform packages, listed so `capture.sh` notices drift.
Left out: `~/.config/opencode/opencode.gateway.json` (holds the LiteLLM key;
`dotfiles`' `mcp-config` writes it), `~/.config/lan-mouse/`, and the
post-update hooks and `omarchy-asahi-mic.service`, which Omarchy installs
itself.
## The HP laptop: its customizations, and why
**Displays** (`profiles/machines/hp-laptop/config/hypr/monitors.lua`) are the
fiddliest part. The file is the single source of truth for monitor rules on
purpose — overlapping rules spread across several files is what previously made
external displays unrecoverable. Three outputs are named by EDID description
rather than connector, so they match whichever port they land on: the built-in
Chimei panel at 1.25x, the LG IPS236 on HDMI at 1x, and the LG TV through the
Anker dock. The TV is deliberately left on `preferred` (3840x2160@30) — the dock
negotiates DP alt-mode with only two lanes, so forcing 4K60 produced a "monitor
has no mode" loop. `GDK_SCALE=1` with a catch-all monitor scale of 1 (the
built-in panel and the TV set 1.25 in their own rules); the stock pairing is 2
and `auto`.
`hyprland.lua` sources `hyprmoncfg-monitors.lua` last so the `hyprmoncfg`
daemon's generated layout wins over anything above it.
`profiles/machines/hp-laptop/bin/fix-stuck-displays` recovers the related
kernel-side failure: after suspend/resume i915 can leave an unplugged connector
"disconnected" but still "enabled" on a pipe Hyprland has forgotten, which
blocks every other output from modesetting. It forces the connector on, disables
it through Hyprland to release the pipe, then returns it to auto-detect. It
needs passwordless sudo for `tee /sys/class/drm/*/status`.
**Keyboard/mouse sharing** (`profiles/features/handoff/`: `bin/omarchy-handoff`,
`systemd/user/lan-mouse.service`) drives `lan-mouse` to share input with the
other Omarchy laptop (the Air), bound to `SUPER+SHIFT+K` (`SUPER+K` is already
Keybindings). The script's header comment is the real documentation — it records
the non-obvious facts that cost hours: `capture_backend` must be `layer-shell`
(the portal backend is a silent no-op on Hyprland 0.56.2),
`[authorized_fingerprints]` is keyed by fingerprint and the reversed form fails
silently, and the log variable is `LAN_MOUSE_LOG_LEVEL`, not `RUST_LOG`. It runs
as a user unit rather than from `autostart.lua` because `o.launch_on_start`
sends both streams to `/dev/null`, hiding every pairing failure.
Three further facts were only established after the above was first written, and
they are what actually made it work:
- **Fingerprints are case-sensitive.** lan-mouse generates them lowercase and
compares with a plain map lookup — no normalisation — while `openssl` prints
uppercase and `cli authorize-key` stores whatever it is handed. An uppercase
digest is the *correct* digest, valid TOML, non-empty, passes every guard, and
never matches: the DTLS handshake dies `bad_certificate`, indistinguishable
from a wrong key or a dropped packet. This was the root cause of a full day of
dead crossings. `omarchy-handoff fixcase` repairs it; `doctor` now fails on
it.
- **Authorization is receiver-side only, not mutual.** `connect.rs` sets
`insecure_skip_verify`, so the *connecting* side never checks the peer's
certificate — only the listener consults `authorized_fingerprints`. Each
direction therefore depends solely on the receiver's list, two-way control
needs both lists, and a sender cannot tell whether it is talking to the real
peer. Input goes to whatever answers on that IP:port.
- **`ufw` is default-deny inbound**, and a dropped inbound 4242 presents
*identically* to a bad certificate. `install.sh handoff` opens it for the
addresses in the profile's `handoff_peer_ips`. Check it first: `journalctl -k
-g 'UFW BLOCK'`.
`profiles/features/handoff/handoff/patch/` carries a two-line source patch so
the daemon forwards the accelerated deltas, which is the only way to make the
sender's `sensitivity` and `accel_profile` affect remote pointer feel at all; it
also adds `LAN_MOUSE_POINTER_SCALE` for the residual difference in logical pixel
density between the two screens. `install.sh` builds it per-machine — the two
laptops are different architectures — and `lan-mouse.service` runs the result.
The Air's `LAN_MOUSE_POINTER_SCALE=0.7` is a systemd drop-in in its own layer
(`lan-mouse.service.d/pointer-scale.conf`), so the shared unit stays at 1.0.
`profiles/machines/hp-laptop/system/sysctl.d/99-omarchy-handoff-arp.conf` goes
with it: Wi-Fi and the dock's ethernet both sit on `192.168.1.0/24`, and with
the default ARP settings the subnet's mapping collapses onto whichever NIC
replied last.
**Bar and shell** (`profiles/personal-desktop/config/omarchy/shell.json`) adds
`omarchy.active-window` to the left, five plugin widgets, and `indie.herdr-hud`
as a non-bar plugin. Idle is stretched a long way past stock — screensaver at 20
minutes, lock at 30, against 2.5 and 5.
**Keybindings** live in two layer files.
`profiles/personal-desktop/config/hypr/layers/40-personal-desktop.lua` rebinds
`SUPER+SHIFT+A` from the stock ChatGPT webapp to Claude and adds four Herdr HUD
bindings; the handoff feature's `50-handoff.lua` adds Handoff.
**Input** (`profiles/machines/hp-laptop/config/hypr/input.lua`) enables the
three-finger horizontal workspace gesture that ships commented out, and flattens
pointer acceleration for `unknown-device*`.
That second rule is **a no-op, and the reasoning originally given for it was
wrong.** It was added believing lan-mouse forwards deltas the sending compositor
had already accelerated, which the receiver then amplified a second time.
Upstream does the opposite: it forwards the *unaccelerated* deltas and discards
the accelerated pair in the same event
(`profiles/features/handoff/handoff/patch/REBUILD.md`). And the rule cannot act
either way — `hl.device({name="unknown-device"})` does match the virtual pointer
(provable by toggling `scroll_factor`), but `sensitivity` and `accel_profile`
are libinput pointer-acceleration settings and a `wlr-virtual-pointer` is not a
libinput device, so they have nothing to act on. Measured 2026-10-10 on both
machines with `lan-mouse test-emulation` + `hyprctl cursorpos`: travel was
398x198px at `sensitivity = 0` and identical at `-0.9`, exactly 1:1 with the raw
deltas. The lines are kept because they are harmless and the `unknown-device`
naming note is worth preserving; the real fix is `handoff/patch/`. Do not add
sensitivity or accel_profile rules here expecting them to change remote pointer
feel.
**Terminals** are all dropped to a smaller font (9 -> 8) except ghostty, which
went the other way to 12. `shell.toml` sets the bar font base size to 10.
**Branding** (`profiles/personal/config/omarchy/branding/`, shared with the Air)
replaces the about and screensaver ASCII art.
**`hid_apple` fnmode=2** makes the F-keys act as F-keys on an Apple keyboard.
It only takes effect after `sudo mkinitcpio -P` or a module reload.
## Local plugin work
`indie.herdr-hud` is the one piece that exists on no remote. Its `local` branch
carries commits — touchpad scroll feel, agent ordering, context-size display —
and they stay off the upstream PR deliberately.
`profiles/personal-desktop/plugin-patches/` carries them as a git bundle plus a
patch for the uncommitted working tree, and `install.sh` replays both after
cloning. Two untracked scripts in the same plugin, `bin/herdr-agents` and
`bin/herdr-web`, ride along as `indie.herdr-hud--bin--<name>`.
`capture.sh plugins` re-bundles and re-diffs these, so local work keeps flowing
back into the repo.
## Not in here, on purpose
- **Secrets.** `~/.config/lan-mouse/` holds a private key and per-machine
fingerprints; `~/.config/novnc/auth.env`, SSH keys and the git credential
helpers are likewise machine-local. Start from
`profiles/features/handoff/handoff/config/config.toml.example-{air,laptop}`
(which carry the `capture_backend`, edge and lowercase-fingerprint rules),
then pair with `omarchy-handoff pair <peer> <lowercase-sha256>` and
`omarchy-handoff doctor`. A fresh install regenerates the certificate, so this
machine's fingerprint changes and the *peer* has to re-pair. Back up
`lan-mouse.pem` beforehand if you would rather keep it stable — somewhere
private, not here.
- **Shell, git and dev tooling.** That is already its own repo, `dotfiles`
(`git.nic-oconnor.com/public/dotfiles`), which handles zsh, mise, 1Password
and the `~/.local/bin` tool shims. The only overlap avoided here is
`~/.config/git/config`, whose customizations are identity and credential
helpers — `dotfiles` territory.
- **The remote-access stack.** `wayvnc`, `novnc`, `sunshine`, `freerdp`,
`remmina` and `omarchy-remote-desktop` are installed on this laptop but scoped
out. `kokd.remote-desktop` is still in `plugins.txt` since it is a bar plugin,
but it is not placed in `shell.json`.
- **Two plugins with no remote yet.** `niccoconnor.mantel` and
`niccoconnor.keyhints` are installed on the HP and placed in `shell.json`, but
neither has an `origin`, so `plugins.txt` cannot pin them (`capture.sh` says
so). Until they are pushed, another machine gets a bar with those two slots
empty.
- **Work tooling.** The keycloak helpers (`kcadm`, `kclogin`, `wren-keycloak`)
and `paperclipai.service`.
- **aether-managed themes.** `pink-gt3rs`, `porsche-cayman` and `porsche-rain`
carry a `.aether-managed` marker and are reinstallable with `aether`, so their
~18MB of wallpapers and previews stay out.
- **`btop.conf`.** Its only diff against stock is v1.4.7 default drift, not a
real customization.
- **Backup files.** Fifteen `.bak.*` and `.prefix.*` files are sitting in
`~/.config/hypr` and `~/.config/omarchy` from earlier editing sessions. They
are not tracked and are safe to delete.
## Adding things
**A file.** `capture.sh` only refreshes what the repo already tracks, so start
it by hand, in the layer it belongs to:
```bash
cp ~/.config/foo/bar.conf profiles/machines/hp-laptop/config/foo/bar.conf
git add profiles/machines/hp-laptop/config/foo/bar.conf
```
Check first that it actually differs from stock —
`diff /usr/share/omarchy/config/foo/bar.conf ~/.config/foo/bar.conf` — or you
will be tracking a file Omarchy already ships. A Hyprland setting goes in that
layer's `config/hypr/layers/NN-<layer>.lua`, not in `bindings.lua` or
`looknfeel.lua`.
**A role.** `mkdir profiles/roles/<name>`, give it a `profile` (`description=`,
optionally `dotfiles=(...)`) and a `packages.txt`, plus
`config/hypr/layers/2N-<name>.lua` if it needs bindings. Use `cmd=` for any
package `dotfiles` might already provide, and an arch for anything Arch Linux
ARM lacks. To offer it on Coder desktops, add it to the template's Roles
parameter in `covert-cloud`.
**A machine.** `mkdir profiles/machines/<name>` with a `profile` naming
`hostnames=`, `arch=` and `include=(base ...)`, then its `monitors.lua`,
`input.lua`, `packages.txt` and `/etc` drop-ins. Run `./install.sh --profile
<name> --dry-run` on it, then `./capture.sh` after any hand-tuning.