Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1e5f05876 |
@@ -1,185 +1,68 @@
|
|||||||
# omarchy-setup
|
# omarchy-setup
|
||||||
|
|
||||||
Omarchy customizations on top of a stock install, as **profiles**: one per
|
Every customization this laptop has on top of a stock Omarchy install, in one
|
||||||
machine, each built from shared **layers** and generic **roles**.
|
place, so a second machine can be brought to the same state.
|
||||||
|
|
||||||
| Profile | Machine |
|
Captured from Omarchy **4.0.4-1** on **2026-10-10**.
|
||||||
|---|---|
|
|
||||||
| `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
|
```bash
|
||||||
git clone https://git.nic-oconnor.com/public/omarchy-setup.git ~/omarchy-setup
|
git clone <this repo> ~/omarchy-setup
|
||||||
cd ~/omarchy-setup
|
cd ~/omarchy-setup
|
||||||
./install.sh --profiles # what exists
|
./install.sh --dry-run # see what it would change
|
||||||
./install.sh --profile hp-laptop --show # its layers, and which layer owns each file
|
./install.sh # do it
|
||||||
./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
|
`install.sh` is idempotent and backs up anything it overwrites as
|
||||||
`<file>.bak.<epoch>`. It records the profile and roles it applied in
|
`<file>.bak.<epoch>`. `capture.sh` runs the other way, pulling live changes back
|
||||||
`~/.config/omarchy-setup/profile`, so later runs (and `capture.sh`) need no
|
into the repo so this stays the source of truth.
|
||||||
flags. `capture.sh` runs the other way, pulling live changes back into the
|
|
||||||
layer that owns each file, so the repo stays the source of truth.
|
|
||||||
|
|
||||||
## How profiles are built
|
## What is in here
|
||||||
|
|
||||||
Everything lives under `profiles/`. A layer is a directory with a `profile`
|
| Path | What |
|
||||||
file (bash: `description`, `include=(...)`, and for machines `hostnames`,
|
|
||||||
`arch`, `handoff_peer_ips`) and any of the same pieces this repo always had:
|
|
||||||
|
|
||||||
| Path in a layer | Installed to |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| `config/` | `~/.config`, mirrored |
|
| `config/` | Mirrors `~/.config`. Only files that differ from Omarchy's shipped defaults. |
|
||||||
| `themes/` | `~/.config/omarchy/themes` |
|
| `themes/` | The three hand-built themes, wallpapers included. |
|
||||||
| `plugins.txt`, `plugin-patches/` | `~/.config/omarchy/plugins`, pinned; local work replayed |
|
| `plugins.txt` | The 10 Omarchy shell plugins, each pinned to a commit. |
|
||||||
| `bin/` | `~/.local/bin` |
|
| `plugin-patches/` | Local plugin work that exists on no remote (see below). |
|
||||||
| `systemd/user/` | user units, plus `<unit>.service.d/*.conf` drop-ins |
|
| `bin/` | `omarchy-handoff` and `fix-stuck-displays`. |
|
||||||
| `system/` | `/etc`, mirrored (sudo) |
|
| `handoff/` | The lan-mouse source patch that fixes pointer feel, plus example `config.toml`s. Not a `~/.config` mirror. |
|
||||||
| `handoff/` | the lan-mouse patch and example configs (`install.sh handoff`) |
|
| `systemd/user/` | `lan-mouse.service`, `voxtype.service`. |
|
||||||
| `packages.txt` | `omarchy pkg add` / `omarchy pkg aur add` |
|
| `system/` | `/etc` drop-ins: the ARP sysctl and `hid_apple` fnmode. |
|
||||||
| `setup.sh` | run last, for state outside `~/.config` (markers, gsettings, a first-run theme) |
|
| `packages.txt` | Packages beyond the Omarchy baseline, each with a reason. |
|
||||||
|
| `hosts/<hostname>/` | Per-host overlay of `config/` and `system/`: a file there replaces the shared copy on that host only. See `hosts/omarchy-air/README.md`. |
|
||||||
|
|
||||||
A profile resolves to an ordered list of layers: its `include`s first (depth
|
## The customizations, and why
|
||||||
first, each once), then any `--roles`, then the profile itself. **For a path
|
|
||||||
several layers provide, the last one wins**, so a machine overrides the roles
|
|
||||||
and shared layers beneath it. `./install.sh --show` prints the result.
|
|
||||||
|
|
||||||
| Layer | Used by | What |
|
**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
|
||||||
| `base` | all | `hyprland.lua` with the `hypr/layers/` loader; `bindings.lua` kept at stock; Super+Ctrl+T toggles one persistent btop |
|
across several files is what previously made external displays unrecoverable.
|
||||||
| `personal` | both laptops | branding, default agent (`claude`), herdr, opencode, voxtype |
|
Three outputs are named by EDID description rather than connector, so they match
|
||||||
| `personal-desktop` | HP | the three hand-built themes, the shell plugins, bar layout, Claude/Herdr bindings |
|
whichever port they land on: the built-in Chimei panel at 1.25x, the LG IPS236 on
|
||||||
| `features/handoff` | both laptops | `omarchy-handoff` + patched lan-mouse, its unit and binding |
|
HDMI at 1x, and the LG TV through the Anker dock. The TV is deliberately left on
|
||||||
| `roles/*` | per profile, or `--roles` | packages, and any bindings the role needs |
|
`preferred` (3840x2160@30) — the dock negotiates DP alt-mode with only two lanes,
|
||||||
| `machines/*` | — | displays, input, hardware packages, `/etc` drop-ins, per-machine overrides |
|
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`.
|
||||||
|
|
||||||
**Hyprland settings never share a file.** `base`'s `hyprland.lua` loads every
|
`hyprland.lua` sources `hyprmoncfg-monitors.lua` last so the `hyprmoncfg` daemon's
|
||||||
`~/.config/hypr/layers/*.lua` in name order (Omarchy's own `require_all`, with
|
generated layout wins over anything above it.
|
||||||
reload), after `monitors`/`input`/`bindings`/`looknfeel`/`autostart`. Each
|
|
||||||
layer ships its own `NN-<layer>.lua` there — `base` 10, roles 20s, `personal-desktop` 40,
|
|
||||||
features 50, machines 80 — and `bindings.lua` stays stock so nothing is bound
|
|
||||||
twice. `install.sh` warns about a file in `hypr/layers/` that no layer of the
|
|
||||||
current profile provides (a role you dropped).
|
|
||||||
|
|
||||||
**Packages** lines are `name [arch...] [cmd=a,b] # why`, in `[repo]`, `[aur]`,
|
`bin/fix-stuck-displays` recovers the related kernel-side failure: after
|
||||||
`[optional]` or `[ignore]` sections. An arch limits a line to `x86_64` or
|
suspend/resume i915 can leave an unplugged connector "disconnected" but still
|
||||||
`aarch64` (Arch Linux ARM lacks `opencode`, `openai-codex`, `mise` and
|
"enabled" on a pipe Hyprland has forgotten, which blocks every other output from
|
||||||
`shellcheck`). `cmd=` skips the package when one of those commands is already
|
modesetting. It forces the connector on, disables it through Hyprland to release
|
||||||
on PATH — on the laptops, `dotfiles`' `bin/` puts most devops and agent CLIs in
|
the pipe, then returns it to auto-detect. It needs passwordless sudo for
|
||||||
`~/.local/bin`, and the Arch package would only duplicate them; on a Coder
|
`tee /sys/class/drm/*/status`.
|
||||||
desktop, which has no `dotfiles`, the package is the tool. `[ignore]` is for
|
|
||||||
packages a machine has on purpose but this repo does not manage, so
|
|
||||||
`capture.sh packages` stays quiet about them.
|
|
||||||
|
|
||||||
Roles name the `dotfiles` modules they pair with (`dotfiles=(...)`), and
|
**Keyboard/mouse sharing** (`bin/omarchy-handoff`, `systemd/user/lan-mouse.service`)
|
||||||
`install.sh` prints them when `~/.dotfiles` is absent. It never runs
|
drives `lan-mouse` to share input with the other Omarchy laptop, bound to
|
||||||
`dotfiles` itself: that needs a 1Password token, which a Coder desktop does
|
`SUPER+SHIFT+K` (`SUPER+K` is already Keybindings). The script's header comment
|
||||||
not have.
|
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
|
||||||
## Coder desktops
|
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
|
||||||
The image is built by `covert-cloud`
|
`RUST_LOG`. It runs as a user unit rather than from `autostart.lua` because
|
||||||
(`platform/workspaces/omarchy-desktop/image/`). Its `provision/user.sh` clones
|
`o.launch_on_start` sends both streams to `/dev/null`, hiding every pairing
|
||||||
this repo at a **pinned commit** into `~/.local/share/omarchy-setup` and runs
|
failure.
|
||||||
`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
|
Three further facts were only established after the above was first written, and
|
||||||
they are what actually made it work:
|
they are what actually made it work:
|
||||||
@@ -190,8 +73,7 @@ they are what actually made it work:
|
|||||||
digest is the *correct* digest, valid TOML, non-empty, passes every guard, and
|
digest is the *correct* digest, valid TOML, non-empty, passes every guard, and
|
||||||
never matches: the DTLS handshake dies `bad_certificate`, indistinguishable
|
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
|
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
|
dead crossings. `omarchy-handoff fixcase` repairs it; `doctor` now fails on it.
|
||||||
it.
|
|
||||||
- **Authorization is receiver-side only, not mutual.** `connect.rs` sets
|
- **Authorization is receiver-side only, not mutual.** `connect.rs` sets
|
||||||
`insecure_skip_verify`, so the *connecting* side never checks the peer's
|
`insecure_skip_verify`, so the *connecting* side never checks the peer's
|
||||||
certificate — only the listener consults `authorized_fingerprints`. Each
|
certificate — only the listener consults `authorized_fingerprints`. Each
|
||||||
@@ -199,151 +81,119 @@ they are what actually made it work:
|
|||||||
needs both lists, and a sender cannot tell whether it is talking to the real
|
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.
|
peer. Input goes to whatever answers on that IP:port.
|
||||||
- **`ufw` is default-deny inbound**, and a dropped inbound 4242 presents
|
- **`ufw` is default-deny inbound**, and a dropped inbound 4242 presents
|
||||||
*identically* to a bad certificate. `install.sh handoff` opens it for the
|
*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
|
peer's addresses. Check it first: `journalctl -k -g 'UFW BLOCK'`.
|
||||||
-g 'UFW BLOCK'`.
|
|
||||||
|
|
||||||
`profiles/features/handoff/handoff/patch/` carries a two-line source patch so
|
`handoff/patch/` carries a two-line source patch so the daemon forwards the
|
||||||
the daemon forwards the accelerated deltas, which is the only way to make the
|
accelerated deltas, which is the only way to make the sender's `sensitivity` and
|
||||||
sender's `sensitivity` and `accel_profile` affect remote pointer feel at all; it
|
`accel_profile` affect remote pointer feel at all; it also adds
|
||||||
also adds `LAN_MOUSE_POINTER_SCALE` for the residual difference in logical pixel
|
`LAN_MOUSE_POINTER_SCALE` for the residual difference in logical pixel density
|
||||||
density between the two screens. `install.sh` builds it per-machine — the two
|
between the two screens. `install.sh` builds it per-machine — the two laptops are
|
||||||
laptops are different architectures — and `lan-mouse.service` runs the result.
|
different architectures — and `systemd/user/lan-mouse.service` runs the result.
|
||||||
The Air's `LAN_MOUSE_POINTER_SCALE=0.7` is a systemd drop-in in its own layer
|
|
||||||
(`lan-mouse.service.d/pointer-scale.conf`), so the shared unit stays at 1.0.
|
|
||||||
|
|
||||||
`profiles/machines/hp-laptop/system/sysctl.d/99-omarchy-handoff-arp.conf` goes
|
`system/sysctl.d/99-omarchy-handoff-arp.conf` goes with it: Wi-Fi and the dock's
|
||||||
with it: Wi-Fi and the dock's ethernet both sit on `192.168.1.0/24`, and with
|
ethernet both sit on `192.168.1.0/24`, and with the default ARP settings the
|
||||||
the default ARP settings the subnet's mapping collapses onto whichever NIC
|
subnet's mapping collapses onto whichever NIC replied last.
|
||||||
replied last.
|
|
||||||
|
|
||||||
**Bar and shell** (`profiles/personal-desktop/config/omarchy/shell.json`) adds
|
**Bar and shell** (`config/omarchy/shell.json`) adds `omarchy.active-window` to
|
||||||
`omarchy.active-window` to the left, five plugin widgets, and `indie.herdr-hud`
|
the left, five plugin widgets, and `indie.herdr-hud` as a non-bar plugin. Idle is
|
||||||
as a non-bar plugin. Idle is stretched a long way past stock — screensaver at 20
|
stretched a long way past stock — screensaver at 20 minutes, lock at 30, against
|
||||||
minutes, lock at 30, against 2.5 and 5.
|
2.5 and 5.
|
||||||
|
|
||||||
**Keybindings** live in two layer files.
|
**Keybindings** (`config/hypr/bindings.lua`) rebinds `SUPER+SHIFT+A` from the
|
||||||
`profiles/personal-desktop/config/hypr/layers/40-personal-desktop.lua` rebinds
|
stock ChatGPT webapp to Claude, and adds four Herdr HUD bindings plus Handoff.
|
||||||
`SUPER+SHIFT+A` from the stock ChatGPT webapp to Claude and adds four Herdr HUD
|
|
||||||
bindings; the handoff feature's `50-handoff.lua` adds Handoff.
|
|
||||||
|
|
||||||
**Input** (`profiles/machines/hp-laptop/config/hypr/input.lua`) enables the
|
**Input** (`config/hypr/input.lua`) enables the three-finger horizontal
|
||||||
three-finger horizontal workspace gesture that ships commented out, and flattens
|
workspace gesture that ships commented out, and flattens pointer acceleration for
|
||||||
pointer acceleration for `unknown-device*`.
|
`unknown-device*`.
|
||||||
|
|
||||||
That second rule is **a no-op, and the reasoning originally given for it was
|
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
|
wrong.** It was added believing lan-mouse forwards deltas the sending compositor
|
||||||
had already accelerated, which the receiver then amplified a second time.
|
had already accelerated, which the receiver then amplified a second time. Upstream
|
||||||
Upstream does the opposite: it forwards the *unaccelerated* deltas and discards
|
does the opposite: it forwards the *unaccelerated* deltas and discards the
|
||||||
the accelerated pair in the same event
|
accelerated pair in the same event (`handoff/patch/REBUILD.md`). And the rule
|
||||||
(`profiles/features/handoff/handoff/patch/REBUILD.md`). And the rule cannot act
|
cannot act either way — `hl.device({name="unknown-device"})` does match the
|
||||||
either way — `hl.device({name="unknown-device"})` does match the virtual pointer
|
virtual pointer (provable by toggling `scroll_factor`), but `sensitivity` and
|
||||||
(provable by toggling `scroll_factor`), but `sensitivity` and `accel_profile`
|
`accel_profile` are libinput pointer-acceleration settings and a
|
||||||
are libinput pointer-acceleration settings and a `wlr-virtual-pointer` is not a
|
`wlr-virtual-pointer` is not a libinput device, so they have nothing to act on.
|
||||||
libinput device, so they have nothing to act on. Measured 2026-10-10 on both
|
Measured 2026-10-10 on both machines with `lan-mouse test-emulation` + `hyprctl
|
||||||
machines with `lan-mouse test-emulation` + `hyprctl cursorpos`: travel was
|
cursorpos`: travel was 398x198px at `sensitivity = 0` and identical at `-0.9`,
|
||||||
398x198px at `sensitivity = 0` and identical at `-0.9`, exactly 1:1 with the raw
|
exactly 1:1 with the raw deltas. The lines are kept because they are harmless and
|
||||||
deltas. The lines are kept because they are harmless and the `unknown-device`
|
the `unknown-device` naming note is worth preserving; the real fix is
|
||||||
naming note is worth preserving; the real fix is `handoff/patch/`. Do not add
|
`handoff/patch/`. Do not add sensitivity or accel_profile rules here expecting
|
||||||
sensitivity or accel_profile rules here expecting them to change remote pointer
|
them to change remote pointer feel.
|
||||||
feel.
|
|
||||||
|
|
||||||
**Terminals** are all dropped to a smaller font (9 -> 8) except ghostty, which
|
**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.
|
went the other way to 12. `shell.toml` sets the bar font base size to 10.
|
||||||
|
|
||||||
**Branding** (`profiles/personal/config/omarchy/branding/`, shared with the Air)
|
**Branding** (`config/omarchy/branding/`) replaces the about and screensaver
|
||||||
replaces the about and screensaver ASCII art.
|
ASCII art.
|
||||||
|
|
||||||
**`hid_apple` fnmode=2** makes the F-keys act as F-keys on an Apple keyboard.
|
**`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.
|
It only takes effect after `sudo mkinitcpio -P` or a module reload.
|
||||||
|
|
||||||
## Local plugin work
|
## Local plugin work
|
||||||
|
|
||||||
`indie.herdr-hud` is the one piece that exists on no remote. Its `local` branch
|
`indie.herdr-hud` is the one piece that exists on no remote. Ten commits live on
|
||||||
carries commits — touchpad scroll feel, agent ordering, context-size display —
|
its `local` branch — touchpad scroll feel, agent ordering, context-size display —
|
||||||
and they stay off the upstream PR deliberately.
|
and they stay off the upstream PR deliberately. `plugin-patches/` carries them as
|
||||||
`profiles/personal-desktop/plugin-patches/` carries them as a git bundle plus a
|
a git bundle plus a patch for the uncommitted working tree, and `install.sh`
|
||||||
patch for the uncommitted working tree, and `install.sh` replays both after
|
replays both after cloning. `io.github.tallsam.navbar-cat` has a one-file working
|
||||||
cloning. Two untracked scripts in the same plugin, `bin/herdr-agents` and
|
tree change captured the same way.
|
||||||
`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
|
`capture.sh plugins` re-bundles and re-diffs these, so local work keeps flowing
|
||||||
back into the repo.
|
back into the repo.
|
||||||
|
|
||||||
`noconnor.agents` is the Agents bar widget held at the Omarchy 4.0.4 panel. 4.5
|
|
||||||
replaced it with a compact one (a meter per limit, the percentage only on
|
|
||||||
hover, no tokens by day or by model). It is a clone of `omarchy.agents`
|
|
||||||
(`omarchy plugin clone`) with `Panel.qml`, `Main.qml` and `README.md` taken from
|
|
||||||
upstream tag `v4.0.4`; `Agent.qml` and `assets/` stay at 4.5, and the usage
|
|
||||||
data still comes from the system's `omarchy-agent-usage-*` collectors. It has
|
|
||||||
no git history of its own, so it is carried as plain files in
|
|
||||||
`profiles/personal-desktop/config/omarchy/plugins/noconnor.agents/` and the
|
|
||||||
config step installs it and pulls it back. The 4.5 panel's account controls
|
|
||||||
(Use, Autoswitch, adding an account) are not in it. After installing it on a
|
|
||||||
running shell, `omarchy restart shell`: until then the built-in instance keeps
|
|
||||||
the `omarchy.agents` IPC target.
|
|
||||||
|
|
||||||
## Not in here, on purpose
|
## Not in here, on purpose
|
||||||
|
|
||||||
- **Secrets.** `~/.config/lan-mouse/` holds a private key and per-machine
|
- **Secrets.** `~/.config/lan-mouse/` holds a private key and per-machine
|
||||||
fingerprints; `~/.config/novnc/auth.env`, SSH keys and the git credential
|
fingerprints; `~/.config/novnc/auth.env`, SSH keys and the git credential
|
||||||
helpers are likewise machine-local. Start from
|
helpers are likewise machine-local. Start from
|
||||||
`profiles/features/handoff/handoff/config/config.toml.example-{air,laptop}`
|
`handoff/config/config.toml.example-{air,laptop}` (which carry the
|
||||||
(which carry the `capture_backend`, edge and lowercase-fingerprint rules),
|
`capture_backend`, edge and lowercase-fingerprint rules), then pair with
|
||||||
then pair with `omarchy-handoff pair <peer> <lowercase-sha256>` and
|
`omarchy-handoff pair <peer> <lowercase-sha256>` and `omarchy-handoff doctor`.
|
||||||
`omarchy-handoff doctor`. A fresh install regenerates the certificate, so this
|
A fresh install regenerates the certificate, so this machine's fingerprint
|
||||||
machine's fingerprint changes and the *peer* has to re-pair. Back up
|
changes and the *peer* has to re-pair. Back up `lan-mouse.pem` beforehand if
|
||||||
`lan-mouse.pem` beforehand if you would rather keep it stable — somewhere
|
you would rather keep it stable — somewhere private, not here.
|
||||||
private, not here.
|
|
||||||
- **Shell, git and dev tooling.** That is already its own repo, `dotfiles`
|
- **Shell, git and dev tooling.** That is already its own repo, `dotfiles`
|
||||||
(`git.nic-oconnor.com/public/dotfiles`), which handles zsh, mise, 1Password
|
(`git.nic-oconnor.com/public/dotfiles`), which handles zsh, mise, 1Password and
|
||||||
and the `~/.local/bin` tool shims. The only overlap avoided here is
|
the `~/.local/bin` tool shims. The only overlap avoided here is
|
||||||
`~/.config/git/config`, whose customizations are identity and credential
|
`~/.config/git/config`, whose customizations are identity and credential
|
||||||
helpers — `dotfiles` territory.
|
helpers — `dotfiles` territory.
|
||||||
- **The remote-access stack.** `wayvnc`, `novnc`, `sunshine`, `freerdp`,
|
- **The remote-access stack.** `wayvnc`, `novnc`, `sunshine`, `freerdp`, `remmina`
|
||||||
`remmina` and `omarchy-remote-desktop` are installed on this laptop but scoped
|
and `omarchy-remote-desktop` are installed on this laptop but scoped out.
|
||||||
out. `kokd.remote-desktop` is still in `plugins.txt` since it is a bar plugin,
|
`kokd.remote-desktop` is still in `plugins.txt` since it is a bar plugin, but
|
||||||
but it is not placed in `shell.json`.
|
it is not placed in `shell.json`.
|
||||||
- **Two plugins with no remote yet.** `niccoconnor.mantel` and
|
- **Work tooling.** The keycloak helpers (`kcadm`, `kclogin`, `wren-keycloak`) and
|
||||||
`niccoconnor.keyhints` are installed on the HP and placed in `shell.json`, but
|
`paperclipai.service`.
|
||||||
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`
|
- **aether-managed themes.** `pink-gt3rs`, `porsche-cayman` and `porsche-rain`
|
||||||
carry a `.aether-managed` marker and are reinstallable with `aether`, so their
|
carry a `.aether-managed` marker and are reinstallable with `aether`, so their
|
||||||
~18MB of wallpapers and previews stay out.
|
~18MB of wallpapers and previews stay out.
|
||||||
- **`btop.conf`.** Its only diff against stock is v1.4.7 default drift, not a
|
- **`btop.conf`.** Its only diff against stock is v1.4.7 default drift, not a
|
||||||
real customization.
|
real customization.
|
||||||
- **Backup files.** Fifteen `.bak.*` and `.prefix.*` files are sitting in
|
- **Backup files.** Fifteen `.bak.*` and `.prefix.*` files are sitting in
|
||||||
`~/.config/hypr` and `~/.config/omarchy` from earlier editing sessions. They
|
`~/.config/hypr` and `~/.config/omarchy` from earlier editing sessions. They are
|
||||||
are not tracked and are safe to delete.
|
not tracked and are safe to delete.
|
||||||
|
|
||||||
## Adding things
|
## Adding a file
|
||||||
|
|
||||||
**A file.** `capture.sh` only refreshes what the repo already tracks, so start
|
`capture.sh` only refreshes what the repo already tracks, so start it by hand:
|
||||||
it by hand, in the layer it belongs to:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp ~/.config/foo/bar.conf profiles/machines/hp-laptop/config/foo/bar.conf
|
cp ~/.config/foo/bar.conf config/foo/bar.conf # path mirrors ~/.config
|
||||||
git add profiles/machines/hp-laptop/config/foo/bar.conf
|
git add config/foo/bar.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Check first that it actually differs from stock —
|
`install.sh config` and `capture.sh config` both walk `config/` recursively, so
|
||||||
`diff /usr/share/omarchy/config/foo/bar.conf ~/.config/foo/bar.conf` — or you
|
nothing else needs changing. Check first that the file actually differs from
|
||||||
will be tracking a file Omarchy already ships. A Hyprland setting goes in that
|
stock — `diff /usr/share/omarchy/config/foo/bar.conf ~/.config/foo/bar.conf` —
|
||||||
layer's `config/hypr/layers/NN-<layer>.lua`, not in `bindings.lua` or
|
or you will be tracking a file Omarchy already ships.
|
||||||
`looknfeel.lua`.
|
|
||||||
|
|
||||||
**A role.** `mkdir profiles/roles/<name>`, give it a `profile` (`description=`,
|
### Per-host files
|
||||||
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
|
Hardware-specific files (monitor scale, fnmode) go under `hosts/<hostname>/`,
|
||||||
`hostnames=`, `arch=` and `include=(base ...)`, then its `monitors.lua`,
|
mirroring `config/` or `system/`, rather than into the shared tree. The shared
|
||||||
`input.lua`, `packages.txt` and `/etc` drop-ins. Run `./install.sh --profile
|
tree comes from `omarchy-laptop`, and `capture.sh` refreshes it only there (or
|
||||||
<name> --dry-run` on it, then `./capture.sh` after any hand-tuning.
|
with `CAPTURE_SHARED=1`). On any other host it refreshes just that host's overlay,
|
||||||
|
so a machine running stock files cannot overwrite the shared customizations.
|
||||||
|
`OMARCHY_SETUP_HOST` overrides the detected hostname for both scripts.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fi
|
|||||||
systemctl --user stop hyprmoncfgd
|
systemctl --user stop hyprmoncfgd
|
||||||
|
|
||||||
for d in "${stuck[@]}"; do
|
for d in "${stuck[@]}"; do
|
||||||
out=${d##*/}; out=${out#card*-}
|
out=${d##*/card*-}
|
||||||
log "releasing $out"
|
log "releasing $out"
|
||||||
echo on | sudo -n tee "$d/status" >/dev/null
|
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"
|
wait_for 5 sh -c "hyprctl monitors all | grep -q '^Monitor $out '" || log "$out never appeared in Hyprland"
|
||||||
@@ -2,14 +2,11 @@
|
|||||||
# Pull the live machine's state back into this repo - the reverse of install.sh.
|
# 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.
|
# Run it after customizing something by hand, then review `git diff` and commit.
|
||||||
# Each live file goes back into the layer that OWNS it for this machine's
|
# It only refreshes paths the repo already tracks; it never invents new ones, so
|
||||||
# profile (the last layer providing that path; `./install.sh --show` lists
|
# adding a newly customized file is a deliberate act (see README, "Adding a file").
|
||||||
# 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 # this machine's profile, every step
|
# ./capture.sh # refresh everything
|
||||||
# ./capture.sh --profile hp-laptop # name it explicitly
|
# ./capture.sh config # only the named steps
|
||||||
# ./capture.sh config # only the named steps
|
|
||||||
# ./capture.sh --list
|
# ./capture.sh --list
|
||||||
#
|
#
|
||||||
# Steps: config themes plugins bin services system packages
|
# Steps: config themes plugins bin services system packages
|
||||||
@@ -18,20 +15,25 @@ set -uo pipefail
|
|||||||
|
|
||||||
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
STEPS=(config themes plugins bin services system packages)
|
STEPS=(config themes plugins bin services system packages)
|
||||||
|
HOST="${OMARCHY_SETUP_HOST:-$(hostname)}"
|
||||||
|
HOST_DIR="$REPO/hosts/$HOST"
|
||||||
|
# The shared config/ and system/ trees were captured from this host. Capturing
|
||||||
|
# them anywhere else would overwrite its customizations with the other machine's
|
||||||
|
# (often stock) files, so elsewhere only the host overlay is refreshed unless
|
||||||
|
# CAPTURE_SHARED=1 says the change really is meant for every machine.
|
||||||
|
SHARED_HOST="${OMARCHY_SETUP_SHARED_HOST:-omarchy-laptop}"
|
||||||
|
CAPTURE_SHARED="${CAPTURE_SHARED:-0}"
|
||||||
|
[[ $HOST == "$SHARED_HOST" ]] && CAPTURE_SHARED=1
|
||||||
|
|
||||||
c_ch=$'\033[33m'; c_skip=$'\033[2m'; c_warn=$'\033[33m'; c_err=$'\033[31m'; c_hd=$'\033[1m'; c_off=$'\033[0m'
|
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_err=; c_hd=; c_off=; }
|
[[ -t 1 ]] || { c_ch=; c_skip=; c_warn=; c_hd=; c_off=; }
|
||||||
say() { printf '%s\n' "$*"; }
|
say() { printf '%s\n' "$*"; }
|
||||||
head_() { printf '\n%s==> %s%s\n' "$c_hd" "$*" "$c_off"; }
|
head_() { printf '\n%s==> %s%s\n' "$c_hd" "$*" "$c_off"; }
|
||||||
chg() { printf ' %sM %s%s\n' "$c_ch" "$*" "$c_off"; }
|
chg() { printf ' %sM %s%s\n' "$c_ch" "$*" "$c_off"; }
|
||||||
skip() { printf ' %s= %s%s\n' "$c_skip" "$*" "$c_off"; }
|
skip() { printf ' %s= %s%s\n' "$c_skip" "$*" "$c_off"; }
|
||||||
warn() { printf ' %s! %s%s\n' "$c_warn" "$*" "$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"; }
|
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 <live> <repo-copy> <label>
|
||||||
pull_back() {
|
pull_back() {
|
||||||
local live=$1 dest=$2 label=$3
|
local live=$1 dest=$2 label=$3
|
||||||
@@ -39,141 +41,137 @@ pull_back() {
|
|||||||
if cmp -s "$live" "$dest"; then skip "$label"; else cp "$live" "$dest"; chg "$label"; fi
|
if cmp -s "$live" "$dest"; then skip "$label"; else cp "$live" "$dest"; chg "$label"; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# pull_tree <subdir> <live-root> -- every owned file under <layer>/<subdir>
|
# layered <tree> -- NUL-separated "<repo-copy>\t<rel>" across the shared tree and
|
||||||
pull_tree() {
|
# this host's overlay, the overlay copy winning, so a live file is pulled back into
|
||||||
local sub=$1 root=$2 rel layer
|
# whichever copy this host actually installs from (see install.sh).
|
||||||
while IFS=$'\t' read -r rel layer; do
|
layered() {
|
||||||
pull_back "$root/$rel" "$PROFILES/$layer/$sub/$rel" "$rel [$layer]"
|
local tree=$1 f rel
|
||||||
done < <(owners "$sub")
|
declare -A pick=()
|
||||||
|
for base in "$REPO/$tree" "$HOST_DIR/$tree"; do
|
||||||
|
[[ -d $base ]] || continue
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
rel=${f#"$base/"}; pick[$rel]=$f
|
||||||
|
done < <(find "$base" -type f -print0)
|
||||||
|
done
|
||||||
|
for rel in "${!pick[@]}"; do printf '%s\t%s\0' "${pick[$rel]}" "$rel"; done | sort -z -t$'\t' -k2
|
||||||
}
|
}
|
||||||
|
|
||||||
step_config() { head_ "~/.config -> layers' config/"; pull_tree config "$HOME/.config"; }
|
step_config() {
|
||||||
step_bin() { head_ "~/.local/bin -> layers' bin/"; pull_tree bin "$HOME/.local/bin"; }
|
head_ "~/.config -> config/ (+ hosts/$HOST/config)"
|
||||||
step_services() { head_ "systemd user units -> layers' systemd/"; pull_tree systemd/user "$HOME/.config/systemd/user"; }
|
local dest rel
|
||||||
step_system() { head_ "/etc drop-ins -> layers' system/"; pull_tree system /etc; }
|
while IFS=$'\t' read -r -d '' dest rel; do
|
||||||
|
local tag=$rel; [[ $dest == "$HOST_DIR"/* ]] && tag="$rel (hosts/$HOST)"
|
||||||
|
if [[ $dest != "$HOST_DIR"/* ]] && (( ! CAPTURE_SHARED )); then skip "$rel (shared; not on $SHARED_HOST)"; continue; fi
|
||||||
|
pull_back "$HOME/.config/$rel" "$dest" "$tag"
|
||||||
|
done < <(layered config)
|
||||||
|
}
|
||||||
|
|
||||||
step_themes() {
|
step_themes() {
|
||||||
head_ "Themes"
|
head_ "Themes"
|
||||||
local l t name live
|
local t name live
|
||||||
declare -A theme=()
|
for t in "$REPO"/themes/*/; do
|
||||||
for l in "${LAYERS[@]}"; do
|
[[ -d $t ]] || continue
|
||||||
for t in "$PROFILES/$l"/themes/*/; do [[ -d $t ]] && theme[$(basename -- "$t")]=$l; done
|
name=$(basename -- "$t")
|
||||||
done
|
|
||||||
for name in $(printf '%s\n' "${!theme[@]}" | sort); do
|
|
||||||
t="$PROFILES/${theme[$name]}/themes/$name/"
|
|
||||||
live="$HOME/.config/omarchy/themes/$name"
|
live="$HOME/.config/omarchy/themes/$name"
|
||||||
if [[ ! -d $live ]]; then gone "$name - no longer installed"; continue; fi
|
if [[ ! -d $live ]]; then gone "$name - no longer installed"; continue; fi
|
||||||
if diff -rq "$live" "$t" >/dev/null 2>&1; then
|
if diff -rq "$live" "$t" >/dev/null 2>&1; then
|
||||||
skip "$name [${theme[$name]}]"
|
skip "$name"
|
||||||
else
|
else
|
||||||
rm -rf "$t"; mkdir -p "$t"; cp -r "$live/." "$t/"
|
rm -rf "$t"; mkdir -p "$t"; cp -r "$live/." "$t/"
|
||||||
chg "$name [${theme[$name]}]"
|
chg "$name"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
# Flag themes installed by hand that no layer carries.
|
# Flag themes installed by hand that the repo does not carry.
|
||||||
local lname
|
local l lname
|
||||||
for l in "$HOME"/.config/omarchy/themes/*/; do
|
for l in "$HOME"/.config/omarchy/themes/*/; do
|
||||||
[[ -d $l ]] || continue
|
[[ -d $l ]] || continue
|
||||||
lname=$(basename -- "$l")
|
lname=$(basename -- "$l")
|
||||||
[[ -n ${theme[$lname]:-} ]] && continue
|
[[ -d "$REPO/themes/$lname" ]] && continue
|
||||||
[[ -e "$l/.aether-managed" ]] && continue
|
[[ -e "$l/.aether-managed" ]] && continue
|
||||||
[[ " ${P_AETHER[*]} " == *" $lname "* ]] && continue
|
warn "$lname is installed but not in this repo (cp -r it into themes/ to track it)"
|
||||||
warn "$lname is installed but in no layer (cp -r it into a layer's themes/ to track it)"
|
|
||||||
done
|
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() {
|
step_plugins() {
|
||||||
head_ "Plugin pins and local work"
|
head_ "Plugin pins and local work"
|
||||||
local dir="$HOME/.config/omarchy/plugins" l f
|
local dir="$HOME/.config/omarchy/plugins"
|
||||||
local files=()
|
local tmp; tmp=$(mktemp)
|
||||||
while IFS= read -r l; do files+=("$l"); done < <(layers_with plugins.txt)
|
{
|
||||||
if (( ! ${#files[@]} )); then skip "no layer carries plugins"; return 0; fi
|
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
|
||||||
|
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"
|
||||||
|
done
|
||||||
|
} > "$tmp"
|
||||||
|
if cmp -s "$tmp" "$REPO/plugins.txt"; then skip "plugins.txt"; else mv "$tmp" "$REPO/plugins.txt"; chg "plugins.txt"; fi
|
||||||
|
rm -f "$tmp"
|
||||||
|
|
||||||
declare -A listed=()
|
# herdr-hud's local-only branch, re-bundled
|
||||||
local id u s line tmp
|
local p="$dir/indie.herdr-hud"
|
||||||
for l in "${files[@]}"; do
|
if [[ -d $p/.git ]] && git -C "$p" rev-parse --verify --quiet local >/dev/null; then
|
||||||
f="$PROFILES/$l/plugins.txt"; tmp=$(mktemp)
|
local b="$REPO/plugin-patches/indie.herdr-hud.bundle"
|
||||||
while IFS= read -r line; do
|
git -C "$p" bundle create "$b.new" origin/main..local >/dev/null 2>&1
|
||||||
if [[ -z $line || $line == \#* ]]; then printf '%s\n' "$line" >> "$tmp"; continue; fi
|
# Bundles embed a timestamp-free pack but differ byte-wise; compare the tips instead.
|
||||||
IFS=$'\t' read -r id u s <<<"$line"
|
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
|
||||||
listed[$id]=1
|
rm -f "$b.new"; skip "indie.herdr-hud.bundle"
|
||||||
if [[ ! -d $dir/$id/.git ]]; then
|
else
|
||||||
gone "$id - no longer installed, dropped from $l/plugins.txt"
|
mv "$b.new" "$b"; chg "indie.herdr-hud.bundle"
|
||||||
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
|
|
||||||
id=$(basename -- "$p")
|
|
||||||
[[ -n ${listed[$id]:-} ]] && continue
|
|
||||||
# Vendored as plain files under a layer's config/; step_config owns it.
|
|
||||||
[[ -n $(layers_with "config/omarchy/plugins/$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
|
|
||||||
fi
|
fi
|
||||||
if cmp -s "$tmp" "$f"; then skip "$l/plugins.txt"; else mv "$tmp" "$f"; chg "$l/plugins.txt"; fi
|
[[ -f $p/bin/herdr-agents ]] && pull_back "$p/bin/herdr-agents" \
|
||||||
rm -f "$tmp"
|
"$REPO/plugin-patches/indie.herdr-hud--bin--herdr-agents" "indie.herdr-hud bin/herdr-agents"
|
||||||
done
|
fi
|
||||||
|
|
||||||
# Local plugin work, per layer: re-bundle 'local' branches, re-diff working
|
local f id
|
||||||
# trees, pull back --bin-- files. See install.sh step_plugins.
|
for f in "$REPO"/plugin-patches/*-worktree.patch; do
|
||||||
local b new p untracked
|
[[ -f $f ]] || continue
|
||||||
for l in "${LAYERS[@]}"; do
|
id=$(basename -- "$f"); id=${id%-worktree.patch}
|
||||||
[[ -d $PROFILES/$l/plugin-patches ]] || continue
|
if [[ ! -d $dir/$id/.git ]]; then gone "$id - not installed"; continue; fi
|
||||||
for b in "$PROFILES/$l"/plugin-patches/*.bundle; do
|
local tmp2; tmp2=$(mktemp)
|
||||||
[[ -f $b ]] || continue
|
git -C "$dir/$id" diff > "$tmp2"
|
||||||
id=$(basename -- "$b" .bundle); p="$dir/$id"
|
if cmp -s "$tmp2" "$f"; then skip "$id-worktree.patch"; else mv "$tmp2" "$f"; chg "$id-worktree.patch"; fi
|
||||||
[[ -d $p/.git ]] && git -C "$p" rev-parse --verify --quiet local >/dev/null || { gone "$id: no 'local' branch here"; continue; }
|
rm -f "$tmp2"
|
||||||
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 "$new" "$b"; chg "$id.bundle [$l]"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
for f in "$PROFILES/$l"/plugin-patches/*-worktree.patch; do
|
|
||||||
[[ -f $f ]] || continue
|
|
||||||
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
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Report drift in the package delta without rewriting the hand-annotated files:
|
step_bin() {
|
||||||
# each line carries a "why" comment that a regenerated list would lose.
|
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=$'\t' read -r -d '' dest rel; do
|
||||||
|
if [[ $dest != "$HOST_DIR"/* ]] && (( ! CAPTURE_SHARED )); then skip "/etc/$rel (shared; not on $SHARED_HOST)"; continue; fi
|
||||||
|
pull_back "/etc/$rel" "$dest" "/etc/$rel"
|
||||||
|
done < <(layered system)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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.
|
||||||
step_packages() {
|
step_packages() {
|
||||||
head_ "Package delta (report only)"
|
head_ "Package delta (report only)"
|
||||||
load_packages
|
|
||||||
local listed; listed=$(mktemp)
|
local listed; listed=$(mktemp)
|
||||||
local e name archs cmds layer
|
sed 's/#.*//' "$REPO/packages.txt" | tr -d '[:blank:]' | grep -v '^$' | grep -v '^\[' | sort -u > "$listed"
|
||||||
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)
|
local baseline; baseline=$(mktemp)
|
||||||
{
|
{
|
||||||
@@ -182,64 +180,32 @@ step_packages() {
|
|||||||
cat /usr/share/omarchy/install/omarchy-base.packages 2>/dev/null
|
cat /usr/share/omarchy/install/omarchy-base.packages 2>/dev/null
|
||||||
} | grep -v '^$' | sort -u > "$baseline"
|
} | grep -v '^$' | sort -u > "$baseline"
|
||||||
|
|
||||||
local untracked; untracked=$(pacman -Qqe 2>/dev/null | sort -u | comm -23 - "$baseline" | comm -23 - "$listed" | tr '\n' ' ')
|
local extra; extra=$(mktemp)
|
||||||
local missing=()
|
pacman -Qqe 2>/dev/null | sort -u | comm -23 - "$baseline" > "$extra"
|
||||||
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
|
|
||||||
|
|
||||||
if [[ -n ${untracked// } ]]; then
|
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' ' ')
|
||||||
warn "installed but in no layer's packages.txt: $untracked"
|
local removed; removed=$(comm -13 "$extra" "$listed" | tr '\n' ' ')
|
||||||
say " add each to the layer it belongs to, or to $PROFILE/packages.txt [ignore]"
|
|
||||||
else
|
[[ -n ${untracked// } ]] && warn "installed but not in packages.txt: $untracked" || skip "nothing installed that packages.txt misses"
|
||||||
skip "nothing installed that the layers miss"
|
[[ -n ${removed// } ]] && warn "in packages.txt but not installed: $removed"
|
||||||
fi
|
|
||||||
(( ${#missing[@]} )) && warn "listed but not installed: ${missing[*]}"
|
|
||||||
say " packages.txt keeps a 'why' comment per package, so edit it by hand."
|
say " packages.txt keeps a 'why' comment per package, so edit it by hand."
|
||||||
rm -f "$listed" "$baseline"
|
rm -f "$listed" "$baseline" "$extra"
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
local want=() profile="" force=0 a
|
local want=()
|
||||||
while (( $# )); do
|
for a in "$@"; do
|
||||||
a=$1; shift
|
|
||||||
case $a in
|
case $a in
|
||||||
--profile|-p) profile=${1:?--profile needs a name}; shift ;;
|
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
|
||||||
--profile=*) profile=${a#*=} ;;
|
-h|--help) sed -n '2,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
--force) force=1 ;;
|
-*) warn "unknown option: $a"; exit 2 ;;
|
||||||
--list|-l) printf '%s\n' "${STEPS[@]}"; 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 ;;
|
*) if [[ " ${STEPS[*]} " == *" $a "* ]]; then want+=("$a"); else warn "unknown step: $a"; exit 2; fi ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
(( ${#want[@]} )) || want=("${STEPS[@]}")
|
(( ${#want[@]} )) || want=("${STEPS[@]}")
|
||||||
|
|
||||||
local saved="" roles=""
|
say "${c_hd}capture${c_off} repo=$REPO host=$(hostname)"
|
||||||
if def=$(default_profile); then IFS=$'\t' read -r saved roles <<<"$def"; fi
|
for s in "${want[@]}"; do "step_$s"; done
|
||||||
[[ -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"
|
head_ "Done"
|
||||||
say " Review with: git -C $REPO diff"
|
say " Review with: git -C $REPO diff"
|
||||||
|
|||||||
@@ -28,6 +28,16 @@
|
|||||||
-- o.bind("SUPER + H", nil, "voxtype record toggle")
|
-- o.bind("SUPER + H", nil, "voxtype record toggle")
|
||||||
-- o.bind("SUPER + PERIOD", nil, "omarchy-shell shell toggle omarchy.emojis")
|
-- o.bind("SUPER + PERIOD", nil, "omarchy-shell shell toggle omarchy.emojis")
|
||||||
|
|
||||||
-- omarchy-setup keeps this file at stock on purpose. Profile layers add their
|
-- Open Claude instead of the stock ChatGPT webapp.
|
||||||
-- bindings as hypr/layers/<NN>-<layer>.lua, so a binding added here AND by a
|
hl.unbind("SUPER + SHIFT + A")
|
||||||
-- layer would fire twice. Put machine-only bindings in that machine's layer.
|
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")
|
||||||
@@ -22,12 +22,6 @@ require("hypr.bindings")
|
|||||||
require("hypr.looknfeel")
|
require("hypr.looknfeel")
|
||||||
require("hypr.autostart")
|
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.
|
-- Toggle config flags dynamically.
|
||||||
require("default.hypr.toggles")
|
require("default.hypr.toggles")
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 1.2
|
|||||||
-- displays unrecoverable before.
|
-- displays unrecoverable before.
|
||||||
|
|
||||||
local omarchy_gdk_scale = 1
|
local omarchy_gdk_scale = 1
|
||||||
local omarchy_monitor_scale = 1
|
local omarchy_monitor_scale = 1.25
|
||||||
|
|
||||||
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
|
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
|
||||||
|
|
||||||
@@ -7,14 +7,12 @@
|
|||||||
"bar": {
|
"bar": {
|
||||||
"position": "top",
|
"position": "top",
|
||||||
"transparent": false,
|
"transparent": false,
|
||||||
|
"centerAnchor": "omarchy.clock",
|
||||||
"layout": {
|
"layout": {
|
||||||
"left": [
|
"left": [
|
||||||
{
|
{
|
||||||
"id": "omarchy.menu"
|
"id": "omarchy.menu"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "niccoconnor.mantel"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "omarchy.workspaces"
|
"id": "omarchy.workspaces"
|
||||||
},
|
},
|
||||||
@@ -23,92 +21,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"center": [
|
"center": [
|
||||||
|
{
|
||||||
|
"id": "omarchy.indicators"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "omarchy.clock",
|
"id": "omarchy.clock",
|
||||||
"format": "dddd HH:mm",
|
"format": "dddd HH:mm",
|
||||||
"formatAlt": "d MMMM 'W'ww yyyy",
|
"formatAlt": "d MMMM 'W'ww yyyy",
|
||||||
"verticalFormat": "HH\n—\nmm"
|
"verticalFormat": "HH\n—\nmm"
|
||||||
}
|
|
||||||
],
|
|
||||||
"right": [
|
|
||||||
{
|
|
||||||
"id": "omarchy.tray"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "omarchy.keyboard-layout"
|
"id": "omarchy.keyboard-layout"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "izeesoft.omarchy-phone"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "omarchy.spacer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "io.github.juancasanueva.plugin-manager",
|
|
||||||
"tiledExpandedPanel": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "io.github.thisisgm.omapods"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "crmne.hyprmoncfg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ilyazar.btop"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "vstoms.netbird"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "noconnor.agents"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "omarchy.bluetooth"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "omarchy.network"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "omarchy.audio"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "omarchy.monitor"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "omarchy.power"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"centerBegin": [
|
|
||||||
{
|
|
||||||
"id": "omarchy.indicators"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"centerEnd": [
|
|
||||||
{
|
|
||||||
"id": "omarchy.elsewhen",
|
|
||||||
"cities": [
|
|
||||||
{
|
|
||||||
"name": "New York",
|
|
||||||
"zone": "America/New_York"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "London",
|
|
||||||
"zone": "Europe/London"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Dubai",
|
|
||||||
"zone": "Asia/Dubai"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Tokyo",
|
|
||||||
"zone": "Asia/Tokyo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Los Angeles",
|
|
||||||
"zone": "America/Los_Angeles"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "omarchy.weather"
|
"id": "omarchy.weather"
|
||||||
},
|
},
|
||||||
@@ -125,18 +49,46 @@
|
|||||||
"id": "omarchy.system-update"
|
"id": "omarchy.system-update"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tray": []
|
"right": [
|
||||||
|
{
|
||||||
|
"id": "omarchy.tray"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "io.github.thisisgm.omapods"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "crmne.hyprmoncfg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ilyazar.btop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vstoms.netbird"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "omarchy.agents"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "omarchy.bluetooth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "omarchy.network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "omarchy.audio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "omarchy.monitor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "omarchy.power"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"id": "indie.herdr-hud"
|
"id": "indie.herdr-hud"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "niccoconnor.keyhints"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "devmercenario.antigravity"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# omarchy-air
|
||||||
|
|
||||||
|
MacBook Air M2 (`Mac14,2`) running Omarchy **4.0.3rc4-1** on Asahi (aarch64).
|
||||||
|
Captured **2026-10-16**.
|
||||||
|
|
||||||
|
Every file here overlays the shared tree on this host only: `install.sh` installs
|
||||||
|
`hosts/omarchy-air/config/X` instead of `config/X`, and `capture.sh` pulls back
|
||||||
|
into it. Paths the overlay leaves out fall through to the shared files, which come
|
||||||
|
from `omarchy-laptop` (the HP).
|
||||||
|
|
||||||
|
## What is in the overlay, and why
|
||||||
|
|
||||||
|
| File | Why it is per-host |
|
||||||
|
|---|---|
|
||||||
|
| `config/hypr/monitors.lua` | Stock file with `scale = 1.33333` and `GDK_SCALE = 1` for the 2560x1664 Retina panel. The shared one names the HP's Chimei panel and its LG outputs by EDID, which mean nothing here. |
|
||||||
|
| `config/hypr/input.lua` | Stock settings plus the measurement notes showing why per-device `sensitivity`/`accel_profile` rules are inert for lan-mouse's virtual pointer. Air keeps stock input. It does not take the shared file's 3-finger workspace gesture, its `natural_scroll` comment change, or its `unknown-device` flat-accel rules. Drop this file if you want the HP's behaviour here. |
|
||||||
|
| `config/herdr/config.toml` | Stock plus `onboarding = false`. |
|
||||||
|
| `config/git/config` | Stock Omarchy git config plus identity, `fetch.prune`, libsecret, and a `git.nic-oconnor.com` helper that reads `$GITEA_KEY` from the environment. The file holds no secret. |
|
||||||
|
| `config/mimeapps.list` | nvim for text and source files, chromium for web, HEY for `mailto:`, and the `claude-cli` URL handler. |
|
||||||
|
| `system/modprobe.d/hid_apple.conf` | `fnmode=1`, which is what Omarchy's own `fix-fkeys.sh` writes. Without this the shared copy would push the HP's `fnmode=2` onto this keyboard. |
|
||||||
|
| `system/modprobe.d/asahi-notch.conf` | `show_notch=1`, written by Omarchy's `apple/enable-notch.sh`. Tracked so it stays explicit. |
|
||||||
|
|
||||||
|
`hypr/bindings.lua` is deliberately **not** overlaid. The shared file is a strict
|
||||||
|
superset of what air had: the same `SUPER+SHIFT+K` handoff chord plus Claude and
|
||||||
|
herdr-hud bindings.
|
||||||
|
|
||||||
|
## Already shared, identical here
|
||||||
|
|
||||||
|
`bin/omarchy-handoff`, `systemd/user/lan-mouse.service` and
|
||||||
|
`systemd/user/voxtype.service` match the shared copies byte for byte.
|
||||||
|
`handoff/config/config.toml.example-air` covers this machine's lan-mouse config;
|
||||||
|
the live file differs only because the daemon strips comments.
|
||||||
|
|
||||||
|
## Not captured, on purpose
|
||||||
|
|
||||||
|
- **Themes and plugins.** None are installed here. `~/.config/omarchy/plugins/`
|
||||||
|
is empty, so the shared `themes/` and `plugins.txt` would be new on this machine
|
||||||
|
rather than a conflict.
|
||||||
|
- **Packages.** Nothing here was added to `packages.txt`. Beyond the shared list,
|
||||||
|
air's explicit packages are almost all the Asahi baseline that Omarchy's
|
||||||
|
installer provides (`linux-asahi`, `m1n1`, `uboot-asahi`, `asahi-*`,
|
||||||
|
`vulkan-asahi`, `speakersafetyd`, ...). The ones added by hand are
|
||||||
|
`obsidian-appimage`, `wf-recorder`, `omarchy-steam-fex` and `dotnet-runtime-2.1`.
|
||||||
|
Many shared entries are Intel/x86-only (`intel-ucode`, `vulkan-intel`,
|
||||||
|
`lib32-*`, `linux-omarchy`, `sof-firmware`, `thermald`) and some are x86-only
|
||||||
|
AUR binaries (`visual-studio-code-bin`, `hyprmoncfg-bin`, `netbird-bin`,
|
||||||
|
`1password`), so **do not run `install.sh packages` here as-is.**
|
||||||
|
- **`~/.local/bin` tooling** (kubectl, helm, tofu, opencode, op, the agent CLIs,
|
||||||
|
...) and `~/.config/dotfiles/*.env` come from the separate `~/.dotfiles`
|
||||||
|
bootstrap, not Omarchy, and the env files hold secrets.
|
||||||
|
- **Omarchy's own** post-update invitation hooks, `omarchy-asahi-mic.service`
|
||||||
|
and the other `omarchy-*` user units are all installer-provided.
|
||||||
|
- **Chromium `Preferences`** is runtime state, not configuration.
|
||||||
|
|
||||||
|
## Running the scripts here
|
||||||
|
|
||||||
|
`./install.sh --dry-run config system` is safe. It would bring in the shared
|
||||||
|
terminal fonts, `hyprland.lua`, `autostart.lua`, `bindings.lua`, `shell.json`,
|
||||||
|
`shell.toml` and the ARP sysctl, which are all the HP's customizations rather than
|
||||||
|
hardware settings. The `hyprmoncfg` include in `hyprland.lua` is guarded, so it
|
||||||
|
does nothing without the plugin.
|
||||||
|
|
||||||
|
On this host `./capture.sh` refreshes only the overlay. Shared files are skipped
|
||||||
|
unless `CAPTURE_SHARED=1`, because otherwise air's stock copies would overwrite
|
||||||
|
the HP's customizations.
|
||||||
@@ -1,38 +1,34 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Apply an omarchy-setup profile to the machine it runs on.
|
# Apply this repo's Omarchy customizations to the machine it runs on.
|
||||||
#
|
#
|
||||||
# Safe to re-run: every step is a no-op when the target already matches, and any
|
# 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.
|
# file it would overwrite is backed up to <file>.bak.<epoch> first.
|
||||||
#
|
#
|
||||||
# ./install.sh # this machine's profile, every step
|
# ./install.sh # everything
|
||||||
# ./install.sh --profile hp-laptop # pick the profile
|
# ./install.sh config themes # only the named steps
|
||||||
# ./install.sh --roles devops,ai-agents # add roles on top of the profile
|
# ./install.sh --list # show the steps
|
||||||
# ./install.sh config themes # only the named steps
|
# ./install.sh --dry-run # print what would change, touch nothing
|
||||||
# ./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
|
|
||||||
#
|
#
|
||||||
# Exits non-zero if any step reported an error (x).
|
# Steps: packages config themes plugins bin services system
|
||||||
#
|
|
||||||
# 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
|
set -uo pipefail
|
||||||
|
|
||||||
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
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)"
|
STAMP="$(date +%s)"
|
||||||
DRY=0
|
DRY=0
|
||||||
NO_CLOBBER=0
|
STEPS=(packages config themes plugins bin handoff services system)
|
||||||
FORCE=0
|
|
||||||
STEPS=(packages config themes plugins bin handoff services system setup)
|
# Per-host overlay: hosts/<hostname>/{config,system}/ mirror the shared trees, and
|
||||||
|
# a file there replaces the shared copy of the same path on that host only. It is
|
||||||
|
# how hardware-specific files (monitor scale, fnmode) stay off the wrong machine.
|
||||||
|
HOST="${OMARCHY_SETUP_HOST:-$(hostname)}"
|
||||||
|
HOST_DIR="$REPO/hosts/$HOST"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
c_ok=$'\033[32m'; c_skip=$'\033[2m'; c_warn=$'\033[33m'; c_err=$'\033[31m'; c_hd=$'\033[1m'; c_off=$'\033[0m'
|
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=; }
|
[[ -t 1 ]] || { c_ok=; c_skip=; c_warn=; c_err=; c_hd=; c_off=; }
|
||||||
@@ -42,14 +38,10 @@ head_() { printf '\n%s==> %s%s\n' "$c_hd" "$*" "$c_off"; }
|
|||||||
ok() { printf ' %s+%s %s\n' "$c_ok" "$c_off" "$*"; }
|
ok() { printf ' %s+%s %s\n' "$c_ok" "$c_off" "$*"; }
|
||||||
skip() { printf ' %s= %s%s\n' "$c_skip" "$*" "$c_off"; }
|
skip() { printf ' %s= %s%s\n' "$c_skip" "$*" "$c_off"; }
|
||||||
warn() { printf ' %s! %s%s\n' "$c_warn" "$*" "$c_off"; }
|
warn() { printf ' %s! %s%s\n' "$c_warn" "$*" "$c_off"; }
|
||||||
ERRORS=0
|
err() { printf ' %sx %s%s\n' "$c_err" "$*" "$c_off"; }
|
||||||
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; }
|
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 <src> <dest> [mode] -- copy only when content differs, backing up first
|
||||||
install_file() {
|
install_file() {
|
||||||
local src=$1 dest=$2 mode=${3:-}
|
local src=$1 dest=$2 mode=${3:-}
|
||||||
@@ -58,10 +50,6 @@ install_file() {
|
|||||||
skip "$rel"
|
skip "$rel"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if [[ -e $dest ]] && (( NO_CLOBBER )); then
|
|
||||||
warn "$rel differs from the profile - kept (--no-clobber)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if (( DRY )); then
|
if (( DRY )); then
|
||||||
printf ' %s? would write %s%s\n' "$c_skip" "$rel" "$c_off"
|
printf ' %s? would write %s%s\n' "$c_skip" "$rel" "$c_off"
|
||||||
return 0
|
return 0
|
||||||
@@ -79,110 +67,91 @@ install_file() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# hypr_reload -- reload Hyprland if one is running, even from outside the
|
# layered <tree> -- NUL-separated "<src>\t<rel>" for every file in the shared tree
|
||||||
# session (a Coder workspace-start script has no HYPRLAND_INSTANCE_SIGNATURE).
|
# and the host overlay, the host copy winning where both have the same path.
|
||||||
hypr_reload() {
|
layered() {
|
||||||
(( DRY )) && return 0
|
local tree=$1 f rel
|
||||||
command -v hyprctl >/dev/null || return 0
|
declare -A pick=()
|
||||||
local sig=${HYPRLAND_INSTANCE_SIGNATURE:-}
|
for base in "$REPO/$tree" "$HOST_DIR/$tree"; do
|
||||||
if [[ -z $sig ]]; then
|
[[ -d $base ]] || continue
|
||||||
sig=$(hyprctl instances -j 2>/dev/null | jq -r '.[0].instance // empty' 2>/dev/null)
|
while IFS= read -r -d '' f; do
|
||||||
fi
|
rel=${f#"$base/"}; pick[$rel]=$f
|
||||||
[[ -n $sig ]] || return 0
|
done < <(find "$base" -type f -print0)
|
||||||
HYPRLAND_INSTANCE_SIGNATURE=$sig hyprctl reload >/dev/null 2>&1
|
done
|
||||||
local errs; errs=$(HYPRLAND_INSTANCE_SIGNATURE=$sig hyprctl configerrors 2>/dev/null)
|
for rel in "${!pick[@]}"; do printf '%s\t%s\0' "${pick[$rel]}" "$rel"; done | sort -z -t$'\t' -k2
|
||||||
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
|
# ---------------------------------------------------------------- packages
|
||||||
step_packages() {
|
step_packages() {
|
||||||
head_ "Packages"
|
head_ "Packages"
|
||||||
command -v omarchy >/dev/null || { err "omarchy not on PATH - is this an Omarchy system?"; return 1; }
|
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
|
local section="" repo=() aur=() optional=()
|
||||||
for e in "${PKG_repo[@]}"; do
|
while IFS= read -r line; do
|
||||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"
|
line=${line%%#*}; line=${line//[[:space:]]/}
|
||||||
if ! pkg_applies "$archs"; then (( offarch++ )); continue; fi
|
[[ -z $line ]] && continue
|
||||||
if pacman -Qq "$name" &>/dev/null; then (( present++ ))
|
case $line in
|
||||||
elif pkg_cmd_present "$cmds"; then bycmd+=("$name")
|
'[repo]') section=repo; continue ;;
|
||||||
else missing+=("$name"); fi
|
'[aur]') section=aur; continue ;;
|
||||||
done
|
'[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
|
if (( ${#missing[@]} )); then
|
||||||
say " installing from repos: ${missing[*]}"
|
say " installing from repos: ${missing[*]}"
|
||||||
run omarchy pkg add "${missing[@]}" || err "some repo packages failed"
|
run omarchy pkg add "${missing[@]}" || err "some repo packages failed"
|
||||||
else
|
else
|
||||||
skip "all $present repo packages present"
|
skip "all ${#repo[@]} repo packages present"
|
||||||
fi
|
fi
|
||||||
(( ${#bycmd[@]} )) && skip "already on PATH from elsewhere, not installed: ${bycmd[*]}"
|
|
||||||
(( offarch )) && skip "$offarch not built for $(uname -m), skipped"
|
|
||||||
|
|
||||||
missing=()
|
missing=()
|
||||||
for e in "${PKG_aur[@]}"; do
|
for p in "${aur[@]}"; do pacman -Qq "$p" &>/dev/null || missing+=("$p"); done
|
||||||
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
|
if (( ${#missing[@]} )); then
|
||||||
say " installing from AUR: ${missing[*]}"
|
say " installing from AUR: ${missing[*]}"
|
||||||
run omarchy pkg aur add "${missing[@]}" || err "some AUR packages failed"
|
run omarchy pkg aur add "${missing[@]}" || err "some AUR packages failed"
|
||||||
else
|
else
|
||||||
skip "all ${#PKG_aur[@]} AUR packages present"
|
skip "all ${#aur[@]} AUR packages present"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for e in "${PKG_optional[@]}"; do
|
for p in "${optional[@]}"; do
|
||||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"
|
pacman -Qq "$p" &>/dev/null || warn "optional, not installed: $p (omarchy pkg add $p)"
|
||||||
pkg_applies "$archs" || continue
|
|
||||||
pacman -Qq "$name" &>/dev/null || warn "optional, not installed: $name (omarchy pkg add $name)"
|
|
||||||
done
|
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
|
# ---------------------------------------------------------------- config
|
||||||
step_config() {
|
step_config() {
|
||||||
head_ "Config files -> ~/.config"
|
head_ "Config files -> ~/.config"
|
||||||
local rel layer
|
[[ -d $HOST_DIR/config ]] && say " host overlay: hosts/$HOST/config"
|
||||||
declare -A mine=()
|
local src rel
|
||||||
while IFS=$'\t' read -r rel layer; do
|
while IFS=$'\t' read -r -d '' src rel; do
|
||||||
install_file "$PROFILES/$layer/config/$rel" "$HOME/.config/$rel"
|
install_file "$src" "$HOME/.config/$rel"
|
||||||
mine[$rel]=1
|
done < <(layered config)
|
||||||
done < <(owners config)
|
|
||||||
|
|
||||||
# A hypr/layers/ file that no layer of this profile provides is left over from
|
if (( ! DRY )) && command -v hyprctl >/dev/null && [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then
|
||||||
# a role or profile that no longer applies. It still loads, so say so.
|
hyprctl reload >/dev/null 2>&1
|
||||||
local f
|
local errs; errs=$(hyprctl configerrors 2>/dev/null)
|
||||||
for f in "$HOME"/.config/hypr/layers/*.lua; do
|
if [[ -n $errs && $errs != "no errors"* ]]; then
|
||||||
[[ -e $f ]] || continue
|
err "Hyprland reported config errors:"; printf '%s\n' "$errs" | sed 's/^/ /'
|
||||||
[[ -n ${mine[hypr/layers/${f##*/}]:-} ]] && continue
|
else
|
||||||
warn "~/.config/hypr/layers/${f##*/} is from no layer of this profile - still loaded; rm it if unwanted"
|
ok "hyprctl reload - no config errors"
|
||||||
done
|
fi
|
||||||
|
fi
|
||||||
hypr_reload
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------- themes
|
# ---------------------------------------------------------------- themes
|
||||||
step_themes() {
|
step_themes() {
|
||||||
head_ "Themes -> ~/.config/omarchy/themes"
|
head_ "Themes -> ~/.config/omarchy/themes"
|
||||||
local l t name dest
|
local t name dest
|
||||||
declare -A theme=()
|
for t in "$REPO"/themes/*/; do
|
||||||
for l in "${LAYERS[@]}"; do
|
[[ -d $t ]] || continue
|
||||||
for t in "$PROFILES/$l"/themes/*/; do
|
name=$(basename -- "$t")
|
||||||
[[ -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"
|
dest="$HOME/.config/omarchy/themes/$name"
|
||||||
if [[ -d $dest ]] && diff -rq "$t" "$dest" >/dev/null 2>&1; then
|
if [[ -d $dest ]] && diff -rq "$t" "$dest" >/dev/null 2>&1; then
|
||||||
skip "$name"
|
skip "$name"
|
||||||
@@ -192,103 +161,77 @@ step_themes() {
|
|||||||
ok "$name"
|
ok "$name"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if (( ${#P_AETHER[@]} )); then
|
say ""
|
||||||
say ""
|
say " Themes managed by aether (not in this repo - reinstall with \`aether\`):"
|
||||||
say " Themes managed by aether (not in this repo - reinstall with \`aether\`):"
|
say " pink-gt3rs porsche-cayman porsche-rain"
|
||||||
say " ${P_AETHER[*]}"
|
say " Apply one with: omarchy theme set <name>"
|
||||||
fi
|
|
||||||
(( ${#theme[@]} )) && say " Apply one with: omarchy theme set <name>"
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------- plugins
|
# ---------------------------------------------------------------- plugins
|
||||||
step_plugins() {
|
step_plugins() {
|
||||||
head_ "Shell plugins -> ~/.config/omarchy/plugins"
|
head_ "Shell plugins -> ~/.config/omarchy/plugins"
|
||||||
local dir="$HOME/.config/omarchy/plugins" l
|
local dir="$HOME/.config/omarchy/plugins"
|
||||||
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"
|
[[ -d $dir ]] || run mkdir -p "$dir"
|
||||||
|
|
||||||
local p
|
local id url sha p
|
||||||
for id in $(printf '%s\n' "${!url[@]}" | sort); do
|
while IFS=$'\t' read -r id url sha; do
|
||||||
p="$dir/$id"; s=${sha[$id]}
|
[[ -z ${id:-} || $id == \#* ]] && continue
|
||||||
|
p="$dir/$id"
|
||||||
if [[ -d $p/.git ]]; then
|
if [[ -d $p/.git ]]; then
|
||||||
if [[ $(git -C "$p" rev-parse HEAD 2>/dev/null) == "$s" ]]; then
|
if [[ $(git -C "$p" rev-parse HEAD 2>/dev/null) == "$sha" ]]; then
|
||||||
skip "$id @ ${s:0:7}"
|
skip "$id @ ${sha:0:7}"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if [[ -n $(git -C "$p" status --porcelain 2>/dev/null) ]]; then
|
if [[ -n $(git -C "$p" status --porcelain 2>/dev/null) ]]; then
|
||||||
warn "$id has local changes - leaving it alone (wanted ${s:0:7})"
|
warn "$id has local changes - leaving it alone (wanted ${sha:0:7})"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
run git -C "$p" fetch --quiet origin
|
run git -C "$p" fetch --quiet origin
|
||||||
run git -C "$p" checkout --quiet "$s" && ok "$id -> ${s:0:7}"
|
run git -C "$p" checkout --quiet "$sha" && ok "$id -> ${sha:0:7}"
|
||||||
else
|
else
|
||||||
if (( DRY )); then
|
if (( DRY )); then
|
||||||
printf ' %s? would clone %s -> %s%s\n' "$c_skip" "$id" "${s:0:7}" "$c_off"
|
printf ' %s? would clone %s -> %s%s\n' "$c_skip" "$id" "${sha:0:7}" "$c_off"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
git clone --quiet "${url[$id]}" "$p" || { err "clone failed: $id (${url[$id]})"; continue; }
|
git clone --quiet "$url" "$p" || { err "clone failed: $id ($url)"; continue; }
|
||||||
git -C "$p" checkout --quiet "$s" || warn "$id: sha $s not found, left on default branch"
|
git -C "$p" checkout --quiet "$sha" || warn "$id: sha $sha not found, left on default branch"
|
||||||
ok "$id @ ${s:0:7}"
|
ok "$id @ ${sha:0:7}"
|
||||||
fi
|
fi
|
||||||
done
|
done < "$REPO/plugins.txt"
|
||||||
|
|
||||||
# Local plugin work that exists on no remote, per layer:
|
apply_herdr_hud
|
||||||
# <id>.bundle a 'local' branch, replayed and checked out
|
apply_worktree_patch io.github.tallsam.navbar-cat
|
||||||
# <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 ""
|
||||||
say " Bar placement lives in config/omarchy/shell.json, installed by the config"
|
say " Bar placement lives in config/omarchy/shell.json, already installed by the"
|
||||||
say " step. The shell hot-reloads it; \`omarchy restart shell\` if not."
|
say " config step. The shell hot-reloads it; \`omarchy restart shell\` if not."
|
||||||
}
|
}
|
||||||
|
|
||||||
# apply_bundle <id> <bundle> -- replay a plugin's 'local' branch. indie.herdr-hud
|
# indie.herdr-hud carries 10 commits that intentionally never went to the
|
||||||
# carries commits that intentionally never went to the upstream PR.
|
# upstream PR, plus an uncommitted working tree. Both are replayed here.
|
||||||
apply_bundle() {
|
apply_herdr_hud() {
|
||||||
local id=$1 bundle=$2
|
local p="$HOME/.config/omarchy/plugins/indie.herdr-hud"
|
||||||
local p="$HOME/.config/omarchy/plugins/$id"
|
local bundle="$REPO/plugin-patches/indie.herdr-hud.bundle"
|
||||||
[[ -d $p/.git ]] || return 0
|
[[ -d $p/.git && -f $bundle ]] || return 0
|
||||||
|
|
||||||
if git -C "$p" rev-parse --verify --quiet local >/dev/null; then
|
if git -C "$p" rev-parse --verify --quiet local >/dev/null; then
|
||||||
skip "$id: 'local' branch already present"
|
skip "indie.herdr-hud: 'local' branch already present"
|
||||||
elif (( DRY )); then
|
elif (( DRY )); then
|
||||||
printf ' %s? would replay the %s local branch%s\n' "$c_skip" "$id" "$c_off"
|
printf ' %s? would replay the herdr-hud local branch%s\n' "$c_skip" "$c_off"
|
||||||
|
return 0
|
||||||
else
|
else
|
||||||
git -C "$p" fetch --quiet "$bundle" 'refs/heads/local:refs/heads/local' \
|
git -C "$p" fetch --quiet "$bundle" 'refs/heads/local:refs/heads/local' \
|
||||||
&& { git -C "$p" checkout --quiet local && ok "$id: replayed 'local' branch"; } \
|
&& { git -C "$p" checkout --quiet local && ok "indie.herdr-hud: replayed 'local' branch"; } \
|
||||||
|| warn "$id: could not replay the local branch from the bundle"
|
|| warn "indie.herdr-hud: could not replay the local branch from the bundle"
|
||||||
fi
|
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() {
|
apply_worktree_patch() {
|
||||||
local id=$1 patch=$2
|
local id=$1
|
||||||
local p="$HOME/.config/omarchy/plugins/$id"
|
local p="$HOME/.config/omarchy/plugins/$id"
|
||||||
|
local patch="$REPO/plugin-patches/$id-worktree.patch"
|
||||||
[[ -d $p/.git && -s $patch ]] || return 0
|
[[ -d $p/.git && -s $patch ]] || return 0
|
||||||
if git -C "$p" apply --reverse --check "$patch" &>/dev/null; then
|
if git -C "$p" apply --reverse --check "$patch" &>/dev/null; then
|
||||||
skip "$id: working-tree patch already applied"
|
skip "$id: working-tree patch already applied"
|
||||||
@@ -304,74 +247,52 @@ apply_worktree_patch() {
|
|||||||
# ---------------------------------------------------------------- bin
|
# ---------------------------------------------------------------- bin
|
||||||
step_bin() {
|
step_bin() {
|
||||||
head_ "Scripts -> ~/.local/bin"
|
head_ "Scripts -> ~/.local/bin"
|
||||||
local rel layer n=0
|
local f
|
||||||
while IFS=$'\t' read -r rel layer; do
|
for f in "$REPO"/bin/*; do
|
||||||
install_file "$PROFILES/$layer/bin/$rel" "$HOME/.local/bin/$rel" 755
|
[[ -f $f ]] || continue
|
||||||
(( n++ ))
|
install_file "$f" "$HOME/.local/bin/$(basename -- "$f")" 755
|
||||||
done < <(owners bin)
|
done
|
||||||
(( n )) || { skip "no layer carries scripts"; return 0; }
|
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*":$HOME/.local/bin:"*) ;;
|
*":$HOME/.local/bin:"*) ;;
|
||||||
*) warn "~/.local/bin is not on PATH" ;;
|
*) warn "~/.local/bin is not on PATH" ;;
|
||||||
esac
|
esac
|
||||||
[[ -n $(layers_with bin/fix-stuck-displays) ]] && {
|
say ""
|
||||||
say ""
|
say " fix-stuck-displays needs passwordless sudo for 'tee /sys/class/drm/*/status'."
|
||||||
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"
|
||||||
return 0
|
say " Its config (~/.config/lan-mouse/) holds a private key and per-machine"
|
||||||
|
say " fingerprints, so it is not in this repo by design."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------- services
|
# ---------------------------------------------------------------- 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() {
|
step_services() {
|
||||||
head_ "systemd user units"
|
head_ "systemd user units"
|
||||||
local rel layer names=()
|
local f name changed=0
|
||||||
while IFS=$'\t' read -r rel layer; do
|
for f in "$REPO"/systemd/user/*.service; do
|
||||||
[[ $rel == *.service ]] && names+=("$rel")
|
[[ -f $f ]] || continue
|
||||||
done < <(owners systemd/user)
|
name=$(basename -- "$f")
|
||||||
(( ${#names[@]} )) || { skip "no layer carries units"; return 0; }
|
local dest="$HOME/.config/systemd/user/$name"
|
||||||
UNITS_CHANGED=0
|
[[ -e $dest ]] && cmp -s "$f" "$dest" || changed=1
|
||||||
for rel in "${names[@]}"; do install_unit "$rel"; done
|
install_file "$f" "$dest"
|
||||||
(( UNITS_CHANGED )) && run systemctl --user daemon-reload 2>/dev/null
|
done
|
||||||
for rel in "${names[@]}"; do enable_unit "$rel"; 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
|
||||||
say ""
|
say ""
|
||||||
say " Units are WantedBy=graphical-session.target, so they start with Hyprland"
|
say " Both are WantedBy=graphical-session.target, so they start with Hyprland"
|
||||||
say " rather than now. 'systemctl --user start <unit>' to bring one up early."
|
say " rather than now. 'systemctl --user start <unit>' to bring one up early."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------- handoff
|
# ---------------------------------------------------------------- handoff
|
||||||
step_handoff() {
|
step_handoff() {
|
||||||
head_ "omarchy-handoff (lan-mouse)"
|
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
|
# Self-sufficient on purpose: `install.sh handoff` alone brings input sharing up
|
||||||
# on an otherwise stock machine, without the packages/config/themes steps that
|
# on an otherwise stock machine, without the packages/config/themes steps that
|
||||||
@@ -379,8 +300,8 @@ step_handoff() {
|
|||||||
# the full install.sh afterwards does no extra work.
|
# the full install.sh afterwards does no extra work.
|
||||||
# lan-mouse: the signed package, for the GUI/CLI and as the unpatched fallback.
|
# lan-mouse: the signed package, for the GUI/CLI and as the unpatched fallback.
|
||||||
# rust: only to compile handoff/patch/, removable afterwards.
|
# rust: only to compile handoff/patch/, removable afterwards.
|
||||||
# NOT python-evdev: omarchy-air runs this whole setup without it installed, so
|
# NOT python-evdev: packages.txt calls it a dependency, but omarchy-air runs
|
||||||
# it is not required for sharing.
|
# this whole setup without it installed, so it is not required for sharing.
|
||||||
local need=() p
|
local need=() p
|
||||||
for p in lan-mouse rust; do
|
for p in lan-mouse rust; do
|
||||||
pacman -Qq "$p" &>/dev/null || need+=("$p")
|
pacman -Qq "$p" &>/dev/null || need+=("$p")
|
||||||
@@ -392,11 +313,11 @@ step_handoff() {
|
|||||||
skip "lan-mouse, rust"
|
skip "lan-mouse, rust"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
install_file "$PROFILES/$layer/bin/omarchy-handoff" "$HOME/.local/bin/omarchy-handoff" 0755
|
install_file "$REPO/bin/omarchy-handoff" "$HOME/.local/bin/omarchy-handoff" 0755
|
||||||
|
|
||||||
# 1. The patched daemon. lan-mouse.service points at this path, so it has to
|
# 1. The patched daemon. systemd/user/lan-mouse.service points at this path, so
|
||||||
# exist before the unit is enabled. Per-architecture: the HP is x86_64 and
|
# it has to exist before the services step enables the unit. Per-architecture:
|
||||||
# omarchy-air is aarch64, so each builds its own.
|
# this laptop is x86_64 and omarchy-air is aarch64, so each builds its own.
|
||||||
if [[ -x $HOME/.local/bin/lan-mouse-patched ]]; then
|
if [[ -x $HOME/.local/bin/lan-mouse-patched ]]; then
|
||||||
skip "~/.local/bin/lan-mouse-patched (delete it to force a rebuild)"
|
skip "~/.local/bin/lan-mouse-patched (delete it to force a rebuild)"
|
||||||
elif ! command -v cargo >/dev/null; then
|
elif ! command -v cargo >/dev/null; then
|
||||||
@@ -405,27 +326,27 @@ step_handoff() {
|
|||||||
printf ' %s? would build ~/.local/bin/lan-mouse-patched from handoff/patch/%s\n' "$c_skip" "$c_off"
|
printf ' %s? would build ~/.local/bin/lan-mouse-patched from handoff/patch/%s\n' "$c_skip" "$c_off"
|
||||||
else
|
else
|
||||||
say " building patched daemon from handoff/patch/ (~1 min; see handoff/patch/REBUILD.md for why)"
|
say " building patched daemon from handoff/patch/ (~1 min; see handoff/patch/REBUILD.md for why)"
|
||||||
if "$hd/patch/build.sh"; then ok "~/.local/bin/lan-mouse-patched"
|
if "$REPO/handoff/patch/build.sh"; then ok "~/.local/bin/lan-mouse-patched"
|
||||||
else err "build failed - the unit will not start until this succeeds"; fi
|
else err "build failed - the unit will not start until this succeeds"; fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. The unit (and this machine's drop-ins, e.g. the Air's pointer scale).
|
# 2. The unit, which points at the binary built above.
|
||||||
UNITS_CHANGED=0
|
install_file "$REPO/systemd/user/lan-mouse.service" \
|
||||||
install_unit lan-mouse.service
|
"$HOME/.config/systemd/user/lan-mouse.service" 0644
|
||||||
(( UNITS_CHANGED )) && run systemctl --user daemon-reload 2>/dev/null
|
if (( ! DRY )); then
|
||||||
enable_unit lan-mouse.service
|
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
|
||||||
|
|
||||||
# 3. The firewall. THE bug that cost a full day: ufw is default-deny inbound, so
|
# 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
|
# the peer's DTLS handshake dies at the wire and presents EXACTLY like a bad
|
||||||
# certificate. Authorization is receiver-side only (connect.rs sets
|
# certificate. Authorization is receiver-side only (connect.rs sets
|
||||||
# insecure_skip_verify), so the RECEIVER's firewall must allow 4242/udp or
|
# insecure_skip_verify), so the RECEIVER's firewall must allow 4242/udp or
|
||||||
# that direction is silently dead.
|
# 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
|
if ! command -v ufw >/dev/null; then
|
||||||
skip "ufw not installed - nothing to open"
|
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
|
else
|
||||||
local ip rules=""
|
local ip rules=""
|
||||||
# Both reading and changing the ruleset need root. If sudo can't run without
|
# Both reading and changing the ruleset need root. If sudo can't run without
|
||||||
@@ -433,7 +354,7 @@ step_handoff() {
|
|||||||
# instead of emitting raw sudo errors and half-applying.
|
# instead of emitting raw sudo errors and half-applying.
|
||||||
if (( ! DRY )) && ! sudo -n true 2>/dev/null && [[ ! -t 0 ]]; then
|
if (( ! DRY )) && ! sudo -n true 2>/dev/null && [[ ! -t 0 ]]; then
|
||||||
warn "sudo needs a password and there is no terminal; run these yourself:"
|
warn "sudo needs a password and there is no terminal; run these yourself:"
|
||||||
for ip in "${ips[@]}"; do
|
for ip in "${HANDOFF_PEER_IPS[@]}"; do
|
||||||
say " sudo ufw allow from $ip to any port 4242 proto udp comment 'lan-mouse peer $ip'"
|
say " sudo ufw allow from $ip to any port 4242 proto udp comment 'lan-mouse peer $ip'"
|
||||||
done
|
done
|
||||||
say ""
|
say ""
|
||||||
@@ -442,7 +363,7 @@ step_handoff() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
(( DRY )) || rules=$(sudo ufw status 2>/dev/null || true)
|
(( DRY )) || rules=$(sudo ufw status 2>/dev/null || true)
|
||||||
for ip in "${ips[@]}"; do
|
for ip in "${HANDOFF_PEER_IPS[@]}"; do
|
||||||
if [[ $rules == *"4242"*"$ip"* ]]; then
|
if [[ $rules == *"4242"*"$ip"* ]]; then
|
||||||
skip "ufw allows 4242/udp from $ip"
|
skip "ufw allows 4242/udp from $ip"
|
||||||
elif (( DRY )); then
|
elif (( DRY )); then
|
||||||
@@ -458,7 +379,7 @@ step_handoff() {
|
|||||||
|
|
||||||
say ""
|
say ""
|
||||||
say " ~/.config/lan-mouse/ is NOT in this repo (private key + per-machine"
|
say " ~/.config/lan-mouse/ is NOT in this repo (private key + per-machine"
|
||||||
say " fingerprints). Start from $layer/handoff/config/config.toml.example-* and:"
|
say " fingerprints). Start from handoff/config/config.toml.example-* and:"
|
||||||
say " omarchy-handoff fingerprint # give it to the peer, LOWERCASE"
|
say " omarchy-handoff fingerprint # give it to the peer, LOWERCASE"
|
||||||
say " omarchy-handoff pair <peer> <lowercase-sha256>"
|
say " omarchy-handoff pair <peer> <lowercase-sha256>"
|
||||||
say " omarchy-handoff doctor # must pass before arming"
|
say " omarchy-handoff doctor # must pass before arming"
|
||||||
@@ -468,12 +389,12 @@ step_handoff() {
|
|||||||
|
|
||||||
# ---------------------------------------------------------------- system
|
# ---------------------------------------------------------------- system
|
||||||
step_system() {
|
step_system() {
|
||||||
head_ "System drop-ins -> /etc (needs sudo)"
|
head_ "System drop-ins (needs sudo)"
|
||||||
local rel layer dest n=0 sysctl=0 modprobe=0
|
[[ -d $HOST_DIR/system ]] && say " host overlay: hosts/$HOST/system"
|
||||||
while IFS=$'\t' read -r rel layer; do
|
local src rel dest
|
||||||
(( n++ ))
|
while IFS=$'\t' read -r -d '' src rel; do
|
||||||
dest="/etc/$rel"
|
dest="/etc/$rel"
|
||||||
if [[ -e $dest ]] && cmp -s "$PROFILES/$layer/system/$rel" "$dest"; then
|
if [[ -e $dest ]] && cmp -s "$src" "$dest"; then
|
||||||
skip "$dest"
|
skip "$dest"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
@@ -481,85 +402,27 @@ step_system() {
|
|||||||
printf ' %s? would write %s%s\n' "$c_skip" "$dest" "$c_off"
|
printf ' %s? would write %s%s\n' "$c_skip" "$dest" "$c_off"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
sudo install -D -m 0644 "$PROFILES/$layer/system/$rel" "$dest" && ok "$dest"
|
sudo install -D -m 0644 "$src" "$dest" && ok "$dest"
|
||||||
[[ $rel == sysctl.d/* ]] && sysctl=1
|
done < <(layered system)
|
||||||
[[ $rel == modprobe.d/* ]] && modprobe=1
|
|
||||||
done < <(owners system)
|
|
||||||
(( n )) || { skip "no layer carries /etc drop-ins"; return 0; }
|
|
||||||
|
|
||||||
(( sysctl )) && sudo sysctl --system >/dev/null 2>&1 && ok "sysctl reloaded"
|
if (( ! DRY )); then
|
||||||
if (( modprobe )); then
|
sudo sysctl --system >/dev/null 2>&1 && ok "sysctl reloaded"
|
||||||
say ""
|
say ""
|
||||||
say " modprobe.d options (hid_apple fnmode, appledrm notch) only take effect once"
|
say " hid_apple.conf (fnmode) only takes effect once the"
|
||||||
say " the initramfs is rebuilt or the module reloads:"
|
say " initramfs is rebuilt or the module reloads:"
|
||||||
say " sudo mkinitcpio -P # or: sudo modprobe -r <module> && sudo modprobe <module>"
|
say " sudo mkinitcpio -P # or: sudo modprobe -r hid_apple && sudo modprobe hid_apple"
|
||||||
fi
|
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
|
||||||
main() {
|
main() {
|
||||||
local want=() profile="" roles_arg="" roles_given=0 show_only=0 a
|
local want=()
|
||||||
while (( $# )); do
|
for a in "$@"; do
|
||||||
a=$1; shift
|
|
||||||
case $a in
|
case $a in
|
||||||
--dry-run|-n) DRY=1 ;;
|
--dry-run|-n) DRY=1 ;;
|
||||||
--no-clobber) NO_CLOBBER=1 ;;
|
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
|
||||||
--force) FORCE=1 ;;
|
-h|--help) sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
--profile|-p) profile=${1:?--profile needs a name}; shift ;;
|
-*) err "unknown option: $a"; exit 2 ;;
|
||||||
--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) 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")
|
if [[ " ${STEPS[*]} " == *" $a "* ]]; then want+=("$a")
|
||||||
else err "unknown step: $a (one of: ${STEPS[*]})"; exit 2; fi ;;
|
else err "unknown step: $a (one of: ${STEPS[*]})"; exit 2; fi ;;
|
||||||
@@ -567,49 +430,15 @@ main() {
|
|||||||
done
|
done
|
||||||
(( ${#want[@]} )) || want=("${STEPS[@]}")
|
(( ${#want[@]} )) || want=("${STEPS[@]}")
|
||||||
|
|
||||||
local saved_profile="" saved_roles=""
|
say "${c_hd}omarchy-setup${c_off} repo=$REPO host=$HOST"
|
||||||
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"
|
(( 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"
|
command -v omarchy >/dev/null || warn "omarchy not found; this looks like a non-Omarchy host"
|
||||||
|
|
||||||
if [[ -n $P_ARCH && $P_ARCH != "$(uname -m)" ]]; then
|
for s in "${want[@]}"; do "step_$s"; done
|
||||||
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"
|
head_ "Done"
|
||||||
say " Re-running is safe. Backups from this run are suffixed .bak.$STAMP"
|
say " Re-running is safe. Backups from this run are suffixed .bak.$STAMP"
|
||||||
say " Pull live changes back into the repo with ./capture.sh"
|
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 "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
# 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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# 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,5 +1,5 @@
|
|||||||
diff --git i/HerdrHud.qml w/HerdrHud.qml
|
diff --git i/HerdrHud.qml w/HerdrHud.qml
|
||||||
index 018311d..4a73ee0 100644
|
index 018311d..d01449c 100644
|
||||||
--- i/HerdrHud.qml
|
--- i/HerdrHud.qml
|
||||||
+++ w/HerdrHud.qml
|
+++ w/HerdrHud.qml
|
||||||
@@ -7,6 +7,7 @@ import Quickshell.Wayland
|
@@ -7,6 +7,7 @@ import Quickshell.Wayland
|
||||||
@@ -10,12 +10,11 @@ index 018311d..4a73ee0 100644
|
|||||||
import "."
|
import "."
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -21,12 +22,14 @@ Item {
|
@@ -21,12 +22,13 @@ Item {
|
||||||
readonly property string pluginId: (manifest && manifest.id) || "indie.herdr-hud"
|
readonly property string pluginId: (manifest && manifest.id) || "indie.herdr-hud"
|
||||||
readonly property string pluginDir: (manifest && manifest.__sourceDir) || ""
|
readonly property string pluginDir: (manifest && manifest.__sourceDir) || ""
|
||||||
readonly property string bridgePath: decodeURIComponent(String(Qt.resolvedUrl("bin/herdr-monitor")).replace(/^file:\/\//, ""))
|
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 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 homeDir: Quickshell.env("HOME")
|
||||||
readonly property string configDir: homeDir + "/.config/herdr-hud-native"
|
readonly property string configDir: homeDir + "/.config/herdr-hud-native"
|
||||||
readonly property string statePath: configDir + "/state.json"
|
readonly property string statePath: configDir + "/state.json"
|
||||||
@@ -26,7 +25,7 @@ index 018311d..4a73ee0 100644
|
|||||||
property bool overlayVisible: true
|
property bool overlayVisible: true
|
||||||
property bool openingRequested: false
|
property bool openingRequested: false
|
||||||
property bool demoMode: false
|
property bool demoMode: false
|
||||||
@@ -35,10 +38,136 @@ Item {
|
@@ -35,10 +37,136 @@ Item {
|
||||||
property var agents: []
|
property var agents: []
|
||||||
property string selectedPane: ""
|
property string selectedPane: ""
|
||||||
property var unread: ({})
|
property var unread: ({})
|
||||||
@@ -167,25 +166,7 @@ index 018311d..4a73ee0 100644
|
|||||||
property var lastSequence: ({})
|
property var lastSequence: ({})
|
||||||
property var alertQueue: []
|
property var alertQueue: []
|
||||||
property bool alertsEnabled: true
|
property bool alertsEnabled: true
|
||||||
@@ -66,6 +195,10 @@ Item {
|
@@ -190,6 +318,9 @@ 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.rosterWidth) > 0) rosterWidth = clamp(Number(parsed.rosterWidth), 150, 330)
|
||||||
if (Number(parsed.panelWidth) > 0) panelWidth = Math.max(minPanelWidth, Number(parsed.panelWidth))
|
if (Number(parsed.panelWidth) > 0) panelWidth = Math.max(minPanelWidth, Number(parsed.panelWidth))
|
||||||
if (Number(parsed.panelHeight) > 0) panelHeight = Math.max(minPanelHeight, Number(parsed.panelHeight))
|
if (Number(parsed.panelHeight) > 0) panelHeight = Math.max(minPanelHeight, Number(parsed.panelHeight))
|
||||||
@@ -195,14 +176,7 @@ index 018311d..4a73ee0 100644
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
positions = ({})
|
positions = ({})
|
||||||
@@ -203,13 +340,17 @@ Item {
|
@@ -209,7 +340,10 @@ Item {
|
||||||
stateFile.setText(JSON.stringify({
|
|
||||||
version: 1,
|
|
||||||
uiMode: uiMode,
|
|
||||||
+ viewMode: viewMode,
|
|
||||||
alertsEnabled: alertsEnabled,
|
|
||||||
launcherStyle: launcherStyle,
|
|
||||||
overlayVisible: overlayVisible,
|
|
||||||
positions: positions,
|
positions: positions,
|
||||||
rosterWidth: Math.round(rosterWidth),
|
rosterWidth: Math.round(rosterWidth),
|
||||||
panelWidth: Math.round(panelWidth),
|
panelWidth: Math.round(panelWidth),
|
||||||
@@ -214,32 +188,7 @@ index 018311d..4a73ee0 100644
|
|||||||
}, null, 2) + "\n")
|
}, null, 2) + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +508,24 @@ Item {
|
@@ -486,9 +620,10 @@ 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
|
return muted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,17 +202,7 @@ index 018311d..4a73ee0 100644
|
|||||||
return type.charAt(0).toUpperCase() + type.slice(1)
|
return type.charAt(0).toUpperCase() + type.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,7 +713,8 @@ Item {
|
@@ -581,6 +716,7 @@ 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 nextUnread = cloneObject(unread)
|
||||||
var nextSequence = ({})
|
var nextSequence = ({})
|
||||||
var nextWorkingSince = ({})
|
var nextWorkingSince = ({})
|
||||||
@@ -271,7 +210,7 @@ index 018311d..4a73ee0 100644
|
|||||||
var live = ({})
|
var live = ({})
|
||||||
var previousByPane = ({})
|
var previousByPane = ({})
|
||||||
for (var p = 0; p < agents.length; p++) previousByPane[String(agents[p].pane_id || "")] = agents[p]
|
for (var p = 0; p < agents.length; p++) previousByPane[String(agents[p].pane_id || "")] = agents[p]
|
||||||
@@ -595,13 +757,20 @@ Item {
|
@@ -595,13 +731,20 @@ Item {
|
||||||
nextWorkingSince[identity] = workingSince[identity] || Date.now()
|
nextWorkingSince[identity] = workingSince[identity] || Date.now()
|
||||||
}
|
}
|
||||||
live[pane] = true
|
live[pane] = true
|
||||||
@@ -292,14 +231,13 @@ index 018311d..4a73ee0 100644
|
|||||||
workingSince = nextWorkingSince
|
workingSince = nextWorkingSince
|
||||||
activityNow = Date.now()
|
activityNow = Date.now()
|
||||||
var currentAgent = agentForPane(selectedPane)
|
var currentAgent = agentForPane(selectedPane)
|
||||||
@@ -616,7 +785,13 @@ Item {
|
@@ -616,7 +759,12 @@ Item {
|
||||||
}
|
}
|
||||||
completed.forEach(function(agent) { root.queueAlert(agent) })
|
completed.forEach(function(agent) { root.queueAlert(agent) })
|
||||||
errorText = ""
|
errorText = ""
|
||||||
- if (!agentForPane(selectedPane)) selectAgent(rows.length ? String(rows[0].pane_id || "") : "")
|
- if (!agentForPane(selectedPane)) selectAgent(rows.length ? String(rows[0].pane_id || "") : "")
|
||||||
+ if (pendingPane && live[pendingPane]) {
|
+ if (pendingPane && live[pendingPane]) {
|
||||||
+ selectAgent(pendingPane)
|
+ selectAgent(pendingPane)
|
||||||
+ if (webView && opened) openInWeb(pendingPane)
|
|
||||||
+ pendingPane = ""
|
+ pendingPane = ""
|
||||||
+ }
|
+ }
|
||||||
+ if (confirmClosePane && !live[confirmClosePane]) confirmClosePane = ""
|
+ if (confirmClosePane && !live[confirmClosePane]) confirmClosePane = ""
|
||||||
@@ -307,7 +245,7 @@ index 018311d..4a73ee0 100644
|
|||||||
dataRevision++
|
dataRevision++
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
alertBaseline = false
|
alertBaseline = false
|
||||||
@@ -627,6 +802,86 @@ Item {
|
@@ -627,6 +775,86 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,18 +332,7 @@ index 018311d..4a73ee0 100644
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
mkdirProc.running = true
|
mkdirProc.running = true
|
||||||
Qt.callLater(root.refreshRoster)
|
Qt.callLater(root.refreshRoster)
|
||||||
@@ -637,8 +892,8 @@ Item {
|
@@ -689,6 +917,21 @@ 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) }
|
onExited: function(exitCode) { root.applyRoster(rosterOut.text, rosterErr.text, exitCode) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,59 +354,7 @@ index 018311d..4a73ee0 100644
|
|||||||
property string previewIdentity: ""
|
property string previewIdentity: ""
|
||||||
Process {
|
Process {
|
||||||
id: alertPreviewProc
|
id: alertPreviewProc
|
||||||
@@ -786,6 +1056,11 @@ Item {
|
@@ -1034,6 +1277,186 @@ 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.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
@@ -666,7 +541,7 @@ index 018311d..4a73ee0 100644
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 0
|
spacing: 0
|
||||||
@@ -1051,15 +1532,36 @@ Item {
|
@@ -1051,15 +1474,36 @@ Item {
|
||||||
anchors.margins: 8
|
anchors.margins: 8
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
@@ -711,7 +586,7 @@ index 018311d..4a73ee0 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
@@ -1093,12 +1595,16 @@ Item {
|
@@ -1093,12 +1537,16 @@ Item {
|
||||||
z: 1
|
z: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,13 +598,13 @@ index 018311d..4a73ee0 100644
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
- onClicked: root.selectAgent(String(agentRow.modelData.pane_id || ""))
|
- onClicked: root.selectAgent(String(agentRow.modelData.pane_id || ""))
|
||||||
+ onClicked: root.activateAgent(agentRow.pane)
|
+ onClicked: root.selectAgent(agentRow.pane)
|
||||||
}
|
}
|
||||||
+ HoverHandler { id: rowHover }
|
+ HoverHandler { id: rowHover }
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -1123,7 +1629,7 @@ Item {
|
@@ -1123,7 +1571,7 @@ Item {
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -738,7 +613,7 @@ index 018311d..4a73ee0 100644
|
|||||||
color: root.accent
|
color: root.accent
|
||||||
font.family: root.chromeFont
|
font.family: root.chromeFont
|
||||||
font.pixelSize: 15
|
font.pixelSize: 15
|
||||||
@@ -1132,32 +1638,59 @@ Item {
|
@@ -1132,32 +1580,59 @@ Item {
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@@ -807,71 +682,11 @@ index 018311d..4a73ee0 100644
|
|||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
Layout.topMargin: 2
|
Layout.topMargin: 2
|
||||||
text: String(agentRow.index + 1)
|
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
|
diff --git i/README.md w/README.md
|
||||||
index 685096b..cb5b0b9 100644
|
index 685096b..37df3e5 100644
|
||||||
--- i/README.md
|
--- i/README.md
|
||||||
+++ w/README.md
|
+++ w/README.md
|
||||||
@@ -38,12 +38,27 @@ rm -rf ~/.config/herdr-hud-native
|
@@ -44,6 +44,14 @@ 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
|
- Approval-dialog keyboard input and TUI rendering
|
||||||
- Omarchy and WoW designs
|
- Omarchy and WoW designs
|
||||||
- Agent switching, launcher, alerts, hidden-panel notifications
|
- Agent switching, launcher, alerts, hidden-panel notifications
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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]
|
||||||
@@ -4,10 +4,9 @@ akshar.radio-atlas https://github.com/AksharP5/omarchy-radio-atlas.git ffa7186b5
|
|||||||
crmne.hyprmoncfg https://github.com/crmne/omarchy-hyprmoncfg.git 193e5611f934eec42460ef75f3f46c2a8bd0252a
|
crmne.hyprmoncfg https://github.com/crmne/omarchy-hyprmoncfg.git 193e5611f934eec42460ef75f3f46c2a8bd0252a
|
||||||
ilyazar.btop https://github.com/omarchy-QOL/omarchy-btop-activity.git 1e896f6f37bf880dee3e55e36ff04f967745c527
|
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
|
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
|
io.github.thisisgm.omapods https://github.com/thisisgm/omarchy-pods fff7fec600a5b9a61cdb40e93eccbcceb4b8f824
|
||||||
kb2uka.port-doctor https://github.com/Kb2uka/omarchy-port-doctor.git baa13451bfc27d59e8c831c217852279a20901aa
|
kb2uka.port-doctor https://github.com/Kb2uka/omarchy-port-doctor.git baa13451bfc27d59e8c831c217852279a20901aa
|
||||||
kokd.remote-desktop https://github.com/kostadinkadiev/omarchy-remote-desktop.git 7913b89521f88fac40620f495d1e07835bbb55b4
|
kokd.remote-desktop https://github.com/kostadinkadiev/omarchy-remote-desktop.git 7913b89521f88fac40620f495d1e07835bbb55b4
|
||||||
omaplug https://github.com/fross100/omaplug.git 9da73fc13fc2027be5b44915b4ab89440beb74ab
|
omaplug https://github.com/fross100/omaplug.git 9da73fc13fc2027be5b44915b4ab89440beb74ab
|
||||||
vstoms.netbird https://github.com/vstoms/omarchy-netbird.git d530c76b13324b26dafd79934529b4395e18a11c
|
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
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Qt 6.12 ships a QtQuick `Color` singleton that shadows the shell's own
|
|
||||||
# qs.Commons `Color` palette, so every Color.bar.* / Color.polkit.* resolves to
|
|
||||||
# undefined: the bar is a black strip with no widgets, and the lock screen and
|
|
||||||
# polkit dialog lose their colours. Omarchy <= 4.0.4 still has the clash.
|
|
||||||
#
|
|
||||||
# The fix renames the shell's singleton to ShellColor and rewrites its users.
|
|
||||||
# An omarchy package upgrade puts the stock files back, so this is idempotent
|
|
||||||
# and safe to re-run after every update; it does nothing once upstream renames
|
|
||||||
# the singleton itself.
|
|
||||||
#
|
|
||||||
# omarchy-fix-qt-color [--check] [shell-dir] (default /usr/share/omarchy/shell)
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
check=0
|
|
||||||
[[ ${1-} == --check ]] && { check=1; shift; }
|
|
||||||
shell=${1:-/usr/share/omarchy/shell}
|
|
||||||
qmldir=$shell/Commons/qmldir
|
|
||||||
|
|
||||||
[[ -f $qmldir ]] || { echo "no $qmldir - not an Omarchy shell" >&2; exit 2; }
|
|
||||||
if ! grep -q '^singleton Color ' "$qmldir"; then
|
|
||||||
echo "= shell Color singleton not present (already renamed, or fixed upstream)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
qt=$(pacman -Q qt6-declarative 2>/dev/null | awk '{print $2}') || true
|
|
||||||
if [[ -n $qt ]] && [[ $(vercmp "$qt" 6.12) -lt 0 ]]; then
|
|
||||||
echo "= qt6-declarative $qt predates the Color clash; nothing to do"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
(( check )) && { echo "! shell needs the Qt Color fix (qt6-declarative $qt)"; exit 1; }
|
|
||||||
|
|
||||||
sudo=(); [[ -w $qmldir ]] || sudo=(sudo)
|
|
||||||
backup=$shell.bak-qt612
|
|
||||||
[[ -e $backup ]] || "${sudo[@]}" cp -a "$shell" "$backup"
|
|
||||||
|
|
||||||
"${sudo[@]}" sed -i 's/^singleton Color /singleton ShellColor /' "$qmldir"
|
|
||||||
find "$shell" -type f \( -name '*.qml' -o -name '*.js' \) -print0 \
|
|
||||||
| xargs -0 grep -lZE '(^|[^A-Za-z0-9_."/])Color\.' \
|
|
||||||
| xargs -0 -r "${sudo[@]}" sed -i -E 's/(^|[^A-Za-z0-9_."\/])Color\./\1ShellColor./g'
|
|
||||||
echo "+ renamed the shell's Color singleton to ShellColor (backup: $backup)"
|
|
||||||
echo " restart the shell to pick it up: omarchy-restart-shell"
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
-- Activity (btop) as a persistent drop-down: a single btop starts at login on
|
|
||||||
-- its own special workspace, and SUPER+CTRL+T shows/hides it instead of
|
|
||||||
-- launching a fresh btop (and losing its graph history) every time.
|
|
||||||
|
|
||||||
local app_id = "org.omarchy.activity"
|
|
||||||
local launch = "omarchy-launch-tui --app-id=" .. app_id .. " btop"
|
|
||||||
|
|
||||||
-- Route the window by class: launch-tui goes through setsid/uwsm-app, so an
|
|
||||||
-- exec rule can't follow the PID.
|
|
||||||
o.window(app_id, { workspace = "special:activity silent" })
|
|
||||||
|
|
||||||
-- Start it at login, and re-seed it if btop was quit and the toggle finds the
|
|
||||||
-- workspace empty.
|
|
||||||
o.exec_on_start(launch)
|
|
||||||
hl.workspace_rule({
|
|
||||||
workspace = "special:activity",
|
|
||||||
on_created_empty = launch,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Was: launch a new btop each time.
|
|
||||||
hl.unbind("SUPER + CTRL + T")
|
|
||||||
o.bind("SUPER + CTRL + T", "Activity", hl.dsp.workspace.toggle_special("activity"))
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# 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, persistent Activity (btop) toggle"
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# base setup, run by install.sh's `setup` step after the files are in place.
|
|
||||||
# Idempotent; honours OMARCHY_SETUP_DRY=1.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
# Qt 6.12's QtQuick Color singleton blanks the bar (see bin/omarchy-fix-qt-color).
|
|
||||||
# An omarchy upgrade restores the stock shell, so re-run install.sh's setup step,
|
|
||||||
# or the script itself, after updating.
|
|
||||||
fix=$OMARCHY_SETUP_LAYER/bin/omarchy-fix-qt-color
|
|
||||||
if [[ ! -d /usr/share/omarchy/shell ]]; then
|
|
||||||
echo " = no Omarchy shell; Qt Color fix not needed"
|
|
||||||
elif "$fix" --check >/dev/null; then
|
|
||||||
echo " = shell is clear of the Qt Color clash"
|
|
||||||
elif [[ ${OMARCHY_SETUP_DRY:-0} == 1 ]]; then
|
|
||||||
echo " ? would: $fix"
|
|
||||||
elif "$fix" | sed 's/^/ /'; then
|
|
||||||
pgrep -x quickshell >/dev/null && command -v omarchy-restart-shell >/dev/null && omarchy-restart-shell
|
|
||||||
else
|
|
||||||
echo " ! Qt Color fix failed; run $fix by hand"
|
|
||||||
fi
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- 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")
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# 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)"
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-- 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 } })
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
-- 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 })
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"theme": "system",
|
|
||||||
"autoupdate": false
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
# 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" }
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
[aur]
|
|
||||||
netbird-bin # NetBird mesh client; setup.sh installs and enables its service
|
|
||||||
|
|
||||||
[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
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# 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.
|
|
||||||
# 2026-11-01: plus git/config, mimeapps.list, NetBird and ufw rules (setup.sh).
|
|
||||||
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)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# macbook-air-m2 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; }
|
|
||||||
|
|
||||||
# NetBird: the AUR package ships no unit; `netbird service install` writes
|
|
||||||
# /etc/systemd/system/netbird.service. Joining the network (`netbird up`) is
|
|
||||||
# interactive and stays a manual step.
|
|
||||||
if command -v netbird >/dev/null; then
|
|
||||||
if [[ -f /etc/systemd/system/netbird.service ]]; then
|
|
||||||
echo " = netbird.service installed"
|
|
||||||
else
|
|
||||||
run sudo netbird service install && echo " + netbird.service"
|
|
||||||
fi
|
|
||||||
if systemctl is-enabled --quiet netbird 2>/dev/null; then
|
|
||||||
echo " = netbird.service enabled"
|
|
||||||
else
|
|
||||||
run sudo systemctl enable --now netbird && echo " + netbird.service enabled (now run: netbird up)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Firewall, beyond Omarchy's stock rules (LocalSend, Docker DNS) and the 4242
|
|
||||||
# lan-mouse rule the handoff step opens per peer:
|
|
||||||
# 22/tcp sshd, from the home LAN (the HP reaches it as `ssh air`)
|
|
||||||
# 7746/udp ocovo fabric, from the home LAN and over NetBird (wt0)
|
|
||||||
# 7790/tcp ocovo, anywhere
|
|
||||||
ufw_rule() {
|
|
||||||
if [[ ${OMARCHY_SETUP_DRY:-0} == 1 ]]; then echo " ? would: ufw $*"; return; fi
|
|
||||||
# ufw reports "Skipping adding existing rule" for a duplicate, so this is idempotent.
|
|
||||||
local out; out=$(sudo ufw "$@" 2>&1)
|
|
||||||
[[ $out == *Skipping* ]] && echo " = ufw $*" || echo " + ufw $*"
|
|
||||||
}
|
|
||||||
if command -v ufw >/dev/null; then
|
|
||||||
if [[ ${OMARCHY_SETUP_DRY:-0} != 1 ]] && ! sudo -n true 2>/dev/null && [[ ! -t 0 ]]; then
|
|
||||||
echo " ! sudo needs a password and there is no terminal; ufw rules not applied"
|
|
||||||
else
|
|
||||||
ufw_rule allow from 192.168.1.0/24 to any port 22 proto tcp
|
|
||||||
ufw_rule allow from 192.168.1.0/24 to any port 7746 proto udp comment 'ocovo fabric'
|
|
||||||
ufw_rule allow in on wt0 to any port 7746 proto udp
|
|
||||||
ufw_rule allow 7790/tcp
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Created by NetBird to prevent systemd-networkd from removing
|
|
||||||
# routes and policy rules managed by NetBird.
|
|
||||||
|
|
||||||
[Network]
|
|
||||||
ManageForeignRoutes=no
|
|
||||||
ManageForeignRoutingPolicyRules=no
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
-- 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 '{}'")
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
// One agent's usage record, read straight off the data file that
|
|
||||||
// omarchy-agent-usage-update maintains. The panel never learns how the
|
|
||||||
// numbers were made — a record that appears in the usage directory is an
|
|
||||||
// agent, whoever wrote it.
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
property string agentId: ""
|
|
||||||
property string path: ""
|
|
||||||
property var record: null
|
|
||||||
|
|
||||||
// The text the current record was parsed from, so a reload that finds the
|
|
||||||
// file unchanged doesn't hand every listener an identical new record.
|
|
||||||
property string parsedText: ""
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: agentFile
|
|
||||||
path: root.path
|
|
||||||
watchChanges: true
|
|
||||||
printErrors: false
|
|
||||||
onFileChanged: reload()
|
|
||||||
onLoaded: root.parse(text())
|
|
||||||
onLoadFailed: root.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// The usage update rewrites records with an atomic mv, which replaces the
|
|
||||||
// inode. Should inotify fail to rearm on the new one (watch quota, ENOSPC),
|
|
||||||
// the FileView goes quiet, so Main.qml also reloads every record after each
|
|
||||||
// update run (#9974).
|
|
||||||
function reload() { agentFile.reload() }
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
root.parsedText = ""
|
|
||||||
root.record = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function parse(content) {
|
|
||||||
var text = String(content || "")
|
|
||||||
if (root.record && text === root.parsedText) return
|
|
||||||
try {
|
|
||||||
var parsed = JSON.parse(text)
|
|
||||||
root.record = parsed && typeof parsed === "object" ? parsed : null
|
|
||||||
root.parsedText = root.record ? text : ""
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("agents", "Ignoring bad usage record", root.path, e)
|
|
||||||
root.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,748 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
// The display side of agent usage. All extraction lives behind
|
|
||||||
// omarchy-agent-usage-update, which writes one JSON record per agent into
|
|
||||||
// the usage directory; this file only discovers those records, watches them
|
|
||||||
// for changes, and optionally merges snapshots synced from other machines.
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
property var settings: ({})
|
|
||||||
|
|
||||||
readonly property string home: Quickshell.env("HOME") || ""
|
|
||||||
readonly property string usageDir: (Quickshell.env("XDG_STATE_HOME") || home + "/.local/state") + "/omarchy/agents/usage"
|
|
||||||
|
|
||||||
// ------------------------------------------------------------- discovery
|
|
||||||
|
|
||||||
property var agentIds: []
|
|
||||||
property var agents: []
|
|
||||||
property int dataRevision: 0
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: listProcess
|
|
||||||
running: false
|
|
||||||
command: ["find", root.usageDir, "-maxdepth", "1", "-name", "*.json", "-printf", "%f\n"]
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
waitForEnd: true
|
|
||||||
onStreamFinished: root.applyAgentListing(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rescanAgents() {
|
|
||||||
if (!listProcess.running) listProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyAgentListing(output) {
|
|
||||||
var ids = []
|
|
||||||
var lines = String(output || "").split("\n")
|
|
||||||
for (var i = 0; i < lines.length; i++) {
|
|
||||||
var name = lines[i].trim()
|
|
||||||
if (name.slice(-5) === ".json") ids.push(name.slice(0, -5))
|
|
||||||
}
|
|
||||||
ids.sort()
|
|
||||||
// Same list, same objects: reassigning the model would tear down every
|
|
||||||
// FileView just to build identical ones.
|
|
||||||
if (JSON.stringify(ids) !== JSON.stringify(agentIds)) agentIds = ids
|
|
||||||
}
|
|
||||||
|
|
||||||
Instantiator {
|
|
||||||
id: agentInstantiator
|
|
||||||
model: root.agentIds
|
|
||||||
|
|
||||||
delegate: Agent {
|
|
||||||
required property var modelData
|
|
||||||
agentId: modelData
|
|
||||||
path: root.usageDir + "/" + modelData + ".json"
|
|
||||||
onRecordChanged: root.recordsChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
onObjectAdded: (index, object) => root.rebuildAgents()
|
|
||||||
onObjectRemoved: (index, object) => root.rebuildAgents()
|
|
||||||
}
|
|
||||||
|
|
||||||
function rebuildAgents() {
|
|
||||||
var result = []
|
|
||||||
for (var i = 0; i < agentInstantiator.count; i++) {
|
|
||||||
var agent = agentInstantiator.objectAt(i)
|
|
||||||
if (agent) result.push(agent)
|
|
||||||
}
|
|
||||||
agents = result
|
|
||||||
recordsChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordsChanged() {
|
|
||||||
dataRevision++
|
|
||||||
scheduleLimitsRetry()
|
|
||||||
scheduleSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
// A collector that could not reach its limits endpoint at all — typically
|
|
||||||
// the seconds after login before the network is up — writes retryAdvised
|
|
||||||
// into its record. Honor it with one sooner try instead of waiting out the
|
|
||||||
// full refresh interval; a run that reaches the endpoint clears the flag.
|
|
||||||
// Only the advising agents rerun, so an outage at one provider does not
|
|
||||||
// put every other collector on a 30-second treadmill.
|
|
||||||
property var retryAgentIds: []
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: limitsRetry
|
|
||||||
interval: 30000
|
|
||||||
repeat: false
|
|
||||||
onTriggered: root.runUpdate("limits", root.retryAgentIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleLimitsRetry() {
|
|
||||||
var advising = []
|
|
||||||
for (var i = 0; i < agents.length; i++) {
|
|
||||||
var record = agents[i] ? agents[i].record : null
|
|
||||||
if (record && record.retryAdvised === true && providerEnabled(String(record.id || "")))
|
|
||||||
advising.push(String(record.id))
|
|
||||||
}
|
|
||||||
retryAgentIds = advising
|
|
||||||
if (advising.length > 0) limitsRetry.restart()
|
|
||||||
else limitsRetry.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
rescanAgents()
|
|
||||||
if (syncConfigured()) scheduleSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------- refresh
|
|
||||||
|
|
||||||
property int refreshIntervalSec: Math.max(30, Number(setting("refreshIntervalSec", 900)))
|
|
||||||
property string pendingUpdateKind: ""
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: root.refreshIntervalSec * 1000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
triggeredOnStart: true
|
|
||||||
onTriggered: root.runUpdate("normal")
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: updateProcess
|
|
||||||
running: false
|
|
||||||
onExited: {
|
|
||||||
root.rescanAgents()
|
|
||||||
if (root.pendingUpdateKind !== "") {
|
|
||||||
var kind = root.pendingUpdateKind
|
|
||||||
root.pendingUpdateKind = ""
|
|
||||||
root.runUpdate(kind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
waitForEnd: true
|
|
||||||
onStreamFinished: if (text.trim() !== "") console.warn("agents", text.trim())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCommand(kind, agentIds) {
|
|
||||||
var command = ["omarchy-agent-usage-update"]
|
|
||||||
if (kind === "force") command.push("--force")
|
|
||||||
if (kind === "limits") command.push("--limits-only")
|
|
||||||
var providers = settings && settings.providers ? settings.providers : {}
|
|
||||||
for (var id in providers) {
|
|
||||||
if (providers[id] && providers[id].enabled === false) command.push("--except", id)
|
|
||||||
}
|
|
||||||
if (agentIds) {
|
|
||||||
for (var i = 0; i < agentIds.length; i++) command.push(agentIds[i])
|
|
||||||
}
|
|
||||||
return command
|
|
||||||
}
|
|
||||||
|
|
||||||
function runUpdate(kind, agentIds) {
|
|
||||||
if (updateProcess.running) {
|
|
||||||
// Collapse queued requests to one full rerun; a forced refresh outranks
|
|
||||||
// the cheaper kinds it might have been queued behind.
|
|
||||||
if (kind === "force" || root.pendingUpdateKind === "") root.pendingUpdateKind = kind
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateProcess.command = updateCommand(kind, agentIds)
|
|
||||||
updateProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function refresh() { refreshAll(true) }
|
|
||||||
function refreshAll(force) { runUpdate(force === true ? "force" : "normal") }
|
|
||||||
|
|
||||||
// Opening the panel wants the numbers that go stale on the wire, not
|
|
||||||
// another walk over every transcript on disk — the collectors reuse their
|
|
||||||
// recent scans in this mode.
|
|
||||||
function refreshLimits() { runUpdate("limits") }
|
|
||||||
|
|
||||||
// ------------------------------------------------------------- providers
|
|
||||||
|
|
||||||
// An agent earns a place in the bar and the panel by being switched on in
|
|
||||||
// settings and having actually produced numbers — locally or on a synced
|
|
||||||
// device. With nothing to show, the whole module collapses out of the bar
|
|
||||||
// rather than sitting there dimmed.
|
|
||||||
property var enabledProviders: {
|
|
||||||
var rev = dataRevision
|
|
||||||
var syncRev = syncRevision
|
|
||||||
var result = []
|
|
||||||
var localIds = {}
|
|
||||||
for (var i = 0; i < agents.length; i++) {
|
|
||||||
var record = agents[i] ? agents[i].record : null
|
|
||||||
if (!record || !record.id) continue
|
|
||||||
var id = String(record.id)
|
|
||||||
localIds[id] = true
|
|
||||||
if (!providerEnabled(id)) continue
|
|
||||||
var display = displayProvider(record)
|
|
||||||
if (providerHasData(display)) result.push(display)
|
|
||||||
}
|
|
||||||
// An agent that only ever ran on another machine has no local record, but
|
|
||||||
// its synced numbers still deserve a tab. Rate limits stay blank — they
|
|
||||||
// are per-account and never travel.
|
|
||||||
var syncedProviders = syncConfigured() && aggregateData && aggregateData.providers ? aggregateData.providers : {}
|
|
||||||
for (var syncedId in syncedProviders) {
|
|
||||||
if (localIds[syncedId] || !providerEnabled(syncedId)) continue
|
|
||||||
var stats = syncedProviders[syncedId] || {}
|
|
||||||
var syncedDisplay = displayProvider({ id: syncedId, name: stats.providerName || syncedId })
|
|
||||||
if (providerHasData(syncedDisplay)) result.push(syncedDisplay)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function providerEnabled(id) {
|
|
||||||
if (!settings || !settings.providers || !settings.providers[id]) return true
|
|
||||||
return settings.providers[id].enabled !== false
|
|
||||||
}
|
|
||||||
|
|
||||||
// All-time keeps a quiet day from hiding an agent; today's counts admit a
|
|
||||||
// machine whose only source is history.jsonl, which knows nothing older.
|
|
||||||
function providerHasData(p) {
|
|
||||||
return numberValue(p.totalPrompts) > 0 || numberValue(p.totalSessions) > 0
|
|
||||||
|| numberValue(p.activeDays) > 0 || numberValue(p.todayPrompts) > 0
|
|
||||||
|| numberValue(p.todaySessions) > 0 || (p.limits && p.limits.length > 0)
|
|
||||||
|| !!p.balance
|
|
||||||
}
|
|
||||||
|
|
||||||
// A prepaid agent's credit ledger. Like rate limits, the balance is
|
|
||||||
// per-account and never merged across devices.
|
|
||||||
function balanceValue(raw) {
|
|
||||||
if (!raw || typeof raw !== "object") return null
|
|
||||||
var remaining = Number(raw.remaining)
|
|
||||||
var funded = Number(raw.funded)
|
|
||||||
if (!isFinite(remaining) || remaining < 0) return null
|
|
||||||
return {
|
|
||||||
remaining: remaining,
|
|
||||||
funded: isFinite(funded) && funded > 0 ? funded : 0,
|
|
||||||
spent: Math.max(0, Number(raw.spent) || 0),
|
|
||||||
currency: String(raw.currency || "USD"),
|
|
||||||
estimated: raw.estimated === true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayProvider(record) {
|
|
||||||
var stats = syncedStatsFor(String(record.id))
|
|
||||||
var synced = !!stats
|
|
||||||
var deviceCount = synced ? Number(stats.deviceCount || aggregateData.deviceCount || 0) : 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
providerId: String(record.id),
|
|
||||||
providerName: String(record.name || record.id),
|
|
||||||
ready: record.ready === true || synced,
|
|
||||||
usageStatusText: String(record.usageStatusText || ""),
|
|
||||||
authHelpText: String(record.authHelpText || ""),
|
|
||||||
|
|
||||||
// Rate limits and balances stay per-account and are never merged
|
|
||||||
// across devices.
|
|
||||||
limits: Array.isArray(record.limits) ? record.limits : [],
|
|
||||||
tierLabel: String(record.tierLabel || ""),
|
|
||||||
balance: balanceValue(record.balance),
|
|
||||||
|
|
||||||
todayPrompts: synced ? numberValue(stats.todayPrompts) : numberValue(record.todayPrompts),
|
|
||||||
todaySessions: synced ? numberValue(stats.todaySessions) : numberValue(record.todaySessions),
|
|
||||||
todayTotalTokens: synced ? numberValue(stats.todayTotalTokens) : numberValue(record.todayTotalTokens),
|
|
||||||
todayTokensByModel: synced ? (stats.todayTokensByModel || ({})) : (record.todayTokensByModel || ({})),
|
|
||||||
recentDays: synced ? (stats.recentDays || []) : (record.recentDays || []),
|
|
||||||
totalPrompts: synced ? numberValue(stats.totalPrompts) : numberValue(record.totalPrompts),
|
|
||||||
totalSessions: synced ? numberValue(stats.totalSessions) : numberValue(record.totalSessions),
|
|
||||||
activeDays: synced ? numberValue(stats.activeDays) : numberValue(record.activeDays),
|
|
||||||
modelUsage: synced ? (stats.modelUsage || ({})) : (record.modelUsage || ({})),
|
|
||||||
hasLocalStats: synced ? (stats.hasLocalStats !== false) : (record.hasLocalStats !== false),
|
|
||||||
hasPromptStats: synced ? (stats.hasPromptStats !== false) : (record.hasPromptStats !== false),
|
|
||||||
|
|
||||||
syncEnabled: synced,
|
|
||||||
syncDeviceCount: deviceCount,
|
|
||||||
syncUpdatedAt: aggregateData && aggregateData.updatedAt ? aggregateData.updatedAt : ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setting(name, fallback) {
|
|
||||||
var value = settings ? settings[name] : undefined
|
|
||||||
return value === undefined || value === null ? fallback : value
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------ sync
|
|
||||||
|
|
||||||
property var syncModeSetting: setting("syncMode", setting("syncEnabled", false))
|
|
||||||
property bool syncEnabled: parseSyncEnabled(syncModeSetting)
|
|
||||||
property string syncDir: String(setting("syncDir", ""))
|
|
||||||
property string syncFileName: String(setting("syncFileName", ""))
|
|
||||||
property string syncDeviceId: String(setting("syncDeviceId", ""))
|
|
||||||
property string detectedHostname: ""
|
|
||||||
readonly property string syncEffectiveDir: expandPath(syncDir)
|
|
||||||
readonly property string syncEffectiveFileName: safeSnapshotFileName(syncFileName, syncDeviceId)
|
|
||||||
readonly property string syncEffectiveDeviceId: safeDeviceId(syncDeviceId || syncEffectiveFileName.replace(/\.json$/i, ""))
|
|
||||||
readonly property string syncSnapshotPath: syncConfigured() ? syncEffectiveDir + "/" + syncEffectiveFileName : home + "/.cache/omarchy/agents-disabled.json"
|
|
||||||
property var aggregateData: ({})
|
|
||||||
property int syncRevision: 0
|
|
||||||
property bool syncRunning: false
|
|
||||||
property bool syncRequestedWhileRunning: false
|
|
||||||
property string syncStatusText: ""
|
|
||||||
property double aggregateUpdatedAtMs: aggregateData && aggregateData.updatedAtMs ? Number(aggregateData.updatedAtMs) : 0
|
|
||||||
|
|
||||||
onSyncEnabledChanged: syncSettingsChanged()
|
|
||||||
onSyncDirChanged: syncSettingsChanged()
|
|
||||||
onSyncFileNameChanged: if (syncConfigured()) scheduleSync()
|
|
||||||
onSyncDeviceIdChanged: if (syncConfigured()) scheduleSync()
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: syncDebounce
|
|
||||||
interval: 1000
|
|
||||||
repeat: false
|
|
||||||
onTriggered: root.runSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: syncMkdirProcess
|
|
||||||
running: false
|
|
||||||
onRunningChanged: root.updateSyncRunning()
|
|
||||||
onExited: function(exitCode) {
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
if (root.syncConfigured()) root.syncStatusText = "Usage sync mkdir failed"
|
|
||||||
root.finishSyncRun()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
root.writeSyncSnapshot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: syncScanProcess
|
|
||||||
running: false
|
|
||||||
onRunningChanged: root.updateSyncRunning()
|
|
||||||
onExited: function(exitCode) {
|
|
||||||
if (exitCode !== 0 && root.syncConfigured()) root.syncStatusText = "Usage sync scan failed"
|
|
||||||
root.finishSyncRun()
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
waitForEnd: true
|
|
||||||
onStreamFinished: root.parseSyncScanOutput(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
waitForEnd: true
|
|
||||||
onStreamFinished: if (text.trim() !== "") console.warn("agents/sync", text.trim())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: syncSnapshotFile
|
|
||||||
path: root.syncSnapshotPath
|
|
||||||
watchChanges: false
|
|
||||||
atomicWrites: true
|
|
||||||
printErrors: false
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: hostnameFile
|
|
||||||
path: "/etc/hostname"
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: root.detectedHostname = String(text() || "").trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSyncEnabled(value) {
|
|
||||||
if (value === true) return true
|
|
||||||
var text = String(value || "").trim().toLowerCase()
|
|
||||||
return text === "on" || text === "enabled" || text === "true" || text === "yes" || text === "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncConfigured() {
|
|
||||||
return root.syncEnabled === true && String(root.syncDir || "").trim() !== ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncSettingsChanged() {
|
|
||||||
if (syncConfigured()) {
|
|
||||||
scheduleSync()
|
|
||||||
} else {
|
|
||||||
syncDebounce.stop()
|
|
||||||
syncRequestedWhileRunning = false
|
|
||||||
aggregateData = ({})
|
|
||||||
syncStatusText = ""
|
|
||||||
syncRevision++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSyncRunning() {
|
|
||||||
root.syncRunning = syncMkdirProcess.running || syncScanProcess.running
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleSync() {
|
|
||||||
if (!syncConfigured()) return
|
|
||||||
syncDebounce.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function runSync() {
|
|
||||||
if (!syncConfigured()) return
|
|
||||||
if (root.syncRunning) {
|
|
||||||
syncRequestedWhileRunning = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
syncRequestedWhileRunning = false
|
|
||||||
syncStatusText = ""
|
|
||||||
syncMkdirProcess.command = ["mkdir", "-p", root.syncEffectiveDir]
|
|
||||||
syncMkdirProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeSyncSnapshot() {
|
|
||||||
if (!syncConfigured()) {
|
|
||||||
finishSyncRun()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syncSnapshotFile.setText(JSON.stringify(localSnapshot(), null, 2) + "\n")
|
|
||||||
Qt.callLater(root.startSyncScan)
|
|
||||||
}
|
|
||||||
|
|
||||||
function startSyncScan() {
|
|
||||||
if (!syncConfigured()) {
|
|
||||||
finishSyncRun()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var script = "dir=$0; [[ -d \"$dir\" ]] || exit 0; shopt -s nullglob; for f in \"$dir\"/*.json; do [[ -f \"$f\" ]] || continue; printf '===%s===\\n' \"$f\"; cat \"$f\"; printf '\\n=== EOM ===\\n'; done"
|
|
||||||
syncScanProcess.command = ["bash", "-c", script, root.syncEffectiveDir]
|
|
||||||
syncScanProcess.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishSyncRun() {
|
|
||||||
if (syncRequestedWhileRunning && syncConfigured()) {
|
|
||||||
syncRequestedWhileRunning = false
|
|
||||||
scheduleSync()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function expandPath(path) {
|
|
||||||
var value = String(path || "").trim()
|
|
||||||
if (value === "") return ""
|
|
||||||
if (value === "~") return home
|
|
||||||
if (value.indexOf("~/") === 0) return home + value.substring(1)
|
|
||||||
if (value.indexOf("$HOME/") === 0) return home + value.substring(5)
|
|
||||||
if (value.charAt(0) !== "/") return home + "/" + value
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeDeviceId(raw) {
|
|
||||||
var value = String(raw || "").trim()
|
|
||||||
if (value === "") value = Quickshell.env("HOSTNAME") || root.detectedHostname || Quickshell.env("HOST") || Quickshell.env("USER") || "device"
|
|
||||||
value = value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "")
|
|
||||||
if (value === "") value = "device"
|
|
||||||
return value.length > 80 ? value.substring(0, 80) : value
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeSnapshotFileName(rawFileName, rawDeviceId) {
|
|
||||||
var value = String(rawFileName || "").trim()
|
|
||||||
if (value === "") value = safeDeviceId(rawDeviceId) + ".json"
|
|
||||||
value = value.split("/").pop().replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "")
|
|
||||||
if (value === "") value = safeDeviceId(rawDeviceId) + ".json"
|
|
||||||
if (!/\.json$/i.test(value)) value += ".json"
|
|
||||||
return value.length > 100 ? value.substring(0, 95) + ".json" : value
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSyncScanOutput(output) {
|
|
||||||
var lines = String(output || "").split("\n")
|
|
||||||
var snapshots = []
|
|
||||||
var currentPath = ""
|
|
||||||
var currentJson = []
|
|
||||||
|
|
||||||
function flush() {
|
|
||||||
if (currentPath === "") return
|
|
||||||
var raw = currentJson.join("\n").trim()
|
|
||||||
try {
|
|
||||||
var parsed = JSON.parse(raw)
|
|
||||||
if (parsed && parsed.providers) snapshots.push(parsed)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("agents/sync", "Ignoring bad snapshot", currentPath, e)
|
|
||||||
}
|
|
||||||
currentPath = ""
|
|
||||||
currentJson = []
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < lines.length; i++) {
|
|
||||||
var line = lines[i]
|
|
||||||
var start = line.match(/^===(.+)===$/)
|
|
||||||
if (start && line !== "=== EOM ===") {
|
|
||||||
flush()
|
|
||||||
currentPath = start[1]
|
|
||||||
currentJson = []
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (line === "=== EOM ===") {
|
|
||||||
flush()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (currentPath !== "") currentJson.push(line)
|
|
||||||
}
|
|
||||||
flush()
|
|
||||||
|
|
||||||
aggregateData = aggregateSnapshots(snapshots)
|
|
||||||
syncStatusText = ""
|
|
||||||
syncRevision++
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloneValue(value, fallback) {
|
|
||||||
if (value === undefined || value === null) return fallback
|
|
||||||
try {
|
|
||||||
return JSON.parse(JSON.stringify(value))
|
|
||||||
} catch (e) {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function numberValue(value) {
|
|
||||||
var n = Number(value || 0)
|
|
||||||
return isFinite(n) ? Math.round(n) : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function dateString(date) {
|
|
||||||
var y = date.getFullYear()
|
|
||||||
var m = String(date.getMonth() + 1).padStart(2, "0")
|
|
||||||
var d = String(date.getDate()).padStart(2, "0")
|
|
||||||
return y + "-" + m + "-" + d
|
|
||||||
}
|
|
||||||
|
|
||||||
function recentDateStrings() {
|
|
||||||
var result = []
|
|
||||||
for (var offset = 6; offset >= 0; offset--) {
|
|
||||||
var date = new Date()
|
|
||||||
date.setDate(date.getDate() - offset)
|
|
||||||
result.push(dateString(date))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function emptyTokenBucket() {
|
|
||||||
return { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, cacheCreationInputTokens: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device-scoped stats add up across machines; account-scoped stats
|
|
||||||
// (Fireworks' billing API) are replicas of the same upstream truth on
|
|
||||||
// every synced device, so the widest value wins — summing them would
|
|
||||||
// double every token per machine.
|
|
||||||
function combineNumber(additive, current, value) {
|
|
||||||
return additive ? numberValue(current) + numberValue(value) : Math.max(numberValue(current), numberValue(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
function combineObjectNumbers(additive, target, source) {
|
|
||||||
if (!source) return
|
|
||||||
for (var key in source) target[key] = combineNumber(additive, target[key], source[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
function aggregateSnapshots(snapshots) {
|
|
||||||
var dates = recentDateStrings()
|
|
||||||
var devices = {}
|
|
||||||
var providers = {}
|
|
||||||
|
|
||||||
function providerAcc(id) {
|
|
||||||
if (providers[id]) return providers[id]
|
|
||||||
var recentByDay = {}
|
|
||||||
for (var d = 0; d < dates.length; d++) recentByDay[dates[d]] = 0
|
|
||||||
providers[id] = {
|
|
||||||
providerId: id,
|
|
||||||
providerName: "",
|
|
||||||
ready: false,
|
|
||||||
hasLocalStats: false,
|
|
||||||
hasPromptStats: false,
|
|
||||||
todayPrompts: 0,
|
|
||||||
todaySessions: 0,
|
|
||||||
todayTotalTokens: 0,
|
|
||||||
todayTokensByModel: ({}),
|
|
||||||
recentByDay: recentByDay,
|
|
||||||
totalPrompts: 0,
|
|
||||||
totalSessions: 0,
|
|
||||||
activeDays: 0,
|
|
||||||
activeDates: ({}),
|
|
||||||
modelUsage: ({}),
|
|
||||||
devices: ({})
|
|
||||||
}
|
|
||||||
return providers[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < snapshots.length; i++) {
|
|
||||||
var snapshot = snapshots[i]
|
|
||||||
var device = safeDeviceId(snapshot.deviceId || "device")
|
|
||||||
devices[device] = true
|
|
||||||
var snapshotProviders = snapshot.providers || {}
|
|
||||||
for (var providerId in snapshotProviders) {
|
|
||||||
var stats = snapshotProviders[providerId] || {}
|
|
||||||
var acc = providerAcc(String(providerId))
|
|
||||||
acc.devices[device] = true
|
|
||||||
if (stats.providerName && acc.providerName === "") acc.providerName = String(stats.providerName)
|
|
||||||
acc.ready = acc.ready || stats.ready === true
|
|
||||||
acc.hasLocalStats = acc.hasLocalStats || stats.hasLocalStats !== false
|
|
||||||
// Snapshots from before the field existed only came from agents that
|
|
||||||
// count prompts, so a missing value reads as true.
|
|
||||||
acc.hasPromptStats = acc.hasPromptStats || stats.hasPromptStats !== false
|
|
||||||
var additive = String(stats.scope || "device") !== "account"
|
|
||||||
acc.todayPrompts = combineNumber(additive, acc.todayPrompts, stats.todayPrompts)
|
|
||||||
acc.todaySessions = combineNumber(additive, acc.todaySessions, stats.todaySessions)
|
|
||||||
acc.todayTotalTokens = combineNumber(additive, acc.todayTotalTokens, stats.todayTotalTokens)
|
|
||||||
acc.totalPrompts = combineNumber(additive, acc.totalPrompts, stats.totalPrompts)
|
|
||||||
acc.totalSessions = combineNumber(additive, acc.totalSessions, stats.totalSessions)
|
|
||||||
// Active days overlap between machines, so union the dates rather than
|
|
||||||
// summing counts. Snapshots written before activeDates existed only
|
|
||||||
// carry a count; the widest one stands in for them.
|
|
||||||
var activeDates = Array.isArray(stats.activeDates) ? stats.activeDates : []
|
|
||||||
for (var ad = 0; ad < activeDates.length; ad++) acc.activeDates[String(activeDates[ad])] = true
|
|
||||||
acc.activeDays = Math.max(acc.activeDays, numberValue(stats.activeDays))
|
|
||||||
combineObjectNumbers(additive, acc.todayTokensByModel, stats.todayTokensByModel || {})
|
|
||||||
|
|
||||||
var recent = Array.isArray(stats.recentDays) ? stats.recentDays : []
|
|
||||||
for (var r = 0; r < recent.length; r++) {
|
|
||||||
var day = recent[r] || {}
|
|
||||||
var date = String(day.date || "")
|
|
||||||
if (acc.recentByDay[date] !== undefined)
|
|
||||||
acc.recentByDay[date] = combineNumber(additive, acc.recentByDay[date], day.messageCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
var usage = stats.modelUsage || {}
|
|
||||||
for (var modelId in usage) {
|
|
||||||
var bucket = acc.modelUsage[modelId]
|
|
||||||
if (!bucket) bucket = acc.modelUsage[modelId] = emptyTokenBucket()
|
|
||||||
combineObjectNumbers(additive, bucket, usage[modelId] || {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var outProviders = {}
|
|
||||||
for (var id in providers) {
|
|
||||||
var acc = providers[id]
|
|
||||||
var recentDays = []
|
|
||||||
for (var di = 0; di < dates.length; di++) recentDays.push({ date: dates[di], messageCount: acc.recentByDay[dates[di]] || 0 })
|
|
||||||
var providerDevices = Object.keys(acc.devices).sort()
|
|
||||||
outProviders[id] = {
|
|
||||||
providerId: acc.providerId,
|
|
||||||
providerName: acc.providerName,
|
|
||||||
ready: acc.ready || providerDevices.length > 0,
|
|
||||||
hasLocalStats: acc.hasLocalStats,
|
|
||||||
hasPromptStats: acc.hasPromptStats,
|
|
||||||
todayPrompts: acc.todayPrompts,
|
|
||||||
todaySessions: acc.todaySessions,
|
|
||||||
todayTotalTokens: acc.todayTotalTokens,
|
|
||||||
todayTokensByModel: acc.todayTokensByModel,
|
|
||||||
recentDays: recentDays,
|
|
||||||
totalPrompts: acc.totalPrompts,
|
|
||||||
totalSessions: acc.totalSessions,
|
|
||||||
activeDays: Math.max(acc.activeDays, Object.keys(acc.activeDates).length),
|
|
||||||
modelUsage: acc.modelUsage,
|
|
||||||
deviceCount: providerDevices.length,
|
|
||||||
devices: providerDevices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
schemaVersion: 1,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
updatedAtMs: Date.now(),
|
|
||||||
deviceCount: Object.keys(devices).length,
|
|
||||||
devices: Object.keys(devices).sort(),
|
|
||||||
providers: outProviders
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshots keep the field names older Omarchy versions wrote, so a fleet
|
|
||||||
// of machines on mixed versions still merges cleanly in both directions.
|
|
||||||
function providerSnapshot(record) {
|
|
||||||
return {
|
|
||||||
providerId: String(record.id),
|
|
||||||
providerName: String(record.name || record.id),
|
|
||||||
ready: record.ready === true,
|
|
||||||
hasLocalStats: record.hasLocalStats !== false,
|
|
||||||
hasPromptStats: record.hasPromptStats !== false,
|
|
||||||
scope: String(record.scope || "device"),
|
|
||||||
todayPrompts: numberValue(record.todayPrompts),
|
|
||||||
todaySessions: numberValue(record.todaySessions),
|
|
||||||
todayTotalTokens: numberValue(record.todayTotalTokens),
|
|
||||||
todayTokensByModel: cloneValue(record.todayTokensByModel, ({})),
|
|
||||||
recentDays: cloneValue(record.recentDays, []),
|
|
||||||
totalPrompts: numberValue(record.totalPrompts),
|
|
||||||
totalSessions: numberValue(record.totalSessions),
|
|
||||||
activeDays: numberValue(record.activeDays),
|
|
||||||
activeDates: cloneValue(record.activeDates, []),
|
|
||||||
modelUsage: cloneValue(record.modelUsage, ({}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function localSnapshot() {
|
|
||||||
var providerMap = {}
|
|
||||||
for (var i = 0; i < agents.length; i++) {
|
|
||||||
var record = agents[i] ? agents[i].record : null
|
|
||||||
if (!record || !record.id) continue
|
|
||||||
if (!providerEnabled(String(record.id))) continue
|
|
||||||
providerMap[String(record.id)] = providerSnapshot(record)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
schemaVersion: 1,
|
|
||||||
deviceId: syncEffectiveDeviceId,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
providers: providerMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncedStatsFor(providerId) {
|
|
||||||
var rev = syncRevision
|
|
||||||
if (!syncConfigured() || !aggregateData || !aggregateData.providers) return null
|
|
||||||
return aggregateData.providers[providerId] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- format
|
|
||||||
|
|
||||||
function formatTokenCount(n) {
|
|
||||||
if (n === undefined || n === null) return "0"
|
|
||||||
if (n >= 1e9) return (n / 1e9).toFixed(1) + "B"
|
|
||||||
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"
|
|
||||||
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K"
|
|
||||||
return String(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
function modelWordCase(word) {
|
|
||||||
if (word === "gpt") return "GPT"
|
|
||||||
if (word === "deepseek") return "DeepSeek"
|
|
||||||
return word.charAt(0).toUpperCase() + word.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model ids arrive hyphenated with the version split across segments
|
|
||||||
// (`claude-opus-4-8`, `gpt-5.6-sol`). Rejoin the numeric run into one
|
|
||||||
// version and title-case the words around it.
|
|
||||||
function friendlyModelName(id) {
|
|
||||||
if (!id) return "Unknown"
|
|
||||||
var name = String(id).replace(/^claude-/, "").replace(/-\d{8}$/, "")
|
|
||||||
var parts = name.split("-")
|
|
||||||
var words = []
|
|
||||||
var version = []
|
|
||||||
for (var i = 0; i < parts.length; i++) {
|
|
||||||
var part = parts[i]
|
|
||||||
if (part === "") continue
|
|
||||||
if (/^\d/.test(part)) {
|
|
||||||
version.push(part)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (version.length > 0) {
|
|
||||||
words.push(version.join("."))
|
|
||||||
version = []
|
|
||||||
}
|
|
||||||
words.push(modelWordCase(part))
|
|
||||||
}
|
|
||||||
if (version.length > 0) words.push(version.join("."))
|
|
||||||
return words.length > 0 ? words.join(" ") : "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,942 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Commons
|
|
||||||
import qs.Ui
|
|
||||||
|
|
||||||
Panel {
|
|
||||||
id: root
|
|
||||||
moduleName: "omarchy.agents"
|
|
||||||
ipcTarget: "omarchy.agents"
|
|
||||||
manageIpc: false
|
|
||||||
|
|
||||||
readonly property color foreground: bar ? bar.foreground : Color.foreground
|
|
||||||
readonly property color urgent: bar ? bar.urgent : Color.urgent
|
|
||||||
readonly property color dim: Qt.darker(foreground, 1.55)
|
|
||||||
readonly property color surface: Color.popups.background
|
|
||||||
readonly property color track: Style.selectedFillFor(foreground, Color.accent)
|
|
||||||
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
|
||||||
|
|
||||||
readonly property var providers: usage.enabledProviders
|
|
||||||
// The selection follows the provider, not the slot it happens to sit in: a
|
|
||||||
// provider whose first scan lands while the panel is open would otherwise
|
|
||||||
// shift the list underneath you and swap out what you were reading.
|
|
||||||
property string selectedProviderId: ""
|
|
||||||
readonly property int providerIndex: {
|
|
||||||
for (var i = 0; i < providers.length; i++)
|
|
||||||
if (providers[i].providerId === selectedProviderId) return i
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
readonly property var provider: providers.length > 0 ? providers[providerIndex] : null
|
|
||||||
|
|
||||||
property bool cursorActive: false
|
|
||||||
|
|
||||||
// Countdowns and "updated" read this instead of Date.now() so the
|
|
||||||
// panel keeps telling the truth while it sits open.
|
|
||||||
property double nowMs: Date.now()
|
|
||||||
|
|
||||||
readonly property var limits: limitWindows(provider)
|
|
||||||
readonly property var models: modelRows(provider)
|
|
||||||
readonly property var headline: bindingWindow(provider)
|
|
||||||
readonly property var balance: provider ? (provider.balance || null) : null
|
|
||||||
// A prepaid account runs low the way a subscription window fills up: the
|
|
||||||
// last 10% of the funded credits lights the same alarm.
|
|
||||||
readonly property bool balanceAlarming: !!balance && balance.funded > 0
|
|
||||||
&& balance.remaining / balance.funded <= 0.1
|
|
||||||
readonly property bool alarming: (!!headline && headline.percent >= 0.9) || balanceAlarming
|
|
||||||
|
|
||||||
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
|
|
||||||
function alpha(c, a) { return Qt.rgba(c.r, c.g, c.b, a) }
|
|
||||||
|
|
||||||
function selectProvider(index) {
|
|
||||||
if (providers.length === 0) return
|
|
||||||
var wrapped = ((index % providers.length) + providers.length) % providers.length
|
|
||||||
selectedProviderId = providers[wrapped].providerId
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshNow() {
|
|
||||||
usage.refreshAll(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function launchAgent() {
|
|
||||||
if (root.bar) root.bar.run("omarchy-agent --pick")
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- limits
|
|
||||||
//
|
|
||||||
// Both providers report the same two shapes: a short rolling session window
|
|
||||||
// and a long weekly one. Everything below normalizes them into one record so
|
|
||||||
// the meters and the hero speak a single language.
|
|
||||||
|
|
||||||
// Claude spells its windows out ("Session (5-hour)"), Codex abbreviates
|
|
||||||
// them ("5h window", "30m window"). Both have to land on the same record.
|
|
||||||
function windowIsLong(text) {
|
|
||||||
return text.indexOf("week") >= 0 || text.indexOf("7-day") >= 0 || text.indexOf("seven") >= 0
|
|
||||||
|| text.indexOf("month") >= 0 || text.indexOf("30-day") >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function windowSpanMs(label) {
|
|
||||||
var text = String(label || "").toLowerCase()
|
|
||||||
if (text.indexOf("month") >= 0 || text.indexOf("30-day") >= 0) return 30 * 24 * 3600 * 1000
|
|
||||||
if (windowIsLong(text)) return 7 * 24 * 3600 * 1000
|
|
||||||
var hours = text.match(/(\d+)\s*-?\s*h(?:our)?\b/)
|
|
||||||
if (hours) return Number(hours[1]) * 3600 * 1000
|
|
||||||
var minutes = text.match(/(\d+)\s*-?\s*m(?:in(?:ute)?s?)?\b/)
|
|
||||||
if (minutes) return Number(minutes[1]) * 60 * 1000
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function windowTitle(label) {
|
|
||||||
var text = String(label || "").toLowerCase()
|
|
||||||
if (text.indexOf("month") >= 0) return "Monthly"
|
|
||||||
if (windowIsLong(text)) return "Weekly"
|
|
||||||
if (text.indexOf("session") >= 0 || windowSpanMs(label) > 0) return "Session"
|
|
||||||
var plain = String(label || "").replace(/\s*\(.*\)\s*/, "").trim()
|
|
||||||
return plain === "" ? "Limit" : plain
|
|
||||||
}
|
|
||||||
|
|
||||||
// A collector that already knows which window a limit belongs to says so,
|
|
||||||
// and that beats reading it back out of the label: a model-scoped limit is
|
|
||||||
// titled after its model, and a name like "Opus 5 (1M context)" would parse
|
|
||||||
// as a one-minute window.
|
|
||||||
function limitWindow(label, percent, resetAt, title) {
|
|
||||||
return {
|
|
||||||
title: String(title || "") !== "" ? String(title) : windowTitle(label),
|
|
||||||
percent: Number(percent),
|
|
||||||
resetAt: String(resetAt || "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function limitWindows(p) {
|
|
||||||
if (!p) return []
|
|
||||||
var out = []
|
|
||||||
var list = p.limits || []
|
|
||||||
for (var i = 0; i < list.length; i++) {
|
|
||||||
var entry = list[i] || {}
|
|
||||||
var percent = Number(entry.percent)
|
|
||||||
if (percent >= 0) out.push(limitWindow(entry.label, percent, entry.resetsAt, entry.title))
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// The window that decides how much room is left — the fullest one, since
|
|
||||||
// that is what stops the next prompt.
|
|
||||||
function bindingWindow(p) {
|
|
||||||
var windows = limitWindows(p)
|
|
||||||
var best = null
|
|
||||||
for (var i = 0; i < windows.length; i++) {
|
|
||||||
if (!best || windows[i].percent > best.percent) best = windows[i]
|
|
||||||
}
|
|
||||||
return best
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetMsFor(w) {
|
|
||||||
if (!w || w.resetAt === "") return -1
|
|
||||||
var ms = new Date(w.resetAt).getTime()
|
|
||||||
return isFinite(ms) ? ms - root.nowMs : -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(ms) {
|
|
||||||
if (!(ms > 0)) return "now"
|
|
||||||
var minutes = Math.floor(ms / 60000)
|
|
||||||
var hours = Math.floor(minutes / 60)
|
|
||||||
var days = Math.floor(hours / 24)
|
|
||||||
if (days > 0) return days + "d " + (hours % 24) + "h"
|
|
||||||
if (hours > 0) return hours + "h " + (minutes % 60) + "m"
|
|
||||||
return Math.max(1, minutes) + "m"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- balance
|
|
||||||
//
|
|
||||||
// Prepaid agents report a credit ledger instead of rate-limit windows: the
|
|
||||||
// record's balance object carries remaining, funded, and spent amounts.
|
|
||||||
|
|
||||||
function currencyPrefix(currency) {
|
|
||||||
var code = String(currency || "USD").toUpperCase()
|
|
||||||
if (code === "USD") return "$"
|
|
||||||
if (code === "EUR") return "€"
|
|
||||||
if (code === "GBP") return "£"
|
|
||||||
return code + " "
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMoney(value, currency) {
|
|
||||||
var amount = Number(value)
|
|
||||||
if (!isFinite(amount)) amount = 0
|
|
||||||
return currencyPrefix(currency) + amount.toFixed(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
function balanceDetailText(b) {
|
|
||||||
if (!b || !(b.funded > 0)) return ""
|
|
||||||
var text = formatMoney(b.spent, b.currency) + " spent of " + formatMoney(b.funded, b.currency) + " funded"
|
|
||||||
if (b.estimated) text += " · estimated"
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- content
|
|
||||||
|
|
||||||
// The plan you pay for, under the name of the tool it pays for. Limits live
|
|
||||||
// in their own section; the hero just says what this is.
|
|
||||||
function heroMeta(p) {
|
|
||||||
if (!p) return ""
|
|
||||||
if (String(p.usageStatusText || "") !== "") return p.usageStatusText
|
|
||||||
var tier = String(p.tierLabel || "")
|
|
||||||
if (tier === "") return "Subscription"
|
|
||||||
return tier.charAt(0).toUpperCase() + tier.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local calendar date, recomputed from nowMs so a panel left open across
|
|
||||||
// midnight moves the "Today" row with the clock.
|
|
||||||
function todayDate() {
|
|
||||||
var now = new Date(root.nowMs)
|
|
||||||
return now.getFullYear()
|
|
||||||
+ "-" + String(now.getMonth() + 1).padStart(2, "0")
|
|
||||||
+ "-" + String(now.getDate()).padStart(2, "0")
|
|
||||||
}
|
|
||||||
|
|
||||||
function dayName(date) {
|
|
||||||
var parsed = new Date(String(date || "") + "T00:00:00")
|
|
||||||
if (isNaN(parsed.getTime())) return String(date || "")
|
|
||||||
return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][parsed.getDay()]
|
|
||||||
}
|
|
||||||
|
|
||||||
function dayLabel(date, today) {
|
|
||||||
if (today) return "Today"
|
|
||||||
return dayName(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
function dayTooltip(day, today) {
|
|
||||||
if (!day) return ""
|
|
||||||
var parsed = new Date(String(day.date) + "T00:00:00")
|
|
||||||
var label = isNaN(parsed.getTime())
|
|
||||||
? String(day.date)
|
|
||||||
: dayName(day.date) + " " + (parsed.getMonth() + 1) + "/" + parsed.getDate()
|
|
||||||
var text = label + " · " + usage.formatTokenCount(Number(day.messageCount || 0)) + " tokens"
|
|
||||||
// Prompt and session counts only exist for today, so they ride along here
|
|
||||||
// instead of taking a section of their own. Billing-API agents never
|
|
||||||
// count prompts, and "0 prompts" would read as a quiet day, not a gap.
|
|
||||||
if (today && provider && provider.hasPromptStats !== false)
|
|
||||||
text += " · " + Number(provider.todayPrompts || 0) + " prompts · "
|
|
||||||
+ Number(provider.todaySessions || 0) + " sessions"
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
function weekPeak(p) {
|
|
||||||
var days = p ? (p.recentDays || []) : []
|
|
||||||
var peak = 0
|
|
||||||
for (var i = 0; i < days.length; i++) peak = Math.max(peak, Number(days[i].messageCount || 0))
|
|
||||||
return peak
|
|
||||||
}
|
|
||||||
|
|
||||||
function modelRows(p) {
|
|
||||||
var usageByModel = p ? (p.modelUsage || {}) : {}
|
|
||||||
var rows = []
|
|
||||||
for (var id in usageByModel) {
|
|
||||||
var bucket = usageByModel[id] || {}
|
|
||||||
var input = Number(bucket.inputTokens || 0)
|
|
||||||
var output = Number(bucket.outputTokens || 0)
|
|
||||||
var cacheRead = Number(bucket.cacheReadInputTokens || 0)
|
|
||||||
var cacheWrite = Number(bucket.cacheCreationInputTokens || 0)
|
|
||||||
rows.push({
|
|
||||||
name: usage.friendlyModelName(id),
|
|
||||||
total: input + output + cacheRead + cacheWrite,
|
|
||||||
input: input,
|
|
||||||
output: output,
|
|
||||||
cacheRead: cacheRead,
|
|
||||||
cacheWrite: cacheWrite
|
|
||||||
})
|
|
||||||
}
|
|
||||||
rows.sort(function(a, b) { return b.total - a.total })
|
|
||||||
return rows.slice(0, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
function modelTooltip(row) {
|
|
||||||
if (!row) return ""
|
|
||||||
return "In " + usage.formatTokenCount(row.input)
|
|
||||||
+ " · out " + usage.formatTokenCount(row.output)
|
|
||||||
+ " · cache read " + usage.formatTokenCount(row.cacheRead)
|
|
||||||
+ " · cache write " + usage.formatTokenCount(row.cacheWrite)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only speaks up when the numbers cover more than this machine.
|
|
||||||
function footerText() {
|
|
||||||
if (usage.syncStatusText !== "") return usage.syncStatusText
|
|
||||||
if (provider && provider.syncEnabled && provider.syncDeviceCount > 0)
|
|
||||||
return "Merged from " + provider.syncDeviceCount + " device" + (provider.syncDeviceCount === 1 ? "" : "s")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Agents that ship a white mark carry an `assets/<id>-light.svg` twin for
|
|
||||||
// light surfaces; marks that work on both (Claude's brand-orange) ship one
|
|
||||||
// file. The luminance check decides which candidate to try first.
|
|
||||||
function colorChannelLuminance(value) {
|
|
||||||
var channel = Number(value)
|
|
||||||
if (!isFinite(channel)) return 0
|
|
||||||
return channel <= 0.03928 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4)
|
|
||||||
}
|
|
||||||
|
|
||||||
function colorLuminance(color) {
|
|
||||||
return 0.2126 * colorChannelLuminance(color.r)
|
|
||||||
+ 0.7152 * colorChannelLuminance(color.g)
|
|
||||||
+ 0.0722 * colorChannelLuminance(color.b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marks resolve by convention, so a new agent's data file needs nothing
|
|
||||||
// from this panel: assets/<id>.svg if it ships one, the module's bar glyph
|
|
||||||
// if it doesn't.
|
|
||||||
function iconCandidatesForProvider(p, surfaceColor) {
|
|
||||||
if (!p) return []
|
|
||||||
var candidates = []
|
|
||||||
if (colorLuminance(surfaceColor || Color.background) >= 0.5)
|
|
||||||
candidates.push(Qt.resolvedUrl("assets/" + p.providerId + "-light.svg"))
|
|
||||||
candidates.push(Qt.resolvedUrl("assets/" + p.providerId + ".svg"))
|
|
||||||
return candidates
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nothing to report, nothing in the bar: Bar.qml collapses a slot whose item
|
|
||||||
// is invisible, so the icon appears the moment the first scan finds usage and
|
|
||||||
// stays away entirely on a machine that has never run either CLI.
|
|
||||||
visible: providers.length > 0
|
|
||||||
implicitWidth: button.implicitWidth
|
|
||||||
implicitHeight: button.implicitHeight
|
|
||||||
|
|
||||||
onProviderIndexChanged: if (panelFlick) panelFlick.contentY = 0
|
|
||||||
onOpenedChanged: if (opened) {
|
|
||||||
cursorActive = false
|
|
||||||
nowMs = Date.now()
|
|
||||||
if (panelFlick) panelFlick.contentY = 0
|
|
||||||
usage.refreshLimits()
|
|
||||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
|
||||||
}
|
|
||||||
|
|
||||||
Main {
|
|
||||||
id: usage
|
|
||||||
settings: root.settings
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cheap enough to keep running: it only re-evaluates text bindings, and a
|
|
||||||
// stale "resets in 2h" on a panel that is open is worse than a timer.
|
|
||||||
Timer {
|
|
||||||
interval: 30000
|
|
||||||
running: root.opened
|
|
||||||
repeat: true
|
|
||||||
onTriggered: root.nowMs = Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
|
||||||
target: root.ipcTarget
|
|
||||||
function open(): void { root.open() }
|
|
||||||
function close(): void { root.close() }
|
|
||||||
function show(): void { root.open() }
|
|
||||||
function hide(): void { root.close() }
|
|
||||||
function toggle(): void { root.toggle() }
|
|
||||||
function refresh(): string { root.refreshNow(); return "ok" }
|
|
||||||
function next(): string { root.selectProvider(root.providerIndex + 1); return "ok" }
|
|
||||||
}
|
|
||||||
|
|
||||||
BarIconButton {
|
|
||||||
id: button
|
|
||||||
anchors.fill: parent
|
|
||||||
bar: root.bar
|
|
||||||
text: ""
|
|
||||||
active: root.alarming
|
|
||||||
onPressed: function(buttonCode) {
|
|
||||||
if (buttonCode === Qt.RightButton) root.launchAgent()
|
|
||||||
else if (buttonCode === Qt.MiddleButton) root.selectProvider(root.providerIndex + 1)
|
|
||||||
else root.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyboardPanel {
|
|
||||||
id: panel
|
|
||||||
anchorItem: button
|
|
||||||
owner: root
|
|
||||||
bar: root.bar
|
|
||||||
open: root.opened
|
|
||||||
focusTarget: keyCatcher
|
|
||||||
contentWidth: panel.fittedContentWidth(Style.space(380))
|
|
||||||
// Taller than the control panels on purpose: this one is a dashboard, and
|
|
||||||
// the whole point is reading limits and history without scrolling.
|
|
||||||
contentHeight: panel.fittedContentHeight(column.implicitHeight, Style.space(640))
|
|
||||||
|
|
||||||
PanelKeyCatcher {
|
|
||||||
id: keyCatcher
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
onMoveRequested: function(dx, dy) {
|
|
||||||
if (dx !== 0) {
|
|
||||||
root.cursorActive = true
|
|
||||||
root.selectProvider(root.providerIndex + dx)
|
|
||||||
}
|
|
||||||
if (dy !== 0)
|
|
||||||
panelFlick.contentY = root.clamp(panelFlick.contentY + dy * Style.space(56), 0,
|
|
||||||
Math.max(0, panelFlick.contentHeight - panelFlick.height))
|
|
||||||
}
|
|
||||||
onActivateRequested: root.refreshNow()
|
|
||||||
onCloseRequested: root.close()
|
|
||||||
onTabRequested: function(direction) { root.switchPanel(direction) }
|
|
||||||
onTextKey: function(t) { if (t === "r" || t === "R") root.refreshNow() }
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
id: panelFlick
|
|
||||||
anchors.fill: parent
|
|
||||||
contentWidth: width
|
|
||||||
contentHeight: column.implicitHeight
|
|
||||||
clip: true
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
|
||||||
interactive: contentHeight > height
|
|
||||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: column
|
|
||||||
width: panelFlick.width
|
|
||||||
spacing: Style.space(12)
|
|
||||||
|
|
||||||
// ---------- Hero: provider mark · name · plan ----------
|
|
||||||
PanelHero {
|
|
||||||
id: hero
|
|
||||||
visible: !!root.provider
|
|
||||||
width: parent.width
|
|
||||||
title: root.provider ? root.provider.providerName : ""
|
|
||||||
meta: root.heroMeta(root.provider)
|
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
|
|
||||||
iconComponent: Component {
|
|
||||||
Item {
|
|
||||||
id: heroMark
|
|
||||||
property var candidates: root.iconCandidatesForProvider(root.provider, root.surface)
|
|
||||||
// Provider objects are rebuilt on every refresh, which churns the
|
|
||||||
// array's identity without changing its content. Restart the fallback
|
|
||||||
// walk only when the URLs change: re-pointing source at a URL whose
|
|
||||||
// load already failed emits no statusChanged, so an identity-only
|
|
||||||
// reset would strand the walker on a missing -light twin.
|
|
||||||
property string candidatesKey: candidates.join("\n")
|
|
||||||
property int candidateIndex: 0
|
|
||||||
onCandidatesKeyChanged: candidateIndex = 0
|
|
||||||
|
|
||||||
width: Style.font.display
|
|
||||||
height: Style.font.display
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: heroMarkImage
|
|
||||||
anchors.fill: parent
|
|
||||||
source: heroMark.candidateIndex < heroMark.candidates.length ? heroMark.candidates[heroMark.candidateIndex] : ""
|
|
||||||
sourceSize.width: Style.font.display * 2
|
|
||||||
sourceSize.height: Style.font.display * 2
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
// Advancing source from inside its own status change trips the
|
|
||||||
// binding-loop detector; defer the step one tick.
|
|
||||||
onStatusChanged: if (status === Image.Error && heroMark.candidateIndex < heroMark.candidates.length)
|
|
||||||
Qt.callLater(function() { heroMark.candidateIndex++ })
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: heroMarkImage.status !== Image.Ready
|
|
||||||
text: button.text
|
|
||||||
color: root.foreground
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.display
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
visible: root.providers.length === 0
|
|
||||||
width: parent.width
|
|
||||||
topPadding: Style.space(24)
|
|
||||||
text: "No AI coding subscriptions found.\nAgents show up here once you've used them."
|
|
||||||
color: root.dim
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.body
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Provider switch ----------
|
|
||||||
Row {
|
|
||||||
id: providerSwitch
|
|
||||||
visible: root.providers.length > 1
|
|
||||||
width: parent.width
|
|
||||||
spacing: Style.spacing.md
|
|
||||||
|
|
||||||
readonly property real cellWidth: root.providers.length > 0
|
|
||||||
? (width - spacing * (root.providers.length - 1)) / root.providers.length
|
|
||||||
: 0
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.providers
|
|
||||||
|
|
||||||
Button {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: providerSwitch.cellWidth
|
|
||||||
text: modelData.providerName
|
|
||||||
selected: index === root.providerIndex
|
|
||||||
hasCursor: root.cursorActive && index === root.providerIndex
|
|
||||||
bordered: true
|
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
fontSize: Style.font.bodySmall
|
|
||||||
verticalPadding: Style.spacing.controlPaddingY
|
|
||||||
onClicked: {
|
|
||||||
root.cursorActive = true
|
|
||||||
root.selectProvider(index)
|
|
||||||
}
|
|
||||||
onHovered: function(isHovered) { if (isHovered) root.cursorActive = true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Status ----------
|
|
||||||
BorderSurface {
|
|
||||||
visible: !!root.provider && String(root.provider.usageStatusText || "") !== ""
|
|
||||||
width: parent.width
|
|
||||||
implicitHeight: statusText.implicitHeight + Style.spacing.xl * 2
|
|
||||||
color: root.alpha(root.urgent, 0.10)
|
|
||||||
borderSpec: Border.flat(root.alpha(root.urgent, 0.35), 1)
|
|
||||||
radius: Style.cornerRadius
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: statusText
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: Style.space(12)
|
|
||||||
anchors.rightMargin: Style.space(12)
|
|
||||||
text: root.provider ? String(root.provider.authHelpText || "") : ""
|
|
||||||
color: root.dim
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Balance / limits ----------
|
|
||||||
PanelSeparator {
|
|
||||||
visible: balanceSection.visible || limitsSection.visible
|
|
||||||
foreground: root.foreground
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: balanceSection
|
|
||||||
visible: !!root.balance
|
|
||||||
width: parent.width
|
|
||||||
spacing: Style.space(10)
|
|
||||||
|
|
||||||
// The meter shows what is left, not what is used: a prepaid
|
|
||||||
// account drains toward empty rather than filling toward a cap.
|
|
||||||
readonly property real ratio: root.balance && root.balance.funded > 0
|
|
||||||
? root.clamp(root.balance.remaining / root.balance.funded, 0, 1)
|
|
||||||
: -1
|
|
||||||
|
|
||||||
PanelSectionHeader {
|
|
||||||
width: parent.width
|
|
||||||
text: "BALANCE"
|
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
implicitHeight: Math.max(balanceLabel.implicitHeight, balanceValue.implicitHeight)
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: balanceLabel
|
|
||||||
text: "Prepaid credits"
|
|
||||||
color: root.foreground
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.body
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: balanceValue
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
text: root.balance ? root.formatMoney(root.balance.remaining, root.balance.currency) : ""
|
|
||||||
color: root.balanceAlarming ? root.urgent : root.foreground
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Meter {
|
|
||||||
visible: balanceSection.ratio >= 0
|
|
||||||
width: parent.width
|
|
||||||
value: balanceSection.ratio
|
|
||||||
alarming: root.balanceAlarming
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
visible: text !== ""
|
|
||||||
width: parent.width
|
|
||||||
text: root.balanceDetailText(root.balance)
|
|
||||||
color: root.dim
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: limitsSection
|
|
||||||
visible: root.limits.length > 0
|
|
||||||
width: parent.width
|
|
||||||
spacing: Style.space(10)
|
|
||||||
|
|
||||||
PanelSectionHeader {
|
|
||||||
text: "LIMITS"
|
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.limits
|
|
||||||
|
|
||||||
LimitRow {
|
|
||||||
required property var modelData
|
|
||||||
width: limitsSection.width
|
|
||||||
window: modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Usage ----------
|
|
||||||
PanelSeparator {
|
|
||||||
visible: usageSection.visible
|
|
||||||
foreground: root.foreground
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: usageSection
|
|
||||||
visible: !!root.provider && root.provider.recentDays && root.provider.recentDays.length > 0
|
|
||||||
width: parent.width
|
|
||||||
spacing: Style.spacing.md
|
|
||||||
|
|
||||||
readonly property var days: root.provider ? (root.provider.recentDays || []) : []
|
|
||||||
readonly property real peak: Math.max(1, root.weekPeak(root.provider))
|
|
||||||
|
|
||||||
PanelSectionHeader {
|
|
||||||
width: parent.width
|
|
||||||
text: "TOKENS BY DAY"
|
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: usageSection.days
|
|
||||||
|
|
||||||
DayRow {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: usageSection.width
|
|
||||||
day: modelData
|
|
||||||
ratio: Number(modelData.messageCount || 0) / usageSection.peak
|
|
||||||
// By date, not by position: the Claude stats-cache fallback can
|
|
||||||
// hand us a window that stops short of today.
|
|
||||||
today: String(modelData.date || "") === root.todayDate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Models ----------
|
|
||||||
PanelSeparator {
|
|
||||||
visible: modelSection.visible
|
|
||||||
foreground: root.foreground
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: modelSection
|
|
||||||
visible: root.models.length > 0
|
|
||||||
width: parent.width
|
|
||||||
spacing: Style.spacing.md
|
|
||||||
|
|
||||||
PanelSectionHeader {
|
|
||||||
width: parent.width
|
|
||||||
text: "TOKENS BY MODEL"
|
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.models
|
|
||||||
|
|
||||||
ModelRow {
|
|
||||||
required property var modelData
|
|
||||||
width: modelSection.width
|
|
||||||
row: modelData
|
|
||||||
// Scaled to the heaviest model, so the top row is always full —
|
|
||||||
// the same scale-to-peak the weekly chart uses for its busiest day.
|
|
||||||
share: modelData.total / Math.max(1, root.models[0].total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
visible: text !== ""
|
|
||||||
width: parent.width
|
|
||||||
topPadding: Style.space(2)
|
|
||||||
text: root.footerText()
|
|
||||||
color: root.dim
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A limit window: label and percentage, meter, and reset countdown.
|
|
||||||
component LimitRow: Column {
|
|
||||||
id: limitRow
|
|
||||||
property var window: null
|
|
||||||
|
|
||||||
readonly property bool alarming: window && window.percent >= 0.9
|
|
||||||
|
|
||||||
spacing: Style.space(6)
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
implicitHeight: Math.max(limitLabel.implicitHeight, limitValue.implicitHeight)
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: limitLabel
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
// A model-scoped window is titled after its model, and those names run
|
|
||||||
// long enough to reach the percentage, so the title gives way first.
|
|
||||||
text: limitRow.window ? limitRow.window.title : ""
|
|
||||||
color: root.foreground
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.body
|
|
||||||
elide: Text.ElideRight
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: limitValue.left
|
|
||||||
anchors.rightMargin: Style.spacing.sm
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: limitValue
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
text: limitRow.window && limitRow.window.percent >= 0
|
|
||||||
? Math.round(limitRow.window.percent * 100) + "%"
|
|
||||||
: "—"
|
|
||||||
color: limitRow.alarming ? root.urgent : root.foreground
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Meter {
|
|
||||||
width: parent.width
|
|
||||||
value: limitRow.window ? limitRow.window.percent : -1
|
|
||||||
alarming: limitRow.alarming
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: resetText
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
width: parent.width
|
|
||||||
text: {
|
|
||||||
var remainingMs = root.resetMsFor(limitRow.window)
|
|
||||||
return remainingMs > 0 ? "Resets in " + root.formatDuration(remainingMs) : ""
|
|
||||||
}
|
|
||||||
color: root.dim
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rounded track showing the percentage of the allowance used.
|
|
||||||
component Meter: Item {
|
|
||||||
id: meter
|
|
||||||
property real value: -1
|
|
||||||
property bool alarming: false
|
|
||||||
property real thickness: Math.max(Style.space(4), Math.round(Style.spacing.controlHeight * 0.14))
|
|
||||||
|
|
||||||
implicitHeight: thickness
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: meterTrack
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: height / 2
|
|
||||||
color: root.track
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: meterTrack.left
|
|
||||||
anchors.verticalCenter: meterTrack.verticalCenter
|
|
||||||
height: meterTrack.height
|
|
||||||
radius: meterTrack.radius
|
|
||||||
width: meterTrack.width * root.clamp(meter.value, 0, 1)
|
|
||||||
color: meter.alarming ? root.urgent : root.foreground
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation { duration: 160; easing.type: Easing.OutCubic }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// One row per day: label, bar, tokens. Today is picked out in full
|
|
||||||
// foreground so the week reads as a run-up to right now.
|
|
||||||
component DayRow: Item {
|
|
||||||
id: dayRow
|
|
||||||
property var day: null
|
|
||||||
property real ratio: 0
|
|
||||||
property bool today: false
|
|
||||||
|
|
||||||
implicitHeight: Math.max(dayLabel.implicitHeight, dayValue.implicitHeight) + Style.spacing.sm
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: dayLabel
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
text: root.dayLabel(dayRow.day ? dayRow.day.date : "", dayRow.today)
|
|
||||||
color: dayRow.today ? root.foreground : root.dim
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
font.bold: dayRow.today
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: Style.space(52)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: dayTrack
|
|
||||||
anchors.left: dayLabel.right
|
|
||||||
anchors.right: dayValue.left
|
|
||||||
anchors.leftMargin: Style.space(8)
|
|
||||||
anchors.rightMargin: Style.space(10)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
height: Math.max(Style.space(4), Math.round(Style.spacing.controlHeight * 0.14))
|
|
||||||
radius: height / 2
|
|
||||||
color: root.track
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
height: parent.height
|
|
||||||
radius: parent.radius
|
|
||||||
width: parent.width * root.clamp(dayRow.ratio, 0, 1)
|
|
||||||
color: dayRow.today ? root.foreground : root.alpha(root.foreground, 0.55)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation { duration: 160; easing.type: Easing.OutCubic }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: dayValue
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
text: usage.formatTokenCount(dayRow.day ? Number(dayRow.day.messageCount || 0) : 0)
|
|
||||||
color: dayRow.today ? root.foreground : root.dim
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.caption
|
|
||||||
font.bold: true
|
|
||||||
horizontalAlignment: Text.AlignRight
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: Style.space(52)
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: dayHover
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelToolTip {
|
|
||||||
visible: dayHover.containsMouse
|
|
||||||
text: root.dayTooltip(dayRow.day, dayRow.today)
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model rows read as a table: the share bar fills the row behind the label
|
|
||||||
// instead of stacking under it, which keeps the whole dashboard on one screen.
|
|
||||||
component ModelRow: Item {
|
|
||||||
id: modelRow
|
|
||||||
property var row: null
|
|
||||||
property real share: 0
|
|
||||||
|
|
||||||
implicitHeight: modelName.implicitHeight + Style.spacing.lg
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Style.cornerRadius
|
|
||||||
color: root.alpha(root.foreground, 0.05)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
width: parent.width * root.clamp(modelRow.share, 0, 1)
|
|
||||||
radius: Style.cornerRadius
|
|
||||||
color: root.alpha(root.foreground, 0.14)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation { duration: 160; easing.type: Easing.OutCubic }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: modelName
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
text: modelRow.row ? modelRow.row.name : ""
|
|
||||||
color: root.foreground
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.bodySmall
|
|
||||||
elide: Text.ElideRight
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Style.space(8)
|
|
||||||
anchors.right: modelTokens.left
|
|
||||||
anchors.rightMargin: Style.space(8)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: modelTokens
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
text: modelRow.row ? usage.formatTokenCount(modelRow.row.total) : ""
|
|
||||||
color: root.dim
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: Style.font.bodySmall
|
|
||||||
font.bold: true
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Style.space(8)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: modelHover
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelToolTip {
|
|
||||||
visible: modelHover.containsMouse
|
|
||||||
text: root.modelTooltip(modelRow.row)
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
# Agents
|
|
||||||
|
|
||||||
One bar icon and one panel for every AI coding subscription on the machine.
|
|
||||||
The panel is strictly a display: it watches the usage records that
|
|
||||||
`omarchy-agent-usage-update` writes to `~/.local/state/omarchy/agents/usage/`
|
|
||||||
and draws whatever appears there. `Panel.qml` owns the bar button and the
|
|
||||||
popup; `Main.qml` discovers and watches the records (and handles the optional
|
|
||||||
cross-device aggregation); `Agent.qml` is the per-record file watcher.
|
|
||||||
|
|
||||||
## Panel
|
|
||||||
|
|
||||||
- **Hero** — the mark, the tool, and the plan it runs on ("Max 20x", "Pro").
|
|
||||||
Auth and endpoint problems replace the plan line and repeat in a card.
|
|
||||||
- **Subscription switch** — one chip per enabled agent (`h`/`l` or click).
|
|
||||||
It appears only when more than one agent is enabled.
|
|
||||||
- **Limits** — the percentage of each allowance used, a matching meter, and
|
|
||||||
the time until the session or weekly window resets.
|
|
||||||
- **Balance** — prepaid agents report a credit ledger instead of limits:
|
|
||||||
remaining credit, a fuel-gauge meter that drains toward empty, and
|
|
||||||
funded-versus-spent detail.
|
|
||||||
- **Tokens by day** — one row per day for the last week: day, bar, tokens, with today
|
|
||||||
bolded at the bottom. Hover today for its prompt and session count.
|
|
||||||
- **Tokens by model** — tokens per model with the bar behind each row scaled
|
|
||||||
to the heaviest model,
|
|
||||||
the same way the weekly chart scales to its busiest day. Hover for the
|
|
||||||
input / output / cache split.
|
|
||||||
|
|
||||||
A subscription appears only when it is enabled in settings and has actually
|
|
||||||
recorded usage — on this machine or on a synced one. With one such agent
|
|
||||||
there is no switch row at all; with none, the module leaves the bar entirely
|
|
||||||
rather than sitting there with nothing to say. A CLI installed mid-session
|
|
||||||
shows up at the next refresh, so nothing polls the disk waiting for it.
|
|
||||||
|
|
||||||
That self-hiding is why the widget ships in the default bar layout: a machine
|
|
||||||
that has never run an AI coding agent draws nothing, and the icon arrives on
|
|
||||||
its own the first time a scan finds usage. Drop it with
|
|
||||||
`omarchy plugin disable omarchy.agents`.
|
|
||||||
|
|
||||||
## Data
|
|
||||||
|
|
||||||
Each agent is one JSON record in `~/.local/state/omarchy/agents/usage/`,
|
|
||||||
written by `omarchy-agent-usage-update`. That command runs one
|
|
||||||
`omarchy-agent-usage-<agent>` collector per agent; the widget invokes it
|
|
||||||
on its refresh timer and whenever you ask for a refresh, and picks up any
|
|
||||||
record that lands in the directory regardless of who wrote it.
|
|
||||||
|
|
||||||
Adding an agent therefore never touches this plugin: ship a collector that
|
|
||||||
prints the record contract (see the `claude` and `codex` collectors in
|
|
||||||
`bin/`), and the panel gains a tab. An `assets/<id>.svg` mark is optional —
|
|
||||||
with an `assets/<id>-light.svg` twin if the mark needs a dark variant for
|
|
||||||
light surfaces — and the bar glyph stands in when there is none.
|
|
||||||
|
|
||||||
| Collector | Limits | Local stats |
|
|
||||||
|---|---|---|
|
|
||||||
| `claude` | Anthropic's OAuth usage endpoint (5-hour session + 7-day weekly) | `~/.claude/projects` transcripts, opencode sessions on an Anthropic provider, plus `stats-cache.json` and `history.jsonl` as fallback |
|
|
||||||
| `codex` | The Codex app-server RPC | native Codex CLI session files (plus pi and opencode sessions) |
|
|
||||||
| `fireworks` | Estimated prepaid balance: configured funding minus rated account costs | Fireworks billing API, grouped by day and model for the last 30 days |
|
|
||||||
|
|
||||||
Claude limits need a signed-in CLI; without credentials the panel says so and
|
|
||||||
falls back to local stats only. A non-default Claude directory is honored via
|
|
||||||
`CLAUDE_CONFIG_DIR`, Codex via `CODEX_HOME`. Fireworks reads
|
|
||||||
`FIREWORKS_API_KEY` and `FIREWORKS_ACCOUNT_ID` first, then
|
|
||||||
`~/.fireworks/auth.ini` (which `firectl set-api-key` creates), then the key
|
|
||||||
opencode stores in `~/.local/share/opencode/auth.json` when Fireworks is
|
|
||||||
signed in there.
|
|
||||||
|
|
||||||
### Fireworks balance
|
|
||||||
|
|
||||||
The collector first asks the account's `:getBalance` endpoint for the real
|
|
||||||
prepaid ledger. That endpoint exists but is permission-gated, and as of
|
|
||||||
August 2026 no console-issued API key passes it — Fireworks appears to
|
|
||||||
reserve it for the dashboard session. The probe stays because it is cheap
|
|
||||||
and the live figure lights up automatically if Fireworks ever opens it to
|
|
||||||
keys. Until then the collector falls back to estimating the balance from
|
|
||||||
configuration in `~/.config/omarchy/agents/fireworks.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"accountId": "",
|
|
||||||
"fundedAmount": 20,
|
|
||||||
"fundedAt": "2026-07-01"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Set `fundedAmount` to the credits purchased and optionally `fundedAt` to the
|
|
||||||
purchase date; with no date, the collector uses the account creation time. It
|
|
||||||
subtracts rated account costs and the panel labels the result as estimated.
|
|
||||||
For a later top-up, increase `fundedAmount` by the new credit while keeping
|
|
||||||
the original `fundedAt`, so both the funding and spend still cover the same
|
|
||||||
period. `accountId` only matters when one API key can access several
|
|
||||||
accounts. Without a configured `fundedAmount` the tab still shows token
|
|
||||||
usage, just no balance. With a live ledger, `fundedAmount` is optional and
|
|
||||||
only adds the meter and the spent-of-funded line under the real figure.
|
|
||||||
|
|
||||||
## Interactions
|
|
||||||
|
|
||||||
- Bar icon: left = panel, right = launch agent, middle = next subscription.
|
|
||||||
- Panel: `h`/`l` switch subscription, `j`/`k` scroll, `r` or Enter refresh,
|
|
||||||
Tab moves to the neighboring bar panel, Esc closes.
|
|
||||||
- IPC: `omarchy-shell omarchy.agents <open|close|toggle|refresh|next>`.
|
|
||||||
|
|
||||||
## Settings
|
|
||||||
|
|
||||||
Settings live in the widget's entry in `~/.config/omarchy/shell.json`. The
|
|
||||||
top-level keys can be set with
|
|
||||||
`omarchy bar set omarchy.agents <key> <value>`:
|
|
||||||
|
|
||||||
| Key | Default | What it does |
|
|
||||||
|---|---|---|
|
|
||||||
| `refreshIntervalSec` | `900` | How often the usage records regenerate |
|
|
||||||
| `syncMode` | `"Off"` | `"On"` writes this machine's snapshot and merges the others |
|
|
||||||
| `syncDir` | `""` | A folder synced by Syncthing, Dropbox, rsync, … |
|
|
||||||
| `syncFileName` | `<hostname>.json` | This machine's snapshot file |
|
|
||||||
| `syncDeviceId` | hostname | Stable device name inside the snapshot |
|
|
||||||
|
|
||||||
Numbers need `--json`, or they land in `shell.json` as strings:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
omarchy bar set omarchy.agents refreshIntervalSec 300 --json
|
|
||||||
omarchy bar set omarchy.agents syncDir '~/Sync/agent-usage'
|
|
||||||
```
|
|
||||||
|
|
||||||
Per-agent enablement is nested, and `set` writes its key literally rather
|
|
||||||
than walking a dotted path — so pass the whole `providers` object as JSON (or
|
|
||||||
edit `shell.json` directly):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
omarchy bar set omarchy.agents providers '{
|
|
||||||
"claude": { "enabled": true },
|
|
||||||
"codex": { "enabled": false },
|
|
||||||
"fireworks": { "enabled": true }
|
|
||||||
}' --json
|
|
||||||
```
|
|
||||||
|
|
||||||
`enabled` defaults to `true` for every discovered agent; set it to `false` to
|
|
||||||
hide a subscription that is installed. Disabled agents are also skipped when
|
|
||||||
the records regenerate.
|
|
||||||
|
|
||||||
With `syncMode` on, every `*.json` snapshot in `syncDir` is merged, so today,
|
|
||||||
the last 7 days, and the all-time totals cover every machine you code on —
|
|
||||||
active days are unioned by date rather than summed. Rate limits stay
|
|
||||||
per-account and are never merged. A record may declare `"scope": "account"`
|
|
||||||
when its stats are account-global rather than machine-local (Fireworks'
|
|
||||||
billing API); those merge by taking the widest value instead of summing, so
|
|
||||||
the same account synced from two machines is not counted twice.
|
|
||||||
|
|
||||||
One caveat on "all-time": the Codex collector only reads native session files
|
|
||||||
touched in the last 30 days, and Fireworks requests the last 30 days from its
|
|
||||||
billing API, so their totals and day counts cover that window. Claude's cover
|
|
||||||
every transcript still on disk.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 91.583 78.750" fill="#111"><title>Antigravity</title><path d="M79.917,75.250 C84.583,78.750 91.583,76.417 85.167,70.000 C65.917,51.333 70.000,0.000 46.083,0.000 C22.167,0.000 26.250,51.333 7.000,70.000 C0.000,77.000 7.583,78.750 12.250,75.250 C30.333,63.000 29.167,41.417 46.083,41.417 C63.000,41.417 61.833,63.000 79.917,75.250 Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 393 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 91.583 78.750" fill="#fff"><title>Antigravity</title><path d="M79.917,75.250 C84.583,78.750 91.583,76.417 85.167,70.000 C65.917,51.333 70.000,0.000 46.083,0.000 C22.167,0.000 26.250,51.333 7.000,70.000 C0.000,77.000 7.583,78.750 12.250,75.250 C30.333,63.000 29.167,41.417 46.083,41.417 C63.000,41.417 61.833,63.000 79.917,75.250 Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 393 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="257" preserveAspectRatio="xMidYMid" viewBox="0 0 256 257"><path fill="#D97757" d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#111" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path clip-rule="evenodd" d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#fff" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path clip-rule="evenodd" d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#111" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>GitHub Copilot</title><path fill-rule="evenodd" d="M3.5 7C4.1 6.4 4.9 6 5.7 6l9.7.1c.8 0 1.6.3 2.2.9l2.6 2.6c.6.6.9 1.4.9 2.2V16c0 .6-.4 1-1 1h-1c0-.1-.1-.2-.1-.3a2.7 2.7 0 1 0-5.3.3h-5.2a2.7 2.7 0 1 0-4.5 0c0-.1-.1-.1-.2-.1a1.8 1.8 0 0 1-1.8-1.8V11c0-.8.3-1.6.9-2.2.5-.5 1.1-.8 1.7-1zM5.7 8c-.2 0-.4.1-.5.2-.3.3-.4.7-.4 1v4.3c.9-.8 2.1-1.2 3.3-1.2s2.4.5 3.3 1.2V9.9l-5.7-.2h-.6zm9.3 1.1v2.3c.9-.8 2.1-1.3 3.3-1.3s2.4.5 3.3 1.2V10.6c0-.4-.1-.7-.4-1-.3-.3-.6-.4-.9-.5l-5.3-.1zm-8.5 6c-.8 0-1.5.4-1.9 1.2a2.4 2.4 0 1 0 3.8 0 2 2 0 0 0-.6-.7 2 2 0 0 0-1.3-.5zm7.8 0c-.8 0-1.5.4-1.9 1.2a2.4 2.4 0 1 0 3.8 0 2 2 0 0 0-.6-.7 2 2 0 0 0-1.3-.5z" clip-rule="evenodd"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 821 B |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#fff" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>GitHub Copilot</title><path fill-rule="evenodd" d="M3.5 7C4.1 6.4 4.9 6 5.7 6l9.7.1c.8 0 1.6.3 2.2.9l2.6 2.6c.6.6.9 1.4.9 2.2V16c0 .6-.4 1-1 1h-1c0-.1-.1-.2-.1-.3a2.7 2.7 0 1 0-5.3.3h-5.2a2.7 2.7 0 1 0-4.5 0c0 .1-.1.1-.2.1a1.8 1.8 0 0 1-1.8-1.8V11c0-.8.3-1.6.9-2.2.5-.5 1.1-.8 1.7-1zM5.7 8c-.2 0-.4.1-.5.2-.3.3-.4.7-.4 1v4.3c.9-.8 2.1-1.2 3.3-1.2s2.4.5 3.3 1.2V9.9l-5.7-.2h-.6zm9.3 1.1v2.3c.9-.8 2.1-1.3 3.3-1.3s2.4.5 3.3 1.2V10.6c0-.4-.1-.7-.4-1-.3-.3-.6-.4-.9-.5l-5.3-.1zm-8.5 6c-.8 0-1.5.4-1.9 1.2a2.4 2.4 0 1 0 3.8 0 2 2 0 0 0-.6-.7 2 2 0 0 0-1.3-.5zm7.8 0c-.8 0-1.5.4-1.9 1.2a2.4 2.4 0 1 0 3.8 0 2 2 0 0 0-.6-.7 2 2 0 0 0-1.3-.5z" clip-rule="evenodd"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 819 B |
@@ -1,6 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
||||||
<g fill="none" stroke="#111" stroke-linejoin="round" stroke-width="4">
|
|
||||||
<path d="M32 6 54 19v26L32 58 10 45V19z"/>
|
|
||||||
<path d="M32 32 10 19M32 32l22-13M32 32v26"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 245 B |
@@ -1,6 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
||||||
<g fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="4">
|
|
||||||
<path d="M32 6 54 19v26L32 58 10 45V19z"/>
|
|
||||||
<path d="M32 32 10 19M32 32l22-13M32 32v26"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 245 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#4D6BFE" fill-rule="evenodd">
|
|
||||||
<title>DeepSeek</title>
|
|
||||||
<path d="M6.6 8.8C5 7.2 3.4 6.4 2.2 6.4C3 8 4.6 10 6.4 11.2C4.8 13 3.2 15.6 2.6 16.6C3.8 18 5.8 16 7.6 13.6C9.4 14.6 11.8 15 13.6 14.6L14.6 16.8L13.2 17.2L12.6 14.8C15.8 15.4 18.4 15.2 19.8 14.2C22 13.2 22.6 11.4 21.8 9.8C21 8 18 6.6 14.8 7.2C12 6.6 8.6 7.6 6.6 8.8ZM17.6 9.9A1.05 1.05 0 1 0 17.6 12A1.05 1.05 0 1 0 17.6 9.9Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 460 B |
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 29.9185 34" fill="none">
|
|
||||||
<path d="M14.1174 13.9205C13.0208 13.2872 11.6461 13.2872 10.5495 13.9205L7.70744 15.5732C7.61476 15.6195 7.52209 15.6659 7.41397 15.6968C7.39852 15.6968 7.36763 15.6968 7.35218 15.7122C7.25951 15.7277 7.15139 15.7431 7.05871 15.7585H7.04327C7.04327 15.7585 7.02782 15.7585 7.01238 15.7585C6.9197 15.7585 6.82703 15.7431 6.73435 15.7277C6.71891 15.7277 6.70346 15.7277 6.68802 15.7277C6.59534 15.6968 6.50267 15.6659 6.40999 15.6195C6.40999 15.6195 6.39454 15.6195 6.3791 15.6195L0.679612 12.3296C0.278022 12.0979 0.0154432 11.6654 0.0154432 11.1866V4.6067C0.0154432 4.12788 0.262576 3.6954 0.679612 3.46371L6.3791 0.173765C6.78069 -0.0579216 7.2904 -0.0579216 7.69199 0.173765L13.3915 3.46371C13.3915 3.46371 13.3915 3.46371 13.4069 3.46371C13.4996 3.51005 13.5768 3.57183 13.6541 3.64906C13.6541 3.64906 13.6695 3.66451 13.6849 3.67995C13.7467 3.74174 13.8085 3.81897 13.8549 3.8962C13.8549 3.8962 13.8703 3.91164 13.8703 3.94253C13.9166 4.03521 13.963 4.12788 13.9784 4.22056C13.9784 4.236 13.9784 4.26689 13.9939 4.28234C14.0093 4.37501 14.0402 4.48313 14.0402 4.59125V7.8812C14.0402 9.14776 14.7198 10.3371 15.8165 10.9704C16.9131 11.6036 18.2878 11.6036 19.3844 10.9704L22.2265 9.31766C22.3191 9.27132 22.4118 9.22499 22.5199 9.19409C22.5354 9.19409 22.5663 9.19409 22.5817 9.17865C22.6744 9.1632 22.7825 9.14776 22.8752 9.13231H22.9215C23.0142 9.13231 23.1069 9.14776 23.1995 9.1632C23.215 9.1632 23.2304 9.1632 23.2459 9.1632C23.3386 9.19409 23.4312 9.22499 23.5239 9.27132C23.5239 9.27132 23.5393 9.27132 23.5548 9.27132L29.2543 12.5613C29.6559 12.793 29.9185 13.2254 29.9185 13.7043V20.2842C29.9185 20.763 29.6713 21.1955 29.2543 21.4271L23.5548 24.7171C23.5548 24.7171 23.5393 24.7171 23.5239 24.7171C23.4312 24.7634 23.3386 24.8098 23.2459 24.8252C23.2304 24.8252 23.215 24.8252 23.1995 24.8252C23.1069 24.8407 23.0142 24.8561 22.9215 24.8561C22.9215 24.8561 22.9061 24.8561 22.8906 24.8561C22.798 24.8561 22.6898 24.8407 22.5972 24.8098C22.5817 24.8098 22.5508 24.8098 22.5354 24.7943C22.4427 24.7634 22.3346 24.7325 22.2419 24.6708L19.3999 23.0181C18.3032 22.3848 16.9286 22.3848 15.8319 23.0181C14.7353 23.6513 14.0556 24.8407 14.0556 26.1072V29.3972C14.0556 29.5053 14.0402 29.6134 14.0093 29.7061C14.0093 29.7215 14.0093 29.7524 13.9939 29.7679C13.963 29.8605 13.9321 29.9532 13.8857 30.0459C13.8857 30.0459 13.8703 30.0768 13.8549 30.0922C13.8085 30.1694 13.7467 30.2467 13.6849 30.3085C13.6849 30.3085 13.6695 30.3239 13.6541 30.3394C13.5768 30.4166 13.4996 30.4784 13.4069 30.5247C13.4069 30.5247 13.4069 30.5247 13.3915 30.5247L7.69199 33.8146C7.4912 33.9382 7.25951 34 7.02782 34C6.79614 34 6.56445 33.9382 6.36365 33.8146L0.664168 30.5247C0.262579 30.293 0 29.8605 0 29.3817V22.8018C0 22.323 0.247133 21.8905 0.664168 21.6588L6.36365 18.3689C6.36365 18.3689 6.3791 18.3689 6.39455 18.3689C6.48722 18.3225 6.5799 18.2762 6.67257 18.2608C6.67257 18.2608 6.70346 18.2608 6.71891 18.2608C6.81158 18.2299 6.90425 18.2299 6.99693 18.2299H7.04327C7.13594 18.2299 7.24406 18.2453 7.33674 18.2762C7.35218 18.2762 7.38307 18.2762 7.39852 18.2762C7.49119 18.3071 7.59932 18.338 7.69199 18.3998L10.534 20.0525C11.6307 20.6857 13.0053 20.6857 14.102 20.0525C15.1986 19.4192 15.8782 18.2299 15.8782 16.9633C15.8782 15.6968 15.1986 14.5074 14.102 13.8742V13.8587L14.1174 13.9205Z" fill="#111"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 29.9185 34" fill="none">
|
|
||||||
<path d="M14.1174 13.9205C13.0208 13.2872 11.6461 13.2872 10.5495 13.9205L7.70744 15.5732C7.61476 15.6195 7.52209 15.6659 7.41397 15.6968C7.39852 15.6968 7.36763 15.6968 7.35218 15.7122C7.25951 15.7277 7.15139 15.7431 7.05871 15.7585H7.04327C7.04327 15.7585 7.02782 15.7585 7.01238 15.7585C6.9197 15.7585 6.82703 15.7431 6.73435 15.7277C6.71891 15.7277 6.70346 15.7277 6.68802 15.7277C6.59534 15.6968 6.50267 15.6659 6.40999 15.6195C6.40999 15.6195 6.39454 15.6195 6.3791 15.6195L0.679612 12.3296C0.278022 12.0979 0.0154432 11.6654 0.0154432 11.1866V4.6067C0.0154432 4.12788 0.262576 3.6954 0.679612 3.46371L6.3791 0.173765C6.78069 -0.0579216 7.2904 -0.0579216 7.69199 0.173765L13.3915 3.46371C13.3915 3.46371 13.3915 3.46371 13.4069 3.46371C13.4996 3.51005 13.5768 3.57183 13.6541 3.64906C13.6541 3.64906 13.6695 3.66451 13.6849 3.67995C13.7467 3.74174 13.8085 3.81897 13.8549 3.8962C13.8549 3.8962 13.8703 3.91164 13.8703 3.94253C13.9166 4.03521 13.963 4.12788 13.9784 4.22056C13.9784 4.236 13.9784 4.26689 13.9939 4.28234C14.0093 4.37501 14.0402 4.48313 14.0402 4.59125V7.8812C14.0402 9.14776 14.7198 10.3371 15.8165 10.9704C16.9131 11.6036 18.2878 11.6036 19.3844 10.9704L22.2265 9.31766C22.3191 9.27132 22.4118 9.22499 22.5199 9.19409C22.5354 9.19409 22.5663 9.19409 22.5817 9.17865C22.6744 9.1632 22.7825 9.14776 22.8752 9.13231H22.9215C23.0142 9.13231 23.1069 9.14776 23.1995 9.1632C23.215 9.1632 23.2304 9.1632 23.2459 9.1632C23.3386 9.19409 23.4312 9.22499 23.5239 9.27132C23.5239 9.27132 23.5393 9.27132 23.5548 9.27132L29.2543 12.5613C29.6559 12.793 29.9185 13.2254 29.9185 13.7043V20.2842C29.9185 20.763 29.6713 21.1955 29.2543 21.4271L23.5548 24.7171C23.5548 24.7171 23.5393 24.7171 23.5239 24.7171C23.4312 24.7634 23.3386 24.8098 23.2459 24.8252C23.2304 24.8252 23.215 24.8252 23.1995 24.8252C23.1069 24.8407 23.0142 24.8561 22.9215 24.8561C22.9215 24.8561 22.9061 24.8561 22.8906 24.8561C22.798 24.8561 22.6898 24.8407 22.5972 24.8098C22.5817 24.8098 22.5508 24.8098 22.5354 24.7943C22.4427 24.7634 22.3346 24.7325 22.2419 24.6708L19.3999 23.0181C18.3032 22.3848 16.9286 22.3848 15.8319 23.0181C14.7353 23.6513 14.0556 24.8407 14.0556 26.1072V29.3972C14.0556 29.5053 14.0402 29.6134 14.0093 29.7061C14.0093 29.7215 14.0093 29.7524 13.9939 29.7679C13.963 29.8605 13.9321 29.9532 13.8857 30.0459C13.8857 30.0459 13.8703 30.0768 13.8549 30.0922C13.8085 30.1694 13.7467 30.2467 13.6849 30.3085C13.6849 30.3085 13.6695 30.3239 13.6541 30.3394C13.5768 30.4166 13.4996 30.4784 13.4069 30.5247C13.4069 30.5247 13.4069 30.5247 13.3915 30.5247L7.69199 33.8146C7.4912 33.9382 7.25951 34 7.02782 34C6.79614 34 6.56445 33.9382 6.36365 33.8146L0.664168 30.5247C0.262579 30.293 0 29.8605 0 29.3817V22.8018C0 22.323 0.247133 21.8905 0.664168 21.6588L6.36365 18.3689C6.36365 18.3689 6.3791 18.3689 6.39455 18.3689C6.48722 18.3225 6.5799 18.2762 6.67257 18.2608C6.67257 18.2608 6.70346 18.2608 6.71891 18.2608C6.81158 18.2299 6.90425 18.2299 6.99693 18.2299H7.04327C7.13594 18.2299 7.24406 18.2453 7.33674 18.2762C7.35218 18.2762 7.38307 18.2762 7.39852 18.2762C7.49119 18.3071 7.59932 18.338 7.69199 18.3998L10.534 20.0525C11.6307 20.6857 13.0053 20.6857 14.102 20.0525C15.1986 19.4192 15.8782 18.2299 15.8782 16.9633C15.8782 15.6968 15.1986 14.5074 14.102 13.8742V13.8587L14.1174 13.9205Z" fill="#fff"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 8 32 15.81" fill="#6720FF"><title>Fireworks AI</title><path d="M15.9851 19.1274C15.0969 19.1274 14.2999 18.6001 13.96 17.7838L9.86258 8H12.2608L15.9991 16.9499L19.7339 8H22.1321L18.0102 17.7873C17.6686 18.6001 16.8733 19.1274 15.9851 19.1274Z"/><path d="M21.3316 23.8029C20.4469 23.8029 19.6533 23.2792 19.31 22.4698C18.9649 21.6535 19.1436 20.7215 19.7672 20.0891L27.2299 12.5302L28.1618 14.7287L21.3298 21.636L31.068 21.5817L32 23.7802L21.3333 23.8065L21.3298 23.8029H21.3316Z"/><path d="M0 23.7766L0.931955 21.5781L10.6702 21.6324L3.83993 14.7234L4.77189 12.5249L12.2345 20.0838C12.8582 20.7145 13.0386 21.65 12.6918 22.4645C12.3484 23.2756 11.5513 23.7977 10.6702 23.7977L0.00350359 23.7731L0 23.7766Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 766 B |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#111" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 748 B |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#fff" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 748 B |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#111" role="img" viewBox="-15 -15 259 259" xmlns="http://www.w3.org/2000/svg"><title>Grok Bot</title><path fill-rule="evenodd" d="M228.541 114.228C228.541 130.133 225.184 145.994 218.738 160.534C212.674 174.217 203.904 186.669 193.065 196.988C155.933 232.34 99.497 238.596 55.5255 212.24C45.097 205.99 35.6851 198.072 27.7451 188.866C19.1926 178.953 12.3686 167.569 7.65781 155.351C2.60712 142.264 0 128.257 0 114.228C0 98.3219 3.35751 82.4611 9.80315 67.9215C15.8672 54.2382 24.6377 41.7862 35.4767 31.4668C72.6081 -3.88483 129.044 -10.1413 173.016 16.2153C183.444 22.4653 192.856 30.3829 200.796 39.5896C209.349 49.5018 216.173 60.8859 220.883 73.1037C225.934 86.1906 228.541 100.198 228.541 114.228Z M130.36 45.98L132.71 46.19L134.98 46.81L137.11 47.83L138.97 49.28L140.47 51.09L141.68 53.12L142.73 55.23L143.76 57.36L144.78 59.49L145.79 61.62L146.79 63.76L147.76 65.91L148.71 68.07L149.63 70.25L150.52 72.43L151.37 74.63L151.99 76.91L152.10 79.26L151.64 81.57L150.59 83.68L149.04 85.45L147.10 86.78L144.90 87.62L142.56 87.93L140.22 87.71L137.98 86.99L135.93 85.82L134.17 84.24L132.78 82.34L131.69 80.25L130.77 78.08L129.87 75.89L128.94 73.72L128.00 71.56L127.03 69.40L126.05 67.26L125.05 65.12L124.03 62.99L122.93 60.90L121.87 58.79L121.03 56.59L120.72 54.26L121.10 51.93L122.15 49.83L123.75 48.10L125.76 46.89L128.01 46.19Z M176.61 37.08L178.72 37.59L180.70 38.48L182.52 39.65L184.20 41.03L185.71 42.59L187.03 44.31L188.20 46.14L189.26 48.03L190.27 49.96L191.26 51.89L192.23 53.84L193.16 55.80L194.05 57.78L194.92 59.77L195.74 61.78L196.53 63.80L197.27 65.84L197.97 67.90L198.47 70.01L198.63 72.18L198.40 74.33L197.58 76.33L195.95 77.72L193.83 78.08L191.71 77.65L189.76 76.69L188.03 75.38L186.53 73.82L185.28 72.05L184.25 70.13L183.40 68.14L182.63 66.11L181.87 64.07L181.07 62.05L180.25 60.04L179.39 58.05L178.49 56.07L177.57 54.10L176.61 52.15L175.62 50.22L174.59 48.31L173.53 46.41L172.54 44.48L171.86 42.42L171.76 40.26L172.62 38.30L174.45 37.19Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#fff" role="img" viewBox="-15 -15 259 259" xmlns="http://www.w3.org/2000/svg"><title>Grok Bot</title><path fill-rule="evenodd" d="M228.541 114.228C228.541 130.133 225.184 145.994 218.738 160.534C212.674 174.217 203.904 186.669 193.065 196.988C155.933 232.34 99.497 238.596 55.5255 212.24C45.097 205.99 35.6851 198.072 27.7451 188.866C19.1926 178.953 12.3686 167.569 7.65781 155.351C2.60712 142.264 0 128.257 0 114.228C0 98.3219 3.35751 82.4611 9.80315 67.9215C15.8672 54.2382 24.6377 41.7862 35.4767 31.4668C72.6081 -3.88483 129.044 -10.1413 173.016 16.2153C183.444 22.4653 192.856 30.3829 200.796 39.5896C209.349 49.5018 216.173 60.8859 220.883 73.1037C225.934 86.1906 228.541 100.198 228.541 114.228Z M130.36 45.98L132.71 46.19L134.98 46.81L137.11 47.83L138.97 49.28L140.47 51.09L141.68 53.12L142.73 55.23L143.76 57.36L144.78 59.49L145.79 61.62L146.79 63.76L147.76 65.91L148.71 68.07L149.63 70.25L150.52 72.43L151.37 74.63L151.99 76.91L152.10 79.26L151.64 81.57L150.59 83.68L149.04 85.45L147.10 86.78L144.90 87.62L142.56 87.93L140.22 87.71L137.98 86.99L135.93 85.82L134.17 84.24L132.78 82.34L131.69 80.25L130.77 78.08L129.87 75.89L128.94 73.72L128.00 71.56L127.03 69.40L126.05 67.26L125.05 65.12L124.03 62.99L122.93 60.90L121.87 58.79L121.03 56.59L120.72 54.26L121.10 51.93L122.15 49.83L123.75 48.10L125.76 46.89L128.01 46.19Z M176.61 37.08L178.72 37.59L180.70 38.48L182.52 39.65L184.20 41.03L185.71 42.59L187.03 44.31L188.20 46.14L189.26 48.03L190.27 49.96L191.26 51.89L192.23 53.84L193.16 55.80L194.05 57.78L194.92 59.77L195.74 61.78L196.53 63.80L197.27 65.84L197.97 67.90L198.47 70.01L198.63 72.18L198.40 74.33L197.58 76.33L195.95 77.72L193.83 78.08L191.71 77.65L189.76 76.69L188.03 75.38L186.53 73.82L185.28 72.05L184.25 70.13L183.40 68.14L182.63 66.11L181.87 64.07L181.07 62.05L180.25 60.04L179.39 58.05L178.49 56.07L177.57 54.10L176.61 52.15L175.62 50.22L174.59 48.31L173.53 46.41L172.54 44.48L171.86 42.42L171.76 40.26L172.62 38.30L174.45 37.19Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 33 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="104" height="104" viewBox="0 0 104 104" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="52" cy="52" r="52" fill="#FF521D"/>
|
|
||||||
<path d="M68.4459 24.9438C69.8639 24.1762 71.6471 23.7178 73.4821 24.2163C73.8278 24.2966 74.1339 24.3916 74.4362 24.5024L75.9186 25.0464L75.5104 26.5727C75.3469 27.1827 75.202 27.723 75.0387 28.3325L74.6442 29.8042L73.1354 29.5972C73.0242 29.5819 72.9246 29.5753 72.8082 29.5747C72.2474 29.5865 71.7542 29.8645 71.2662 30.6772C70.8883 31.307 70.5939 32.1486 70.3815 33.1079C74.871 36.0331 77.8444 41.0994 77.8444 46.8638C77.8443 49.5292 77.2072 52.0505 76.0758 54.2798C71.5108 64.3996 54.1783 84.8815 23.4655 79.0083C21.8797 78.7048 20.9918 77.4165 20.902 76.1245C20.8141 74.8561 21.479 73.487 22.8981 72.8813C31.0698 69.394 35.5884 65.7091 38.5983 61.2329C41.6553 56.6866 43.2606 51.1907 45.3287 43.7251C46.7942 36.1645 53.4462 30.4547 61.4362 30.4546L61.8834 30.4604C62.4398 30.4755 62.9894 30.519 63.5309 30.5884C63.9515 29.7288 64.4424 28.8863 65.0211 28.0659L65.1745 27.8423C65.9623 26.731 67.1108 25.6667 68.4459 24.9438Z" fill="#18181A"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><title>Kimi</title><path fill="#111" d="M272.597333 244.522667c3.925333 0 7.125333 3.925333 7.125334 8.832v284.074666c0 7.338667 6.954667 11.477333 11.605333 6.826667l300.245333-297.813333a6.272 6.272 0 0 1 4.48-1.92h149.973334c6.741333 0 9.685333 10.496 4.437333 15.701333l-278.656 276.48c-3.968 3.925333-3.413333 11.648 1.066667 14.634667l215.552 156.032c24.021333 16.213333 50.602667 26.752 77.824 31.018666 3.626667 0.512 6.357333 4.224 6.357333 8.704v117.546667c0 5.12-3.584 9.216-7.722667 8.789333a294.741333 294.741333 0 0 1-132.565333-49.28l-249.002667-183.210666a6.058667 6.058667 0 0 0-7.850666 0.853333l-93.098667 92.373333a9.813333 9.813333 0 0 0-2.645333 6.869334v124.8c0 4.864-3.2 8.832-7.125334 8.832H169.258667c-3.925333 0-7.125333-3.968-7.125334-8.832V253.354667c0-4.906667 3.2-8.789333 7.125334-8.832h103.338666z"/><path fill="#1783FF" d="M885.76 106.666667a73.856 73.856 0 0 1 0 147.712h-65.152a8.704 8.704 0 0 1-8.704-8.704V180.48c0-40.746667 33.066667-73.856 73.813333-73.856z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><title>Kimi</title><path fill="#fff" d="M272.597333 244.522667c3.925333 0 7.125333 3.925333 7.125334 8.832v284.074666c0 7.338667 6.954667 11.477333 11.605333 6.826667l300.245333-297.813333a6.272 6.272 0 0 1 4.48-1.92h149.973334c6.741333 0 9.685333 10.496 4.437333 15.701333l-278.656 276.48c-3.968 3.925333-3.413333 11.648 1.066667 14.634667l215.552 156.032c24.021333 16.213333 50.602667 26.752 77.824 31.018666 3.626667 0.512 6.357333 4.224 6.357333 8.704v117.546667c0 5.12-3.584 9.216-7.722667 8.789333a294.741333 294.741333 0 0 1-132.565333-49.28l-249.002667-183.210666a6.058667 6.058667 0 0 0-7.850666 0.853333l-93.098667 92.373333a9.813333 9.813333 0 0 0-2.645333 6.869334v124.8c0 4.864-3.2 8.832-7.125334 8.832H169.258667c-3.925333 0-7.125333-3.968-7.125334-8.832V253.354667c0-4.906667 3.2-8.789333 7.125334-8.832h103.338666z"/><path fill="#1783FF" d="M885.76 106.666667a73.856 73.856 0 0 1 0 147.712h-65.152a8.704 8.704 0 0 1-8.704-8.704V180.48c0-40.746667 33.066667-73.856 73.813333-73.856z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#000000" d="M16.278,2C17.434,2 18.371,2.927 18.371,4.07L18.371,16.571C18.388,16.967 18.719,17.282 19.115,17.28C19.511,17.282 19.841,16.966 19.858,16.571L19.858,9.099C19.864,7.973 20.792,7.05 21.918,7.05C21.922,7.05 21.936,7.05 21.94,7.05C23.066,7.05 23.995,7.974 24,9.1L24,15.661C23.998,16.017 23.704,16.308 23.348,16.306C23.347,16.306 23.345,16.306 23.344,16.306C22.989,16.306 22.697,16.016 22.695,15.661L22.695,9.1C22.693,8.683 22.35,8.342 21.933,8.342L21.925,8.342C21.508,8.342 21.165,8.683 21.163,9.1L21.163,16.572C21.157,17.685 20.239,18.598 19.126,18.598L19.104,18.598C17.991,18.598 17.073,17.685 17.067,16.572L17.067,4.072C17.05,3.652 16.699,3.317 16.279,3.319C15.859,3.316 15.508,3.651 15.49,4.071L15.489,19.975C15.482,21.092 14.558,22.006 13.441,22C13.437,22 13.434,22 13.43,22C12.317,22 11.399,21.087 11.393,19.974L11.393,18.04C11.393,17.684 11.685,17.395 12.045,17.395C12.405,17.395 12.697,17.684 12.697,18.04L12.697,19.974C12.697,20.237 12.839,20.48 13.069,20.612C13.299,20.743 13.583,20.743 13.813,20.612C14.042,20.482 14.185,20.238 14.185,19.974L14.185,4.07C14.185,2.927 15.122,2 16.278,2ZM10.604,2C11.76,2 12.697,2.927 12.697,4.07L12.697,15.593C12.695,15.947 12.403,16.238 12.049,16.238L12.041,16.238C11.687,16.238 11.395,15.947 11.393,15.593L11.393,4.07C11.39,3.64 11.034,3.288 10.604,3.29C10.174,3.288 9.818,3.64 9.815,4.07L9.815,18.083C9.808,19.212 8.874,20.137 7.745,20.131C6.615,20.137 5.681,19.213 5.674,18.083L5.674,9.1C5.672,8.683 5.329,8.342 4.912,8.342L4.904,8.342C4.487,8.342 4.144,8.683 4.142,9.1L4.142,12.9C4.136,14.026 3.208,14.949 2.082,14.949L2.06,14.949C0.934,14.949 0.006,14.026 0,12.9L0,11.522C0,11.165 0.292,10.876 0.652,10.876C1.012,10.876 1.305,11.166 1.305,11.522L1.305,12.9C1.305,13.318 1.648,13.657 2.071,13.657C2.494,13.657 2.837,13.318 2.837,12.9L2.837,9.099C2.844,7.97 3.778,7.045 4.907,7.051C6.037,7.045 6.971,7.969 6.978,9.099L6.978,18.083C6.978,18.502 7.321,18.841 7.745,18.841C8.168,18.841 8.511,18.502 8.511,18.083L8.511,4.07C8.511,2.927 9.448,2 10.604,2Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#ffffff" d="M16.278,2C17.434,2 18.371,2.927 18.371,4.07L18.371,16.571C18.388,16.967 18.719,17.282 19.115,17.28C19.511,17.282 19.841,16.966 19.858,16.571L19.858,9.099C19.864,7.973 20.792,7.05 21.918,7.05C21.922,7.05 21.936,7.05 21.94,7.05C23.066,7.05 23.995,7.974 24,9.1L24,15.661C23.998,16.017 23.704,16.308 23.348,16.306C23.347,16.306 23.345,16.306 23.344,16.306C22.989,16.306 22.697,16.016 22.695,15.661L22.695,9.1C22.693,8.683 22.35,8.342 21.933,8.342L21.925,8.342C21.508,8.342 21.165,8.683 21.163,9.1L21.163,16.572C21.157,17.685 20.239,18.598 19.126,18.598L19.104,18.598C17.991,18.598 17.073,17.685 17.067,16.572L17.067,4.072C17.05,3.652 16.699,3.317 16.279,3.319C15.859,3.316 15.508,3.651 15.49,4.071L15.489,19.975C15.482,21.092 14.558,22.006 13.441,22C13.437,22 13.434,22 13.43,22C12.317,22 11.399,21.087 11.393,19.974L11.393,18.04C11.393,17.684 11.685,17.395 12.045,17.395C12.405,17.395 12.697,17.684 12.697,18.04L12.697,19.974C12.697,20.237 12.839,20.48 13.069,20.612C13.299,20.743 13.583,20.743 13.813,20.612C14.042,20.482 14.185,20.238 14.185,19.974L14.185,4.07C14.185,2.927 15.122,2 16.278,2ZM10.604,2C11.76,2 12.697,2.927 12.697,4.07L12.697,15.593C12.695,15.947 12.403,16.238 12.049,16.238L12.041,16.238C11.687,16.238 11.395,15.947 11.393,15.593L11.393,4.07C11.39,3.64 11.034,3.288 10.604,3.29C10.174,3.288 9.818,3.64 9.815,4.07L9.815,18.083C9.808,19.212 8.874,20.137 7.745,20.131C6.615,20.137 5.681,19.213 5.674,18.083L5.674,9.1C5.672,8.683 5.329,8.342 4.912,8.342L4.904,8.342C4.487,8.342 4.144,8.683 4.142,9.1L4.142,12.9C4.136,14.026 3.208,14.949 2.082,14.949L2.06,14.949C0.934,14.949 0.006,14.026 0,12.9L0,11.522C0,11.165 0.292,10.876 0.652,10.876C1.012,10.876 1.305,11.166 1.305,11.522L1.305,12.9C1.305,13.318 1.648,13.657 2.071,13.657C2.494,13.657 2.837,13.318 2.837,12.9L2.837,9.099C2.844,7.97 3.778,7.045 4.907,7.051C6.037,7.045 6.971,7.969 6.978,9.099L6.978,18.083C6.978,18.502 7.321,18.841 7.745,18.841C8.168,18.841 8.511,18.502 8.511,18.083L8.511,4.07C8.511,2.927 9.448,2 10.604,2Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Muse</title><path d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Muse</title><path d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
||||||
<title>Ollama</title>
|
|
||||||
<path fill="#111" fill-rule="evenodd" d="M22 12c-2-7 2-10 5-10c2 0 3 2 5 2s3-2 5-2c3 0 7 3 5 10c6 3 10 9 10 15c0 12-9 20-20 20c-11 0-20-8-20-20c0-6 4-12 10-15zM26 32c0-3 2-5 6-5s6 2 6 5-2 6-6 6-6-3-6-6zM26 21a2 2 0 1 0 .01 0zM38 21a2 2 0 1 0 .01 0z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 346 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
||||||
<title>Ollama</title>
|
|
||||||
<path fill="#fff" fill-rule="evenodd" d="M22 12c-2-7 2-10 5-10c2 0 3 2 5 2s3-2 5-2c3 0 7 3 5 10c6 3 10 9 10 15c0 12-9 20-20 20c-11 0-20-8-20-20c0-6 4-12 10-15zM26 32c0-3 2-5 6-5s6 2 6 5-2 6-6 6-6-3-6-6zM26 21a2 2 0 1 0 .01 0zM38 21a2 2 0 1 0 .01 0z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 346 B |
@@ -1 +0,0 @@
|
|||||||
<svg width='240' height='300' viewBox='0 0 240 300' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_1401_86274)'><mask id='mask0_1401_86274' style='mask-type:luminance' maskUnits='userSpaceOnUse' x='0' y='0' width='240' height='300'><path d='M240 0H0V300H240V0Z' fill='white'/></mask><g mask='url(#mask0_1401_86274)'><path d='M180 240H60V120H180V240Z' fill='#CFCECD'/><path d='M180 60H60V240H180V60ZM240 300H0V0H240V300Z' fill='#211E1E'/></g></g><defs><clipPath id='clip0_1401_86274'><rect width='240' height='300' fill='white'/></clipPath></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 577 B |
@@ -1 +0,0 @@
|
|||||||
<svg width='240' height='300' viewBox='0 0 240 300' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_1401_86283)'><mask id='mask0_1401_86283' style='mask-type:luminance' maskUnits='userSpaceOnUse' x='0' y='0' width='240' height='300'><path d='M240 0H0V300H240V0Z' fill='white'/></mask><g mask='url(#mask0_1401_86283)'><path d='M180 240H60V120H180V240Z' fill='#4B4646'/><path d='M180 60H60V240H180V60ZM240 300H0V0H240V300Z' fill='#F1ECEC'/></g></g><defs><clipPath id='clip0_1401_86283'><rect width='240' height='300' fill='white'/></clipPath></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 577 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 401.4 293.7" fill="#7624F4"><title>OpenRouter</title><path d="M303.9475,17.19926c42.79734,0,77.48933,34.69327,77.48933,77.48933s-34.69199,77.48933-77.48933,77.48933l76.86166,76.86244c9.76367,9.76313,2.84903,26.45667-10.95697,26.45667h-220.88335c-71.32686,0-129.14889-57.82202-129.14889-129.14889S77.64197,17.19926,148.96884,17.19926h154.97866ZM148.96884,68.85881c-42.79607,0-77.48933,34.69327-77.48933,77.48933s34.69327,77.48933,77.48933,77.48933,77.48933-34.69327,77.48933-77.48933-34.69327-77.48933-77.48933-77.48933Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 582 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 401.4 293.7" fill="#C8FF00"><title>OpenRouter</title><path d="M303.9475,17.19926c42.79734,0,77.48933,34.69327,77.48933,77.48933s-34.69199,77.48933-77.48933,77.48933l76.86166,76.86244c9.76367,9.76313,2.84903,26.45667-10.95697,26.45667h-220.88335c-71.32686,0-129.14889-57.82202-129.14889-129.14889S77.64197,17.19926,148.96884,17.19926h154.97866ZM148.96884,68.85881c-42.79607,0-77.48933,34.69327-77.48933,77.48933s34.69327,77.48933,77.48933,77.48933,77.48933-34.69327,77.48933-77.48933-34.69327-77.48933-77.48933-77.48933Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 582 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 141.38 140"><title>Qwen Code</title><path fill="#6D44E8" d="m140.93 85-16.35-28.33-1.93-3.34 8.66-15a3.323 3.323 0 0 0 0-3.34l-9.62-16.67c-.3-.51-.72-.93-1.22-1.22s-1.07-.45-1.67-.45H82.23l-8.66-15a3.33 3.33 0 0 0-2.89-1.67H51.43c-.59 0-1.17.16-1.66.45-.5.29-.92.71-1.22 1.22L32.19 29.98l-1.92 3.33H12.96c-.59 0-1.17.16-1.66.45-.5.29-.93.71-1.22 1.22L.45 51.66a3.323 3.323 0 0 0 0 3.34l18.28 31.67-8.66 15a3.32 3.32 0 0 0 0 3.34l9.62 16.67c.3.51.72.93 1.22 1.22s1.07.45 1.67.45h36.56l8.66 15a3.35 3.35 0 0 0 2.89 1.67h19.25a3.34 3.34 0 0 0 2.89-1.67l18.28-31.67h17.32c.6 0 1.17-.16 1.67-.45s.92-.71 1.22-1.22l9.62-16.67a3.323 3.323 0 0 0 0-3.34ZM51.44 3.33 61.07 20l-9.63 16.66h76.98l-9.62 16.66H45.67l-11.54-20zM57.21 120H22.58l9.63-16.67h19.25l-38.5-66.67h19.25l9.62 16.67L68.78 100l-11.55 20Zm61.59-33.34-9.62-16.67-38.49 66.67-9.63-16.67 9.63-16.66 26.94-46.67h23.1l17.32 30z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 942 B |