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
@@ -1,67 +1,185 @@
|
||||
# omarchy-setup
|
||||
|
||||
Every customization this laptop has on top of a stock Omarchy install, in one
|
||||
place, so a second machine can be brought to the same state.
|
||||
Omarchy customizations on top of a stock install, as **profiles**: one per
|
||||
machine, each built from shared **layers** and generic **roles**.
|
||||
|
||||
Captured from Omarchy **4.0.4-1** on **2026-10-10**.
|
||||
| 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 <this repo> ~/omarchy-setup
|
||||
git clone https://git.nic-oconnor.com/public/omarchy-setup.git ~/omarchy-setup
|
||||
cd ~/omarchy-setup
|
||||
./install.sh --dry-run # see what it would change
|
||||
./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>`. `capture.sh` runs the other way, pulling live changes back
|
||||
into the repo so this stays the source of truth.
|
||||
`<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.
|
||||
|
||||
## What is in here
|
||||
## How profiles are built
|
||||
|
||||
| Path | What |
|
||||
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/` | Mirrors `~/.config`. Only files that differ from Omarchy's shipped defaults. |
|
||||
| `themes/` | The three hand-built themes, wallpapers included. |
|
||||
| `plugins.txt` | The 10 Omarchy shell plugins, each pinned to a commit. |
|
||||
| `plugin-patches/` | Local plugin work that exists on no remote (see below). |
|
||||
| `bin/` | `omarchy-handoff` and `fix-stuck-displays`. |
|
||||
| `handoff/` | The lan-mouse source patch that fixes pointer feel, plus example `config.toml`s. Not a `~/.config` mirror. |
|
||||
| `systemd/user/` | `lan-mouse.service`, `voxtype.service`. |
|
||||
| `system/` | `/etc` drop-ins: the ARP sysctl and `hid_apple` fnmode. |
|
||||
| `packages.txt` | Packages beyond the Omarchy baseline, each with a reason. |
|
||||
| `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) |
|
||||
|
||||
## The customizations, and why
|
||||
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.
|
||||
|
||||
**Displays** (`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 1.25
|
||||
monitor scale; the stock pairing is 2 and `auto`.
|
||||
| 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.lua` sources `hyprmoncfg-monitors.lua` last so the `hyprmoncfg` daemon's
|
||||
generated layout wins over anything above it.
|
||||
**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).
|
||||
|
||||
`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`.
|
||||
**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.
|
||||
|
||||
**Keyboard/mouse sharing** (`bin/omarchy-handoff`, `systemd/user/lan-mouse.service`)
|
||||
drives `lan-mouse` to share input with the other Omarchy laptop, 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.
|
||||
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:
|
||||
@@ -72,7 +190,8 @@ they are what actually made it work:
|
||||
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.
|
||||
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
|
||||
@@ -80,65 +199,73 @@ they are what actually made it work:
|
||||
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
|
||||
peer's addresses. Check it first: `journalctl -k -g 'UFW BLOCK'`.
|
||||
*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'`.
|
||||
|
||||
`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 `systemd/user/lan-mouse.service` runs the result.
|
||||
`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.
|
||||
|
||||
`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.
|
||||
`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** (`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.
|
||||
**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** (`config/hypr/bindings.lua`) rebinds `SUPER+SHIFT+A` from the
|
||||
stock ChatGPT webapp to Claude, and adds four Herdr HUD bindings plus Handoff.
|
||||
**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** (`config/hypr/input.lua`) enables the three-finger horizontal
|
||||
workspace gesture that ships commented out, and flattens pointer acceleration for
|
||||
`unknown-device*`.
|
||||
**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 (`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.
|
||||
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** (`config/omarchy/branding/`) replaces the about and screensaver
|
||||
ASCII art.
|
||||
**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. Ten commits live on
|
||||
its `local` branch — touchpad scroll feel, agent ordering, context-size display —
|
||||
and they stay off the upstream PR deliberately. `plugin-patches/` carries them as
|
||||
a git bundle plus a patch for the uncommitted working tree, and `install.sh`
|
||||
replays both after cloning. `io.github.tallsam.navbar-cat` has a one-file working
|
||||
tree change captured the same way.
|
||||
`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.
|
||||
@@ -148,42 +275,62 @@ back into the repo.
|
||||
- **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
|
||||
`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.
|
||||
`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
|
||||
(`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`.
|
||||
- **Work tooling.** The keycloak helpers (`kcadm`, `kclogin`, `wren-keycloak`) and
|
||||
`paperclipai.service`.
|
||||
- **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.
|
||||
`~/.config/hypr` and `~/.config/omarchy` from earlier editing sessions. They
|
||||
are not tracked and are safe to delete.
|
||||
|
||||
## Adding a file
|
||||
## Adding things
|
||||
|
||||
`capture.sh` only refreshes what the repo already tracks, so start it by hand:
|
||||
**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 config/foo/bar.conf # path mirrors ~/.config
|
||||
git add config/foo/bar.conf
|
||||
cp ~/.config/foo/bar.conf profiles/machines/hp-laptop/config/foo/bar.conf
|
||||
git add profiles/machines/hp-laptop/config/foo/bar.conf
|
||||
```
|
||||
|
||||
`install.sh config` and `capture.sh config` both walk `config/` recursively, so
|
||||
nothing else needs changing. Check first that the file 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.
|
||||
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.
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
# Pull the live machine's state back into this repo - the reverse of install.sh.
|
||||
#
|
||||
# Run it after customizing something by hand, then review `git diff` and commit.
|
||||
# It only refreshes paths the repo already tracks; it never invents new ones, so
|
||||
# adding a newly customized file is a deliberate act (see README, "Adding a file").
|
||||
# Each live file goes back into the layer that OWNS it for this machine's
|
||||
# profile (the last layer providing that path; `./install.sh --show` lists
|
||||
# them). It only refreshes paths the repo already tracks, so adding a newly
|
||||
# customized file is a deliberate act (see README, "Adding a file").
|
||||
#
|
||||
# ./capture.sh # refresh everything
|
||||
# ./capture.sh # this machine's profile, every step
|
||||
# ./capture.sh --profile hp-laptop # name it explicitly
|
||||
# ./capture.sh config # only the named steps
|
||||
# ./capture.sh --list
|
||||
#
|
||||
@@ -16,15 +19,19 @@ set -uo pipefail
|
||||
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STEPS=(config themes plugins bin services system packages)
|
||||
|
||||
c_ch=$'\033[33m'; c_skip=$'\033[2m'; c_warn=$'\033[33m'; c_hd=$'\033[1m'; c_off=$'\033[0m'
|
||||
[[ -t 1 ]] || { c_ch=; c_skip=; c_warn=; c_hd=; c_off=; }
|
||||
c_ch=$'\033[33m'; c_skip=$'\033[2m'; c_warn=$'\033[33m'; c_err=$'\033[31m'; c_hd=$'\033[1m'; c_off=$'\033[0m'
|
||||
[[ -t 1 ]] || { c_ch=; c_skip=; c_warn=; c_err=; c_hd=; c_off=; }
|
||||
say() { printf '%s\n' "$*"; }
|
||||
head_() { printf '\n%s==> %s%s\n' "$c_hd" "$*" "$c_off"; }
|
||||
chg() { printf ' %sM %s%s\n' "$c_ch" "$*" "$c_off"; }
|
||||
skip() { printf ' %s= %s%s\n' "$c_skip" "$*" "$c_off"; }
|
||||
warn() { printf ' %s! %s%s\n' "$c_warn" "$*" "$c_off"; }
|
||||
err() { printf ' %sx %s%s\n' "$c_err" "$*" "$c_off"; }
|
||||
gone() { printf ' %s? %s%s\n' "$c_warn" "$*" "$c_off"; }
|
||||
|
||||
# shellcheck source=lib/layers.sh
|
||||
source "$REPO/lib/layers.sh"
|
||||
|
||||
# pull_back <live> <repo-copy> <label>
|
||||
pull_back() {
|
||||
local live=$1 dest=$2 label=$3
|
||||
@@ -32,121 +39,139 @@ pull_back() {
|
||||
if cmp -s "$live" "$dest"; then skip "$label"; else cp "$live" "$dest"; chg "$label"; fi
|
||||
}
|
||||
|
||||
step_config() {
|
||||
head_ "~/.config -> config/"
|
||||
local dest rel
|
||||
while IFS= read -r -d '' dest; do
|
||||
rel=${dest#"$REPO/config/"}
|
||||
pull_back "$HOME/.config/$rel" "$dest" "$rel"
|
||||
done < <(find "$REPO/config" -type f -print0 | sort -z)
|
||||
# pull_tree <subdir> <live-root> -- every owned file under <layer>/<subdir>
|
||||
pull_tree() {
|
||||
local sub=$1 root=$2 rel layer
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
pull_back "$root/$rel" "$PROFILES/$layer/$sub/$rel" "$rel [$layer]"
|
||||
done < <(owners "$sub")
|
||||
}
|
||||
|
||||
step_config() { head_ "~/.config -> layers' config/"; pull_tree config "$HOME/.config"; }
|
||||
step_bin() { head_ "~/.local/bin -> layers' bin/"; pull_tree bin "$HOME/.local/bin"; }
|
||||
step_services() { head_ "systemd user units -> layers' systemd/"; pull_tree systemd/user "$HOME/.config/systemd/user"; }
|
||||
step_system() { head_ "/etc drop-ins -> layers' system/"; pull_tree system /etc; }
|
||||
|
||||
step_themes() {
|
||||
head_ "Themes"
|
||||
local t name live
|
||||
for t in "$REPO"/themes/*/; do
|
||||
[[ -d $t ]] || continue
|
||||
name=$(basename -- "$t")
|
||||
local l t name live
|
||||
declare -A theme=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
for t in "$PROFILES/$l"/themes/*/; do [[ -d $t ]] && theme[$(basename -- "$t")]=$l; done
|
||||
done
|
||||
for name in $(printf '%s\n' "${!theme[@]}" | sort); do
|
||||
t="$PROFILES/${theme[$name]}/themes/$name/"
|
||||
live="$HOME/.config/omarchy/themes/$name"
|
||||
if [[ ! -d $live ]]; then gone "$name - no longer installed"; continue; fi
|
||||
if diff -rq "$live" "$t" >/dev/null 2>&1; then
|
||||
skip "$name"
|
||||
skip "$name [${theme[$name]}]"
|
||||
else
|
||||
rm -rf "$t"; mkdir -p "$t"; cp -r "$live/." "$t/"
|
||||
chg "$name"
|
||||
chg "$name [${theme[$name]}]"
|
||||
fi
|
||||
done
|
||||
# Flag themes installed by hand that the repo does not carry.
|
||||
local l lname
|
||||
# Flag themes installed by hand that no layer carries.
|
||||
local lname
|
||||
for l in "$HOME"/.config/omarchy/themes/*/; do
|
||||
[[ -d $l ]] || continue
|
||||
lname=$(basename -- "$l")
|
||||
[[ -d "$REPO/themes/$lname" ]] && continue
|
||||
[[ -n ${theme[$lname]:-} ]] && continue
|
||||
[[ -e "$l/.aether-managed" ]] && continue
|
||||
warn "$lname is installed but not in this repo (cp -r it into themes/ to track it)"
|
||||
[[ " ${P_AETHER[*]} " == *" $lname "* ]] && continue
|
||||
warn "$lname is installed but in no layer (cp -r it into a layer's themes/ to track it)"
|
||||
done
|
||||
}
|
||||
|
||||
# Re-pin every plugin a layer lists to its live sha, drop the ones that are gone,
|
||||
# and add live plugins no layer lists to the last layer that has a plugins.txt.
|
||||
step_plugins() {
|
||||
head_ "Plugin pins and local work"
|
||||
local dir="$HOME/.config/omarchy/plugins"
|
||||
local tmp; tmp=$(mktemp)
|
||||
{
|
||||
echo "# Omarchy shell plugins. Fields: id<TAB>repo<TAB>pinned-sha"
|
||||
echo "# install.sh clones each into ~/.config/omarchy/plugins/<id> and checks out the sha."
|
||||
local p id url sha
|
||||
local dir="$HOME/.config/omarchy/plugins" l f
|
||||
local files=()
|
||||
while IFS= read -r l; do files+=("$l"); done < <(layers_with plugins.txt)
|
||||
if (( ! ${#files[@]} )); then skip "no layer carries plugins"; return 0; fi
|
||||
|
||||
declare -A listed=()
|
||||
local id u s line tmp
|
||||
for l in "${files[@]}"; do
|
||||
f="$PROFILES/$l/plugins.txt"; tmp=$(mktemp)
|
||||
while IFS= read -r line; do
|
||||
if [[ -z $line || $line == \#* ]]; then printf '%s\n' "$line" >> "$tmp"; continue; fi
|
||||
IFS=$'\t' read -r id u s <<<"$line"
|
||||
listed[$id]=1
|
||||
if [[ ! -d $dir/$id/.git ]]; then
|
||||
gone "$id - no longer installed, dropped from $l/plugins.txt"
|
||||
continue
|
||||
fi
|
||||
printf '%s\t%s\t%s\n' "$id" "$u" "$(git -C "$dir/$id" rev-parse HEAD)" >> "$tmp"
|
||||
done < "$f"
|
||||
if [[ $l == "${files[-1]}" ]]; then
|
||||
local p
|
||||
for p in "$dir"/*/; do
|
||||
[[ -d $p/.git ]] || continue
|
||||
id=$(basename -- "$p")
|
||||
url=$(git -C "$p" remote get-url origin 2>/dev/null) || continue
|
||||
sha=$(git -C "$p" rev-parse HEAD)
|
||||
printf '%s\t%s\t%s\n' "$id" "$url" "$sha"
|
||||
[[ -n ${listed[$id]:-} ]] && continue
|
||||
if [[ ! -d $p/.git ]]; then warn "$id is not a git checkout - not tracked"; continue; fi
|
||||
if ! u=$(git -C "$p" remote get-url origin 2>/dev/null); then
|
||||
warn "$id has no origin remote - push it somewhere before it can be pinned"; continue
|
||||
fi
|
||||
printf '%s\t%s\t%s\n' "$id" "$u" "$(git -C "$p" rev-parse HEAD)" >> "$tmp"
|
||||
chg "$id - new, added to $l/plugins.txt"
|
||||
done
|
||||
} > "$tmp"
|
||||
if cmp -s "$tmp" "$REPO/plugins.txt"; then skip "plugins.txt"; else mv "$tmp" "$REPO/plugins.txt"; chg "plugins.txt"; fi
|
||||
fi
|
||||
if cmp -s "$tmp" "$f"; then skip "$l/plugins.txt"; else mv "$tmp" "$f"; chg "$l/plugins.txt"; fi
|
||||
rm -f "$tmp"
|
||||
done
|
||||
|
||||
# herdr-hud's local-only branch, re-bundled
|
||||
local p="$dir/indie.herdr-hud"
|
||||
if [[ -d $p/.git ]] && git -C "$p" rev-parse --verify --quiet local >/dev/null; then
|
||||
local b="$REPO/plugin-patches/indie.herdr-hud.bundle"
|
||||
git -C "$p" bundle create "$b.new" origin/main..local >/dev/null 2>&1
|
||||
# Bundles embed a timestamp-free pack but differ byte-wise; compare the tips instead.
|
||||
if [[ -f $b ]] && [[ $(git -C "$p" bundle list-heads "$b" 2>/dev/null) == $(git -C "$p" bundle list-heads "$b.new" 2>/dev/null) ]]; then
|
||||
rm -f "$b.new"; skip "indie.herdr-hud.bundle"
|
||||
# Local plugin work, per layer: re-bundle 'local' branches, re-diff working
|
||||
# trees, pull back --bin-- files. See install.sh step_plugins.
|
||||
local b new p untracked
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -d $PROFILES/$l/plugin-patches ]] || continue
|
||||
for b in "$PROFILES/$l"/plugin-patches/*.bundle; do
|
||||
[[ -f $b ]] || continue
|
||||
id=$(basename -- "$b" .bundle); p="$dir/$id"
|
||||
[[ -d $p/.git ]] && git -C "$p" rev-parse --verify --quiet local >/dev/null || { gone "$id: no 'local' branch here"; continue; }
|
||||
new="$b.new"
|
||||
git -C "$p" bundle create "$new" origin/main..local >/dev/null 2>&1
|
||||
# Bundles differ byte-wise run to run; compare the tips instead.
|
||||
if [[ $(git -C "$p" bundle list-heads "$b" 2>/dev/null) == $(git -C "$p" bundle list-heads "$new" 2>/dev/null) ]]; then
|
||||
rm -f "$new"; skip "$id.bundle [$l]"
|
||||
else
|
||||
mv "$b.new" "$b"; chg "indie.herdr-hud.bundle"
|
||||
mv "$new" "$b"; chg "$id.bundle [$l]"
|
||||
fi
|
||||
[[ -f $p/bin/herdr-agents ]] && pull_back "$p/bin/herdr-agents" \
|
||||
"$REPO/plugin-patches/indie.herdr-hud--bin--herdr-agents" "indie.herdr-hud bin/herdr-agents"
|
||||
fi
|
||||
|
||||
local f id
|
||||
for f in "$REPO"/plugin-patches/*-worktree.patch; do
|
||||
done
|
||||
for f in "$PROFILES/$l"/plugin-patches/*-worktree.patch; do
|
||||
[[ -f $f ]] || continue
|
||||
id=$(basename -- "$f"); id=${id%-worktree.patch}
|
||||
if [[ ! -d $dir/$id/.git ]]; then gone "$id - not installed"; continue; fi
|
||||
local tmp2; tmp2=$(mktemp)
|
||||
git -C "$dir/$id" diff > "$tmp2"
|
||||
if cmp -s "$tmp2" "$f"; then skip "$id-worktree.patch"; else mv "$tmp2" "$f"; chg "$id-worktree.patch"; fi
|
||||
rm -f "$tmp2"
|
||||
id=$(basename -- "$f"); id=${id%-worktree.patch}; p="$dir/$id"
|
||||
if [[ ! -d $p/.git ]]; then gone "$id - not installed"; continue; fi
|
||||
tmp=$(mktemp)
|
||||
git -C "$p" diff > "$tmp"
|
||||
if cmp -s "$tmp" "$f"; then skip "$id-worktree.patch [$l]"; else mv "$tmp" "$f"; chg "$id-worktree.patch [$l]"; fi
|
||||
rm -f "$tmp"
|
||||
# Untracked files are not in a diff; only --bin-- files carry them.
|
||||
while IFS= read -r untracked; do
|
||||
[[ $untracked == bin/* && -f $PROFILES/$l/plugin-patches/$id--bin--${untracked#bin/} ]] && continue
|
||||
warn "$id: untracked $untracked is not captured (bin/ files: cp to plugin-patches/$id--bin--<name>)"
|
||||
done < <(git -C "$p" ls-files --others --exclude-standard)
|
||||
done
|
||||
for f in "$PROFILES/$l"/plugin-patches/*--bin--*; do
|
||||
[[ -f $f ]] || continue
|
||||
id=$(basename -- "$f")
|
||||
pull_back "$dir/${id%%--bin--*}/bin/${id##*--bin--}" "$f" "${id%%--bin--*} bin/${id##*--bin--} [$l]"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
step_bin() {
|
||||
head_ "~/.local/bin -> bin/"
|
||||
local f
|
||||
for f in "$REPO"/bin/*; do
|
||||
[[ -f $f ]] || continue
|
||||
pull_back "$HOME/.local/bin/$(basename -- "$f")" "$f" "$(basename -- "$f")"
|
||||
done
|
||||
}
|
||||
|
||||
step_services() {
|
||||
head_ "systemd user units"
|
||||
local f name
|
||||
for f in "$REPO"/systemd/user/*.service; do
|
||||
[[ -f $f ]] || continue
|
||||
name=$(basename -- "$f")
|
||||
pull_back "$HOME/.config/systemd/user/$name" "$f" "$name"
|
||||
done
|
||||
}
|
||||
|
||||
step_system() {
|
||||
head_ "/etc drop-ins"
|
||||
local dest rel
|
||||
while IFS= read -r -d '' dest; do
|
||||
rel=${dest#"$REPO/system/"}
|
||||
pull_back "/etc/$rel" "$dest" "/etc/$rel"
|
||||
done < <(find "$REPO/system" -type f -print0 | sort -z)
|
||||
}
|
||||
|
||||
# Report drift in the package delta without rewriting the hand-annotated file:
|
||||
# packages.txt carries a "why" comment per line that a regenerated list would lose.
|
||||
# Report drift in the package delta without rewriting the hand-annotated files:
|
||||
# each line carries a "why" comment that a regenerated list would lose.
|
||||
step_packages() {
|
||||
head_ "Package delta (report only)"
|
||||
load_packages
|
||||
local listed; listed=$(mktemp)
|
||||
sed 's/#.*//' "$REPO/packages.txt" | tr -d '[:blank:]' | grep -v '^$' | grep -v '^\[' | sort -u > "$listed"
|
||||
local e name archs cmds layer
|
||||
for e in "${PKG_repo[@]}" "${PKG_aur[@]}" "${PKG_optional[@]}" "${PKG_ignore[@]}"; do
|
||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"; printf '%s\n' "$name"
|
||||
done | sort -u > "$listed"
|
||||
|
||||
local baseline; baseline=$(mktemp)
|
||||
{
|
||||
@@ -155,32 +180,64 @@ step_packages() {
|
||||
cat /usr/share/omarchy/install/omarchy-base.packages 2>/dev/null
|
||||
} | grep -v '^$' | sort -u > "$baseline"
|
||||
|
||||
local extra; extra=$(mktemp)
|
||||
pacman -Qqe 2>/dev/null | sort -u | comm -23 - "$baseline" > "$extra"
|
||||
local untracked; untracked=$(pacman -Qqe 2>/dev/null | sort -u | comm -23 - "$baseline" | comm -23 - "$listed" | tr '\n' ' ')
|
||||
local missing=()
|
||||
for e in "${PKG_repo[@]}" "${PKG_aur[@]}"; do
|
||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"
|
||||
pkg_applies "$archs" || continue
|
||||
pacman -Qq "$name" &>/dev/null || pkg_cmd_present "$cmds" || missing+=("$name [$layer]")
|
||||
done
|
||||
|
||||
local untracked; untracked=$(comm -23 "$extra" "$listed" | grep -vxE 'base|base-devel|sudo|omarchy|mkinitcpio|linux-firmware|efibootmgr|devtools|cmake|ninja|wayvnc|sunshine|freerdp|remmina|omarchy-remote-desktop-backend|pipewire-debug' | tr '\n' ' ')
|
||||
local removed; removed=$(comm -13 "$extra" "$listed" | tr '\n' ' ')
|
||||
|
||||
[[ -n ${untracked// } ]] && warn "installed but not in packages.txt: $untracked" || skip "nothing installed that packages.txt misses"
|
||||
[[ -n ${removed// } ]] && warn "in packages.txt but not installed: $removed"
|
||||
if [[ -n ${untracked// } ]]; then
|
||||
warn "installed but in no layer's packages.txt: $untracked"
|
||||
say " add each to the layer it belongs to, or to $PROFILE/packages.txt [ignore]"
|
||||
else
|
||||
skip "nothing installed that the layers miss"
|
||||
fi
|
||||
(( ${#missing[@]} )) && warn "listed but not installed: ${missing[*]}"
|
||||
say " packages.txt keeps a 'why' comment per package, so edit it by hand."
|
||||
rm -f "$listed" "$baseline" "$extra"
|
||||
rm -f "$listed" "$baseline"
|
||||
}
|
||||
|
||||
main() {
|
||||
local want=()
|
||||
for a in "$@"; do
|
||||
local want=() profile="" force=0 a
|
||||
while (( $# )); do
|
||||
a=$1; shift
|
||||
case $a in
|
||||
--profile|-p) profile=${1:?--profile needs a name}; shift ;;
|
||||
--profile=*) profile=${a#*=} ;;
|
||||
--force) force=1 ;;
|
||||
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
|
||||
-h|--help) sed -n '2,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
-h|--help) awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
||||
-*) warn "unknown option: $a"; exit 2 ;;
|
||||
*) if [[ " ${STEPS[*]} " == *" $a "* ]]; then want+=("$a"); else warn "unknown step: $a"; exit 2; fi ;;
|
||||
esac
|
||||
done
|
||||
(( ${#want[@]} )) || want=("${STEPS[@]}")
|
||||
|
||||
say "${c_hd}capture${c_off} repo=$REPO host=$(hostname)"
|
||||
for s in "${want[@]}"; do "step_$s"; done
|
||||
local saved="" roles=""
|
||||
if def=$(default_profile); then IFS=$'\t' read -r saved roles <<<"$def"; fi
|
||||
[[ -n $profile ]] || profile=$saved
|
||||
[[ -n $profile ]] || { err "no --profile given and none matches host $(hostname)"; exit 2; }
|
||||
local rp; rp=$(find_layer "$profile") || { err "no such profile: $profile"; exit 2; }
|
||||
[[ $rp == "$saved" ]] || roles=""
|
||||
ROLES=(); [[ -n $roles ]] && IFS=', ' read -ra ROLES <<<"$roles"
|
||||
|
||||
# Before the first layered install, the live files still have the old shape
|
||||
# (e.g. bindings in bindings.lua rather than hypr/layers/), and capturing them
|
||||
# would write machine-specific content into shared layers like base/.
|
||||
if [[ ! -f $STATE_FILE ]] && (( ! force )); then
|
||||
err "no layered install recorded on this machine ($STATE_FILE)."
|
||||
say " Run ./install.sh first, or --force and review the diff of shared layers by hand."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
resolve "$rp" "${ROLES[@]}" || exit 2
|
||||
load_settings
|
||||
|
||||
say "${c_hd}capture${c_off} profile=$PROFILE${ROLES:+ roles=${ROLES[*]}} host=$(hostname)"
|
||||
# Canonical order whatever order they were named in: setup after config, etc.
|
||||
for s in "${STEPS[@]}"; do [[ " ${want[*]} " == *" $s "* ]] && "step_$s"; done
|
||||
|
||||
head_ "Done"
|
||||
say " Review with: git -C $REPO diff"
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Apply this repo's Omarchy customizations to the machine it runs on.
|
||||
# Apply an omarchy-setup profile to the machine it runs on.
|
||||
#
|
||||
# Safe to re-run: every step is a no-op when the target already matches, and any
|
||||
# file it would overwrite is backed up to <file>.bak.<epoch> first.
|
||||
#
|
||||
# ./install.sh # everything
|
||||
# ./install.sh # this machine's profile, every step
|
||||
# ./install.sh --profile hp-laptop # pick the profile
|
||||
# ./install.sh --roles devops,ai-agents # add roles on top of the profile
|
||||
# ./install.sh config themes # only the named steps
|
||||
# ./install.sh --list # show the steps
|
||||
# ./install.sh --dry-run # print what would change, touch nothing
|
||||
# ./install.sh --no-clobber # never overwrite a file that differs
|
||||
# ./install.sh --show # the layers, and which layer owns each file
|
||||
# ./install.sh --profiles | --list # list profiles | steps
|
||||
#
|
||||
# Steps: packages config themes plugins bin services system
|
||||
# Exits non-zero if any step reported an error (x).
|
||||
#
|
||||
# With no --profile it uses the one the last install recorded in
|
||||
# ~/.config/omarchy-setup/profile, else the machine whose hostnames= matches.
|
||||
#
|
||||
# Steps: packages config themes plugins bin handoff services system setup
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# Omarchy's commands (omarchy-theme-set, omarchy-pkg-add) are only on PATH in a
|
||||
# login session; an image build over SSH or a Coder start script has neither.
|
||||
OMARCHY_PATH=${OMARCHY_PATH:-/usr/share/omarchy}
|
||||
export OMARCHY_PATH
|
||||
case ":$PATH:" in *":$OMARCHY_PATH/bin:"*) ;; *) [[ -d $OMARCHY_PATH/bin ]] && PATH="$PATH:$OMARCHY_PATH/bin" ;; esac
|
||||
STAMP="$(date +%s)"
|
||||
DRY=0
|
||||
STEPS=(packages config themes plugins bin handoff services system)
|
||||
|
||||
# omarchy-handoff peer addresses to open the firewall for. The peer is dual-homed
|
||||
# (Wi-Fi + dock NIC) and the dock address disappears on undock, so list both.
|
||||
if [[ -z ${HANDOFF_PEER_IPS+x} ]]; then
|
||||
HANDOFF_PEER_IPS=(192.168.1.24 192.168.1.86)
|
||||
fi
|
||||
NO_CLOBBER=0
|
||||
FORCE=0
|
||||
STEPS=(packages config themes plugins bin handoff services system setup)
|
||||
|
||||
c_ok=$'\033[32m'; c_skip=$'\033[2m'; c_warn=$'\033[33m'; c_err=$'\033[31m'; c_hd=$'\033[1m'; c_off=$'\033[0m'
|
||||
[[ -t 1 ]] || { c_ok=; c_skip=; c_warn=; c_err=; c_hd=; c_off=; }
|
||||
@@ -32,10 +42,14 @@ head_() { printf '\n%s==> %s%s\n' "$c_hd" "$*" "$c_off"; }
|
||||
ok() { printf ' %s+%s %s\n' "$c_ok" "$c_off" "$*"; }
|
||||
skip() { printf ' %s= %s%s\n' "$c_skip" "$*" "$c_off"; }
|
||||
warn() { printf ' %s! %s%s\n' "$c_warn" "$*" "$c_off"; }
|
||||
err() { printf ' %sx %s%s\n' "$c_err" "$*" "$c_off"; }
|
||||
ERRORS=0
|
||||
err() { printf ' %sx %s%s\n' "$c_err" "$*" "$c_off"; ERRORS=$((ERRORS + 1)); }
|
||||
|
||||
run() { if (( DRY )); then printf ' %s? would: %s%s\n' "$c_skip" "$*" "$c_off"; else "$@"; fi; }
|
||||
|
||||
# shellcheck source=lib/layers.sh
|
||||
source "$REPO/lib/layers.sh"
|
||||
|
||||
# install_file <src> <dest> [mode] -- copy only when content differs, backing up first
|
||||
install_file() {
|
||||
local src=$1 dest=$2 mode=${3:-}
|
||||
@@ -44,6 +58,10 @@ install_file() {
|
||||
skip "$rel"
|
||||
return 0
|
||||
fi
|
||||
if [[ -e $dest ]] && (( NO_CLOBBER )); then
|
||||
warn "$rel differs from the profile - kept (--no-clobber)"
|
||||
return 0
|
||||
fi
|
||||
if (( DRY )); then
|
||||
printf ' %s? would write %s%s\n' "$c_skip" "$rel" "$c_off"
|
||||
return 0
|
||||
@@ -61,77 +79,110 @@ install_file() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- packages
|
||||
step_packages() {
|
||||
head_ "Packages"
|
||||
command -v omarchy >/dev/null || { err "omarchy not on PATH - is this an Omarchy system?"; return 1; }
|
||||
|
||||
local section="" repo=() aur=() optional=()
|
||||
while IFS= read -r line; do
|
||||
line=${line%%#*}; line=${line//[[:space:]]/}
|
||||
[[ -z $line ]] && continue
|
||||
case $line in
|
||||
'[repo]') section=repo; continue ;;
|
||||
'[aur]') section=aur; continue ;;
|
||||
'[optional]') section=optional; continue ;;
|
||||
esac
|
||||
case $section in
|
||||
repo) repo+=("$line") ;;
|
||||
aur) aur+=("$line") ;;
|
||||
optional) optional+=("$line") ;;
|
||||
esac
|
||||
done < "$REPO/packages.txt"
|
||||
|
||||
local missing=()
|
||||
for p in "${repo[@]}"; do pacman -Qq "$p" &>/dev/null || missing+=("$p"); done
|
||||
if (( ${#missing[@]} )); then
|
||||
say " installing from repos: ${missing[*]}"
|
||||
run omarchy pkg add "${missing[@]}" || err "some repo packages failed"
|
||||
else
|
||||
skip "all ${#repo[@]} repo packages present"
|
||||
# hypr_reload -- reload Hyprland if one is running, even from outside the
|
||||
# session (a Coder workspace-start script has no HYPRLAND_INSTANCE_SIGNATURE).
|
||||
hypr_reload() {
|
||||
(( DRY )) && return 0
|
||||
command -v hyprctl >/dev/null || return 0
|
||||
local sig=${HYPRLAND_INSTANCE_SIGNATURE:-}
|
||||
if [[ -z $sig ]]; then
|
||||
sig=$(hyprctl instances -j 2>/dev/null | jq -r '.[0].instance // empty' 2>/dev/null)
|
||||
fi
|
||||
|
||||
missing=()
|
||||
for p in "${aur[@]}"; do pacman -Qq "$p" &>/dev/null || missing+=("$p"); done
|
||||
if (( ${#missing[@]} )); then
|
||||
say " installing from AUR: ${missing[*]}"
|
||||
run omarchy pkg aur add "${missing[@]}" || err "some AUR packages failed"
|
||||
else
|
||||
skip "all ${#aur[@]} AUR packages present"
|
||||
fi
|
||||
|
||||
for p in "${optional[@]}"; do
|
||||
pacman -Qq "$p" &>/dev/null || warn "optional, not installed: $p (omarchy pkg add $p)"
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- config
|
||||
step_config() {
|
||||
head_ "Config files -> ~/.config"
|
||||
local src rel
|
||||
while IFS= read -r -d '' src; do
|
||||
rel=${src#"$REPO/config/"}
|
||||
install_file "$src" "$HOME/.config/$rel"
|
||||
done < <(find "$REPO/config" -type f -print0 | sort -z)
|
||||
|
||||
if (( ! DRY )) && command -v hyprctl >/dev/null && [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then
|
||||
hyprctl reload >/dev/null 2>&1
|
||||
local errs; errs=$(hyprctl configerrors 2>/dev/null)
|
||||
[[ -n $sig ]] || return 0
|
||||
HYPRLAND_INSTANCE_SIGNATURE=$sig hyprctl reload >/dev/null 2>&1
|
||||
local errs; errs=$(HYPRLAND_INSTANCE_SIGNATURE=$sig hyprctl configerrors 2>/dev/null)
|
||||
if [[ -n $errs && $errs != "no errors"* ]]; then
|
||||
err "Hyprland reported config errors:"; printf '%s\n' "$errs" | sed 's/^/ /'
|
||||
else
|
||||
ok "hyprctl reload - no config errors"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- packages
|
||||
step_packages() {
|
||||
head_ "Packages"
|
||||
command -v omarchy >/dev/null || { err "omarchy not on PATH - is this an Omarchy system?"; return 1; }
|
||||
load_packages
|
||||
|
||||
local e name archs cmds layer missing=() present=0 bycmd=() offarch=0
|
||||
for e in "${PKG_repo[@]}"; do
|
||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"
|
||||
if ! pkg_applies "$archs"; then (( offarch++ )); continue; fi
|
||||
if pacman -Qq "$name" &>/dev/null; then (( present++ ))
|
||||
elif pkg_cmd_present "$cmds"; then bycmd+=("$name")
|
||||
else missing+=("$name"); fi
|
||||
done
|
||||
if (( ${#missing[@]} )); then
|
||||
say " installing from repos: ${missing[*]}"
|
||||
run omarchy pkg add "${missing[@]}" || err "some repo packages failed"
|
||||
else
|
||||
skip "all $present repo packages present"
|
||||
fi
|
||||
(( ${#bycmd[@]} )) && skip "already on PATH from elsewhere, not installed: ${bycmd[*]}"
|
||||
(( offarch )) && skip "$offarch not built for $(uname -m), skipped"
|
||||
|
||||
missing=()
|
||||
for e in "${PKG_aur[@]}"; do
|
||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"
|
||||
pkg_applies "$archs" || continue
|
||||
pacman -Qq "$name" &>/dev/null || pkg_cmd_present "$cmds" || missing+=("$name")
|
||||
done
|
||||
if (( ${#missing[@]} )); then
|
||||
say " installing from AUR: ${missing[*]}"
|
||||
run omarchy pkg aur add "${missing[@]}" || err "some AUR packages failed"
|
||||
else
|
||||
skip "all ${#PKG_aur[@]} AUR packages present"
|
||||
fi
|
||||
|
||||
for e in "${PKG_optional[@]}"; do
|
||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"
|
||||
pkg_applies "$archs" || continue
|
||||
pacman -Qq "$name" &>/dev/null || warn "optional, not installed: $name (omarchy pkg add $name)"
|
||||
done
|
||||
|
||||
if (( ${#P_DOTFILES[@]} )) && [[ ! -d $HOME/.dotfiles ]]; then
|
||||
local -A u=(); local m; for m in "${P_DOTFILES[@]}"; do u[$m]=1; done
|
||||
say ""
|
||||
say " This profile's roles pair with dotfiles modules: ${!u[*]}"
|
||||
say " (git.nic-oconnor.com/public/dotfiles) - not run from here."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- config
|
||||
step_config() {
|
||||
head_ "Config files -> ~/.config"
|
||||
local rel layer
|
||||
declare -A mine=()
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
install_file "$PROFILES/$layer/config/$rel" "$HOME/.config/$rel"
|
||||
mine[$rel]=1
|
||||
done < <(owners config)
|
||||
|
||||
# A hypr/layers/ file that no layer of this profile provides is left over from
|
||||
# a role or profile that no longer applies. It still loads, so say so.
|
||||
local f
|
||||
for f in "$HOME"/.config/hypr/layers/*.lua; do
|
||||
[[ -e $f ]] || continue
|
||||
[[ -n ${mine[hypr/layers/${f##*/}]:-} ]] && continue
|
||||
warn "~/.config/hypr/layers/${f##*/} is from no layer of this profile - still loaded; rm it if unwanted"
|
||||
done
|
||||
|
||||
hypr_reload
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- themes
|
||||
step_themes() {
|
||||
head_ "Themes -> ~/.config/omarchy/themes"
|
||||
local t name dest
|
||||
for t in "$REPO"/themes/*/; do
|
||||
[[ -d $t ]] || continue
|
||||
name=$(basename -- "$t")
|
||||
local l t name dest
|
||||
declare -A theme=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
for t in "$PROFILES/$l"/themes/*/; do
|
||||
[[ -d $t ]] && theme[$(basename -- "$t")]=$t
|
||||
done
|
||||
done
|
||||
(( ${#theme[@]} )) || skip "no layer carries themes"
|
||||
for name in $(printf '%s\n' "${!theme[@]}" | sort); do
|
||||
t=${theme[$name]}
|
||||
dest="$HOME/.config/omarchy/themes/$name"
|
||||
if [[ -d $dest ]] && diff -rq "$t" "$dest" >/dev/null 2>&1; then
|
||||
skip "$name"
|
||||
@@ -141,77 +192,103 @@ step_themes() {
|
||||
ok "$name"
|
||||
fi
|
||||
done
|
||||
if (( ${#P_AETHER[@]} )); then
|
||||
say ""
|
||||
say " Themes managed by aether (not in this repo - reinstall with \`aether\`):"
|
||||
say " pink-gt3rs porsche-cayman porsche-rain"
|
||||
say " Apply one with: omarchy theme set <name>"
|
||||
say " ${P_AETHER[*]}"
|
||||
fi
|
||||
(( ${#theme[@]} )) && say " Apply one with: omarchy theme set <name>"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- plugins
|
||||
step_plugins() {
|
||||
head_ "Shell plugins -> ~/.config/omarchy/plugins"
|
||||
local dir="$HOME/.config/omarchy/plugins"
|
||||
local dir="$HOME/.config/omarchy/plugins" l
|
||||
declare -A url=() sha=()
|
||||
local id u s
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -f $PROFILES/$l/plugins.txt ]] || continue
|
||||
while IFS=$'\t' read -r id u s; do
|
||||
[[ -z ${id:-} || $id == \#* ]] && continue
|
||||
url[$id]=$u; sha[$id]=$s
|
||||
done < "$PROFILES/$l/plugins.txt"
|
||||
done
|
||||
if (( ! ${#url[@]} )); then skip "no layer carries plugins"; return 0; fi
|
||||
[[ -d $dir ]] || run mkdir -p "$dir"
|
||||
|
||||
local id url sha p
|
||||
while IFS=$'\t' read -r id url sha; do
|
||||
[[ -z ${id:-} || $id == \#* ]] && continue
|
||||
p="$dir/$id"
|
||||
local p
|
||||
for id in $(printf '%s\n' "${!url[@]}" | sort); do
|
||||
p="$dir/$id"; s=${sha[$id]}
|
||||
if [[ -d $p/.git ]]; then
|
||||
if [[ $(git -C "$p" rev-parse HEAD 2>/dev/null) == "$sha" ]]; then
|
||||
skip "$id @ ${sha:0:7}"
|
||||
if [[ $(git -C "$p" rev-parse HEAD 2>/dev/null) == "$s" ]]; then
|
||||
skip "$id @ ${s:0:7}"
|
||||
continue
|
||||
fi
|
||||
if [[ -n $(git -C "$p" status --porcelain 2>/dev/null) ]]; then
|
||||
warn "$id has local changes - leaving it alone (wanted ${sha:0:7})"
|
||||
warn "$id has local changes - leaving it alone (wanted ${s:0:7})"
|
||||
continue
|
||||
fi
|
||||
run git -C "$p" fetch --quiet origin
|
||||
run git -C "$p" checkout --quiet "$sha" && ok "$id -> ${sha:0:7}"
|
||||
run git -C "$p" checkout --quiet "$s" && ok "$id -> ${s:0:7}"
|
||||
else
|
||||
if (( DRY )); then
|
||||
printf ' %s? would clone %s -> %s%s\n' "$c_skip" "$id" "${sha:0:7}" "$c_off"
|
||||
printf ' %s? would clone %s -> %s%s\n' "$c_skip" "$id" "${s:0:7}" "$c_off"
|
||||
continue
|
||||
fi
|
||||
git clone --quiet "$url" "$p" || { err "clone failed: $id ($url)"; continue; }
|
||||
git -C "$p" checkout --quiet "$sha" || warn "$id: sha $sha not found, left on default branch"
|
||||
ok "$id @ ${sha:0:7}"
|
||||
git clone --quiet "${url[$id]}" "$p" || { err "clone failed: $id (${url[$id]})"; continue; }
|
||||
git -C "$p" checkout --quiet "$s" || warn "$id: sha $s not found, left on default branch"
|
||||
ok "$id @ ${s:0:7}"
|
||||
fi
|
||||
done < "$REPO/plugins.txt"
|
||||
done
|
||||
|
||||
apply_herdr_hud
|
||||
apply_worktree_patch io.github.tallsam.navbar-cat
|
||||
# Local plugin work that exists on no remote, per layer:
|
||||
# <id>.bundle a 'local' branch, replayed and checked out
|
||||
# <id>-worktree.patch uncommitted working-tree changes
|
||||
# <id>--bin--<file> an untracked file installed as <plugin>/bin/<file>
|
||||
local f
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -d $PROFILES/$l/plugin-patches ]] || continue
|
||||
for f in "$PROFILES/$l"/plugin-patches/*.bundle; do
|
||||
[[ -f $f ]] && apply_bundle "$(basename -- "$f" .bundle)" "$f"
|
||||
done
|
||||
for f in "$PROFILES/$l"/plugin-patches/*-worktree.patch; do
|
||||
[[ -f $f ]] || continue
|
||||
id=$(basename -- "$f"); apply_worktree_patch "${id%-worktree.patch}" "$f"
|
||||
done
|
||||
for f in "$PROFILES/$l"/plugin-patches/*--bin--*; do
|
||||
[[ -f $f ]] || continue
|
||||
id=$(basename -- "$f")
|
||||
[[ -d $dir/${id%%--bin--*}/.git ]] || continue
|
||||
install_file "$f" "$dir/${id%%--bin--*}/bin/${id##*--bin--}" 755
|
||||
done
|
||||
done
|
||||
|
||||
say ""
|
||||
say " Bar placement lives in config/omarchy/shell.json, already installed by the"
|
||||
say " config step. The shell hot-reloads it; \`omarchy restart shell\` if not."
|
||||
say " Bar placement lives in config/omarchy/shell.json, installed by the config"
|
||||
say " step. The shell hot-reloads it; \`omarchy restart shell\` if not."
|
||||
}
|
||||
|
||||
# indie.herdr-hud carries 10 commits that intentionally never went to the
|
||||
# upstream PR, plus an uncommitted working tree. Both are replayed here.
|
||||
apply_herdr_hud() {
|
||||
local p="$HOME/.config/omarchy/plugins/indie.herdr-hud"
|
||||
local bundle="$REPO/plugin-patches/indie.herdr-hud.bundle"
|
||||
[[ -d $p/.git && -f $bundle ]] || return 0
|
||||
|
||||
# apply_bundle <id> <bundle> -- replay a plugin's 'local' branch. indie.herdr-hud
|
||||
# carries commits that intentionally never went to the upstream PR.
|
||||
apply_bundle() {
|
||||
local id=$1 bundle=$2
|
||||
local p="$HOME/.config/omarchy/plugins/$id"
|
||||
[[ -d $p/.git ]] || return 0
|
||||
if git -C "$p" rev-parse --verify --quiet local >/dev/null; then
|
||||
skip "indie.herdr-hud: 'local' branch already present"
|
||||
skip "$id: 'local' branch already present"
|
||||
elif (( DRY )); then
|
||||
printf ' %s? would replay the herdr-hud local branch%s\n' "$c_skip" "$c_off"
|
||||
return 0
|
||||
printf ' %s? would replay the %s local branch%s\n' "$c_skip" "$id" "$c_off"
|
||||
else
|
||||
git -C "$p" fetch --quiet "$bundle" 'refs/heads/local:refs/heads/local' \
|
||||
&& { git -C "$p" checkout --quiet local && ok "indie.herdr-hud: replayed 'local' branch"; } \
|
||||
|| warn "indie.herdr-hud: could not replay the local branch from the bundle"
|
||||
&& { git -C "$p" checkout --quiet local && ok "$id: replayed 'local' branch"; } \
|
||||
|| warn "$id: could not replay the local branch from the bundle"
|
||||
fi
|
||||
apply_worktree_patch indie.herdr-hud
|
||||
install_file "$REPO/plugin-patches/indie.herdr-hud--bin--herdr-agents" "$p/bin/herdr-agents" 755
|
||||
}
|
||||
|
||||
apply_worktree_patch() {
|
||||
local id=$1
|
||||
local id=$1 patch=$2
|
||||
local p="$HOME/.config/omarchy/plugins/$id"
|
||||
local patch="$REPO/plugin-patches/$id-worktree.patch"
|
||||
[[ -d $p/.git && -s $patch ]] || return 0
|
||||
if git -C "$p" apply --reverse --check "$patch" &>/dev/null; then
|
||||
skip "$id: working-tree patch already applied"
|
||||
@@ -227,52 +304,74 @@ apply_worktree_patch() {
|
||||
# ---------------------------------------------------------------- bin
|
||||
step_bin() {
|
||||
head_ "Scripts -> ~/.local/bin"
|
||||
local f
|
||||
for f in "$REPO"/bin/*; do
|
||||
[[ -f $f ]] || continue
|
||||
install_file "$f" "$HOME/.local/bin/$(basename -- "$f")" 755
|
||||
done
|
||||
local rel layer n=0
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
install_file "$PROFILES/$layer/bin/$rel" "$HOME/.local/bin/$rel" 755
|
||||
(( n++ ))
|
||||
done < <(owners bin)
|
||||
(( n )) || { skip "no layer carries scripts"; return 0; }
|
||||
case ":$PATH:" in
|
||||
*":$HOME/.local/bin:"*) ;;
|
||||
*) warn "~/.local/bin is not on PATH" ;;
|
||||
esac
|
||||
[[ -n $(layers_with bin/fix-stuck-displays) ]] && {
|
||||
say ""
|
||||
say " fix-stuck-displays needs passwordless sudo for 'tee /sys/class/drm/*/status'."
|
||||
say " omarchy-handoff needs a peer and a paired fingerprint before it does anything:"
|
||||
say " omarchy-handoff peer <hostname> <ip> then omarchy-handoff doctor"
|
||||
say " Its config (~/.config/lan-mouse/) holds a private key and per-machine"
|
||||
say " fingerprints, so it is not in this repo by design."
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- services
|
||||
# install_unit <name.service> -- the unit plus its <name>.service.d/*.conf
|
||||
# drop-ins, from whichever layers own them. Sets UNITS_CHANGED.
|
||||
UNITS_CHANGED=0
|
||||
install_unit() {
|
||||
local name=$1 rel layer dest
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
[[ $rel == "$name" || $rel == "$name.d/"* ]] || continue
|
||||
dest="$HOME/.config/systemd/user/$rel"
|
||||
[[ -e $dest ]] && cmp -s "$PROFILES/$layer/systemd/user/$rel" "$dest" || UNITS_CHANGED=1
|
||||
install_file "$PROFILES/$layer/systemd/user/$rel" "$dest" 0644
|
||||
done < <(owners systemd/user)
|
||||
}
|
||||
|
||||
# enable_unit <name.service> -- systemctl --user enable, or, with no user
|
||||
# manager to talk to (an image build), the symlink enable would have made.
|
||||
enable_unit() {
|
||||
local name=$1 want
|
||||
if systemctl --user is-enabled --quiet "$name" 2>/dev/null; then
|
||||
skip "$name already enabled"; return 0
|
||||
fi
|
||||
if (( DRY )); then printf ' %s? would enable %s%s\n' "$c_skip" "$name" "$c_off"; return 0; fi
|
||||
if systemctl --user enable "$name" >/dev/null 2>&1; then ok "enabled $name"; return 0; fi
|
||||
want=$(sed -n 's/^WantedBy=//p' "$HOME/.config/systemd/user/$name" | head -1)
|
||||
[[ -n $want ]] || { err "could not enable $name"; return 1; }
|
||||
mkdir -p "$HOME/.config/systemd/user/$want.wants"
|
||||
ln -sf "../$name" "$HOME/.config/systemd/user/$want.wants/$name" && ok "enabled $name (symlink; no user manager)"
|
||||
}
|
||||
|
||||
step_services() {
|
||||
head_ "systemd user units"
|
||||
local f name changed=0
|
||||
for f in "$REPO"/systemd/user/*.service; do
|
||||
[[ -f $f ]] || continue
|
||||
name=$(basename -- "$f")
|
||||
local dest="$HOME/.config/systemd/user/$name"
|
||||
[[ -e $dest ]] && cmp -s "$f" "$dest" || changed=1
|
||||
install_file "$f" "$dest"
|
||||
done
|
||||
(( changed )) && run systemctl --user daemon-reload
|
||||
for f in "$REPO"/systemd/user/*.service; do
|
||||
[[ -f $f ]] || continue
|
||||
name=$(basename -- "$f")
|
||||
if systemctl --user is-enabled --quiet "$name" 2>/dev/null; then
|
||||
skip "$name already enabled"
|
||||
else
|
||||
run systemctl --user enable "$name" && ok "enabled $name"
|
||||
fi
|
||||
done
|
||||
local rel layer names=()
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
[[ $rel == *.service ]] && names+=("$rel")
|
||||
done < <(owners systemd/user)
|
||||
(( ${#names[@]} )) || { skip "no layer carries units"; return 0; }
|
||||
UNITS_CHANGED=0
|
||||
for rel in "${names[@]}"; do install_unit "$rel"; done
|
||||
(( UNITS_CHANGED )) && run systemctl --user daemon-reload 2>/dev/null
|
||||
for rel in "${names[@]}"; do enable_unit "$rel"; done
|
||||
say ""
|
||||
say " Both are WantedBy=graphical-session.target, so they start with Hyprland"
|
||||
say " Units are WantedBy=graphical-session.target, so they start with Hyprland"
|
||||
say " rather than now. 'systemctl --user start <unit>' to bring one up early."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- handoff
|
||||
step_handoff() {
|
||||
head_ "omarchy-handoff (lan-mouse)"
|
||||
local layer; layer=$(layers_with handoff | tail -1)
|
||||
[[ -n $layer ]] || { skip "not part of this profile"; return 0; }
|
||||
local hd="$PROFILES/$layer/handoff"
|
||||
|
||||
# Self-sufficient on purpose: `install.sh handoff` alone brings input sharing up
|
||||
# on an otherwise stock machine, without the packages/config/themes steps that
|
||||
@@ -280,8 +379,8 @@ step_handoff() {
|
||||
# the full install.sh afterwards does no extra work.
|
||||
# lan-mouse: the signed package, for the GUI/CLI and as the unpatched fallback.
|
||||
# rust: only to compile handoff/patch/, removable afterwards.
|
||||
# NOT python-evdev: packages.txt calls it a dependency, but omarchy-air runs
|
||||
# this whole setup without it installed, so it is not required for sharing.
|
||||
# NOT python-evdev: omarchy-air runs this whole setup without it installed, so
|
||||
# it is not required for sharing.
|
||||
local need=() p
|
||||
for p in lan-mouse rust; do
|
||||
pacman -Qq "$p" &>/dev/null || need+=("$p")
|
||||
@@ -293,11 +392,11 @@ step_handoff() {
|
||||
skip "lan-mouse, rust"
|
||||
fi
|
||||
|
||||
install_file "$REPO/bin/omarchy-handoff" "$HOME/.local/bin/omarchy-handoff" 0755
|
||||
install_file "$PROFILES/$layer/bin/omarchy-handoff" "$HOME/.local/bin/omarchy-handoff" 0755
|
||||
|
||||
# 1. The patched daemon. systemd/user/lan-mouse.service points at this path, so
|
||||
# it has to exist before the services step enables the unit. Per-architecture:
|
||||
# this laptop is x86_64 and omarchy-air is aarch64, so each builds its own.
|
||||
# 1. The patched daemon. lan-mouse.service points at this path, so it has to
|
||||
# exist before the unit is enabled. Per-architecture: the HP is x86_64 and
|
||||
# omarchy-air is aarch64, so each builds its own.
|
||||
if [[ -x $HOME/.local/bin/lan-mouse-patched ]]; then
|
||||
skip "~/.local/bin/lan-mouse-patched (delete it to force a rebuild)"
|
||||
elif ! command -v cargo >/dev/null; then
|
||||
@@ -306,27 +405,27 @@ step_handoff() {
|
||||
printf ' %s? would build ~/.local/bin/lan-mouse-patched from handoff/patch/%s\n' "$c_skip" "$c_off"
|
||||
else
|
||||
say " building patched daemon from handoff/patch/ (~1 min; see handoff/patch/REBUILD.md for why)"
|
||||
if "$REPO/handoff/patch/build.sh"; then ok "~/.local/bin/lan-mouse-patched"
|
||||
if "$hd/patch/build.sh"; then ok "~/.local/bin/lan-mouse-patched"
|
||||
else err "build failed - the unit will not start until this succeeds"; fi
|
||||
fi
|
||||
|
||||
# 2. The unit, which points at the binary built above.
|
||||
install_file "$REPO/systemd/user/lan-mouse.service" \
|
||||
"$HOME/.config/systemd/user/lan-mouse.service" 0644
|
||||
if (( ! DRY )); then
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user is-enabled --quiet lan-mouse.service 2>/dev/null \
|
||||
&& skip "lan-mouse.service already enabled" \
|
||||
|| { systemctl --user enable lan-mouse.service >/dev/null && ok "enabled lan-mouse.service"; }
|
||||
fi
|
||||
# 2. The unit (and this machine's drop-ins, e.g. the Air's pointer scale).
|
||||
UNITS_CHANGED=0
|
||||
install_unit lan-mouse.service
|
||||
(( UNITS_CHANGED )) && run systemctl --user daemon-reload 2>/dev/null
|
||||
enable_unit lan-mouse.service
|
||||
|
||||
# 3. The firewall. THE bug that cost a full day: ufw is default-deny inbound, so
|
||||
# the peer's DTLS handshake dies at the wire and presents EXACTLY like a bad
|
||||
# certificate. Authorization is receiver-side only (connect.rs sets
|
||||
# insecure_skip_verify), so the RECEIVER's firewall must allow 4242/udp or
|
||||
# that direction is silently dead.
|
||||
local ips=("${P_HANDOFF_IPS[@]}")
|
||||
[[ -n ${HANDOFF_PEER_IPS:-} ]] && read -ra ips <<<"$HANDOFF_PEER_IPS"
|
||||
if ! command -v ufw >/dev/null; then
|
||||
skip "ufw not installed - nothing to open"
|
||||
elif (( ! ${#ips[@]} )); then
|
||||
warn "no handoff_peer_ips in this profile - set them, or HANDOFF_PEER_IPS='a b'"
|
||||
else
|
||||
local ip rules=""
|
||||
# Both reading and changing the ruleset need root. If sudo can't run without
|
||||
@@ -334,7 +433,7 @@ step_handoff() {
|
||||
# instead of emitting raw sudo errors and half-applying.
|
||||
if (( ! DRY )) && ! sudo -n true 2>/dev/null && [[ ! -t 0 ]]; then
|
||||
warn "sudo needs a password and there is no terminal; run these yourself:"
|
||||
for ip in "${HANDOFF_PEER_IPS[@]}"; do
|
||||
for ip in "${ips[@]}"; do
|
||||
say " sudo ufw allow from $ip to any port 4242 proto udp comment 'lan-mouse peer $ip'"
|
||||
done
|
||||
say ""
|
||||
@@ -343,7 +442,7 @@ step_handoff() {
|
||||
return 0
|
||||
fi
|
||||
(( DRY )) || rules=$(sudo ufw status 2>/dev/null || true)
|
||||
for ip in "${HANDOFF_PEER_IPS[@]}"; do
|
||||
for ip in "${ips[@]}"; do
|
||||
if [[ $rules == *"4242"*"$ip"* ]]; then
|
||||
skip "ufw allows 4242/udp from $ip"
|
||||
elif (( DRY )); then
|
||||
@@ -359,7 +458,7 @@ step_handoff() {
|
||||
|
||||
say ""
|
||||
say " ~/.config/lan-mouse/ is NOT in this repo (private key + per-machine"
|
||||
say " fingerprints). Start from handoff/config/config.toml.example-* and:"
|
||||
say " fingerprints). Start from $layer/handoff/config/config.toml.example-* and:"
|
||||
say " omarchy-handoff fingerprint # give it to the peer, LOWERCASE"
|
||||
say " omarchy-handoff pair <peer> <lowercase-sha256>"
|
||||
say " omarchy-handoff doctor # must pass before arming"
|
||||
@@ -369,12 +468,12 @@ step_handoff() {
|
||||
|
||||
# ---------------------------------------------------------------- system
|
||||
step_system() {
|
||||
head_ "System drop-ins (needs sudo)"
|
||||
local src rel dest
|
||||
while IFS= read -r -d '' src; do
|
||||
rel=${src#"$REPO/system/"}
|
||||
head_ "System drop-ins -> /etc (needs sudo)"
|
||||
local rel layer dest n=0 sysctl=0 modprobe=0
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
(( n++ ))
|
||||
dest="/etc/$rel"
|
||||
if [[ -e $dest ]] && cmp -s "$src" "$dest"; then
|
||||
if [[ -e $dest ]] && cmp -s "$PROFILES/$layer/system/$rel" "$dest"; then
|
||||
skip "$dest"
|
||||
continue
|
||||
fi
|
||||
@@ -382,26 +481,84 @@ step_system() {
|
||||
printf ' %s? would write %s%s\n' "$c_skip" "$dest" "$c_off"
|
||||
continue
|
||||
fi
|
||||
sudo install -D -m 0644 "$src" "$dest" && ok "$dest"
|
||||
done < <(find "$REPO/system" -type f -print0 | sort -z)
|
||||
sudo install -D -m 0644 "$PROFILES/$layer/system/$rel" "$dest" && ok "$dest"
|
||||
[[ $rel == sysctl.d/* ]] && sysctl=1
|
||||
[[ $rel == modprobe.d/* ]] && modprobe=1
|
||||
done < <(owners system)
|
||||
(( n )) || { skip "no layer carries /etc drop-ins"; return 0; }
|
||||
|
||||
if (( ! DRY )); then
|
||||
sudo sysctl --system >/dev/null 2>&1 && ok "sysctl reloaded"
|
||||
(( sysctl )) && sudo sysctl --system >/dev/null 2>&1 && ok "sysctl reloaded"
|
||||
if (( modprobe )); then
|
||||
say ""
|
||||
say " hid_apple.conf (fnmode=2: F-keys act as F-keys) only takes effect once the"
|
||||
say " initramfs is rebuilt or the module reloads:"
|
||||
say " sudo mkinitcpio -P # or: sudo modprobe -r hid_apple && sudo modprobe hid_apple"
|
||||
say " modprobe.d options (hid_apple fnmode, appledrm notch) only take effect once"
|
||||
say " the initramfs is rebuilt or the module reloads:"
|
||||
say " sudo mkinitcpio -P # or: sudo modprobe -r <module> && sudo modprobe <module>"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- setup
|
||||
# Each layer's setup.sh, for state outside ~/.config (markers, gsettings, a
|
||||
# first-run theme). Run last, in layer order.
|
||||
step_setup() {
|
||||
head_ "Layer setup scripts"
|
||||
local l n=0
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -x $PROFILES/$l/setup.sh ]] || continue
|
||||
(( n++ ))
|
||||
say " $l/setup.sh"
|
||||
OMARCHY_SETUP_DRY=$DRY OMARCHY_SETUP_REPO=$REPO OMARCHY_SETUP_LAYER="$PROFILES/$l" \
|
||||
"$PROFILES/$l/setup.sh" || err "$l/setup.sh failed"
|
||||
done
|
||||
(( n )) || skip "no layer has a setup.sh"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- show
|
||||
show() {
|
||||
say "${c_hd}$PROFILE${c_off} $(layer_description "$PROFILE")"
|
||||
[[ -n $P_ARCH ]] && say " arch: $P_ARCH"
|
||||
say ""
|
||||
say "Layers, in order (later wins):"
|
||||
local l; for l in "${LAYERS[@]}"; do printf ' %-26s %s\n' "$l" "$(layer_description "$l")"; done
|
||||
local sub rel layer
|
||||
for sub in config bin systemd/user system; do
|
||||
local any=0
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
(( any )) || { say ""; say "$sub/"; any=1; }
|
||||
printf ' %-52s %s\n' "$rel" "$layer"
|
||||
done < <(owners "$sub")
|
||||
done
|
||||
}
|
||||
|
||||
list_profiles() {
|
||||
local kind d
|
||||
for kind in machines roles features; do
|
||||
say "${c_hd}$kind${c_off}"
|
||||
for d in "$PROFILES/$kind"/*/; do
|
||||
d=${d%/}; printf ' %-18s %s\n' "${d##*/}" "$(layer_description "$kind/${d##*/}")"
|
||||
done
|
||||
done
|
||||
say "${c_hd}shared${c_off}"
|
||||
for d in base personal personal-desktop; do printf ' %-18s %s\n' "$d" "$(layer_description "$d")"; done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- main
|
||||
main() {
|
||||
local want=()
|
||||
for a in "$@"; do
|
||||
local want=() profile="" roles_arg="" roles_given=0 show_only=0 a
|
||||
while (( $# )); do
|
||||
a=$1; shift
|
||||
case $a in
|
||||
--dry-run|-n) DRY=1 ;;
|
||||
--no-clobber) NO_CLOBBER=1 ;;
|
||||
--force) FORCE=1 ;;
|
||||
--profile|-p) profile=${1:?--profile needs a name}; shift ;;
|
||||
--profile=*) profile=${a#*=} ;;
|
||||
--roles|-r) roles_arg=${1-}; roles_given=1; shift ;;
|
||||
--roles=*) roles_arg=${a#*=}; roles_given=1 ;;
|
||||
--show) show_only=1 ;;
|
||||
--profiles) list_profiles; exit 0 ;;
|
||||
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
|
||||
-h|--help) sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
-h|--help) awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
||||
-*) err "unknown option: $a"; exit 2 ;;
|
||||
*)
|
||||
if [[ " ${STEPS[*]} " == *" $a "* ]]; then want+=("$a")
|
||||
@@ -410,15 +567,49 @@ main() {
|
||||
done
|
||||
(( ${#want[@]} )) || want=("${STEPS[@]}")
|
||||
|
||||
say "${c_hd}omarchy-setup${c_off} repo=$REPO"
|
||||
local saved_profile="" saved_roles=""
|
||||
if def=$(default_profile); then IFS=$'\t' read -r saved_profile saved_roles <<<"$def"; fi
|
||||
if [[ -z $profile ]]; then
|
||||
profile=$saved_profile
|
||||
[[ -n $profile ]] || { err "no --profile given and none matches host $(hostname)"; list_profiles; exit 2; }
|
||||
fi
|
||||
local rp; rp=$(find_layer "$profile") || { err "no such profile: $profile"; list_profiles; exit 2; }
|
||||
# No --roles reuses the roles last installed with this profile; --roles ""
|
||||
# means none (a Coder workspace with every extra role deselected).
|
||||
(( roles_given )) || [[ $rp != "$saved_profile" ]] || roles_arg=$saved_roles
|
||||
ROLES=()
|
||||
[[ -n $roles_arg ]] && IFS=', ' read -ra ROLES <<<"$roles_arg"
|
||||
|
||||
resolve "$rp" "${ROLES[@]}" || exit 2
|
||||
load_settings
|
||||
|
||||
if (( show_only )); then show; exit 0; fi
|
||||
|
||||
say "${c_hd}omarchy-setup${c_off} profile=$PROFILE${ROLES:+ roles=${ROLES[*]}} repo=$REPO"
|
||||
say " layers: ${LAYERS[*]}"
|
||||
(( DRY )) && warn "dry run - nothing will be changed"
|
||||
(( NO_CLOBBER )) && warn "--no-clobber: files that differ from the profile are kept"
|
||||
command -v omarchy >/dev/null || warn "omarchy not found; this looks like a non-Omarchy host"
|
||||
|
||||
for s in "${want[@]}"; do "step_$s"; done
|
||||
if [[ -n $P_ARCH && $P_ARCH != "$(uname -m)" ]]; then
|
||||
err "$PROFILE is for $P_ARCH, this machine is $(uname -m)"
|
||||
(( FORCE )) || { say " --force to apply it anyway"; exit 2; }
|
||||
fi
|
||||
local host; host=$(hostname 2>/dev/null || cat /etc/hostname)
|
||||
if (( ${#P_HOSTNAMES[@]} )) && [[ " ${P_HOSTNAMES[*]} " != *" $host "* ]]; then
|
||||
warn "$PROFILE is for ${P_HOSTNAMES[*]}, this host is $host"
|
||||
fi
|
||||
|
||||
# Canonical order whatever order they were named in: setup after config, etc.
|
||||
for s in "${STEPS[@]}"; do [[ " ${want[*]} " == *" $s "* ]] && "step_$s"; done
|
||||
|
||||
(( DRY )) || save_state
|
||||
|
||||
head_ "Done"
|
||||
say " Re-running is safe. Backups from this run are suffixed .bak.$STAMP"
|
||||
say " Pull live changes back into the repo with ./capture.sh"
|
||||
# Non-zero when any step reported an error, so an image build fails on it.
|
||||
if (( ERRORS )); then err "$ERRORS error(s) above"; exit 1; fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
# shellcheck shell=bash disable=SC1090
|
||||
# Profile resolution, shared by install.sh and capture.sh. Sourced, not run.
|
||||
#
|
||||
# A profile is a directory under profiles/ with a `profile` file (bash, sourced)
|
||||
# and any of: config/ (mirrors ~/.config), themes/, plugins.txt,
|
||||
# plugin-patches/, bin/, systemd/user/, system/ (mirrors /etc), handoff/,
|
||||
# packages.txt, setup.sh. `include=(...)` names the layers it builds on.
|
||||
#
|
||||
# Resolving a profile gives an ordered list of layers: each layer's includes
|
||||
# first (depth first, each layer once), then any --roles, then the profile
|
||||
# itself. For a path that several layers provide, the LAST layer wins, so a
|
||||
# machine always overrides the roles and shared layers under it.
|
||||
#
|
||||
# Expects REPO to be set, and err/warn helpers from the caller.
|
||||
|
||||
PROFILES=$REPO/profiles
|
||||
STATE_FILE=$HOME/.config/omarchy-setup/profile
|
||||
|
||||
LAYERS=() # resolved layer paths relative to profiles/, in order
|
||||
PROFILE="" # the profile asked for, e.g. machines/hp-laptop
|
||||
ROLES=() # extra roles asked for, e.g. devops ai-agents
|
||||
_resolving=()
|
||||
|
||||
# find_layer <name> -> layer path relative to profiles/. Accepts the full path
|
||||
# (machines/hp-laptop) or a bare name looked up in machines/, roles/, features/.
|
||||
find_layer() {
|
||||
local c
|
||||
for c in "$1" "machines/$1" "roles/$1" "features/$1"; do
|
||||
[[ -f $PROFILES/$c/profile ]] && { printf '%s\n' "$c"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
layer_includes() {
|
||||
( include=(); source "$PROFILES/$1/profile"; printf '%s\n' "${include[@]}" )
|
||||
}
|
||||
|
||||
layer_description() {
|
||||
( description=""; source "$PROFILES/$1/profile"; printf '%s\n' "$description" )
|
||||
}
|
||||
|
||||
_add_layer() {
|
||||
local rel l inc
|
||||
rel=$(find_layer "$1") || { err "no such layer: $1"; return 1; }
|
||||
for l in "${LAYERS[@]}"; do [[ $l == "$rel" ]] && return 0; done
|
||||
[[ " ${_resolving[*]} " == *" $rel "* ]] && { err "include cycle through $rel"; return 1; }
|
||||
_resolving+=("$rel")
|
||||
while IFS= read -r inc; do
|
||||
[[ -n $inc ]] || continue
|
||||
_add_layer "$inc" || return 1
|
||||
done < <(layer_includes "$rel")
|
||||
LAYERS+=("$rel")
|
||||
}
|
||||
|
||||
# resolve <profile> [role...] -> fills LAYERS
|
||||
resolve() {
|
||||
local rel inc r
|
||||
LAYERS=(); _resolving=()
|
||||
rel=$(find_layer "$1") || { err "no such profile: $1 (./install.sh --profiles lists them)"; return 1; }
|
||||
PROFILE=$rel
|
||||
_resolving+=("$rel")
|
||||
while IFS= read -r inc; do
|
||||
[[ -n $inc ]] || continue
|
||||
_add_layer "$inc" || return 1
|
||||
done < <(layer_includes "$rel")
|
||||
for r in "${@:2}"; do _add_layer "roles/${r#roles/}" || return 1; done
|
||||
LAYERS+=("$rel")
|
||||
}
|
||||
|
||||
# Settings from the layers' profile files. Arrays accumulate across layers;
|
||||
# handoff_peer_ips and arch come from the last layer that sets them.
|
||||
P_ARCH=""; P_HOSTNAMES=(); P_HANDOFF_IPS=(); P_DOTFILES=(); P_AETHER=()
|
||||
load_settings() {
|
||||
local l
|
||||
P_ARCH=""; P_HOSTNAMES=(); P_HANDOFF_IPS=(); P_DOTFILES=(); P_AETHER=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
local arch="" hostnames=() handoff_peer_ips=() dotfiles=() aether_themes=() include=() description=""
|
||||
source "$PROFILES/$l/profile"
|
||||
[[ -n $arch ]] && P_ARCH=$arch
|
||||
(( ${#hostnames[@]} )) && P_HOSTNAMES=("${hostnames[@]}")
|
||||
(( ${#handoff_peer_ips[@]} )) && P_HANDOFF_IPS=("${handoff_peer_ips[@]}")
|
||||
P_DOTFILES+=("${dotfiles[@]}")
|
||||
P_AETHER+=("${aether_themes[@]}")
|
||||
done
|
||||
}
|
||||
|
||||
# Which profile to use when none is given: the one recorded by the last
|
||||
# install on this machine, else the machine profile whose hostnames= matches.
|
||||
# Prints "profile<TAB>role,role".
|
||||
default_profile() {
|
||||
if [[ -f $STATE_FILE ]]; then
|
||||
local profile="" roles=""
|
||||
source "$STATE_FILE"
|
||||
[[ -n $profile ]] && { printf '%s\t%s\n' "$profile" "$roles"; return 0; }
|
||||
fi
|
||||
local f host; host=$(hostname 2>/dev/null || cat /etc/hostname)
|
||||
for f in "$PROFILES"/machines/*/profile; do
|
||||
if ( hostnames=(); source "$f"; [[ " ${hostnames[*]} " == *" $host "* ]] ); then
|
||||
f=${f%/profile}; printf '%s\t\n' "${f#"$PROFILES/"}"; return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
save_state() {
|
||||
mkdir -p "${STATE_FILE%/*}"
|
||||
local IFS=,
|
||||
printf '# Written by omarchy-setup/install.sh; read when no --profile is given.\nprofile=%q\nroles=%q\n' \
|
||||
"$PROFILE" "${ROLES[*]}" > "$STATE_FILE"
|
||||
}
|
||||
|
||||
# owners <subdir> -> "relpath<TAB>layer" for every file under <layer>/<subdir>
|
||||
# across the layers, the last layer winning each relpath. Sorted by relpath.
|
||||
owners() {
|
||||
local sub=$1 l f
|
||||
declare -A own=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -d $PROFILES/$l/$sub ]] || continue
|
||||
while IFS= read -r -d '' f; do
|
||||
own[${f#"$PROFILES/$l/$sub/"}]=$l
|
||||
done < <(find "$PROFILES/$l/$sub" -type f -print0)
|
||||
done
|
||||
for f in "${!own[@]}"; do printf '%s\t%s\n' "$f" "${own[$f]}"; done | sort
|
||||
}
|
||||
|
||||
# Every layer that has <path> (a file or directory), in order.
|
||||
layers_with() {
|
||||
local l
|
||||
for l in "${LAYERS[@]}"; do [[ -e $PROFILES/$l/$1 ]] && printf '%s\n' "$l"; done
|
||||
}
|
||||
|
||||
# Parse every layer's packages.txt into PKG_<section> arrays of
|
||||
# "name<US>archs<US>cmds<US>layer", US being $PKG_FS. Not a tab: tab is IFS
|
||||
# whitespace, so `read` would collapse an empty archs field and shift the rest.
|
||||
# [ignore] lines may hold several names.
|
||||
PKG_FS=$'\x1f'
|
||||
PKG_repo=(); PKG_aur=(); PKG_optional=(); PKG_ignore=()
|
||||
load_packages() {
|
||||
local l line section tok name archs cmds
|
||||
PKG_repo=(); PKG_aur=(); PKG_optional=(); PKG_ignore=()
|
||||
declare -A seen=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -f $PROFILES/$l/packages.txt ]] || continue
|
||||
section=""
|
||||
while IFS= read -r line; do
|
||||
line=${line%%#*}
|
||||
read -ra tok <<<"$line"
|
||||
(( ${#tok[@]} )) || continue
|
||||
case ${tok[0]} in
|
||||
'[repo]'|'[aur]'|'[optional]'|'[ignore]') section=${tok[0]//[\[\]]/}; continue ;;
|
||||
esac
|
||||
[[ -n $section ]] || continue
|
||||
if [[ $section == ignore ]]; then
|
||||
for name in "${tok[@]}"; do PKG_ignore+=("$name$PKG_FS$PKG_FS$PKG_FS$l"); done
|
||||
continue
|
||||
fi
|
||||
name=${tok[0]}; archs=""; cmds=""
|
||||
for t in "${tok[@]:1}"; do
|
||||
case $t in
|
||||
cmd=*) cmds=${t#cmd=} ;;
|
||||
x86_64|aarch64) archs+="$t," ;;
|
||||
*) warn "$l/packages.txt: unknown token '$t' on $name" ;;
|
||||
esac
|
||||
done
|
||||
[[ -n ${seen[$name]:-} ]] && continue
|
||||
seen[$name]=1
|
||||
local -n arr="PKG_$section"
|
||||
arr+=("$name$PKG_FS${archs%,}$PKG_FS$cmds$PKG_FS$l")
|
||||
unset -n arr
|
||||
done < "$PROFILES/$l/packages.txt"
|
||||
done
|
||||
}
|
||||
|
||||
# pkg_applies <archs> -> true when this machine's arch is in the list (or no list)
|
||||
pkg_applies() { [[ -z $1 || ",$1," == *",$(uname -m),"* ]]; }
|
||||
|
||||
# pkg_cmd_present <cmds> -> true when any of the comma-separated commands exists
|
||||
pkg_cmd_present() {
|
||||
local c
|
||||
[[ -n $1 ]] || return 1
|
||||
for c in ${1//,/ }; do command -v "$c" >/dev/null 2>&1 && return 0; done
|
||||
return 1
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
# Packages this laptop has beyond the Omarchy 4.x baseline.
|
||||
#
|
||||
# Computed as: `pacman -Qqe` minus the omarchy meta-package's dependencies and
|
||||
# install/omarchy-base.packages. Lines are "name # why". Blank lines and
|
||||
# comments are ignored. Section markers drive install.sh --packages:
|
||||
# [repo] official repos + the omarchy repo -> omarchy pkg add
|
||||
# [aur] AUR only -> omarchy pkg aur add
|
||||
# [optional] installed here, not required for the desktop to work
|
||||
#
|
||||
# Deliberately NOT listed: base, base-devel, sudo, mkinitcpio, linux-firmware,
|
||||
# efibootmgr, devtools, cmake, ninja (toolchain/base, present on any Omarchy
|
||||
# install or pulled in as a dependency), and the remote-access stack
|
||||
# (wayvnc, sunshine, freerdp, remmina, omarchy-remote-desktop-backend,
|
||||
# pipewire-debug) which is out of this repo's scope.
|
||||
|
||||
[repo]
|
||||
# --- omarchy-handoff: builds the patched lan-mouse daemon (handoff/patch/) ---
|
||||
rust # only to compile handoff/patch/; ~250MiB installed, removable after
|
||||
|
||||
# --- hardware: this laptop is Intel + an Anker USB-C dock ---
|
||||
linux-omarchy # kernel this install actually boots
|
||||
linux-omarchy-headers # needed to build out-of-tree modules against it
|
||||
intel-ucode # CPU microcode
|
||||
vulkan-intel # Intel Vulkan driver
|
||||
lib32-vulkan-intel # 32-bit Vulkan, for Steam/Proton
|
||||
sof-firmware # Sound Open Firmware - required for this laptop's audio
|
||||
thermald # Intel thermal daemon; without it the CPU throttles hard
|
||||
zram-generator # compressed swap in RAM
|
||||
|
||||
# --- audio: full PipeWire stack with the Pulse/JACK/ALSA shims ---
|
||||
pipewire-pulse
|
||||
pipewire-alsa
|
||||
pipewire-jack
|
||||
libpulse
|
||||
gst-plugin-pipewire
|
||||
|
||||
# --- desktop / input ---
|
||||
lan-mouse # keyboard+mouse sharing driven by bin/omarchy-handoff
|
||||
python-evdev # lan-mouse / input tooling dependency
|
||||
wayland-utils # wayland-info, used when debugging outputs
|
||||
ghostty # terminal (alongside Omarchy's default)
|
||||
qmltermwidget # embedded terminal widget; indie.herdr-hud needs it
|
||||
qt6-connectivity # Bluetooth/QML bits for the omapods plugin
|
||||
qt6-tools # qmllint etc. when editing plugin QML
|
||||
openbsd-netcat # nc for kb2uka.port-doctor checks
|
||||
|
||||
# --- apps ---
|
||||
1password
|
||||
1password-cli # `op`, which the dotfiles repo's secrets module uses
|
||||
visual-studio-code-bin
|
||||
bun-bin
|
||||
|
||||
[aur]
|
||||
hyprmoncfg-bin # display profile daemon; pairs with crmne.hyprmoncfg plugin
|
||||
netbird-bin # NetBird client for the covert.vpn private network
|
||||
voxtype-bin # push-to-talk voice-to-text (voxtype.service)
|
||||
|
||||
[optional]
|
||||
steam # games; pulls a large 32-bit dependency tree
|
||||
@@ -1,48 +0,0 @@
|
||||
diff --git i/Cat.qml w/Cat.qml
|
||||
index 58ed7f6..f1bbe9b 100644
|
||||
--- i/Cat.qml
|
||||
+++ w/Cat.qml
|
||||
@@ -30,13 +30,18 @@ Item {
|
||||
property string omarchyPath: ""
|
||||
|
||||
readonly property string pluginId: manifest && manifest.id ? String(manifest.id) : "io.github.tallsam.navbar-cat"
|
||||
+ // The shell strips `__sourceDir` from third-party manifests, so fall back to
|
||||
+ // this file's own directory.
|
||||
+ readonly property string pluginDir: manifest && manifest.__sourceDir
|
||||
+ ? String(manifest.__sourceDir)
|
||||
+ : String(Qt.resolvedUrl(".")).replace(/^file:\/\//, "").replace(/\/$/, "")
|
||||
readonly property string assetDir: {
|
||||
- var dir = manifest && manifest.__sourceDir ? String(manifest.__sourceDir) : ""
|
||||
+ var dir = pluginDir
|
||||
if (dir === "") return ""
|
||||
return (dir.indexOf("file://") === 0 ? dir : "file://" + dir) + "/assets"
|
||||
}
|
||||
readonly property string helper: {
|
||||
- var dir = manifest && manifest.__sourceDir ? String(manifest.__sourceDir) : ""
|
||||
+ var dir = pluginDir
|
||||
if (dir === "") return ""
|
||||
return dir.replace(/^file:\/\//, "") + "/bin/navbar-cat-cursor"
|
||||
}
|
||||
@@ -52,8 +57,21 @@ Item {
|
||||
|
||||
// Settings live inline on this plugin's entry in shell.json's `plugins[]`,
|
||||
// per the shell's storage rules.
|
||||
+ // Third-party plugins get a scoped `shell` without `shellConfig`, so read
|
||||
+ // shell.json directly when the shell does not hand it over.
|
||||
+ FileView {
|
||||
+ id: shellJson
|
||||
+ path: Quickshell.env("HOME") + "/.config/omarchy/shell.json"
|
||||
+ watchChanges: true
|
||||
+ onFileChanged: reload()
|
||||
+ }
|
||||
+ readonly property var fileConfig: {
|
||||
+ try { return JSON.parse(shellJson.text()) } catch (e) { return null }
|
||||
+ }
|
||||
+
|
||||
readonly property var config: {
|
||||
- var settings = shell && shell.shellConfig ? shell.shellConfig.plugins : null
|
||||
+ var source = shell && shell.shellConfig ? shell.shellConfig : root.fileConfig
|
||||
+ var settings = source ? source.plugins : null
|
||||
if (Array.isArray(settings)) {
|
||||
for (var i = 0; i < settings.length; i++) {
|
||||
if (settings[i] && String(settings[i].id || "") === root.pluginId) return settings[i]
|
||||
@@ -28,16 +28,6 @@
|
||||
-- o.bind("SUPER + H", nil, "voxtype record toggle")
|
||||
-- o.bind("SUPER + PERIOD", nil, "omarchy-shell shell toggle omarchy.emojis")
|
||||
|
||||
-- Open Claude instead of the stock ChatGPT webapp.
|
||||
hl.unbind("SUPER + SHIFT + A")
|
||||
o.bind("SUPER + SHIFT + A", "Claude", { webapp = "https://claude.ai" })
|
||||
|
||||
-- Herdr HUD overlay (indie.herdr-hud native plugin, local branch).
|
||||
o.bind("SUPER + H", "Toggle Herdr HUD", "omarchy-shell shell toggle indie.herdr-hud '{}'")
|
||||
o.bind("SUPER + ALT + H", "Show or hide Herdr HUD button", "omarchy-shell shell call indie.herdr-hud toggleVisibility '{}'")
|
||||
o.bind("SUPER + CTRL + DOWN", "Herdr HUD: next agent", "omarchy-shell shell call indie.herdr-hud nextAgent '{}'")
|
||||
o.bind("SUPER + CTRL + UP", "Herdr HUD: previous agent", "omarchy-shell shell call indie.herdr-hud previousAgent '{}'")
|
||||
|
||||
-- Handoff: share this keyboard/mouse with the other Omarchy laptop.
|
||||
-- SUPER+K is already "Keybindings", so this takes SUPER+SHIFT+K.
|
||||
o.bind("SUPER + SHIFT + K", "Toggle Handoff (keyboard/mouse sharing)", "omarchy-handoff toggle")
|
||||
-- omarchy-setup keeps this file at stock on purpose. Profile layers add their
|
||||
-- bindings as hypr/layers/<NN>-<layer>.lua, so a binding added here AND by a
|
||||
-- layer would fire twice. Put machine-only bindings in that machine's layer.
|
||||
@@ -22,6 +22,12 @@ require("hypr.bindings")
|
||||
require("hypr.looknfeel")
|
||||
require("hypr.autostart")
|
||||
|
||||
-- omarchy-setup layers: every profile layer (role, machine, feature) puts its
|
||||
-- Hyprland settings in its own file under hypr/layers/, so layers never fight
|
||||
-- over bindings.lua or looknfeel.lua. Loaded in name order, after the files
|
||||
-- above; reload=true makes `hyprctl reload` re-run them.
|
||||
require("default.hypr.require_all").files(os.getenv("HOME") .. "/.config/hypr/layers", "hypr.layers", { reload = true })
|
||||
|
||||
-- Toggle config flags dynamically.
|
||||
require("default.hypr.toggles")
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Every machine, including the Coder desktops. Keeps hyprland.lua and
|
||||
# bindings.lua at stock plus the hypr/layers/ loader, so every other layer adds
|
||||
# Hyprland settings as its own file instead of editing a shared one.
|
||||
description="Shared by every machine: the hypr/layers/ loader"
|
||||
@@ -0,0 +1,4 @@
|
||||
-- handoff feature: share this keyboard/mouse with the other Omarchy laptop.
|
||||
-- SUPER+K is already Omarchy's "Keybindings", so this takes SUPER+SHIFT+K.
|
||||
-- Same chord on both laptops so muscle memory carries across.
|
||||
o.bind("SUPER + SHIFT + K", "Toggle Handoff (keyboard/mouse sharing)", "omarchy-handoff toggle")
|
||||
@@ -0,0 +1,4 @@
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
lan-mouse # keyboard+mouse sharing driven by bin/omarchy-handoff; also the unpatched fallback
|
||||
rust # only to compile handoff/patch/; ~250MiB installed, removable after
|
||||
@@ -0,0 +1,4 @@
|
||||
# omarchy-handoff: lan-mouse keyboard/mouse sharing between the two laptops.
|
||||
# Machines that include this set handoff_peer_ips to the OTHER laptop's
|
||||
# addresses; install.sh's handoff step opens 4242/udp in ufw for exactly those.
|
||||
description="Keyboard/mouse sharing between the laptops (lan-mouse, patched)"
|
||||
@@ -19,11 +19,9 @@ Environment=LAN_MOUSE_LOG_LEVEL=info
|
||||
# Falls back to nothing: if the build is missing the unit fails loudly rather
|
||||
# than silently running the unpatched binary with the wrong pointer feel.
|
||||
ExecStart=%h/.local/bin/lan-mouse-patched daemon
|
||||
# Multiplier for the residual logical-ppi difference between the two screens
|
||||
# (default 1.0 = upstream). omarchy-air is 140.1 logical ppi at scale 1.6 vs
|
||||
# ~95.8 for the 23" LG, so 0.7 makes motion cover the same PHYSICAL distance.
|
||||
# Per-machine: adjust or drop this line on the other host.
|
||||
Environment=LAN_MOUSE_POINTER_SCALE=0.7
|
||||
# LAN_MOUSE_POINTER_SCALE (default 1.0) is per-machine, so it is not set here:
|
||||
# a machine that needs it ships lan-mouse.service.d/pointer-scale.conf
|
||||
# (see machines/macbook-air-m2).
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
# Output goes to the journal on purpose. The previous autostart.lua path sent
|
||||
@@ -0,0 +1,5 @@
|
||||
-- coder-desktop: a streamed VM with no GPU. vga=virtio without virgl, so
|
||||
-- Hyprland draws on the CPU (llvmpipe) and every frame is a full-screen copy
|
||||
-- into the scanout buffer. Anything that animates keeps the CPU busy, so no
|
||||
-- window animations.
|
||||
hl.config({ animations = { enabled = false } })
|
||||
@@ -0,0 +1,20 @@
|
||||
-- See https://wiki.hypr.land/Configuring/Basics/Monitors/
|
||||
-- List current monitors and supported resolutions with: hyprctl monitors all
|
||||
|
||||
local omarchy_gdk_scale = 1
|
||||
local omarchy_monitor_scale = "auto"
|
||||
|
||||
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
|
||||
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale })
|
||||
|
||||
-- Configure a specific monitor.
|
||||
-- hl.monitor({ output = "DP-2", mode = "2560x1440@144", position = "0x0", scale = 1 })
|
||||
|
||||
-- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°).
|
||||
-- hl.monitor({ output = "DP-2", mode = "preferred", position = "auto", scale = 1, transform = 1 })
|
||||
|
||||
-- Streamed desktop: the VM's virtio display, fixed to the stream size.
|
||||
-- MUST match --manual-width/--manual-height in the image's selkies.service
|
||||
-- (covert-cloud: platform/workspaces/omarchy-desktop/image/systemd/user/), and
|
||||
-- the image build fails if this line is missing.
|
||||
hl.monitor({ output = "Virtual-1", mode = "1920x1080@60", position = "0x0", scale = 1 })
|
||||
@@ -0,0 +1,15 @@
|
||||
# Coder desktops: Omarchy in a Proxmox VM, streamed by Selkies, reached only
|
||||
# through Coder. Built by covert-cloud's platform/workspaces/omarchy-desktop/
|
||||
# image, which runs `install.sh --profile coder-desktop` at a pinned commit.
|
||||
#
|
||||
# This layer is the desktop's Omarchy look and behaviour. The image keeps the
|
||||
# plumbing: Selkies, the Coder agent, the Super key stand-in, the start page,
|
||||
# autologin and the lockdown (no SSH, no file transfer, locked password).
|
||||
#
|
||||
# Extra roles come from the Coder template's "Roles" parameter, applied at
|
||||
# every workspace start with --roles and --no-clobber. Nothing personal
|
||||
# (personal/, personal-desktop/, handoff) belongs here: other people use these.
|
||||
description="Coder desktop VM (streamed by Selkies): no GPU, fixed 1920x1080"
|
||||
include=(base roles/developer)
|
||||
hostnames=(omarchy-desktop)
|
||||
arch=x86_64
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# coder-desktop setup, run by install.sh's `setup` step after the files are in
|
||||
# place. Everything here is state outside ~/.config. Idempotent; honours
|
||||
# OMARCHY_SETUP_DRY=1.
|
||||
set -uo pipefail
|
||||
|
||||
run() { if [[ ${OMARCHY_SETUP_DRY:-0} == 1 ]]; then echo " ? would: $*"; else "$@"; fi; }
|
||||
state=$HOME/.local/state/omarchy-setup
|
||||
export OMARCHY_PATH=${OMARCHY_PATH:-/usr/share/omarchy}
|
||||
|
||||
# No idle lock or screensaver: Omarchy's stay-awake mode. The desktop user has
|
||||
# no password (the image locks it), so a locked screen could never be unlocked,
|
||||
# and an idle lock would darken the stream. The image build checks for this.
|
||||
marker=$HOME/.local/state/omarchy/indicators/stay-awake
|
||||
if [[ -e $marker ]]; then
|
||||
echo " = stay-awake"
|
||||
else
|
||||
run mkdir -p "${marker%/*}" && run touch "$marker" && echo " + stay-awake"
|
||||
fi
|
||||
|
||||
# Reduced motion, so Chromium reports prefers-reduced-motion and pages that
|
||||
# honour it stop animating (see layers/80-coder-desktop.lua for why). No
|
||||
# session during the image build, hence dbus-run-session.
|
||||
if command -v gsettings >/dev/null; then
|
||||
gs=(gsettings); [[ -n ${DBUS_SESSION_BUS_ADDRESS:-} ]] || gs=(dbus-run-session -- gsettings)
|
||||
if [[ $("${gs[@]}" get org.gnome.desktop.interface enable-animations 2>/dev/null) == false ]]; then
|
||||
echo " = gtk animations off"
|
||||
else
|
||||
run "${gs[@]}" set org.gnome.desktop.interface enable-animations false && echo " + gtk animations off"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Default theme: Omarchy's Retro 82. Only on the first apply, because install.sh
|
||||
# also runs at every workspace start (for the roles parameter) and must not undo
|
||||
# a theme the developer picked. The image also installs the Covert Cloud themes
|
||||
# for anyone who chooses them.
|
||||
if [[ -e $state/coder-desktop-theme ]]; then
|
||||
echo " = default theme already set once (left alone)"
|
||||
else
|
||||
run env OMARCHY_THEME_HEADLESS=1 "$OMARCHY_PATH/bin/omarchy-theme-set" "Retro 82" \
|
||||
&& run mkdir -p "$state" && run touch "$state/coder-desktop-theme" && echo " + theme: Retro 82"
|
||||
fi
|
||||
@@ -34,7 +34,7 @@ fi
|
||||
systemctl --user stop hyprmoncfgd
|
||||
|
||||
for d in "${stuck[@]}"; do
|
||||
out=${d##*/card*-}
|
||||
out=${d##*/}; out=${out#card*-}
|
||||
log "releasing $out"
|
||||
echo on | sudo -n tee "$d/status" >/dev/null
|
||||
wait_for 5 sh -c "hyprctl monitors all | grep -q '^Monitor $out '" || log "$out never appeared in Hyprland"
|
||||
@@ -11,7 +11,7 @@ hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 1.2
|
||||
-- displays unrecoverable before.
|
||||
|
||||
local omarchy_gdk_scale = 1
|
||||
local omarchy_monitor_scale = 1.25
|
||||
local omarchy_monitor_scale = 1
|
||||
|
||||
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# HP laptop (omarchy-laptop) packages beyond the Omarchy 4.x baseline and the
|
||||
# packages its other layers already list.
|
||||
#
|
||||
# Format (every layer's packages.txt): "name [arch...] [cmd=command] # why".
|
||||
# arch only install on these architectures (x86_64, aarch64)
|
||||
# cmd=X skip when command X is already on PATH (e.g. a dotfiles shim)
|
||||
# Sections:
|
||||
# [repo] official repos + the omarchy repo -> omarchy pkg add
|
||||
# [aur] AUR only -> omarchy pkg aur add
|
||||
# [optional] installed here, not required for the desktop to work
|
||||
# [ignore] installed here on purpose but not managed (capture.sh stays quiet)
|
||||
|
||||
[repo]
|
||||
# --- hardware: Intel + an Anker USB-C dock ---
|
||||
linux-omarchy # kernel this install actually boots
|
||||
linux-omarchy-headers # needed to build out-of-tree modules against it
|
||||
intel-ucode # CPU microcode
|
||||
vulkan-intel # Intel Vulkan driver
|
||||
lib32-vulkan-intel # 32-bit Vulkan, for Steam/Proton
|
||||
sof-firmware # Sound Open Firmware - required for this laptop's audio
|
||||
thermald # Intel thermal daemon; without it the CPU throttles hard
|
||||
zram-generator # compressed swap in RAM
|
||||
|
||||
# --- audio: full PipeWire stack with the Pulse/JACK/ALSA shims ---
|
||||
pipewire-pulse
|
||||
pipewire-alsa
|
||||
pipewire-jack
|
||||
libpulse
|
||||
gst-plugin-pipewire
|
||||
|
||||
# --- desktop / input ---
|
||||
python-evdev # input tooling used while debugging lan-mouse (not required by it)
|
||||
wayland-utils # wayland-info, used when debugging outputs
|
||||
ghostty # terminal (alongside Omarchy's default)
|
||||
|
||||
# --- apps ---
|
||||
1password
|
||||
1password-cli # `op`, which the dotfiles repo's secrets module uses
|
||||
bun-bin
|
||||
|
||||
[optional]
|
||||
steam # games; pulls a large 32-bit dependency tree
|
||||
|
||||
[ignore]
|
||||
# toolchain/base, present on any Omarchy install or pulled in as a dependency
|
||||
base base-devel sudo omarchy mkinitcpio linux-firmware efibootmgr devtools cmake ninja
|
||||
# the remote-access stack, out of this repo's scope
|
||||
wayvnc sunshine freerdp remmina omarchy-remote-desktop-backend pipewire-debug
|
||||
@@ -0,0 +1,8 @@
|
||||
# HP laptop (omarchy-laptop): Intel, an Anker USB-C dock, the LG IPS236 on HDMI
|
||||
# and the LG TV through the dock. The machine this repo was first captured from.
|
||||
description="HP laptop (omarchy-laptop): Intel, Anker dock, LG displays"
|
||||
include=(base personal personal-desktop features/handoff roles/developer roles/devops roles/ai-agents)
|
||||
hostnames=(omarchy-laptop)
|
||||
arch=x86_64
|
||||
# The peer for omarchy-handoff is the Air (Wi-Fi only).
|
||||
handoff_peer_ips=(192.168.1.49)
|
||||
@@ -0,0 +1,96 @@
|
||||
onboarding = false
|
||||
# Mirrors the Omarchy tmux config in config/tmux/tmux.conf
|
||||
# tmux session -> herdr workspace, tmux window -> herdr tab, tmux pane -> herdr pane
|
||||
|
||||
[theme]
|
||||
# tmux ran on the terminal's own palette (bg=default, fg=default, ANSI blue accents)
|
||||
name = "terminal"
|
||||
|
||||
[theme.custom]
|
||||
# The active tab is drawn as panel_bg text on an accent background, so panel_bg
|
||||
# has to be dark for it to read - same colors as status-left's "#[fg=black,bg=blue]"
|
||||
panel_bg = "black"
|
||||
|
||||
[terminal]
|
||||
# Matches -c "#{pane_current_path}" on every split, window, and session
|
||||
new_cwd = "follow"
|
||||
|
||||
[keys]
|
||||
prefix = "ctrl+space"
|
||||
|
||||
# Config and help
|
||||
reload_config = "prefix+q"
|
||||
help = "prefix+?"
|
||||
detach = "prefix+d"
|
||||
|
||||
# Copy mode
|
||||
copy_mode = "prefix+["
|
||||
|
||||
# Panes
|
||||
split_horizontal = ["prefix+h", "alt+enter"]
|
||||
split_vertical = ["prefix+v", "alt+shift+enter"]
|
||||
close_pane = ["prefix+x", "alt+esc"]
|
||||
zoom = "prefix+z"
|
||||
last_pane = "prefix+;"
|
||||
|
||||
focus_pane_left = "ctrl+alt+left"
|
||||
focus_pane_down = "ctrl+alt+down"
|
||||
focus_pane_up = "ctrl+alt+up"
|
||||
focus_pane_right = "ctrl+alt+right"
|
||||
|
||||
resize_mode = ["prefix+ctrl+left", "prefix+ctrl+down", "prefix+ctrl+up", "prefix+ctrl+right"]
|
||||
|
||||
# Like resize-pane on C-M-S-arrows
|
||||
resize_pane_left = "ctrl+alt+shift+left"
|
||||
resize_pane_down = "ctrl+alt+shift+down"
|
||||
resize_pane_up = "ctrl+alt+shift+up"
|
||||
resize_pane_right = "ctrl+alt+shift+right"
|
||||
|
||||
# No tmux equivalent; herdr's default prefix+shift+p is taken by previous session
|
||||
rename_pane = "prefix+shift+o"
|
||||
|
||||
# Windows -> tabs
|
||||
new_tab = "prefix+c"
|
||||
rename_tab = "prefix+r"
|
||||
close_tab = "prefix+k"
|
||||
switch_tab = ["prefix+1..9", "alt+1..9"]
|
||||
previous_tab = ["prefix+p", "alt+left"]
|
||||
next_tab = ["prefix+n", "alt+right"]
|
||||
|
||||
# Like swap-window -t -1/+1 on M-S-Left/Right
|
||||
move_tab_previous = "alt+shift+left"
|
||||
move_tab_next = "alt+shift+right"
|
||||
|
||||
# Sessions -> workspaces
|
||||
new_workspace = "prefix+shift+c"
|
||||
rename_workspace = "prefix+shift+r"
|
||||
close_workspace = "prefix+shift+k"
|
||||
previous_workspace = ["prefix+shift+p", "alt+up"]
|
||||
next_workspace = ["prefix+shift+n", "alt+down"]
|
||||
|
||||
[ui]
|
||||
accent = "blue"
|
||||
|
||||
# tmux drew single-line dividers between adjacent panes and no outer frame
|
||||
pane_gaps = false
|
||||
pane_outer_borders = false
|
||||
|
||||
# tmux had no scrollbar column beside its panes
|
||||
pane_scrollbars = false
|
||||
|
||||
# kill-window and kill-session never asked
|
||||
confirm_close = false
|
||||
|
||||
# automatic-rename gave windows a name without prompting
|
||||
prompt_new_tab_name = false
|
||||
|
||||
# set -g mouse on
|
||||
mouse_capture = true
|
||||
|
||||
# status-right had the zoom flag followed by #h
|
||||
tab_bar_right = [{ type = "zoom" }, { type = "hostname" }]
|
||||
|
||||
# set -g set-titles on / set -g set-titles-string '#h:#W', where tmux's #W was
|
||||
# the basename of the pane cwd. This is what Hyprland shows in the group bar,
|
||||
# and it resolves on the server so remote sessions name the remote host.
|
||||
window_title = "{hostname}: {workspace}"
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Keep only your personal input overrides here. Uncommented settings below
|
||||
-- replace Omarchy's defaults.
|
||||
|
||||
-- Keyboard layout and options.
|
||||
-- See https://wiki.hypr.land/Configuring/Basics/Variables/#input
|
||||
-- hl.config({
|
||||
-- input = {
|
||||
-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt.
|
||||
-- kb_layout = "us,dk,eu",
|
||||
-- kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle",
|
||||
--
|
||||
-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards).
|
||||
-- kb_variant = "intl",
|
||||
--
|
||||
-- -- Change speed of keyboard repeat.
|
||||
-- repeat_rate = 40,
|
||||
-- repeat_delay = 250,
|
||||
--
|
||||
-- -- Start with numlock on by default.
|
||||
-- numlock_by_default = true,
|
||||
--
|
||||
-- -- Increase sensitivity for mouse/trackpad (default: 0).
|
||||
-- sensitivity = 0.35,
|
||||
--
|
||||
-- -- Turn off mouse acceleration (default: adaptive).
|
||||
-- accel_profile = "flat",
|
||||
--
|
||||
-- touchpad = {
|
||||
-- -- Use traditional (non-inverse) scrolling.
|
||||
-- natural_scroll = false,
|
||||
--
|
||||
-- -- Re-enable tap-to-click (one-finger tap = left, two-finger = right).
|
||||
-- tap_to_click = true,
|
||||
--
|
||||
-- -- Use two-finger clicks for right-click instead of lower-right corner.
|
||||
-- clickfinger_behavior = true,
|
||||
--
|
||||
-- -- Control the speed of your scrolling.
|
||||
-- scroll_factor = 0.4,
|
||||
--
|
||||
-- -- Enable the touchpad while typing.
|
||||
-- disable_while_typing = false,
|
||||
--
|
||||
-- -- Left-click-and-drag with three fingers.
|
||||
-- drag_3fg = 1,
|
||||
-- },
|
||||
-- },
|
||||
-- })
|
||||
|
||||
-- App-specific touchpad scroll speeds.
|
||||
-- o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 })
|
||||
-- o.window("com.mitchellh.ghostty", { scroll_touchpad = 0.2 })
|
||||
|
||||
-- Enable touchpad gestures for changing workspaces.
|
||||
-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Gestures/
|
||||
-- hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
|
||||
|
||||
-- Enable touchpad gestures for moving focus (helpful on scrolling layout).
|
||||
-- hl.gesture({ fingers = 3, direction = "left", action = function() hl.dispatch(hl.dsp.focus({ direction = "l" })) end })
|
||||
-- hl.gesture({ fingers = 3, direction = "right", action = function() hl.dispatch(hl.dsp.focus({ direction = "r" })) end })
|
||||
|
||||
-- lan-mouse (handoff) virtual pointer: DO NOT add sensitivity/accel_profile rules here.
|
||||
--
|
||||
-- Tried and measured as INERT on 2026-10-10. `hl.device({name="unknown-device", ...})` does
|
||||
-- match the device (proven by toggling scroll_factor and watching hyprctl report it), 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
|
||||
-- travel with `lan-mouse test-emulation` + hyprctl cursorpos was 398x198px at sensitivity 0
|
||||
-- AND at -0.9 -- identical, and exactly 1:1 with the raw deltas. Both machines measure the
|
||||
-- same 398x198, so neither end scales.
|
||||
--
|
||||
-- Corollary: global `accel_profile = "flat"` would also be a no-op for the virtual pointer
|
||||
-- while permanently changing how the real trackpad feels. Pure loss -- do not do it.
|
||||
-- The magnitude of forwarded motion originates on the SENDING machine's trackpad.
|
||||
--
|
||||
-- ROOT CAUSE FOUND 2026-10-10: lan-mouse 0.11.0 forwards UNACCELERATED deltas.
|
||||
-- input-capture/src/layer_shell.rs:887 destructures the relative-pointer event as
|
||||
-- `dx_unaccel: dx, dy_unaccel: dy, ..` -- the `..` throws away the accelerated dx/dy that the
|
||||
-- same event carries. So the accel curve is stripped at the SENDER and nothing replaces it at
|
||||
-- the receiver. Consequence: sensitivity/accel_profile are inert for forwarded motion on BOTH
|
||||
-- ends, so "sync the sensitivity between the laptops" is impossible by configuration.
|
||||
-- The only fix is a source patch to use the accelerated dx/dy.
|
||||
-- See memory: lan-mouse-forwards-unaccelerated-deltas.md
|
||||
@@ -0,0 +1,22 @@
|
||||
-- See https://wiki.hypr.land/Configuring/Basics/Monitors/
|
||||
-- List current monitors and supported resolutions with: hyprctl monitors all
|
||||
|
||||
-- Monitor scale is Hyprland's scale for the output. It sizes everything
|
||||
-- Wayland-native, accepts fractions (1.6, 1.75), and applies immediately.
|
||||
-- "auto" lets Hyprland pick per display.
|
||||
local omarchy_monitor_scale = 1.33333
|
||||
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale })
|
||||
|
||||
-- Configure a specific monitor.
|
||||
-- hl.monitor({ output = "DP-2", mode = "2560x1440@144", position = "0x0", scale = 1 })
|
||||
|
||||
-- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°).
|
||||
-- hl.monitor({ output = "DP-2", mode = "preferred", position = "auto", scale = 1, transform = 1 })
|
||||
|
||||
-- GDK scale is GDK_SCALE, the factor GTK draws its own UI at. It's what
|
||||
-- sizes X11/XWayland windows, which Omarchy leaves unscaled so they stay
|
||||
-- crisp instead of being stretched by the compositor. GTK only honors whole
|
||||
-- numbers, so use the nearest integer to the monitor scale, and restart an
|
||||
-- app for a change to reach it.
|
||||
local omarchy_gdk_scale = 1
|
||||
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"theme": "system",
|
||||
"autoupdate": false
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# Voxtype Configuration
|
||||
#
|
||||
# Location: ~/.config/voxtype/config.toml
|
||||
# All settings can be overridden via CLI flags
|
||||
#
|
||||
# State file for external integrations (Waybar, polybar, etc.)
|
||||
# Use "auto" for default location ($XDG_RUNTIME_DIR/voxtype/state),
|
||||
# a custom path, or "disabled" to turn off. The daemon writes state
|
||||
# ("idle", "recording", "transcribing") to this file whenever it changes.
|
||||
# Required for `voxtype record toggle` and `voxtype status` commands.
|
||||
state_file = "auto"
|
||||
|
||||
[hotkey]
|
||||
# Hotkey is configured in Hyprland. Default is Super + Ctrl + X
|
||||
enabled = false
|
||||
|
||||
[audio]
|
||||
# Audio input device ("default" uses system default)
|
||||
# List devices with: pactl list sources short
|
||||
device = "default"
|
||||
|
||||
# Sample rate in Hz (whisper expects 16000)
|
||||
sample_rate = 16000
|
||||
|
||||
# Maximum recording duration in seconds (safety limit)
|
||||
max_duration_secs = 60
|
||||
|
||||
# Pause MPRIS media players during recording
|
||||
pause_media = true
|
||||
|
||||
# [audio.feedback]
|
||||
# Enable audio feedback sounds (beeps when recording starts/stops)
|
||||
# enabled = true
|
||||
#
|
||||
# Sound theme: "default", "subtle", "mechanical", or path to custom theme directory
|
||||
# theme = "default"
|
||||
#
|
||||
# Volume level (0.0 to 1.0)
|
||||
# volume = 0.7
|
||||
|
||||
[whisper]
|
||||
# Model to use for transcription
|
||||
# Options: tiny, tiny.en, base, base.en, small, small.en, medium, medium.en, large-v3, large-v3-turbo
|
||||
# .en models are English-only but faster and more accurate for English
|
||||
# large-v3-turbo is faster than large-v3 with minimal accuracy loss (recommended for GPU)
|
||||
# Or provide absolute path to a custom .bin model file
|
||||
model = "base.en"
|
||||
|
||||
# Language for transcription
|
||||
# Use "en" for English, "auto" for auto-detection
|
||||
# See: https://github.com/openai/whisper#available-models-and-languages
|
||||
language = "en"
|
||||
|
||||
# Translate non-English speech to English
|
||||
translate = false
|
||||
|
||||
# Number of CPU threads for inference (omit for auto-detection)
|
||||
# threads = 4
|
||||
|
||||
[output]
|
||||
# Primary output mode: "type" or "clipboard"
|
||||
# - type: Simulates keyboard input at cursor position (requires ydotool)
|
||||
# - clipboard: Copies text to clipboard (requires wl-copy)
|
||||
mode = "type"
|
||||
|
||||
# Fall back to clipboard if typing fails
|
||||
fallback_to_clipboard = true
|
||||
|
||||
# Delay between typed characters in milliseconds
|
||||
# 0 = fastest possible, increase if characters are dropped
|
||||
type_delay_ms = 1
|
||||
|
||||
# Post-processing command (optional)
|
||||
# Pipe transcribed text through an external command for cleanup before output.
|
||||
# The command receives text on stdin and outputs processed text on stdout.
|
||||
# Useful for LLM-based text cleanup, grammar correction, filler word removal.
|
||||
# On any failure (timeout, error), falls back to original transcription.
|
||||
#
|
||||
# [output.post_process]
|
||||
# command = "ollama run llama3.2:1b 'Clean up this dictation. Fix grammar, remove filler words. Output only the cleaned text:'"
|
||||
# timeout_ms = 30000 # 30 second timeout (generous for LLM)
|
||||
|
||||
[output.notification]
|
||||
# Show notification when recording starts (hotkey pressed)
|
||||
on_recording_start = false
|
||||
|
||||
# Show notification when recording stops (transcription beginning)
|
||||
on_recording_stop = false
|
||||
|
||||
# Show notification with transcribed text after transcription completes
|
||||
on_transcription = false
|
||||
|
||||
# [text]
|
||||
# Text processing options (word replacements, spoken punctuation)
|
||||
#
|
||||
# Enable spoken punctuation conversion (e.g., say "period" to get ".")
|
||||
# spoken_punctuation = false
|
||||
#
|
||||
# Custom word replacements (case-insensitive)
|
||||
# replacements = { "hyperwhisper" = "hyprwhspr" }
|
||||
@@ -0,0 +1,35 @@
|
||||
# MacBook Air M2 packages beyond the Omarchy baseline and the other layers.
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
|
||||
[repo]
|
||||
# --- Asahi platform: installed by the Asahi installer, listed so a check catches drift ---
|
||||
linux-asahi
|
||||
asahi-meta
|
||||
asahi-audio
|
||||
asahi-scripts
|
||||
asahi-fwextract
|
||||
m1n1
|
||||
uboot-asahi
|
||||
speakersafetyd # speaker protection; without it the amps are capped
|
||||
vulkan-asahi
|
||||
libva-v4l2_request-avd # hardware video decode (omarchy-aarch64)
|
||||
pipewire-pulse
|
||||
pipewire-alsa
|
||||
|
||||
# --- access ---
|
||||
openssh # sshd is enabled: the HP reaches it as `ssh air`
|
||||
|
||||
# --- apps ---
|
||||
obsidian-appimage # omarchy-aarch64
|
||||
wf-recorder # screen recording
|
||||
|
||||
[optional]
|
||||
steam # asahi-alarm build, runs x86 games through FEX
|
||||
omarchy-steam-fex
|
||||
|
||||
[ignore]
|
||||
# Arch Linux ARM base system and keyrings
|
||||
base base-devel sudo omarchy linux-firmware btrfs-progs grub dhcpcd netctl net-tools nano which
|
||||
ex-vi-compat terminus-font archlinuxarm-keyring asahi-alarm-keyring hyprtoolkit
|
||||
neovim # Omarchy ships nvim; this is the ALARM name for it
|
||||
dotnet-runtime-2.1 # explicit, required by nothing; unclear why it is here
|
||||
@@ -0,0 +1,11 @@
|
||||
# MacBook Air M2 (omarchy-air): Asahi Linux on Arch Linux ARM, aarch64.
|
||||
# Captured as it is on 2026-10-16 (Omarchy 4.0.3rc4): no hand-built themes, no
|
||||
# shell plugins, stock bar, stock terminal fonts. Add personal-desktop to
|
||||
# include= to give it the HP's desktop.
|
||||
description="MacBook Air M2 (omarchy-air): Asahi, aarch64"
|
||||
include=(base personal features/handoff roles/developer roles/devops roles/ai-agents)
|
||||
hostnames=(omarchy-air)
|
||||
arch=aarch64
|
||||
# The peer for omarchy-handoff is the HP: Wi-Fi .24 and the dock NIC .86, which
|
||||
# disappears on undock, so both.
|
||||
handoff_peer_ips=(192.168.1.24 192.168.1.86)
|
||||
@@ -0,0 +1 @@
|
||||
options appledrm show_notch=1
|
||||
@@ -0,0 +1 @@
|
||||
options hid_apple fnmode=1
|
||||
@@ -0,0 +1,6 @@
|
||||
# omarchy-air only. Multiplier for the residual logical-ppi difference between
|
||||
# the two screens (handoff/patch adds it; default 1.0 = upstream). The Air is
|
||||
# 140.1 logical ppi against ~95.8 for the 23" LG, so 0.7 makes forwarded motion
|
||||
# cover the same PHYSICAL distance.
|
||||
[Service]
|
||||
Environment=LAN_MOUSE_POINTER_SCALE=0.7
|
||||
@@ -0,0 +1,11 @@
|
||||
-- personal-desktop layer: the bindings that go with this layer's plugins.
|
||||
|
||||
-- Open Claude instead of the stock ChatGPT webapp.
|
||||
hl.unbind("SUPER + SHIFT + A")
|
||||
o.bind("SUPER + SHIFT + A", "Claude", { webapp = "https://claude.ai" })
|
||||
|
||||
-- Herdr HUD overlay (indie.herdr-hud native plugin, local branch).
|
||||
o.bind("SUPER + H", "Toggle Herdr HUD", "omarchy-shell shell toggle indie.herdr-hud '{}'")
|
||||
o.bind("SUPER + ALT + H", "Show or hide Herdr HUD button", "omarchy-shell shell call indie.herdr-hud toggleVisibility '{}'")
|
||||
o.bind("SUPER + CTRL + DOWN", "Herdr HUD: next agent", "omarchy-shell shell call indie.herdr-hud nextAgent '{}'")
|
||||
o.bind("SUPER + CTRL + UP", "Herdr HUD: previous agent", "omarchy-shell shell call indie.herdr-hud previousAgent '{}'")
|
||||
@@ -16,6 +16,9 @@
|
||||
{
|
||||
"id": "omarchy.workspaces"
|
||||
},
|
||||
{
|
||||
"id": "niccoconnor.mantel"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.active-window"
|
||||
}
|
||||
@@ -24,15 +27,15 @@
|
||||
{
|
||||
"id": "omarchy.indicators"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.keyboard-layout"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.clock",
|
||||
"format": "dddd HH:mm",
|
||||
"formatAlt": "d MMMM 'W'ww yyyy",
|
||||
"verticalFormat": "HH\n—\nmm"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.keyboard-layout"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.weather"
|
||||
},
|
||||
@@ -53,6 +56,16 @@
|
||||
{
|
||||
"id": "omarchy.tray"
|
||||
},
|
||||
{
|
||||
"id": "izeesoft.omarchy-phone"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.spacer"
|
||||
},
|
||||
{
|
||||
"id": "io.github.juancasanueva.plugin-manager",
|
||||
"tiledExpandedPanel": true
|
||||
},
|
||||
{
|
||||
"id": "io.github.thisisgm.omapods"
|
||||
},
|
||||
@@ -89,6 +102,9 @@
|
||||
"plugins": [
|
||||
{
|
||||
"id": "indie.herdr-hud"
|
||||
},
|
||||
{
|
||||
"id": "niccoconnor.keyhints"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Packages the plugins in plugins.txt need. See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
qmltermwidget # embedded terminal widget; indie.herdr-hud needs it
|
||||
qt6-connectivity # Bluetooth/QML bits for the omapods plugin
|
||||
qt6-tools # qmllint etc. when editing plugin QML
|
||||
openbsd-netcat # nc for kb2uka.port-doctor checks
|
||||
|
||||
[aur]
|
||||
hyprmoncfg-bin # display profile daemon; pairs with crmne.hyprmoncfg plugin
|
||||
netbird-bin # NetBird client for the covert.vpn private network; pairs with vstoms.netbird
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Show a Herdr pane in the herdr web ui (devswha.herdr-web-ui) as one floating app window.
|
||||
|
||||
The shell cannot host a web view (QtWebEngine aborts inside Quickshell), so the HUD
|
||||
hands agents off to a browser app window instead. The HUD keeps a single window:
|
||||
asking for the pane it already shows brings it forward; asking for another pane
|
||||
replaces it in the same place and size. Failures go to a desktop notification,
|
||||
since the HUD has closed by then.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote, urlsplit
|
||||
|
||||
HOME = Path.home()
|
||||
WEB_ENV = HOME / ".config/herdr/plugins/config/devswha.herdr-web-ui/env"
|
||||
CONFIG = HOME / ".config/herdr-hud-native/web.json"
|
||||
STATE = HOME / ".config/herdr-hud-native/web-window.json"
|
||||
WAIT_SECONDS = 10
|
||||
|
||||
|
||||
class WebError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def base_url() -> str:
|
||||
"""HERDR_HUD_WEB_URL, else {"url": ...} in web.json, else the web ui's own listen address."""
|
||||
override = os.environ.get("HERDR_HUD_WEB_URL", "").strip()
|
||||
if not override:
|
||||
try:
|
||||
override = str(json.loads(CONFIG.read_text()).get("url") or "").strip()
|
||||
except (OSError, ValueError, AttributeError):
|
||||
override = ""
|
||||
if override:
|
||||
return override.rstrip("/")
|
||||
values = {}
|
||||
try:
|
||||
for line in WEB_ENV.read_text().splitlines():
|
||||
key, sep, value = line.partition("=")
|
||||
if sep and key.strip() in ("HOST", "PORT"):
|
||||
values[key.strip()] = value.strip().strip("'\"")
|
||||
except OSError:
|
||||
pass
|
||||
host = values.get("HOST") or "127.0.0.1"
|
||||
if host in ("0.0.0.0", "::", "[::]"):
|
||||
host = "127.0.0.1"
|
||||
return f"http://{host}:{values.get('PORT') or '7317'}"
|
||||
|
||||
|
||||
def hyprctl(*args: str) -> str:
|
||||
done = subprocess.run(["hyprctl", *args], capture_output=True, text=True, timeout=5)
|
||||
if done.returncode != 0 or done.stdout.startswith("Couldn't connect"):
|
||||
raise WebError(done.stdout.strip() or done.stderr.strip() or "hyprctl failed")
|
||||
return done.stdout
|
||||
|
||||
|
||||
def fix_hyprland_signature() -> None:
|
||||
"""A shell that outlived a Hyprland restart keeps a dead signature; use the live one."""
|
||||
try:
|
||||
hyprctl("version")
|
||||
return
|
||||
except WebError:
|
||||
pass
|
||||
runtime = Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")) / "hypr"
|
||||
for candidate in sorted(runtime.glob("*/.socket.sock"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
os.environ["HYPRLAND_INSTANCE_SIGNATURE"] = candidate.parent.name
|
||||
try:
|
||||
hyprctl("version")
|
||||
return
|
||||
except WebError:
|
||||
continue
|
||||
raise WebError("Cannot reach Hyprland")
|
||||
|
||||
|
||||
def dispatch(expression: str) -> None:
|
||||
out = hyprctl("dispatch", expression).strip()
|
||||
if out != "ok":
|
||||
raise WebError(out)
|
||||
|
||||
|
||||
def clients() -> list:
|
||||
return json.loads(hyprctl("clients", "-j"))
|
||||
|
||||
|
||||
def web_windows(host: str) -> dict:
|
||||
# Chromium-family app windows are classed "<browser>-<host>__<path>-<profile>".
|
||||
marker = f"-{host}__"
|
||||
return {c["address"]: c for c in clients() if marker in str(c.get("class", ""))}
|
||||
|
||||
|
||||
def load_state() -> dict:
|
||||
try:
|
||||
state = json.loads(STATE.read_text())
|
||||
return state if isinstance(state, dict) else {}
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def save_state(state: dict) -> None:
|
||||
STATE.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = STATE.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(state, indent=2) + "\n")
|
||||
tmp.replace(STATE)
|
||||
|
||||
|
||||
def active_workspace() -> str:
|
||||
return str(json.loads(hyprctl("activeworkspace", "-j")).get("id", ""))
|
||||
|
||||
|
||||
def bring_forward(address: str) -> None:
|
||||
window = f"address:{address}"
|
||||
workspace = active_workspace()
|
||||
if workspace:
|
||||
dispatch(f"hl.dsp.window.move({{ workspace = '{workspace}', window = '{window}' }})")
|
||||
dispatch(f"hl.dsp.focus({{ window = '{window}' }})")
|
||||
|
||||
|
||||
def default_size() -> tuple:
|
||||
for monitor in json.loads(hyprctl("monitors", "-j")):
|
||||
if monitor.get("focused"):
|
||||
scale = float(monitor.get("scale") or 1)
|
||||
width, height = monitor["width"] / scale, monitor["height"] / scale
|
||||
return round(width * 0.72), round(height * 0.82)
|
||||
return 1280, 860
|
||||
|
||||
|
||||
def place(address: str, previous: dict | None) -> None:
|
||||
window = f"address:{address}"
|
||||
dispatch(f"hl.dsp.window.float({{ action = 'enable', window = '{window}' }})")
|
||||
if previous and previous.get("floating"):
|
||||
width, height = previous["size"]
|
||||
x, y = previous["at"]
|
||||
dispatch(f"hl.dsp.window.resize({{ x = {width}, y = {height}, window = '{window}' }})")
|
||||
dispatch(f"hl.dsp.window.move({{ x = {x}, y = {y}, window = '{window}' }})")
|
||||
else:
|
||||
width, height = default_size()
|
||||
dispatch(f"hl.dsp.window.resize({{ x = {width}, y = {height}, window = '{window}' }})")
|
||||
dispatch(f"hl.dsp.window.center({{ window = '{window}' }})")
|
||||
dispatch(f"hl.dsp.focus({{ window = '{window}' }})")
|
||||
|
||||
|
||||
def open_pane(pane: str) -> None:
|
||||
base = base_url()
|
||||
host = urlsplit(base).hostname or ""
|
||||
if not host:
|
||||
raise WebError(f"Bad web ui address: {base}")
|
||||
fix_hyprland_signature()
|
||||
existing = web_windows(host)
|
||||
state = load_state()
|
||||
tracked = existing.get(str(state.get("address", "")))
|
||||
|
||||
if tracked and state.get("pane") == pane:
|
||||
bring_forward(tracked["address"])
|
||||
return
|
||||
|
||||
if tracked:
|
||||
dispatch(f"hl.dsp.window.close({{ window = 'address:{tracked['address']}' }})")
|
||||
|
||||
url = f"{base}/?pane={quote(pane, safe='')}" if pane else f"{base}/"
|
||||
subprocess.Popen(["omarchy-launch-webapp", url], stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
|
||||
deadline = time.monotonic() + WAIT_SECONDS
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(0.15)
|
||||
fresh = [a for a in web_windows(host) if a not in existing]
|
||||
if fresh:
|
||||
place(fresh[0], tracked)
|
||||
save_state({"address": fresh[0], "pane": pane})
|
||||
return
|
||||
raise WebError("The web ui window did not appear")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3 or sys.argv[1] != "open":
|
||||
print("usage: herdr-web open PANE", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
open_pane(sys.argv[2])
|
||||
except (WebError, OSError, ValueError, KeyError, subprocess.TimeoutExpired) as error:
|
||||
subprocess.run(["notify-send", "Herdr HUD", f"Cannot open the web ui: {error}"], check=False)
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git i/HerdrHud.qml w/HerdrHud.qml
|
||||
index 018311d..d01449c 100644
|
||||
index 018311d..4a73ee0 100644
|
||||
--- i/HerdrHud.qml
|
||||
+++ w/HerdrHud.qml
|
||||
@@ -7,6 +7,7 @@ import Quickshell.Wayland
|
||||
@@ -10,11 +10,12 @@ index 018311d..d01449c 100644
|
||||
import "."
|
||||
|
||||
Item {
|
||||
@@ -21,12 +22,13 @@ Item {
|
||||
@@ -21,12 +22,14 @@ Item {
|
||||
readonly property string pluginId: (manifest && manifest.id) || "indie.herdr-hud"
|
||||
readonly property string pluginDir: (manifest && manifest.__sourceDir) || ""
|
||||
readonly property string bridgePath: decodeURIComponent(String(Qt.resolvedUrl("bin/herdr-monitor")).replace(/^file:\/\//, ""))
|
||||
+ readonly property string agentsPath: decodeURIComponent(String(Qt.resolvedUrl("bin/herdr-agents")).replace(/^file:\/\//, ""))
|
||||
+ readonly property string webPath: decodeURIComponent(String(Qt.resolvedUrl("bin/herdr-web")).replace(/^file:\/\//, ""))
|
||||
readonly property string homeDir: Quickshell.env("HOME")
|
||||
readonly property string configDir: homeDir + "/.config/herdr-hud-native"
|
||||
readonly property string statePath: configDir + "/state.json"
|
||||
@@ -25,7 +26,7 @@ index 018311d..d01449c 100644
|
||||
property bool overlayVisible: true
|
||||
property bool openingRequested: false
|
||||
property bool demoMode: false
|
||||
@@ -35,10 +37,136 @@ Item {
|
||||
@@ -35,10 +38,136 @@ Item {
|
||||
property var agents: []
|
||||
property string selectedPane: ""
|
||||
property var unread: ({})
|
||||
@@ -166,7 +167,25 @@ index 018311d..d01449c 100644
|
||||
property var lastSequence: ({})
|
||||
property var alertQueue: []
|
||||
property bool alertsEnabled: true
|
||||
@@ -190,6 +318,9 @@ Item {
|
||||
@@ -66,6 +195,10 @@ Item {
|
||||
property bool stateReady: false
|
||||
|
||||
property string uiMode: "omarchy"
|
||||
+ // "web" hands agents to the herdr web ui in a browser app window; "terminal" attaches
|
||||
+ // the native terminal in the panel. The shell can't host a web view itself.
|
||||
+ property string viewMode: "web"
|
||||
+ readonly property bool webView: viewMode === "web"
|
||||
readonly property bool wowMode: uiMode === "wow"
|
||||
readonly property int cornerRadius: wowMode ? 4 : 0
|
||||
readonly property string chromeFont: wowMode ? "Georgia" : Style.font.family
|
||||
@@ -184,12 +317,16 @@ Item {
|
||||
if (parsed && typeof parsed === "object") {
|
||||
if (Number.isInteger(parsed.launcherStyle) && parsed.launcherStyle >= 0 && parsed.launcherStyle < 6) launcherStyle = parsed.launcherStyle
|
||||
if (parsed.uiMode === "wow" || parsed.uiMode === "omarchy") uiMode = parsed.uiMode
|
||||
+ if (parsed.viewMode === "web" || parsed.viewMode === "terminal") viewMode = parsed.viewMode
|
||||
if (typeof parsed.alertsEnabled === "boolean") alertsEnabled = parsed.alertsEnabled
|
||||
if (typeof parsed.overlayVisible === "boolean") overlayVisible = parsed.overlayVisible
|
||||
if (parsed.positions && typeof parsed.positions === "object") positions = parsed.positions
|
||||
if (Number(parsed.rosterWidth) > 0) rosterWidth = clamp(Number(parsed.rosterWidth), 150, 330)
|
||||
if (Number(parsed.panelWidth) > 0) panelWidth = Math.max(minPanelWidth, Number(parsed.panelWidth))
|
||||
if (Number(parsed.panelHeight) > 0) panelHeight = Math.max(minPanelHeight, Number(parsed.panelHeight))
|
||||
@@ -176,7 +195,14 @@ index 018311d..d01449c 100644
|
||||
}
|
||||
} catch (error) {
|
||||
positions = ({})
|
||||
@@ -209,7 +340,10 @@ Item {
|
||||
@@ -203,13 +340,17 @@ Item {
|
||||
stateFile.setText(JSON.stringify({
|
||||
version: 1,
|
||||
uiMode: uiMode,
|
||||
+ viewMode: viewMode,
|
||||
alertsEnabled: alertsEnabled,
|
||||
launcherStyle: launcherStyle,
|
||||
overlayVisible: overlayVisible,
|
||||
positions: positions,
|
||||
rosterWidth: Math.round(rosterWidth),
|
||||
panelWidth: Math.round(panelWidth),
|
||||
@@ -188,7 +214,32 @@ index 018311d..d01449c 100644
|
||||
}, null, 2) + "\n")
|
||||
}
|
||||
|
||||
@@ -486,9 +620,10 @@ Item {
|
||||
@@ -367,6 +508,24 @@ Item {
|
||||
})
|
||||
}
|
||||
|
||||
+ function toggleViewMode() {
|
||||
+ viewMode = webView ? "terminal" : "web"
|
||||
+ saveState()
|
||||
+ }
|
||||
+
|
||||
+ // Shows the agent in the herdr web ui and closes the panel, which would cover it.
|
||||
+ function openInWeb(pane) {
|
||||
+ if (demoMode || !pane) return
|
||||
+ Quickshell.execDetached(["python3", webPath, "open", pane])
|
||||
+ requestClose()
|
||||
+ }
|
||||
+
|
||||
+ // A card click: in web view it also opens the agent there.
|
||||
+ function activateAgent(pane) {
|
||||
+ selectAgent(pane)
|
||||
+ if (webView) openInWeb(pane)
|
||||
+ }
|
||||
+
|
||||
// Keyboard agent switching, in the roster's displayed order.
|
||||
function selectRelative(step) {
|
||||
var list = sortedAgents
|
||||
@@ -486,9 +645,10 @@ Item {
|
||||
return muted
|
||||
}
|
||||
|
||||
@@ -202,7 +253,17 @@ index 018311d..d01449c 100644
|
||||
return type.charAt(0).toUpperCase() + type.slice(1)
|
||||
}
|
||||
|
||||
@@ -581,6 +716,7 @@ Item {
|
||||
@@ -553,7 +713,8 @@ Item {
|
||||
if (!live || live.terminal_id !== agent.terminal_id) { showNextAlert(); return }
|
||||
clearAlerts()
|
||||
selectAgent(String(agent.pane_id))
|
||||
- openOnScreen(screenName)
|
||||
+ if (webView) openInWeb(String(agent.pane_id))
|
||||
+ else openOnScreen(screenName)
|
||||
}
|
||||
|
||||
function previewAlert(_arg) {
|
||||
@@ -581,6 +742,7 @@ Item {
|
||||
var nextUnread = cloneObject(unread)
|
||||
var nextSequence = ({})
|
||||
var nextWorkingSince = ({})
|
||||
@@ -210,7 +271,7 @@ index 018311d..d01449c 100644
|
||||
var live = ({})
|
||||
var previousByPane = ({})
|
||||
for (var p = 0; p < agents.length; p++) previousByPane[String(agents[p].pane_id || "")] = agents[p]
|
||||
@@ -595,13 +731,20 @@ Item {
|
||||
@@ -595,13 +757,20 @@ Item {
|
||||
nextWorkingSince[identity] = workingSince[identity] || Date.now()
|
||||
}
|
||||
live[pane] = true
|
||||
@@ -231,13 +292,14 @@ index 018311d..d01449c 100644
|
||||
workingSince = nextWorkingSince
|
||||
activityNow = Date.now()
|
||||
var currentAgent = agentForPane(selectedPane)
|
||||
@@ -616,7 +759,12 @@ Item {
|
||||
@@ -616,7 +785,13 @@ Item {
|
||||
}
|
||||
completed.forEach(function(agent) { root.queueAlert(agent) })
|
||||
errorText = ""
|
||||
- if (!agentForPane(selectedPane)) selectAgent(rows.length ? String(rows[0].pane_id || "") : "")
|
||||
+ if (pendingPane && live[pendingPane]) {
|
||||
+ selectAgent(pendingPane)
|
||||
+ if (webView && opened) openInWeb(pendingPane)
|
||||
+ pendingPane = ""
|
||||
+ }
|
||||
+ if (confirmClosePane && !live[confirmClosePane]) confirmClosePane = ""
|
||||
@@ -245,7 +307,7 @@ index 018311d..d01449c 100644
|
||||
dataRevision++
|
||||
} catch (parseError) {
|
||||
alertBaseline = false
|
||||
@@ -627,6 +775,86 @@ Item {
|
||||
@@ -627,6 +802,86 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +394,18 @@ index 018311d..d01449c 100644
|
||||
Component.onCompleted: {
|
||||
mkdirProc.running = true
|
||||
Qt.callLater(root.refreshRoster)
|
||||
@@ -689,6 +917,21 @@ Item {
|
||||
@@ -637,8 +892,8 @@ Item {
|
||||
parent: root.viewForScreen(root.panelScreenName)
|
||||
? root.viewForScreen(root.panelScreenName).terminalContainer : root
|
||||
anchors.fill: parent
|
||||
- visible: root.opened && root.overlayVisible
|
||||
- target: root.opened && root.overlayVisible && !root.demoMode
|
||||
+ visible: root.opened && root.overlayVisible && !root.webView
|
||||
+ target: root.opened && root.overlayVisible && !root.demoMode && !root.webView
|
||||
? String(root.agentForPane(root.selectedPane)?.terminal_id || "") : ""
|
||||
foreground: root.foreground
|
||||
muted: root.muted
|
||||
@@ -689,6 +944,21 @@ Item {
|
||||
onExited: function(exitCode) { root.applyRoster(rosterOut.text, rosterErr.text, exitCode) }
|
||||
}
|
||||
|
||||
@@ -354,7 +427,59 @@ index 018311d..d01449c 100644
|
||||
property string previewIdentity: ""
|
||||
Process {
|
||||
id: alertPreviewProc
|
||||
@@ -1034,6 +1277,186 @@ Item {
|
||||
@@ -786,6 +1056,11 @@ Item {
|
||||
enabled: overlayWindow.panelVisible
|
||||
onActivated: root.selectRelative(-1)
|
||||
}
|
||||
+ Shortcut {
|
||||
+ sequences: ["Return", "Enter"]
|
||||
+ enabled: overlayWindow.panelVisible && root.webView && !root.newAgentOpen && !!root.selectedPane
|
||||
+ onActivated: root.openInWeb(root.selectedPane)
|
||||
+ }
|
||||
Instantiator {
|
||||
model: 9
|
||||
delegate: Shortcut {
|
||||
@@ -991,6 +1266,31 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
+ Button {
|
||||
+ id: viewButton
|
||||
+ text: root.webView ? "View: Web" : "View: Terminal"
|
||||
+ Layout.preferredWidth: 132
|
||||
+ Layout.fillHeight: true
|
||||
+ hoverEnabled: true
|
||||
+ focusPolicy: Qt.NoFocus
|
||||
+ onClicked: root.toggleViewMode()
|
||||
+ background: Rectangle {
|
||||
+ radius: root.cornerRadius
|
||||
+ color: viewButton.down ? root.alpha(root.accent, 0.28)
|
||||
+ : root.alpha(root.accent, viewButton.hovered ? 0.18 : 0.08)
|
||||
+ border.width: 1
|
||||
+ border.color: viewButton.hovered ? root.accent : root.alpha(root.accent, 0.5)
|
||||
+ }
|
||||
+ contentItem: Text {
|
||||
+ text: viewButton.text + " ⇄"
|
||||
+ color: root.accent
|
||||
+ font.family: root.chromeFont
|
||||
+ font.pixelSize: 12
|
||||
+ font.bold: true
|
||||
+ horizontalAlignment: Text.AlignHCenter
|
||||
+ verticalAlignment: Text.AlignVCenter
|
||||
+ }
|
||||
+ }
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
visible: root.wowMode
|
||||
@@ -1005,6 +1305,7 @@ Item {
|
||||
}
|
||||
Button {
|
||||
id: disconnectTerminalButton
|
||||
+ visible: !root.webView
|
||||
text: terminalPane.clientRunning ? "Disconnect" : "Reconnect"
|
||||
enabled: !!terminalPane.target && !terminalPane.stopping
|
||||
focusPolicy: Qt.NoFocus
|
||||
@@ -1034,6 +1335,186 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
@@ -541,7 +666,7 @@ index 018311d..d01449c 100644
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
@@ -1051,15 +1474,36 @@ Item {
|
||||
@@ -1051,15 +1532,36 @@ Item {
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
|
||||
@@ -586,7 +711,7 @@ index 018311d..d01449c 100644
|
||||
}
|
||||
|
||||
ListView {
|
||||
@@ -1093,12 +1537,16 @@ Item {
|
||||
@@ -1093,12 +1595,16 @@ Item {
|
||||
z: 1
|
||||
}
|
||||
|
||||
@@ -598,13 +723,13 @@ index 018311d..d01449c 100644
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
- onClicked: root.selectAgent(String(agentRow.modelData.pane_id || ""))
|
||||
+ onClicked: root.selectAgent(agentRow.pane)
|
||||
+ onClicked: root.activateAgent(agentRow.pane)
|
||||
}
|
||||
+ HoverHandler { id: rowHover }
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
@@ -1123,7 +1571,7 @@ Item {
|
||||
@@ -1123,7 +1629,7 @@ Item {
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
@@ -613,7 +738,7 @@ index 018311d..d01449c 100644
|
||||
color: root.accent
|
||||
font.family: root.chromeFont
|
||||
font.pixelSize: 15
|
||||
@@ -1132,32 +1580,59 @@ Item {
|
||||
@@ -1132,32 +1638,59 @@ Item {
|
||||
}
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
@@ -682,11 +807,71 @@ index 018311d..d01449c 100644
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.topMargin: 2
|
||||
text: String(agentRow.index + 1)
|
||||
@@ -1269,6 +1802,46 @@ Item {
|
||||
id: terminalSlot
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
+
|
||||
+ Column {
|
||||
+ id: webPrompt
|
||||
+ anchors.centerIn: parent
|
||||
+ width: Math.min(parent.width - 32, 360)
|
||||
+ spacing: 12
|
||||
+ visible: root.webView
|
||||
+ readonly property var agent: root.agentForPane(root.selectedPane)
|
||||
+ Text {
|
||||
+ width: parent.width
|
||||
+ text: webPrompt.agent ? root.agentName(webPrompt.agent) : "No agent selected"
|
||||
+ color: root.foreground
|
||||
+ font.family: root.chromeFont
|
||||
+ font.pixelSize: 16
|
||||
+ font.bold: true
|
||||
+ horizontalAlignment: Text.AlignHCenter
|
||||
+ elide: Text.ElideRight
|
||||
+ }
|
||||
+ Text {
|
||||
+ width: parent.width
|
||||
+ text: "Click an agent, or press Enter, to open it in the herdr web ui."
|
||||
+ color: root.muted
|
||||
+ font.family: root.chromeFont
|
||||
+ font.pixelSize: 12
|
||||
+ wrapMode: Text.Wrap
|
||||
+ horizontalAlignment: Text.AlignHCenter
|
||||
+ }
|
||||
+ Row {
|
||||
+ anchors.horizontalCenter: parent.horizontalCenter
|
||||
+ spacing: 8
|
||||
+ HudChip {
|
||||
+ hud: root
|
||||
+ text: "Open in web ui"
|
||||
+ primary: true
|
||||
+ enabled: !!webPrompt.agent && !root.demoMode
|
||||
+ onClicked: root.openInWeb(root.selectedPane)
|
||||
+ }
|
||||
+ HudChip { hud: root; text: "Use native terminal"; onClicked: root.toggleViewMode() }
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
diff --git i/README.md w/README.md
|
||||
index 685096b..37df3e5 100644
|
||||
index 685096b..cb5b0b9 100644
|
||||
--- i/README.md
|
||||
+++ w/README.md
|
||||
@@ -44,6 +44,14 @@ rm -rf ~/.config/herdr-hud-native
|
||||
@@ -38,12 +38,27 @@ rm -rf ~/.config/herdr-hud-native
|
||||
|
||||
## Features
|
||||
|
||||
+- **View: Web** (default) opens the clicked agent, or an alert's agent, in the
|
||||
+ [herdr web ui](https://github.com/devswha/herdr-web-ui) as one floating browser app window
|
||||
+ (`bin/herdr-web`). Picking another agent replaces it in the same place and size; the panel
|
||||
+ closes so it doesn't cover it. The address comes from `~/.config/herdr-hud-native/web.json`
|
||||
+ (`{"url": "https://…"}`), else the web ui's own `HOST`/`PORT`. Quickshell can't host a web
|
||||
+ view (QtWebEngine aborts in the shell), hence the separate window. **View: Terminal** switches
|
||||
+ back to the in-panel terminal
|
||||
- Native VT terminal attached to the selected Herdr agent
|
||||
- Shift+drag selects terminal text; releasing the mouse copies it to the clipboard and clears the highlight
|
||||
- Ctrl+Shift+V or Shift+Insert pastes the clipboard into the focused terminal; Ctrl+V remains available to terminal applications
|
||||
- Approval-dialog keyboard input and TUI rendering
|
||||
- Omarchy and WoW designs
|
||||
- Agent switching, launcher, alerts, hidden-panel notifications
|
||||
@@ -4,9 +4,10 @@ akshar.radio-atlas https://github.com/AksharP5/omarchy-radio-atlas.git ffa7186b5
|
||||
crmne.hyprmoncfg https://github.com/crmne/omarchy-hyprmoncfg.git 193e5611f934eec42460ef75f3f46c2a8bd0252a
|
||||
ilyazar.btop https://github.com/omarchy-QOL/omarchy-btop-activity.git 1e896f6f37bf880dee3e55e36ff04f967745c527
|
||||
indie.herdr-hud https://github.com/kt3v/omarchy-herdr-hud-native.git 75bef0f8012304a4edfec90c690b9f45fe65a04e
|
||||
io.github.tallsam.navbar-cat https://github.com/tallsam/omarchy-navbar-cat.git 6ddd61a6744bab19b04d131ff4f2e087a504ce20
|
||||
io.github.thisisgm.omapods https://github.com/thisisgm/omarchy-pods fff7fec600a5b9a61cdb40e93eccbcceb4b8f824
|
||||
kb2uka.port-doctor https://github.com/Kb2uka/omarchy-port-doctor.git baa13451bfc27d59e8c831c217852279a20901aa
|
||||
kokd.remote-desktop https://github.com/kostadinkadiev/omarchy-remote-desktop.git 7913b89521f88fac40620f495d1e07835bbb55b4
|
||||
omaplug https://github.com/fross100/omaplug.git 9da73fc13fc2027be5b44915b4ab89440beb74ab
|
||||
vstoms.netbird https://github.com/vstoms/omarchy-netbird.git d530c76b13324b26dafd79934529b4395e18a11c
|
||||
io.github.juancasanueva.plugin-manager https://github.com/juancasanueva/omarchy-plugin-manager.git 0d26a81b528ecb6e2d5580192b20f53d793009fc
|
||||
izeesoft.omarchy-phone https://github.com/AdamMusa/omarchy-phone.git 5b4d275b555abb9e3fed3100ee14af5e83fc0a82
|
||||
@@ -0,0 +1,6 @@
|
||||
# The full personal desktop: hand-built themes, the shell plugins (pinned, with
|
||||
# local plugin work replayed from plugin-patches/), the bar layout and the
|
||||
# bindings that go with them. Only the HP laptop carries this today.
|
||||
description="Nic's desktop: themes, shell plugins, bar layout, Claude/Herdr bindings"
|
||||
# Themes managed by aether (reinstall with `aether`), so not carried here.
|
||||
aether_themes=(pink-gt3rs porsche-cayman porsche-rain)
|
||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 326 KiB After Width: | Height: | Size: 326 KiB |
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 324 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 450 KiB After Width: | Height: | Size: 450 KiB |
@@ -0,0 +1,3 @@
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
voxtype-bin # push-to-talk voice-to-text (voxtype.service); in the omarchy repo on both arches
|
||||
@@ -0,0 +1,3 @@
|
||||
# What both of Nic's laptops share today: branding, default agent, herdr,
|
||||
# opencode and dictation. Not for Coder desktops, which other people use.
|
||||
description="Nic's identity: branding, default agent, herdr, opencode, voxtype"
|
||||
@@ -0,0 +1,7 @@
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
claude-code cmd=claude # omarchy repo, both arches
|
||||
github-copilot-cli cmd=copilot # omarchy repo, both arches
|
||||
gemini-cli cmd=gemini
|
||||
opencode x86_64 cmd=opencode # not in Arch Linux ARM; dotfiles installs it there
|
||||
openai-codex x86_64 cmd=codex # not in Arch Linux ARM; dotfiles installs it there
|
||||
@@ -0,0 +1,9 @@
|
||||
# Coding agents. Omarchy already ships herdr (the agent multiplexer) and
|
||||
# `omarchy agent`, which launches whichever agent `omarchy-default-agent`
|
||||
# names; this installs the agents themselves. Choosing the default is left to
|
||||
# the person (Omarchy menu > Setup > Agent), or to a layer that ships
|
||||
# config/omarchy/defaults/agent, as personal/ does.
|
||||
description="Coding agents: Claude Code, OpenCode, Codex, Gemini, Copilot"
|
||||
# mcp-config points opencode at the covert-gpt LiteLLM gateway (needs secrets);
|
||||
# hermes is the Hermes agent. Neither belongs on a Coder desktop.
|
||||
dotfiles=(bin mcp-config hermes)
|
||||
@@ -0,0 +1,13 @@
|
||||
# See machines/hp-laptop/packages.txt for the format. cmd= skips a package whose
|
||||
# command is already on PATH, e.g. from dotfiles' ~/.local/bin shims.
|
||||
[repo]
|
||||
github-cli cmd=gh # GitHub CLI
|
||||
git-delta cmd=delta # syntax-highlighted git diffs
|
||||
uv cmd=uv # Python packaging and venvs
|
||||
just cmd=just # command runner
|
||||
direnv cmd=direnv # per-directory env (.envrc)
|
||||
shfmt cmd=shfmt # shell formatter
|
||||
shellcheck x86_64 cmd=shellcheck # not built for aarch64 by Arch Linux ARM
|
||||
httpie cmd=http # HTTP client
|
||||
mise x86_64 cmd=mise # runtime manager Omarchy's dev-env uses; HP has mise-bin, ALARM has no mise
|
||||
visual-studio-code-bin cmd=code # editor (omarchy repo)
|
||||
@@ -0,0 +1,8 @@
|
||||
# General software development. Omarchy already ships git, docker (+compose,
|
||||
# buildx), lazygit, lazydocker, nvim, ripgrep, fd, fzf, clang/llvm and
|
||||
# `omarchy install dev-env <lang>` for language runtimes via mise, so this only
|
||||
# adds what is missing from that.
|
||||
description="Software development: gh, delta, uv, just, direnv, VS Code"
|
||||
# dotfiles modules this role pairs with on Nic's machines (advisory; install.sh
|
||||
# prints them, it never runs dotfiles itself): zsh, git
|
||||
dotfiles=(zsh git)
|
||||
@@ -0,0 +1,28 @@
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
# --- Kubernetes ---
|
||||
kubectl cmd=kubectl
|
||||
helm cmd=helm
|
||||
k9s cmd=k9s
|
||||
kubectx cmd=kubectx # kubectx + kubens
|
||||
stern cmd=stern # multi-pod log tailing
|
||||
kustomize cmd=kustomize
|
||||
talosctl cmd=talosctl # the Omni-managed cluster runs Talos
|
||||
cilium-cli cmd=cilium-cli,cilium
|
||||
|
||||
# --- infrastructure as code ---
|
||||
opentofu cmd=tofu
|
||||
terragrunt cmd=terragrunt
|
||||
packer cmd=packer # Proxmox desktop images
|
||||
ansible cmd=ansible
|
||||
go-task cmd=go-task,task # Arch names the binary go-task; dotfiles installs it as task
|
||||
go-yq cmd=yq
|
||||
sops cmd=sops # encrypted secrets in git
|
||||
age cmd=age # sops' key backend
|
||||
|
||||
# --- network debugging ---
|
||||
bind cmd=dig # dig, delv, nslookup
|
||||
mtr cmd=mtr
|
||||
whois cmd=whois
|
||||
nmap cmd=nmap
|
||||
tcpdump cmd=tcpdump
|
||||
@@ -0,0 +1,8 @@
|
||||
# Infrastructure and Kubernetes work: the Omni/Talos cluster, OpenTofu,
|
||||
# Packer/Proxmox images, Ansible.
|
||||
#
|
||||
# On Nic's laptops dotfiles' bin/ already puts most of these in ~/.local/bin,
|
||||
# and each line's cmd= skips the package when that shim exists. On a Coder
|
||||
# desktop (no dotfiles, no 1Password) the Arch packages are the tools.
|
||||
description="Infrastructure/Kubernetes: kubectl, helm, k9s, tofu, talosctl, ansible"
|
||||
dotfiles=(bin ansible secrets)
|
||||