Capture this laptop's Omarchy customizations

Everything on top of a stock Omarchy 4.0.4-1 install, gathered so a second
machine can be brought to the same state: the ~/.config files that actually
differ from the shipped defaults, the three hand-built themes, the ten shell
plugins pinned to commits, the two custom scripts, two user units, and the
/etc drop-ins.

install.sh applies it all idempotently, backing up anything it overwrites.
capture.sh runs the other way so the repo stays the source of truth rather
than a snapshot that rots.

indie.herdr-hud's ten local-only commits ship as a git bundle plus a
working-tree patch, since they stay off the upstream PR deliberately.

Secrets, the dotfiles repo's territory (shell/git/mise), the remote-access
stack and aether-managed themes are all scoped out; README says why for each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 12:07:15 -04:00
co-authored by Claude Opus 5
commit 9ac198da26
61 changed files with 3279 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
*.bak.*
*.prefix.*
*.new
+146
View File
@@ -0,0 +1,146 @@
# omarchy-setup
Every customization this laptop has on top of a stock Omarchy install, in one
place, so a second machine can be brought to the same state.
Captured from Omarchy **4.0.4-1** on **2026-10-10**.
```bash
git clone <this repo> ~/omarchy-setup
cd ~/omarchy-setup
./install.sh --dry-run # see what it would change
./install.sh # do it
```
`install.sh` is idempotent and backs up anything it overwrites as
`<file>.bak.<epoch>`. `capture.sh` runs the other way, pulling live changes back
into the repo so this stays the source of truth.
## What is in here
| Path | What |
|---|---|
| `config/` | Mirrors `~/.config`. Only files that differ from Omarchy's shipped defaults. |
| `themes/` | The three hand-built themes, wallpapers included. |
| `plugins.txt` | The 10 Omarchy shell plugins, each pinned to a commit. |
| `plugin-patches/` | Local plugin work that exists on no remote (see below). |
| `bin/` | `omarchy-handoff` and `fix-stuck-displays`. |
| `systemd/user/` | `lan-mouse.service`, `voxtype.service`. |
| `system/` | `/etc` drop-ins: the ARP sysctl and `hid_apple` fnmode. |
| `packages.txt` | Packages beyond the Omarchy baseline, each with a reason. |
## The customizations, and why
**Displays** (`config/hypr/monitors.lua`) are the fiddliest part. The file is the
single source of truth for monitor rules on purpose — overlapping rules spread
across several files is what previously made external displays unrecoverable.
Three outputs are named by EDID description rather than connector, so they match
whichever port they land on: the built-in Chimei panel at 1.25x, the LG IPS236 on
HDMI at 1x, and the LG TV through the Anker dock. The TV is deliberately left on
`preferred` (3840x2160@30) — the dock negotiates DP alt-mode with only two lanes,
so forcing 4K60 produced a "monitor has no mode" loop. `GDK_SCALE=1` with a 1.25
monitor scale; the stock pairing is 2 and `auto`.
`hyprland.lua` sources `hyprmoncfg-monitors.lua` last so the `hyprmoncfg` daemon's
generated layout wins over anything above it.
`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** (`bin/omarchy-handoff`, `systemd/user/lan-mouse.service`)
drives `lan-mouse` to share input with the other Omarchy laptop, bound to
`SUPER+SHIFT+K` (`SUPER+K` is already Keybindings). The script's header comment
is the real documentation — it records the non-obvious facts that cost hours:
`capture_backend` must be `layer-shell` (the portal backend is a silent no-op on
Hyprland 0.56.2), `[authorized_fingerprints]` is keyed by fingerprint and the
reversed form fails silently, and the log variable is `LAN_MOUSE_LOG_LEVEL`, not
`RUST_LOG`. It runs as a user unit rather than from `autostart.lua` because
`o.launch_on_start` sends both streams to `/dev/null`, hiding every pairing
failure.
`system/sysctl.d/99-omarchy-handoff-arp.conf` goes with it: Wi-Fi and the dock's
ethernet both sit on `192.168.1.0/24`, and with the default ARP settings the
subnet's mapping collapses onto whichever NIC replied last.
**Bar and shell** (`config/omarchy/shell.json`) adds `omarchy.active-window` to
the left, five plugin widgets, and `indie.herdr-hud` as a non-bar plugin. Idle is
stretched a long way past stock — screensaver at 20 minutes, lock at 30, against
2.5 and 5.
**Keybindings** (`config/hypr/bindings.lua`) rebinds `SUPER+SHIFT+A` from the
stock ChatGPT webapp to Claude, and adds four Herdr HUD bindings plus Handoff.
**Input** (`config/hypr/input.lua`) enables the three-finger horizontal
workspace gesture that ships commented out, and flattens pointer acceleration for
`unknown-device*`. That second rule belongs to Handoff: lan-mouse forwards
relative deltas the *sending* compositor has already accelerated, and since
Omarchy leaves `accel_profile` adaptive the receiver amplifies them a second time,
so a forwarded pointer arrives hypersensitive while the local trackpad feels
normal. `wlr-virtual-pointer` gives Hyprland no device name — virtual keyboards
get named, pointers don't — hence matching on `unknown-device`; no real pointer on
this machine uses that name, so the rule cannot touch the trackpad.
**Terminals** are all dropped to a smaller font (9 -> 8) except ghostty, which
went the other way to 12. `shell.toml` sets the bar font base size to 10.
**Branding** (`config/omarchy/branding/`) replaces the about and screensaver
ASCII art.
**`hid_apple` fnmode=2** makes the F-keys act as F-keys on an Apple keyboard.
It only takes effect after `sudo mkinitcpio -P` or a module reload.
## Local plugin work
`indie.herdr-hud` is the one piece that exists on no remote. Ten commits live on
its `local` branch — touchpad scroll feel, agent ordering, context-size display —
and they stay off the upstream PR deliberately. `plugin-patches/` carries them as
a git bundle plus a patch for the uncommitted working tree, and `install.sh`
replays both after cloning. `io.github.tallsam.navbar-cat` has a one-file working
tree change captured the same way.
`capture.sh plugins` re-bundles and re-diffs these, so local work keeps flowing
back into the repo.
## Not in here, on purpose
- **Secrets.** `~/.config/lan-mouse/` holds a private key and per-machine
fingerprints; `~/.config/novnc/auth.env`, SSH keys and the git credential
helpers are likewise machine-local. Pair a new machine with
`omarchy-handoff peer <host> <ip>` and `omarchy-handoff doctor`.
- **Shell, git and dev tooling.** That is already its own repo, `dotfiles`
(`git.nic-oconnor.com/public/dotfiles`), which handles zsh, mise, 1Password and
the `~/.local/bin` tool shims. The only overlap avoided here is
`~/.config/git/config`, whose customizations are identity and credential
helpers — `dotfiles` territory.
- **The remote-access stack.** `wayvnc`, `novnc`, `sunshine`, `freerdp`, `remmina`
and `omarchy-remote-desktop` are installed on this laptop but scoped out.
`kokd.remote-desktop` is still in `plugins.txt` since it is a bar plugin, but
it is not placed in `shell.json`.
- **Work tooling.** The keycloak helpers (`kcadm`, `kclogin`, `wren-keycloak`) and
`paperclipai.service`.
- **aether-managed themes.** `pink-gt3rs`, `porsche-cayman` and `porsche-rain`
carry a `.aether-managed` marker and are reinstallable with `aether`, so their
~18MB of wallpapers and previews stay out.
- **`btop.conf`.** Its only diff against stock is v1.4.7 default drift, not a
real customization.
- **Backup files.** Fifteen `.bak.*` and `.prefix.*` files are sitting in
`~/.config/hypr` and `~/.config/omarchy` from earlier editing sessions. They are
not tracked and are safe to delete.
## Adding a file
`capture.sh` only refreshes what the repo already tracks, so start it by hand:
```bash
cp ~/.config/foo/bar.conf config/foo/bar.conf # path mirrors ~/.config
git add config/foo/bar.conf
```
`install.sh config` and `capture.sh config` both walk `config/` recursively, so
nothing else needs changing. Check first that the file actually differs from
stock — `diff /usr/share/omarchy/config/foo/bar.conf ~/.config/foo/bar.conf` —
or you will be tracking a file Omarchy already ships.
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Release display pipes the kernel left enabled on unplugged connectors.
#
# After suspend/resume (or unplugging a USB-C display) i915 can leave a
# connector "disconnected" but still "enabled" on a pipe that Hyprland has
# forgotten. That pipe then blocks every other external output from
# modesetting (black screens, or a ~5 s re-modeset loop).
#
# Fix: force the connector on so Hyprland sees it, disable it from Hyprland
# (which releases the pipe), force it off, then return it to auto-detect.
# Never `hyprctl reload` here: a reload re-enables the connector while it is
# forced on and re-sticks the pipe.
log() { echo "fix-stuck-displays: $*"; logger -t fix-stuck-displays -- "$*"; }
wait_for() { # wait_for <seconds> <command...>
local n=$(( $1 * 4 )); shift
for _ in $(seq "$n"); do "$@" && return 0; sleep 0.25; done
return 1
}
stuck=()
for d in /sys/class/drm/card*-*; do
[[ -f $d/status && -f $d/enabled ]] || continue
[[ $d == *eDP* ]] && continue
[[ $(<"$d/status") == disconnected && $(<"$d/enabled") == enabled ]] && stuck+=("$d")
done
if (( ${#stuck[@]} == 0 )); then
log "no stuck connectors"
exit 0
fi
systemctl --user stop hyprmoncfgd
for d in "${stuck[@]}"; do
out=${d##*/card*-}
log "releasing $out"
echo on | sudo -n tee "$d/status" >/dev/null
wait_for 5 sh -c "hyprctl monitors all | grep -q '^Monitor $out '" || log "$out never appeared in Hyprland"
hyprctl eval "hl.monitor({ output = \"$out\", disabled = true })" >/dev/null
wait_for 5 grep -qx disabled "$d/enabled" || log "$out still enabled after disable"
echo off | sudo -n tee "$d/status" >/dev/null
sleep 1
echo detect | sudo -n tee "$d/status" >/dev/null
log "$out now $(<"$d/status") $(<"$d/enabled")"
done
# Drop any low-refresh fallback hyprmoncfgd wrote while outputs were failing.
rm -f ~/.config/hyprmoncfg/display-fallbacks.json
# hyprmoncfgd re-applies the matching profile (modes, positions) on startup.
systemctl --user start hyprmoncfgd
+505
View File
@@ -0,0 +1,505 @@
#!/bin/bash
# omarchy:summary=Share keyboard and mouse with a nearby Omarchy machine
# omarchy:group=handoff
# omarchy:args=[status|toggle|on|off|position <edge>|peer <host> <ip>|fingerprint|pair <desc> <sha256>|restart|log [-f|N]|fixcase|doctor]
# omarchy:examples=omarchy-handoff position right
# omarchy:examples=omarchy-handoff toggle
# NOTE: the omarchy: metadata above is cosmetic. Omarchy's dispatcher only scans
# its own bin dir, so `omarchy handoff ...` does NOT work -- invoke this as the
# bare `omarchy-handoff`.
# HARD-WON FACTS -- kept here because lan-mouse rewrites config.toml on every
# CLI activate/deactivate (i.e. every SUPER+SHIFT+K) and strips its comments.
# Startup activation does NOT rewrite it; only the CLI path does.
# * capture_backend MUST be "layer-shell". On Hyprland 0.56.2 the
# "input-capture-portal" backend is a SILENT NO-OP: it initializes cleanly,
# org.freedesktop.portal.InputCapture answers on D-Bus (v1, caps=3), and it
# creates ZERO barriers. No edge ever fires and nothing is logged.
# Verify with: lan-mouse --capture-backend <b> test-capture
# and look for "adding capture for position X - using outputs: [NON-EMPTY]".
# Note layer-shell logs an EMPTY pass for all positions first, then a
# populated pass after enumerating outputs -- empty alone is not a failure.
# * layer-shell arms barriers PER OUTPUT, not just the desktop's outer rim,
# so an interior monitor edge is a valid crossing point.
# * [authorized_fingerprints] is keyed by FINGERPRINT: "<sha256>" = "<desc>".
# Reversed is a valid non-empty table, so paired() below CANNOT detect it;
# auth then fails silently and looks exactly like a dropped packet.
# * capture_backend/position/activate_on_startup/fingerprints all survive the
# rewrite; only comments are lost.
# * The daemon runs as the lan-mouse.service USER UNIT. Do NOT start it from
# autostart.lua's o.launch_on_start(): that sends stdout AND stderr to
# /dev/null, so every pairing failure is invisible. Cost: hours of
# diagnosing from a stale pre-reboot log file while the live daemon logged
# nowhere. `omarchy-handoff log` reads the journal for this reason.
# * The log level env var is LAN_MOUSE_LOG_LEVEL, NOT RUST_LOG. RUST_LOG is
# silently ignored, so it looks like debug logging "doesn't work".
# * Transport is DTLS over UDP (webrtc-dtls), not QUIC. The error
# "Alert is Fatal or Close Notify" (= ErrAlertFatalOrClose) means the REMOTE
# sent a fatal alert, i.e. the PEER REJECTED OUR CERT. Something answered,
# so it is a TRUST failure, never a firewall. Contrast "Connection timed
# out" = nothing answered at all (firewall, or the source-address trap
# below).
# * The fingerprint lan-mouse compares is sha256 of the DER certificate --
# exactly `openssl x509 -noout -fingerprint -sha256`. Not the PEM file hash,
# not the pubkey hash.
# * SOURCE-ADDRESS TRAP (two NICs on one subnet): the daemon listens on
# 0.0.0.0 but does not set IP_PKTINFO, so replies take the kernel's
# route-chosen source. With the dock up, `ip route get <peer>` prefers the
# dock NIC, so a peer connecting to our Wi-Fi address gets replies from the
# dock address and drops them as an unknown 4-tuple -> "Connection timed
# out" on that address only, while ICMP to it works fine. Undocked it
# inverts and works. Listing both peer-side IPs is correct; the unusable
# one just costs a 5s timeout per attempt.
# Thin wrapper over lan-mouse. lan-mouse's own config.toml is the single source
# of truth -- this script only drives its CLI so the edge and pairing are
# changeable without hand-editing TOML, and so a keybind has something to call.
set -euo pipefail
CONFIG="${CONFIG:-$HOME/.config/lan-mouse/config.toml}"
CERT="$HOME/.config/lan-mouse/lan-mouse.pem"
SOCKET="${XDG_RUNTIME_DIR:-/run/user/$UID}/lan-mouse-socket.sock"
LOG="${XDG_STATE_HOME:-$HOME/.local/state}/lan-mouse.log"
notify() { omarchy-notification-send -g "󰌌" "Handoff" "$1" 2>/dev/null || echo "$1"; }
die() { echo "omarchy-handoff: $1" >&2; exit 1; }
# The daemon owns the socket; every cli call is a no-op without it.
require_daemon() {
[[ -S $SOCKET ]] || die "lan-mouse daemon is not running (try: omarchy-handoff restart)"
}
# Resolve a client's numeric id from its hostname, since lan-mouse addresses
# clients by an index that shifts as entries are added or removed.
client_id() {
local host="${1:-}"
require_daemon
if [[ -n $host ]]; then
lan-mouse cli list | sed -n "s/^id \([0-9]\+\): ${host}:.*/\1/p" | head -n1
else
lan-mouse cli list | sed -n 's/^id \([0-9]\+\):.*/\1/p' | head -n1
fi
}
first_host() {
require_daemon
lan-mouse cli list | sed -n 's/^id [0-9]\+: \([^:]*\):.*/\1/p' | head -n1
}
cmd_status() {
if [[ -S $SOCKET ]]; then
echo "daemon: running"
else
echo "daemon: stopped"
return 0
fi
echo "peers:"
lan-mouse cli list | sed 's/^/ /'
echo "fingerprint (this machine):"
echo " $(cmd_fingerprint)"
}
cmd_fingerprint() {
[[ -f $CERT ]] || die "no certificate yet at $CERT (start the daemon once)"
# MUST be lowercase. lan-mouse compares with a plain HashMap key lookup
# (src/listen.rs: authorized.contains_key(&fingerprints[0])) against
# crypto::generate_fingerprint(), which formats "{x:02x}" and .to_lowercase().
# openssl prints UPPERCASE, so passing it through unchanged silently never
# matches -- that was the real cause of the dead crossing on 2026-10-10.
openssl x509 -in "$CERT" -noout -fingerprint -sha256 | cut -d= -f2 | tr "A-Z" "a-z"
}
# Activation is what actually gates input crossing the edge, so on/off/toggle
# map to activate/deactivate rather than stopping the daemon.
is_active() {
local id="$1"
lan-mouse cli list | grep -q "^id ${id}:.*active: true"
}
# Arming the edge with no authorized peer key is the one way this locks up a
# machine: input gets captured, the peer rejects it unauthenticated, and the
# keyboard looks dead. Refuse unless pairing is mutual, or --force is given.
# Parse the actual TOML table rather than slicing text: a line-range slice from
# the header to EOF also counts keys belonging to any table emitted AFTER
# [authorized_fingerprints], and `lan-mouse cli save-config` -- not us -- chooses
# that order. An empty fingerprint table followed by any populated table made the
# text version return true and arm an unpaired machine, i.e. it failed OPEN into
# exactly the lockup this guard exists to prevent.
# A malformed or unreadable config counts as 0, so it fails CLOSED.
paired() {
[[ -f $CONFIG ]] || return 1
local n
n=$(python3 -I -c '
import sys, tomllib
try:
with open(sys.argv[1], "rb") as f:
print(len(tomllib.load(f).get("authorized_fingerprints", {})))
except (OSError, tomllib.TOMLDecodeError):
print(0)
' "$CONFIG")
(( n > 0 ))
}
cmd_on() {
local force=0
[[ ${1:-} == --force ]] && { force=1; shift; }
local id; id=$(client_id "${1:-}")
[[ -n $id ]] || die "no peer configured"
if (( ! force )) && ! paired; then
die "no peer fingerprint authorized -- refusing to capture input.
Crossing the edge now would forward this keyboard and mouse to a peer that
cannot accept it, and the machine would look frozen (release: Ctrl+Shift+Super+Alt).
Pair first: omarchy-handoff pair <name> <sha256>
Override: omarchy-handoff on --force"
fi
lan-mouse cli activate "$id"
notify "Handoff enabled"
}
cmd_off() { local id; id=$(client_id "${1:-}"); [[ -n $id ]] || die "no peer configured"; lan-mouse cli deactivate "$id"; notify "Handoff disabled"; }
cmd_toggle() {
local id; id=$(client_id "${1:-}")
[[ -n $id ]] || die "no peer configured (try: omarchy-handoff peer <host> <ip>)"
if is_active "$id"; then cmd_off "${1:-}"; else cmd_on "${1:-}"; fi
}
# The whole point of the wrapper: the physical side the other laptop sits on is
# one value, not a code change.
cmd_position() {
local edge="${1:-}" id
case "$edge" in
left|right|top|bottom) ;;
*) die "position must be one of: left right top bottom" ;;
esac
id=$(client_id "${2:-}")
[[ -n $id ]] || die "no peer configured"
lan-mouse cli set-position "$id" "$edge"
lan-mouse cli save-config
notify "Peer is now on the $edge"
}
cmd_peer() {
local host="${1:-}" ip="${2:-}" edge="${3:-left}"
[[ -n $host && -n $ip ]] || die "usage: omarchy-handoff peer <hostname> <ip>[,<ip>...] [edge]"
require_daemon
local existing; existing=$(client_id "$host")
[[ -n $existing ]] && lan-mouse cli remove-client "$existing"
lan-mouse cli add-client --hostname "$host" --ips "$ip" --port 4242
cmd_position "$edge" "$host"
}
cmd_pair() {
local desc="${1:-}" fp="${2:-}"
# Normalise case: a fingerprint copied from openssl or a chat message is
# usually uppercase, and lan-mouse's comparison is case-SENSITIVE.
fp=$(tr "A-Z" "a-z" <<<"$fp")
[[ -n $desc && -n $fp ]] || die "usage: omarchy-handoff pair <description> <sha256-fingerprint>"
require_daemon
lan-mouse cli authorize-key "$desc" "$fp"
lan-mouse cli save-config
notify "Authorized $desc"
}
cmd_fixcase() {
[[ -f $CONFIG ]] || die "no config at $CONFIG"
python3 -I -c "
import re, sys
path = sys.argv[1]
with open(path) as f:
s = f.read()
pat = re.compile(\"\\\"([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){31})\\\"\\\\s*=\")
n = sum(1 for m in pat.finditer(s) if any(c.isupper() for c in m.group(1)))
s2 = pat.sub(lambda m: chr(34) + m.group(1).lower() + chr(34) + \" =\", s)
if n:
with open(path, \"w\") as f:
f.write(s2)
print(n)
" "$CONFIG" | {
read -r n
if (( n > 0 )); then
echo "lowercased $n fingerprint(s); restart to load: omarchy-handoff restart"
else
echo "nothing to fix -- all fingerprints already lowercase"
fi
}
}
cmd_restart() {
pkill -x lan-mouse 2>/dev/null || true
mkdir -p "$(dirname "$LOG")"
# Keep the log. Discarding it cost real debugging time once: the portal
# backend's silent no-op is invisible without these lines.
if command -v uwsm-app >/dev/null; then
uwsm-app -- lan-mouse daemon >>"$LOG" 2>&1 &
else
setsid lan-mouse daemon >>"$LOG" 2>&1 &
fi
sleep 2
[[ -S $SOCKET ]] || die "daemon failed to start (see $LOG)"
notify "Handoff daemon restarted"
}
# Show what the capture layer is actually doing -- the only way to tell a real
# barrier from a backend that selected cleanly and armed nothing.
cmd_log() {
# The daemon runs as the lan-mouse.service user unit, so its output is in the
# journal. It USED to be started by o.launch_on_start() in autostart.lua,
# which sent stdout AND stderr to /dev/null -- so for hours this printed a
# stale pre-reboot file while the live daemon logged nowhere at all. Read the
# journal first; fall back to $LOG only for a hand-started daemon.
if systemctl --user --quiet is-enabled lan-mouse.service 2>/dev/null \
|| systemctl --user --quiet is-active lan-mouse.service 2>/dev/null; then
if [[ ${1:-} == -f ]]; then
journalctl --user -u lan-mouse.service -o cat -f
else
journalctl --user -u lan-mouse.service -o cat --no-pager -n "${1:-40}"
fi
return
fi
[[ -f $LOG ]] || die "no journal unit and no log at $LOG (try: omarchy-handoff restart)"
echo "# NOTE: reading fallback file $LOG (lan-mouse.service is not active)" >&2
if [[ ${1:-} == -f ]]; then tail -f "$LOG"; else tail -n "${1:-40}" "$LOG"; fi
}
# Health check. Deliberately exits non-zero on failure: a check that can only
# report success is decoration. Scope is capture + trust-entry SHAPE -- it
# cannot prove the peer trusts our key, only that our own table isn't malformed.
# Pass a saved log as $1 to parse it instead of probing (used to test failures).
cmd_doctor() {
local src="${1:-}" rc=0 probe cb last
echo "== configured backend =="
cb=$(python3 -I -c '
import sys, tomllib
try:
with open(sys.argv[1], "rb") as f:
print(tomllib.load(f).get("capture_backend") or "")
except Exception:
print("")
' "$CONFIG")
if [[ -z $cb ]]; then
echo " (unset -> defaults to input-capture-portal)"
echo " WARNING: that default is a SILENT NO-OP on Hyprland 0.56.2 -- it"
echo " initializes cleanly and creates no barriers. Set capture_backend = \"layer-shell\"."
rc=1
elif [[ $cb != layer-shell ]]; then
echo " $cb"
echo " WARNING: only layer-shell is known to create real barriers here."
rc=1
else
echo " layer-shell (correct)"
fi
echo
echo "== trust entry shape =="
# Checks CASE as well as ordering. An uppercase key is a valid TOML table
# entry and the right digest, so nothing anywhere complains -- the daemon
# just never matches it and both ends send DTLS bad_certificate.
local badcase
badcase=$(python3 -I -c "
import sys, tomllib
try:
with open(sys.argv[1], \"rb\") as f:
fps = tomllib.load(f).get(\"authorized_fingerprints\", {})
except Exception:
fps = {}
print(sum(1 for k in fps if any(c.isupper() for c in k)))
" "$CONFIG" 2>/dev/null || echo 0)
if [[ ${badcase:-0} -gt 0 ]]; then
echo " FAILING -- ${badcase} fingerprint(s) contain UPPERCASE hex."
echo " lan-mouse compares the string verbatim and generates lowercase, so"
echo " an uppercase key never matches: the digest is right, the case is not,"
echo " nothing logs a complaint, and both ends send DTLS bad_certificate."
echo " Fix: omarchy-handoff fixcase"
rc=1
fi
# Catches a REVERSED entry, which paired() cannot: a backwards
# "description" = "<sha256>" line is a valid non-empty table, and auth then
# fails silently and looks exactly like a dropped packet.
python3 -I -c '
import sys, tomllib
try:
with open(sys.argv[1], "rb") as f:
fps = tomllib.load(f).get("authorized_fingerprints", {})
except Exception:
print(" could not parse config"); sys.exit(1)
if not fps:
print(" NO fingerprints authorized -- the peer cannot be trusted yet."); sys.exit(1)
bad = [k for k in fps if k.count(":") < 16]
for k, v in fps.items():
label = "order OK " if k.count(":") >= 16 else "BACKWARDS"
print(f" {label} {k[:24]}... = {v!r}")
if bad:
print(" ERROR: key must be the FINGERPRINT, value the description.")
print(" Fix: lan-mouse cli remove-authorized-key <desc>, then")
print(" lan-mouse cli authorize-key <desc> <sha256>")
sys.exit(1)
' "$CONFIG" || rc=1
echo
echo "== peer reachability =="
# Added after both machines spent hours green on doctor while a firewall ate
# every inbound packet. Capture and trust can both be perfect and nothing
# crosses. This reads the DAEMON'S OWN connect outcomes -- ground truth from
# the real protocol, not a synthetic probe that proves nothing for UDP.
local pip pport ufwline verdict
pip=$(python3 -I -c "
import sys, tomllib
try:
with open(sys.argv[1], \"rb\") as f:
cs = tomllib.load(f).get(\"clients\", [])
ips = cs[0].get(\"ips\", []) if cs else []
print(ips[0] if ips else \"\")
except Exception:
print(\"\")
" "$CONFIG" 2>/dev/null || true)
pport=$(python3 -I -c "
import sys, tomllib
try:
with open(sys.argv[1], \"rb\") as f:
d = tomllib.load(f)
print(d.get(\"clients\", [{}])[0].get(\"port\") or d.get(\"port\") or 4242)
except Exception:
print(4242)
" "$CONFIG" 2>/dev/null || true)
if [[ -z $pip ]]; then
echo " no peer ip configured -- nothing to reach."
rc=1
else
echo " peer: ${pip}:${pport}"
if ping -c1 -W2 -q "$pip" >/dev/null 2>&1; then
echo " ICMP: host is up"
else
echo " ICMP: no reply -- peer is off, asleep, or on another network."
echo " (Everything below will fail for that reason alone.)"
rc=1
fi
# Our own inbound half. This is the check that was missing entirely.
if command -v ufw >/dev/null && sudo -n ufw status >/dev/null 2>&1; then
ufwline=$(sudo -n ufw status 2>/dev/null | grep -E "^${pport}/udp" || true)
if [[ -n $ufwline ]]; then
echo " our ufw: ALLOWS ${pport}/udp -- ${ufwline}"
else
echo " our ufw: NO rule for ${pport}/udp. If the default is deny incoming,"
echo " the peer's handshake never reaches our daemon and it looks like a"
echo " silent drop. Check: sudo ufw status verbose"
rc=1
fi
else
echo " our ufw: not checked (needs passwordless sudo); verify by hand."
fi
# What did the real protocol actually do most recently?
verdict=$(cmd_log 400 2>/dev/null | grep -aE "connect|Alert|connection" | tail -n1 || true)
if [[ -z $verdict ]]; then
echo " daemon log: no connection attempt recorded yet."
echo " (Cross the edge once, then re-run -- the attempt is what tells us"
echo " whether this is a firewall, a trust, or a routing problem.)"
else
echo " last connect line: ${verdict#*] }"
case $verdict in
*"Alert is Fatal or Close Notify"*)
echo " => the PEER ACTIVELY REJECTED our certificate (DTLS fatal alert)."
echo " Not a firewall: something answered. The peer does not trust"
echo " our fingerprint. Ours is:"
echo " $(cmd_fingerprint 2>/dev/null || echo "<no cert>")"
echo " The peer must authorize exactly that, keyed BY FINGERPRINT."
rc=1 ;;
*"Connection timed out"*)
echo " => NOTHING ANSWERED. Either the peer's firewall is dropping"
echo " ${pport}/udp, or our reply left via a different source address"
echo " than the peer sent to (two NICs on one subnet does this --"
echo " check: ip route get ${pip})."
rc=1 ;;
*"connection established"*|*"connected to"*)
echo " => transport is UP." ;;
esac
fi
fi
echo
echo "== barrier probe =="
if [[ -n $src ]]; then
probe=$(cat "$src")
else
# test-capture runs until killed, so timeout's 124 is expected, not a failure.
probe=$(timeout 8 lan-mouse --capture-backend "${cb:-layer-shell}" test-capture 2>&1 || true)
fi
if ! grep -q "adding capture" <<<"$probe"; then
echo " NO BARRIERS CREATED -- capture is dead."
echo " Full probe output:"
sed 's/^/ /' <<<"$probe"
rc=1
else
# test-capture probes ALL FOUR positions and the registration order is NOT
# stable, so a bare `| tail -n1` returns whichever edge happened to land
# last -- not ours. Observed this report "left" and "bottom" on consecutive
# runs from an unchanged config. Validate only OUR configured edge.
# Within that edge, the populated list grows as each output registers, so
# the LAST line for it is the authoritative one.
local pos
pos=$(python3 -I -c '
import sys, tomllib
try:
with open(sys.argv[1], "rb") as f:
cs = tomllib.load(f).get("clients", [])
print(cs[0].get("position", "") if cs else "")
except Exception:
print("")
' "$CONFIG")
if [[ -z $pos ]]; then
echo " cannot determine our configured edge from $CONFIG"
rc=1
else
echo " our configured edge: $pos"
last=$(grep "adding capture for position ${pos} " <<<"$probe" | tail -n1 || true)
if [[ -z $last ]]; then
echo " FAILING -- no barrier was ever created for \"$pos\"."
echo " (Other edges may have armed; that does not help us.)"
rc=1
else
echo " last line for $pos: ${last#*] }"
if [[ $last == *"using outputs: []"* ]]; then
echo " FAILING -- the final pass for $pos lists no outputs."
echo " (An empty pass BEFORE populated ones is normal and transient.)"
rc=1
else
echo " OK -- barrier is real on the output(s) above, so $pos can fire."
fi
fi
fi
fi
echo
if (( rc == 0 )); then
echo "RESULT: OK"
else
echo "RESULT: PROBLEMS FOUND (see above)"
fi
echo " Scope: capture barriers, reachability, and the shape of OUR OWN"
echo " trust entry. It cannot prove the peer trusts our key unless the"
echo " daemon has actually attempted a connection -- cross the edge once."
return $rc
}
case "${1:-status}" in
status) cmd_status ;;
toggle) shift; cmd_toggle "$@" ;;
on|enable) shift; cmd_on "$@" ;;
off|disable) shift; cmd_off "$@" ;;
position) shift; cmd_position "$@" ;;
peer) shift; cmd_peer "$@" ;;
fingerprint) cmd_fingerprint ;;
pair) shift; cmd_pair "$@" ;;
restart) cmd_restart ;;
log) shift; cmd_log "$@" ;;
fixcase) shift; cmd_fixcase "$@" ;;
doctor) shift; cmd_doctor "$@" ;;
*) die "unknown command '${1}'" ;;
esac
Executable
+189
View File
@@ -0,0 +1,189 @@
#!/bin/bash
# Pull the live machine's state back into this repo - the reverse of install.sh.
#
# Run it after customizing something by hand, then review `git diff` and commit.
# It only refreshes paths the repo already tracks; it never invents new ones, so
# adding a newly customized file is a deliberate act (see README, "Adding a file").
#
# ./capture.sh # refresh everything
# ./capture.sh config # only the named steps
# ./capture.sh --list
#
# Steps: config themes plugins bin services system packages
set -uo pipefail
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
STEPS=(config themes plugins bin services system packages)
c_ch=$'\033[33m'; c_skip=$'\033[2m'; c_warn=$'\033[33m'; c_hd=$'\033[1m'; c_off=$'\033[0m'
[[ -t 1 ]] || { c_ch=; c_skip=; c_warn=; c_hd=; c_off=; }
say() { printf '%s\n' "$*"; }
head_() { printf '\n%s==> %s%s\n' "$c_hd" "$*" "$c_off"; }
chg() { printf ' %sM %s%s\n' "$c_ch" "$*" "$c_off"; }
skip() { printf ' %s= %s%s\n' "$c_skip" "$*" "$c_off"; }
warn() { printf ' %s! %s%s\n' "$c_warn" "$*" "$c_off"; }
gone() { printf ' %s? %s%s\n' "$c_warn" "$*" "$c_off"; }
# pull_back <live> <repo-copy> <label>
pull_back() {
local live=$1 dest=$2 label=$3
if [[ ! -e $live ]]; then gone "$label - not on this machine any more"; return; fi
if cmp -s "$live" "$dest"; then skip "$label"; else cp "$live" "$dest"; chg "$label"; fi
}
step_config() {
head_ "~/.config -> config/"
local dest rel
while IFS= read -r -d '' dest; do
rel=${dest#"$REPO/config/"}
pull_back "$HOME/.config/$rel" "$dest" "$rel"
done < <(find "$REPO/config" -type f -print0 | sort -z)
}
step_themes() {
head_ "Themes"
local t name live
for t in "$REPO"/themes/*/; do
[[ -d $t ]] || continue
name=$(basename -- "$t")
live="$HOME/.config/omarchy/themes/$name"
if [[ ! -d $live ]]; then gone "$name - no longer installed"; continue; fi
if diff -rq "$live" "$t" >/dev/null 2>&1; then
skip "$name"
else
rm -rf "$t"; mkdir -p "$t"; cp -r "$live/." "$t/"
chg "$name"
fi
done
# Flag themes installed by hand that the repo does not carry.
local l lname
for l in "$HOME"/.config/omarchy/themes/*/; do
[[ -d $l ]] || continue
lname=$(basename -- "$l")
[[ -d "$REPO/themes/$lname" ]] && continue
[[ -e "$l/.aether-managed" ]] && continue
warn "$lname is installed but not in this repo (cp -r it into themes/ to track it)"
done
}
step_plugins() {
head_ "Plugin pins and local work"
local dir="$HOME/.config/omarchy/plugins"
local tmp; tmp=$(mktemp)
{
echo "# Omarchy shell plugins. Fields: id<TAB>repo<TAB>pinned-sha"
echo "# install.sh clones each into ~/.config/omarchy/plugins/<id> and checks out the sha."
local p id url sha
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"
# herdr-hud's local-only branch, re-bundled
local p="$dir/indie.herdr-hud"
if [[ -d $p/.git ]] && git -C "$p" rev-parse --verify --quiet local >/dev/null; then
local b="$REPO/plugin-patches/indie.herdr-hud.bundle"
git -C "$p" bundle create "$b.new" origin/main..local >/dev/null 2>&1
# Bundles embed a timestamp-free pack but differ byte-wise; compare the tips instead.
if [[ -f $b ]] && [[ $(git -C "$p" bundle list-heads "$b" 2>/dev/null) == $(git -C "$p" bundle list-heads "$b.new" 2>/dev/null) ]]; then
rm -f "$b.new"; skip "indie.herdr-hud.bundle"
else
mv "$b.new" "$b"; chg "indie.herdr-hud.bundle"
fi
[[ -f $p/bin/herdr-agents ]] && pull_back "$p/bin/herdr-agents" \
"$REPO/plugin-patches/indie.herdr-hud--bin--herdr-agents" "indie.herdr-hud bin/herdr-agents"
fi
local f id
for f in "$REPO"/plugin-patches/*-worktree.patch; do
[[ -f $f ]] || continue
id=$(basename -- "$f"); id=${id%-worktree.patch}
if [[ ! -d $dir/$id/.git ]]; then gone "$id - not installed"; continue; fi
local tmp2; tmp2=$(mktemp)
git -C "$dir/$id" diff > "$tmp2"
if cmp -s "$tmp2" "$f"; then skip "$id-worktree.patch"; else mv "$tmp2" "$f"; chg "$id-worktree.patch"; fi
rm -f "$tmp2"
done
}
step_bin() {
head_ "~/.local/bin -> bin/"
local f
for f in "$REPO"/bin/*; do
[[ -f $f ]] || continue
pull_back "$HOME/.local/bin/$(basename -- "$f")" "$f" "$(basename -- "$f")"
done
}
step_services() {
head_ "systemd user units"
local f name
for f in "$REPO"/systemd/user/*.service; do
[[ -f $f ]] || continue
name=$(basename -- "$f")
pull_back "$HOME/.config/systemd/user/$name" "$f" "$name"
done
}
step_system() {
head_ "/etc drop-ins"
local dest rel
while IFS= read -r -d '' dest; do
rel=${dest#"$REPO/system/"}
pull_back "/etc/$rel" "$dest" "/etc/$rel"
done < <(find "$REPO/system" -type f -print0 | sort -z)
}
# Report drift in the package delta without rewriting the hand-annotated file:
# packages.txt carries a "why" comment per line that a regenerated list would lose.
step_packages() {
head_ "Package delta (report only)"
local listed; listed=$(mktemp)
sed 's/#.*//' "$REPO/packages.txt" | tr -d '[:blank:]' | grep -v '^$' | grep -v '^\[' | sort -u > "$listed"
local baseline; baseline=$(mktemp)
{
pacman -Qi omarchy 2>/dev/null | awk '/^Depends On/{sub(/^Depends On *: /,"");print}' \
| tr ' ' '\n' | sed 's/[<>=].*//'
cat /usr/share/omarchy/install/omarchy-base.packages 2>/dev/null
} | grep -v '^$' | sort -u > "$baseline"
local extra; extra=$(mktemp)
pacman -Qqe 2>/dev/null | sort -u | comm -23 - "$baseline" > "$extra"
local untracked; untracked=$(comm -23 "$extra" "$listed" | grep -vxE 'base|base-devel|sudo|omarchy|mkinitcpio|linux-firmware|efibootmgr|devtools|cmake|ninja|wayvnc|sunshine|freerdp|remmina|omarchy-remote-desktop-backend|pipewire-debug' | tr '\n' ' ')
local removed; removed=$(comm -13 "$extra" "$listed" | tr '\n' ' ')
[[ -n ${untracked// } ]] && warn "installed but not in packages.txt: $untracked" || skip "nothing installed that packages.txt misses"
[[ -n ${removed// } ]] && warn "in packages.txt but not installed: $removed"
say " packages.txt keeps a 'why' comment per package, so edit it by hand."
rm -f "$listed" "$baseline" "$extra"
}
main() {
local want=()
for a in "$@"; do
case $a in
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
-h|--help) sed -n '2,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
-*) warn "unknown option: $a"; exit 2 ;;
*) if [[ " ${STEPS[*]} " == *" $a "* ]]; then want+=("$a"); else warn "unknown step: $a"; exit 2; fi ;;
esac
done
(( ${#want[@]} )) || want=("${STEPS[@]}")
say "${c_hd}capture${c_off} repo=$REPO host=$(hostname)"
for s in "${want[@]}"; do "step_$s"; done
head_ "Done"
say " Review with: git -C $REPO diff"
}
main "$@"
+28
View File
@@ -0,0 +1,28 @@
general.import = [ "~/.local/state/omarchy/current/theme/alacritty.toml" ]
[env]
TERM = "xterm-256color"
[terminal]
osc52 = "CopyPaste"
[font]
normal = { family = "JetBrainsMono Nerd Font", style = "Regular" }
bold = { family = "JetBrainsMono Nerd Font", style = "Bold" }
italic = { family = "JetBrainsMono Nerd Font", style = "Italic" }
size = 8
[window]
padding.x = 14
padding.y = 14
decorations = "None"
[keyboard]
bindings = [
{ key = "Insert", mods = "Shift", action = "Paste" },
{ key = "Insert", mods = "Control", action = "Copy" },
# Send Shift+Return as CSI-u so TUIs can distinguish it from Return without treating it as Alt+Return.
{ key = "Return", mods = "Shift", chars = "\u001B[13;2u" },
# Legacy encoding sends Alt+Shift+Return the same as Alt+Return; send CSI-u so tmux can match M-S-Enter.
{ key = "Return", mods = "Alt|Shift", chars = "\u001B[13;4u" }
]
+26
View File
@@ -0,0 +1,26 @@
[main]
include=~/.local/state/omarchy/current/theme/foot.ini
term=xterm-256color
font=JetBrainsMono Nerd Font:size=8
pad=14x14
initial-window-mode=windowed
workers=0
[scrollback]
lines=10000
multiplier=7.0
[cursor]
style=block
blink=no
[key-bindings]
clipboard-copy=Control+Insert Control+Shift+c XF86Copy
primary-paste=none
clipboard-paste=Shift+Insert Control+Shift+v XF86Paste
[text-bindings]
# Send Shift+Return as CSI-u so TUIs can distinguish it from Return.
\x1b[13;2u=Shift+Return
# Send Alt+Shift+Return as CSI-u so tmux can match M-S-Enter.
\x1b[13;4u=Mod1+Shift+Return
+41
View File
@@ -0,0 +1,41 @@
# Dynamic theme colors
config-file = ?"~/.local/state/omarchy/current/theme/ghostty.conf"
# Font
font-family = "JetBrainsMono Nerd Font"
font-style = Regular
font-size = 12
# Window
window-theme = ghostty
window-padding-x = 14
window-padding-y = 14
confirm-close-surface=false
resize-overlay = never
gtk-toolbar-style = flat
# Cursor styling
cursor-style = "block"
cursor-style-blink = false
# Cursor styling + SSH session terminfo
# (all shell integration options must be passed together)
shell-integration-features = no-cursor,ssh-env
# Keyboard bindings
keybind = shift+insert=paste_from_clipboard
keybind = control+insert=copy_to_clipboard
# Send Shift+Enter as CSI-u so TUIs can distinguish it from Enter.
keybind = shift+enter=csi:13;2u
# Legacy encoding sends Alt+Shift+Enter the same as Alt+Enter; send CSI-u so tmux can match M-S-Enter.
keybind = alt+shift+enter=csi:13;4u
keybind = super+control+shift+alt+arrow_down=resize_split:down,100
keybind = super+control+shift+alt+arrow_up=resize_split:up,100
keybind = super+control+shift+alt+arrow_left=resize_split:left,100
keybind = super+control+shift+alt+arrow_right=resize_split:right,100
# Slowdown mouse scrolling
mouse-scroll-multiplier = 0.95
# Fix general slowness on hyprland (https://github.com/ghostty-org/ghostty/discussions/3224)
async-backend = epoll
+107
View File
@@ -0,0 +1,107 @@
onboarding = false
# Mirrors the Omarchy tmux config in config/tmux/tmux.conf
# tmux session -> herdr workspace, tmux window -> herdr tab, tmux pane -> herdr pane
[theme]
# tmux ran on the terminal's own palette (bg=default, fg=default, ANSI blue accents)
name = "terminal"
[theme.custom]
# The active tab is drawn as panel_bg text on an accent background, so panel_bg
# has to be dark for it to read - same colors as status-left's "#[fg=black,bg=blue]"
panel_bg = "black"
[terminal]
# Matches -c "#{pane_current_path}" on every split, window, and session
new_cwd = "follow"
[keys]
prefix = "ctrl+space"
# Config and help
reload_config = "prefix+q"
help = "prefix+?"
detach = "prefix+d"
# Copy mode
copy_mode = "prefix+["
# Panes
split_horizontal = ["prefix+h", "alt+enter"]
split_vertical = ["prefix+v", "alt+shift+enter"]
close_pane = ["prefix+x", "alt+esc"]
zoom = "prefix+z"
last_pane = "prefix+;"
focus_pane_left = "ctrl+alt+left"
focus_pane_down = "ctrl+alt+down"
focus_pane_up = "ctrl+alt+up"
focus_pane_right = "ctrl+alt+right"
resize_mode = ["prefix+ctrl+left", "prefix+ctrl+down", "prefix+ctrl+up", "prefix+ctrl+right"]
# Like resize-pane on C-M-S-arrows
resize_pane_left = "ctrl+alt+shift+left"
resize_pane_down = "ctrl+alt+shift+down"
resize_pane_up = "ctrl+alt+shift+up"
resize_pane_right = "ctrl+alt+shift+right"
# No tmux equivalent; herdr's default prefix+shift+p is taken by previous session
rename_pane = "prefix+shift+o"
# Windows -> tabs
new_tab = "prefix+c"
rename_tab = "prefix+r"
close_tab = "prefix+k"
switch_tab = ["prefix+1..9", "alt+1..9"]
previous_tab = ["prefix+p", "alt+left"]
next_tab = ["prefix+n", "alt+right"]
# Like swap-window -t -1/+1 on M-S-Left/Right
move_tab_previous = "alt+shift+left"
move_tab_next = "alt+shift+right"
# Sessions -> workspaces
new_workspace = "prefix+shift+c"
rename_workspace = "prefix+shift+r"
close_workspace = "prefix+shift+k"
previous_workspace = ["prefix+shift+p", "alt+up"]
next_workspace = ["prefix+shift+n", "alt+down"]
[ui]
accent = "blue"
# tmux drew single-line dividers between adjacent panes and no outer frame
pane_gaps = false
pane_outer_borders = false
# tmux had no scrollbar column beside its panes
pane_scrollbars = false
# kill-window and kill-session never asked
confirm_close = false
# automatic-rename gave windows a name without prompting
prompt_new_tab_name = false
# set -g mouse on
mouse_capture = true
# status-right had the zoom flag followed by #h
tab_bar_right = [{ type = "zoom" }, { type = "hostname" }]
# set -g set-titles on / set -g set-titles-string '#h:#W', where tmux's #W was
# the basename of the pane cwd. This is what Hyprland shows in the group bar,
# and it resolves on the server so remote sessions name the remote host.
window_title = "{hostname}: {workspace}"
agent_panel_sort = "spaces"
# Claude agents show their session name in bold: the /rename name, or Claude's
# generated topic before a rename. herdr-web reports it as the $claude_name pane
# token (~/Work/omarchy-playground/herdr-web/src/agents.js); workspace and tab
# move to the second row.
[ui.sidebar.agents.rows_by_agent]
claude = [
["state_icon", { token = "$claude_name", bold = true }],
[{ token = "workspace", bold = false, dim = true }, { token = "tab", dim = true }],
]
+10
View File
@@ -0,0 +1,10 @@
-- Extra autostart processes.
-- o.launch_on_start("my-service")
-- Keyboard/mouse sharing with the other Omarchy laptop (lan-mouse).
-- Runs as a daemon; input only crosses the screen edge once the peer is
-- activated, so starting it here is inert until Handoff is toggled on.
-- Superseded by the lan-mouse.service systemd user unit (journald logging,
-- ordering after the portal, Restart=on-failure). launch_on_start sent both
-- output streams to /dev/null, which hid every pairing failure.
-- o.launch_on_start("lan-mouse daemon")
+43
View File
@@ -0,0 +1,43 @@
-- Keep only your personal keybinding overrides here. Add new bindings or
-- unbind defaults before replacing them.
-- See current bindings and descriptions:
-- omarchy menu keybindings --print
-- To disable every Omarchy default binding, set this in
-- ~/.config/hypr/hyprland.lua before require("default.hypr.omarchy"), then add
-- only the bindings you want below:
-- omarchy_default_bindings = false
-- To disable all preinstalled app/webapp bindings, set:
-- omarchy_preinstalled_bindings = false
-- Add a new binding.
-- o.bind("SUPER + SHIFT + R", "SSH", "alacritty -e ssh your-server")
-- Change an existing binding by unbinding it first, then binding the key again.
-- This example changes SUPER+SPACE from the launcher to the Omarchy root menu.
-- hl.unbind("SUPER + SPACE")
-- o.bind("SUPER + SPACE", "Omarchy menu", "omarchy-menu toggle root")
-- Disable a default binding without replacing it.
-- hl.unbind("SUPER + SHIFT + B")
-- Logitech MX Keys examples:
-- o.bind("SUPER + SHIFT + S", nil, "omarchy-capture-screenshot")
-- o.bind("SUPER + H", nil, "voxtype record toggle")
-- o.bind("SUPER + PERIOD", nil, "omarchy-shell shell toggle omarchy.emojis")
-- Open Claude instead of the stock ChatGPT webapp.
hl.unbind("SUPER + SHIFT + A")
o.bind("SUPER + SHIFT + A", "Claude", { webapp = "https://claude.ai" })
-- Herdr HUD overlay (indie.herdr-hud native plugin, local branch).
o.bind("SUPER + H", "Toggle Herdr HUD", "omarchy-shell shell toggle indie.herdr-hud '{}'")
o.bind("SUPER + ALT + H", "Show or hide Herdr HUD button", "omarchy-shell shell call indie.herdr-hud toggleVisibility '{}'")
o.bind("SUPER + CTRL + DOWN", "Herdr HUD: next agent", "omarchy-shell shell call indie.herdr-hud nextAgent '{}'")
o.bind("SUPER + CTRL + UP", "Herdr HUD: previous agent", "omarchy-shell shell call indie.herdr-hud previousAgent '{}'")
-- Handoff: share this keyboard/mouse with the other Omarchy laptop.
-- SUPER+K is already "Keybindings", so this takes SUPER+SHIFT+K.
o.bind("SUPER + SHIFT + K", "Toggle Handoff (keyboard/mouse sharing)", "omarchy-handoff toggle")
+32
View File
@@ -0,0 +1,32 @@
-- Learn how to configure Hyprland: https://wiki.hypr.land/Configuring/Start/
-- Omarchy's bootstrap keeps path setup out of this user config.
dofile((os.getenv("OMARCHY_PATH") or "/usr/share/omarchy") .. "/default/hypr/bootstrap.lua")
-- Disable all Omarchy default bindings. Add your own in hypr/bindings.lua.
-- omarchy_default_bindings = false
--
-- Or disable only bindings for Omarchy's preinstalled apps/web apps while
-- keeping core window-manager bindings:
-- omarchy_preinstalled_bindings = false
-- Load Omarchy defaults.
require("default.hypr.omarchy")
-- Put your personal overrides in these files. They're loaded after Omarchy's
-- defaults so package updates can improve the defaults without rewriting your
-- ~/.config/hypr files.
require("hypr.monitors")
require("hypr.input")
require("hypr.bindings")
require("hypr.looknfeel")
require("hypr.autostart")
-- Toggle config flags dynamically.
require("default.hypr.toggles")
-- Add any other personal Hyprland configuration below.
-- o.window("qemu", { workspace = "5" })
-- Added by hyprmoncfg: its generated monitor rules load last, so nothing before this can override the applied layout.
do local path = os.getenv("HOME") .. "/.config/hypr/hyprmoncfg-monitors.lua"; local file = io.open(path, "r"); if file then file:close(); dofile(path) end end
+72
View File
@@ -0,0 +1,72 @@
-- Keep only your personal input overrides here. Uncommented settings below
-- replace Omarchy's defaults.
-- Keyboard layout and options.
-- See https://wiki.hypr.land/Configuring/Basics/Variables/#input
-- hl.config({
-- input = {
-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt.
-- kb_layout = "us,dk,eu",
-- kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle",
--
-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards).
-- kb_variant = "intl",
--
-- -- Change speed of keyboard repeat.
-- repeat_rate = 40,
-- repeat_delay = 250,
--
-- -- Start with numlock on by default.
-- numlock_by_default = true,
--
-- -- Increase sensitivity for mouse/trackpad (default: 0).
-- sensitivity = 0.35,
--
-- -- Turn off mouse acceleration (default: adaptive).
-- accel_profile = "flat",
--
-- touchpad = {
-- -- Use natural (inverse) scrolling.
-- natural_scroll = true,
--
-- -- Use two-finger clicks for right-click instead of lower-right corner.
-- clickfinger_behavior = true,
--
-- -- Control the speed of your scrolling.
-- scroll_factor = 0.4,
--
-- -- Enable the touchpad while typing.
-- disable_while_typing = false,
--
-- -- Left-click-and-drag with three fingers.
-- drag_3fg = 1,
-- },
-- },
-- })
-- App-specific touchpad scroll speeds.
-- o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 })
-- o.window("com.mitchellh.ghostty", { scroll_touchpad = 0.2 })
-- Enable touchpad gestures for changing workspaces.
-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Gestures/
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
-- Enable touchpad gestures for moving focus (helpful on scrolling layout).
-- hl.gesture({ fingers = 3, direction = "left", action = function() hl.dispatch(hl.dsp.focus({ direction = "l" })) end })
-- hl.gesture({ fingers = 3, direction = "right", action = function() hl.dispatch(hl.dsp.focus({ direction = "r" })) end })
-- lan-mouse (omarchy-handoff) virtual pointer: kill the SECOND acceleration pass.
-- lan-mouse forwards RELATIVE deltas that the SENDING compositor has already
-- accelerated. Omarchy leaves accel_profile unset (adaptive), so the receiver
-- amplifies them again and the pointer arrives hypersensitive while the local
-- trackpad feels normal. "flat" applies no curve, so a forwarded delta moves
-- exactly as far as the sender intended.
-- The name is "unknown-device" because wlr-virtual-pointer gives Hyprland no
-- name (virtual KEYBOARDS get named, pointers don't). Verified on this machine
-- with `lan-mouse test-emulation` + `hyprctl devices`: no real device here is
-- named unknown-device* (real pointers are elan0138:*, mouse-passthrough*,
-- dp-3-1), so this rule cannot touch the trackpad. Two entries because the
-- daemon may hold more than one virtual pointer at a time.
hl.device({ name = "unknown-device", sensitivity = 0, accel_profile = "flat" })
hl.device({ name = "unknown-device-1", sensitivity = 0, accel_profile = "flat" })
+48
View File
@@ -0,0 +1,48 @@
-- BEGIN hyprmoncfg wake settings
-- Shared with Omarchy while hyprmoncfg manages displays.
hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 1.25 })
-- END hyprmoncfg wake settings
-- See https://wiki.hypr.land/Configuring/Basics/Monitors/
-- List current monitors and supported resolutions with: hyprctl monitors all
--
-- SINGLE SOURCE OF TRUTH for display config.
-- Do not add monitor rules anywhere else (hyprland.lua, a hyprmoncfg-generated
-- file, etc.) -- overlapping rules from several files were what made external
-- displays unrecoverable before.
local omarchy_gdk_scale = 1
local omarchy_monitor_scale = 1.25
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
-- Catch-all: anything not named below gets its EDID-preferred mode.
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale })
-- Built-in laptop panel.
hl.monitor({
output = "desc:Chimei Innolux Corporation 0x1642",
mode = "1920x1200@60",
position = "auto",
scale = 1.25,
})
-- LG IPS236 on the laptop's own HDMI port.
hl.monitor({
output = "desc:LG Electronics IPS236 105NDLS5A720",
mode = "1920x1080@60",
position = "auto",
scale = 1,
})
-- LG TV via the Anker USB-C dock.
-- Deliberately NOT forced to 3840x2160@60: the dock negotiates DP alt-mode
-- with only 2 lanes (pin assignment D), which leaves no bandwidth headroom at
-- 4K60. Forcing it produced a "monitor has no mode" loop. "preferred" is
-- 3840x2160@30 here, which links reliably. Use 1920x1080@120 instead if you
-- want high refresh over this dock.
hl.monitor({
output = "desc:LG Electronics LG TV SSCR2 0x01010101",
mode = "preferred",
position = "auto",
scale = 1.25,
})
+23
View File
@@ -0,0 +1,23 @@
# Remove the include below to disconnect Kitty from Omarchy's theming system.
include ~/.local/state/omarchy/current/theme/kitty.conf
# Settings below override Omarchy's defaults in /etc/xdg/kitty/kitty.conf.
# Learn more: https://sw.kovidgoyal.net/kitty/conf/
# Font
# font_family JetBrainsMono Nerd Font
# font_size 12
# Window padding
# window_padding_width 14
# Unmap a shortcut, passing it through to the terminal application
# map ctrl+insert
# Set or replace a shortcut
# map ctrl+shift+c copy_to_clipboard
# Remove all inherited shortcuts, including Kitty's built-in shortcuts
# clear_all_shortcuts yes
font_size 8.0
+26
View File
@@ -0,0 +1,26 @@
██████████████████████████████████████████████████████
██████████████████████████████████████████████████████
████ ████ ████
████ ████ ████
████ █████████████████████ ████████ ████
████ █████████████████████ ████████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████████████ ████ ████
████████████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ████ ████ ████
████ ██████████████████████████████████████ ████
████ ██████████████████████████████████████ ████
████ ████ ████
████ ████ ████
█████████████████████████████ ████████████████████
█████████████████████████████ ████████████████████
+10
View File
@@ -0,0 +1,10 @@
▄▄▄
▄█████▄ ▄███████████▄ ▄███████ ▄███████ ▄███████ ▄█ █▄ ▄█ █▄
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▀ ███ ███ ███ ███
███ ███ ███ ███ ███ ▄███▄▄▄███ ▄███▄▄▄██▀ ███ ▄███▄▄▄███▄ ███▄▄▄███
███ ███ ███ ███ ███ ▀███▀▀▀███ ▀███▀▀▀▀ ███ ▀▀███▀▀▀███ ▀▀▀▀▀▀███
███ ███ ███ ███ ███ ███ ███ ██████████ ███ █▄ ███ ███ ▄██ ███
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
▀█████▀ ▀█ ███ █▀ ███ █▀ ███ ███ ███████▀ ███ █▀ ▀█████▀
███ █▀
+1
View File
@@ -0,0 +1 @@
claude
+12
View File
@@ -0,0 +1,12 @@
# btop Activity defaults. Use "More plugin settings > Open settings file"
# to edit your own copy. Valid saves apply immediately; no restart is needed.
# Keep each setting on one line. Omitted settings use the defaults below.
# Interval ladder, in milliseconds, that the Update interval arrows step
# through. Any whole number from 100 ms to one day can still be typed.
poll_intervals = [250, 500, 1000, 2000, 5000]
# What a left click on the bar icon does. "open" launches or focuses btop;
# "toggle" also closes the plugin's btop window. The popup's start action always
# opens or focuses btop, regardless of this setting.
left_click = "toggle"
+94
View File
@@ -0,0 +1,94 @@
{
"version": 1,
"idle": {
"screensaver": 1200,
"lock": 1800
},
"bar": {
"position": "top",
"transparent": false,
"centerAnchor": "omarchy.clock",
"layout": {
"left": [
{
"id": "omarchy.menu"
},
{
"id": "omarchy.workspaces"
},
{
"id": "omarchy.active-window"
}
],
"center": [
{
"id": "omarchy.indicators"
},
{
"id": "omarchy.clock",
"format": "dddd HH:mm",
"formatAlt": "d MMMM 'W'ww yyyy",
"verticalFormat": "HH\n—\nmm"
},
{
"id": "omarchy.keyboard-layout"
},
{
"id": "omarchy.weather"
},
{
"id": "kb2uka.port-doctor"
},
{
"id": "akshar.radio-atlas"
},
{
"id": "omaplug"
},
{
"id": "omarchy.system-update"
}
],
"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": [
{
"id": "indie.herdr-hud"
}
]
}
+2
View File
@@ -0,0 +1,2 @@
[font]
base-size = 10
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://opencode.ai/config.json",
"autoupdate": false
}
Executable
+321
View File
@@ -0,0 +1,321 @@
#!/bin/bash
# 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
# file it would overwrite is backed up to <file>.bak.<epoch> first.
#
# ./install.sh # everything
# ./install.sh config themes # only the named steps
# ./install.sh --list # show the steps
# ./install.sh --dry-run # print what would change, touch nothing
#
# Steps: packages config themes plugins bin services system
set -uo pipefail
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
STAMP="$(date +%s)"
DRY=0
STEPS=(packages config themes plugins bin services system)
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=; }
say() { printf '%s\n' "$*"; }
head_() { printf '\n%s==> %s%s\n' "$c_hd" "$*" "$c_off"; }
ok() { printf ' %s+%s %s\n' "$c_ok" "$c_off" "$*"; }
skip() { printf ' %s= %s%s\n' "$c_skip" "$*" "$c_off"; }
warn() { printf ' %s! %s%s\n' "$c_warn" "$*" "$c_off"; }
err() { printf ' %sx %s%s\n' "$c_err" "$*" "$c_off"; }
run() { if (( DRY )); then printf ' %s? would: %s%s\n' "$c_skip" "$*" "$c_off"; else "$@"; fi; }
# install_file <src> <dest> [mode] -- copy only when content differs, backing up first
install_file() {
local src=$1 dest=$2 mode=${3:-}
local rel=${dest/#"$HOME"/\~}
if [[ -e $dest ]] && cmp -s "$src" "$dest"; then
skip "$rel"
return 0
fi
if (( DRY )); then
printf ' %s? would write %s%s\n' "$c_skip" "$rel" "$c_off"
return 0
fi
mkdir -p "$(dirname -- "$dest")"
if [[ -e $dest ]]; then
cp -a "$dest" "$dest.bak.$STAMP"
cp "$src" "$dest"
[[ -n $mode ]] && chmod "$mode" "$dest"
ok "$rel (previous saved as $(basename -- "$dest").bak.$STAMP)"
else
cp "$src" "$dest"
[[ -n $mode ]] && chmod "$mode" "$dest"
ok "$rel"
fi
}
# ---------------------------------------------------------------- packages
step_packages() {
head_ "Packages"
command -v omarchy >/dev/null || { err "omarchy not on PATH - is this an Omarchy system?"; return 1; }
local section="" repo=() aur=() optional=()
while IFS= read -r line; do
line=${line%%#*}; line=${line//[[:space:]]/}
[[ -z $line ]] && continue
case $line in
'[repo]') section=repo; continue ;;
'[aur]') section=aur; continue ;;
'[optional]') section=optional; continue ;;
esac
case $section in
repo) repo+=("$line") ;;
aur) aur+=("$line") ;;
optional) optional+=("$line") ;;
esac
done < "$REPO/packages.txt"
local missing=()
for p in "${repo[@]}"; do pacman -Qq "$p" &>/dev/null || missing+=("$p"); done
if (( ${#missing[@]} )); then
say " installing from repos: ${missing[*]}"
run omarchy pkg add "${missing[@]}" || err "some repo packages failed"
else
skip "all ${#repo[@]} repo packages present"
fi
missing=()
for p in "${aur[@]}"; do pacman -Qq "$p" &>/dev/null || missing+=("$p"); done
if (( ${#missing[@]} )); then
say " installing from AUR: ${missing[*]}"
run omarchy pkg aur add "${missing[@]}" || err "some AUR packages failed"
else
skip "all ${#aur[@]} AUR packages present"
fi
for p in "${optional[@]}"; do
pacman -Qq "$p" &>/dev/null || warn "optional, not installed: $p (omarchy pkg add $p)"
done
}
# ---------------------------------------------------------------- config
step_config() {
head_ "Config files -> ~/.config"
local src rel
while IFS= read -r -d '' src; do
rel=${src#"$REPO/config/"}
install_file "$src" "$HOME/.config/$rel"
done < <(find "$REPO/config" -type f -print0 | sort -z)
if (( ! DRY )) && command -v hyprctl >/dev/null && [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then
hyprctl reload >/dev/null 2>&1
local errs; errs=$(hyprctl configerrors 2>/dev/null)
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
fi
}
# ---------------------------------------------------------------- themes
step_themes() {
head_ "Themes -> ~/.config/omarchy/themes"
local t name dest
for t in "$REPO"/themes/*/; do
[[ -d $t ]] || continue
name=$(basename -- "$t")
dest="$HOME/.config/omarchy/themes/$name"
if [[ -d $dest ]] && diff -rq "$t" "$dest" >/dev/null 2>&1; then
skip "$name"
else
run mkdir -p "$dest"
run cp -r "$t." "$dest/"
ok "$name"
fi
done
say ""
say " Themes managed by aether (not in this repo - reinstall with \`aether\`):"
say " pink-gt3rs porsche-cayman porsche-rain"
say " Apply one with: omarchy theme set <name>"
}
# ---------------------------------------------------------------- plugins
step_plugins() {
head_ "Shell plugins -> ~/.config/omarchy/plugins"
local dir="$HOME/.config/omarchy/plugins"
[[ -d $dir ]] || run mkdir -p "$dir"
local id url sha p
while IFS=$'\t' read -r id url sha; do
[[ -z ${id:-} || $id == \#* ]] && continue
p="$dir/$id"
if [[ -d $p/.git ]]; then
if [[ $(git -C "$p" rev-parse HEAD 2>/dev/null) == "$sha" ]]; then
skip "$id @ ${sha:0:7}"
continue
fi
if [[ -n $(git -C "$p" status --porcelain 2>/dev/null) ]]; then
warn "$id has local changes - leaving it alone (wanted ${sha:0:7})"
continue
fi
run git -C "$p" fetch --quiet origin
run git -C "$p" checkout --quiet "$sha" && ok "$id -> ${sha:0:7}"
else
if (( DRY )); then
printf ' %s? would clone %s -> %s%s\n' "$c_skip" "$id" "${sha:0:7}" "$c_off"
continue
fi
git clone --quiet "$url" "$p" || { err "clone failed: $id ($url)"; continue; }
git -C "$p" checkout --quiet "$sha" || warn "$id: sha $sha not found, left on default branch"
ok "$id @ ${sha:0:7}"
fi
done < "$REPO/plugins.txt"
apply_herdr_hud
apply_worktree_patch io.github.tallsam.navbar-cat
say ""
say " Bar placement lives in config/omarchy/shell.json, already installed by the"
say " config step. The shell hot-reloads it; \`omarchy restart shell\` if not."
}
# indie.herdr-hud carries 10 commits that intentionally never went to the
# upstream PR, plus an uncommitted working tree. Both are replayed here.
apply_herdr_hud() {
local p="$HOME/.config/omarchy/plugins/indie.herdr-hud"
local bundle="$REPO/plugin-patches/indie.herdr-hud.bundle"
[[ -d $p/.git && -f $bundle ]] || return 0
if git -C "$p" rev-parse --verify --quiet local >/dev/null; then
skip "indie.herdr-hud: 'local' branch already present"
elif (( DRY )); then
printf ' %s? would replay the herdr-hud local branch%s\n' "$c_skip" "$c_off"
return 0
else
git -C "$p" fetch --quiet "$bundle" 'refs/heads/local:refs/heads/local' \
&& { git -C "$p" checkout --quiet local && ok "indie.herdr-hud: replayed 'local' branch"; } \
|| warn "indie.herdr-hud: could not replay the local branch from the bundle"
fi
apply_worktree_patch indie.herdr-hud
install_file "$REPO/plugin-patches/indie.herdr-hud--bin--herdr-agents" "$p/bin/herdr-agents" 755
}
apply_worktree_patch() {
local id=$1
local p="$HOME/.config/omarchy/plugins/$id"
local patch="$REPO/plugin-patches/$id-worktree.patch"
[[ -d $p/.git && -s $patch ]] || return 0
if git -C "$p" apply --reverse --check "$patch" &>/dev/null; then
skip "$id: working-tree patch already applied"
elif (( DRY )); then
printf ' %s? would apply %s working-tree patch%s\n' "$c_skip" "$id" "$c_off"
elif git -C "$p" apply --3way "$patch" &>/dev/null; then
ok "$id: applied working-tree patch"
else
warn "$id: working-tree patch did not apply cleanly - see $patch"
fi
}
# ---------------------------------------------------------------- bin
step_bin() {
head_ "Scripts -> ~/.local/bin"
local f
for f in "$REPO"/bin/*; do
[[ -f $f ]] || continue
install_file "$f" "$HOME/.local/bin/$(basename -- "$f")" 755
done
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) warn "~/.local/bin is not on PATH" ;;
esac
say ""
say " fix-stuck-displays needs passwordless sudo for 'tee /sys/class/drm/*/status'."
say " omarchy-handoff needs a peer and a paired fingerprint before it does anything:"
say " omarchy-handoff peer <hostname> <ip> then omarchy-handoff doctor"
say " Its config (~/.config/lan-mouse/) holds a private key and per-machine"
say " fingerprints, so it is not in this repo by design."
}
# ---------------------------------------------------------------- services
step_services() {
head_ "systemd user units"
local f name changed=0
for f in "$REPO"/systemd/user/*.service; do
[[ -f $f ]] || continue
name=$(basename -- "$f")
local dest="$HOME/.config/systemd/user/$name"
[[ -e $dest ]] && cmp -s "$f" "$dest" || changed=1
install_file "$f" "$dest"
done
(( changed )) && run systemctl --user daemon-reload
for f in "$REPO"/systemd/user/*.service; do
[[ -f $f ]] || continue
name=$(basename -- "$f")
if systemctl --user is-enabled --quiet "$name" 2>/dev/null; then
skip "$name already enabled"
else
run systemctl --user enable "$name" && ok "enabled $name"
fi
done
say ""
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."
}
# ---------------------------------------------------------------- system
step_system() {
head_ "System drop-ins (needs sudo)"
local src rel dest
while IFS= read -r -d '' src; do
rel=${src#"$REPO/system/"}
dest="/etc/$rel"
if [[ -e $dest ]] && cmp -s "$src" "$dest"; then
skip "$dest"
continue
fi
if (( DRY )); then
printf ' %s? would write %s%s\n' "$c_skip" "$dest" "$c_off"
continue
fi
sudo install -D -m 0644 "$src" "$dest" && ok "$dest"
done < <(find "$REPO/system" -type f -print0 | sort -z)
if (( ! DRY )); then
sudo sysctl --system >/dev/null 2>&1 && ok "sysctl reloaded"
say ""
say " hid_apple.conf (fnmode=2: F-keys act as F-keys) only takes effect once the"
say " initramfs is rebuilt or the module reloads:"
say " sudo mkinitcpio -P # or: sudo modprobe -r hid_apple && sudo modprobe hid_apple"
fi
}
# ---------------------------------------------------------------- main
main() {
local want=()
for a in "$@"; do
case $a in
--dry-run|-n) DRY=1 ;;
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
-h|--help) sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
-*) err "unknown option: $a"; exit 2 ;;
*)
if [[ " ${STEPS[*]} " == *" $a "* ]]; then want+=("$a")
else err "unknown step: $a (one of: ${STEPS[*]})"; exit 2; fi ;;
esac
done
(( ${#want[@]} )) || want=("${STEPS[@]}")
say "${c_hd}omarchy-setup${c_off} repo=$REPO"
(( DRY )) && warn "dry run - nothing will be changed"
command -v omarchy >/dev/null || warn "omarchy not found; this looks like a non-Omarchy host"
for s in "${want[@]}"; do "step_$s"; done
head_ "Done"
say " Re-running is safe. Backups from this run are suffixed .bak.$STAMP"
say " Pull live changes back into the repo with ./capture.sh"
}
main "$@"
+56
View File
@@ -0,0 +1,56 @@
# 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]
# --- 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
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""Start and close Herdr agents for the HUD. herdr-monitor stays read-only."""
import json
import os
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
TIMEOUT = 15
# Herdr's supported kinds (`herdr agent start --kind`), with display names.
KINDS = {
"claude": "Claude Code", "codex": "Codex", "gemini": "Gemini CLI", "cursor": "Cursor",
"opencode": "opencode", "copilot": "Copilot CLI", "pi": "pi", "omp": "omp", "grok": "Grok",
"hermes": "Hermes", "amp": "Amp", "droid": "Droid", "cline": "Cline", "kimi": "Kimi",
"kiro": "Kiro", "qwen": "Qwen Code", "kilo": "Kilo", "devin": "Devin", "agy": "Antigravity",
"mastracode": "Mastra Code", "qodercli": "Qoder", "maki": "Maki",
}
EXECUTABLE = {"cursor": "cursor-agent"}
class AgentsError(RuntimeError):
def __init__(self, message: str, code: str = ""):
super().__init__(message)
self.code = code
def herdr(*args: str, timeout: float = TIMEOUT) -> dict:
executable = shutil.which("herdr")
if not executable:
raise AgentsError("Herdr is not installed or not on PATH")
try:
done = subprocess.run([executable, *args], capture_output=True, text=True, timeout=timeout)
except subprocess.TimeoutExpired as error:
raise AgentsError("Herdr did not respond in time") from error
if done.returncode != 0:
# Herdr prints {"error": {"code", "message"}} on stderr.
try:
error = json.loads(done.stderr).get("error") or {}
except (json.JSONDecodeError, AttributeError):
error = {}
raise AgentsError(error.get("message") or done.stderr.strip() or f"herdr {args[0]} failed",
error.get("code", ""))
return json.loads(done.stdout or "{}").get("result", {})
def installed_kinds() -> list[str]:
# Agents run in Herdr's login shell, which may have PATH entries (mise, npm)
# that the desktop shell lacks, so ask that shell.
executables = [EXECUTABLE.get(kind, kind) for kind in KINDS]
script = "for e in " + " ".join(executables) + '; do command -v "$e" >/dev/null 2>&1 && echo "$e"; done'
try:
done = subprocess.run([os.environ.get("SHELL", "/bin/bash"), "-lic", script],
capture_output=True, text=True, timeout=TIMEOUT, stdin=subprocess.DEVNULL)
found = set(done.stdout.split())
except subprocess.TimeoutExpired:
found = {e for e in executables if shutil.which(e)}
return [kind for kind in KINDS if EXECUTABLE.get(kind, kind) in found]
def options() -> dict:
agents = herdr("agent", "list").get("agents", [])
spaces = herdr("workspace", "list").get("workspaces", [])
folders = {str(a.get("cwd")) for a in agents if a.get("cwd")}
work = Path.home() / "Work"
if work.is_dir():
folders |= {str(p) for p in work.iterdir() if p.is_dir() and not p.name.startswith(".")}
# A workspace's folder is where its first agent runs; used to pick a default.
space_cwd = {}
for agent in agents:
space_cwd.setdefault(str(agent.get("workspace_id", "")), agent.get("cwd"))
return {
"kinds": [{"kind": kind, "label": KINDS[kind]} for kind in installed_kinds()],
"folders": sorted(folders),
"workspaces": [{"id": s.get("workspace_id"), "label": s.get("label") or s.get("workspace_id"),
"cwd": space_cwd.get(str(s.get("workspace_id")))} for s in spaces],
}
def slug(text: str, fallback: str) -> str:
# Herdr agent names: [a-z][a-z0-9_-]{0,31}, unique among live agents.
base = re.sub(r"[^a-z0-9_-]+", "-", text.lower())
base = re.sub(r"^[^a-z]+|-+$", "", base)[:24]
return base or fallback
def start(request: dict) -> dict:
kind = str(request.get("kind", ""))
if kind not in installed_kinds():
raise AgentsError(f"{KINDS.get(kind, kind)} isn't installed")
home = Path.home().resolve()
folder = Path(os.path.expanduser(str(request.get("cwd", "")).strip())).resolve()
if not folder.is_dir():
raise AgentsError("That folder doesn't exist")
if folder != home and home not in folder.parents:
raise AgentsError("Pick a folder in your home directory")
name = str(request.get("name", "")).strip()[:60]
prompt = str(request.get("prompt", "")).strip()
workspace = str(request.get("workspace", "") or "")
label = (name or folder.name)[:24]
if workspace:
created = herdr("tab", "create", "--workspace", workspace, "--cwd", str(folder), "--label", label)
else:
created = herdr("workspace", "create", "--cwd", str(folder), "--label", folder.name, "--no-focus")
herdr("tab", "rename", created["tab"]["tab_id"], label)
pane = created["root_pane"]["pane_id"]
tab = created["tab"]["tab_id"]
taken = {a.get("name") for a in herdr("agent", "list").get("agents", [])}
base = slug(name or folder.name, kind)
agent_name, n = base, 2
while agent_name in taken:
agent_name, n = f"{base[:28]}-{n}", n + 1
# Claude shows --name as its terminal title and session name.
args = ["--", "--name", name] if kind == "claude" and name else []
time.sleep(0.8) # let the shell reach its prompt
try:
herdr("agent", "start", agent_name, "--kind", kind, "--pane", pane, "--timeout", "45000", *args, timeout=50)
except AgentsError as error:
# Blocked at startup (a trust-this-folder question, say) still counts:
# the HUD terminal can answer it. Otherwise close the empty tab.
if error.code == "agent_not_ready":
return {"pane_id": pane, "blocked": True}
try:
herdr("tab", "close", tab)
except AgentsError:
pass
raise AgentsError(f"{KINDS[kind]} didn't start: {error}") from None
if prompt:
# Herdr reports the start a moment before it takes prompts for the name.
for attempt in range(20):
try:
herdr("agent", "prompt", pane, prompt)
break
except AgentsError as error:
if error.code != "agent_not_ready" or attempt == 19:
return {"pane_id": pane, "blocked": False, "prompt_error": str(error)}
time.sleep(0.5)
return {"pane_id": pane, "blocked": False}
def claude_attach_id(pane: str) -> str:
info = herdr("pane", "process-info", "--pane", pane).get("process_info", {})
for process in info.get("foreground_processes", []):
argv = process.get("argv") or []
if len(argv) >= 3 and os.path.basename(argv[0]) == "claude" and argv[1] == "attach":
return argv[2]
return ""
def close(pane: str) -> dict:
# A `claude attach` pane only views a background session; closing the pane
# would leave the session running, so stop it (its conversation is kept).
session = ""
try:
session = claude_attach_id(pane)
except AgentsError:
pass
if session:
claude = shutil.which("claude") or str(Path.home() / ".local/share/mise/shims/claude")
done = subprocess.run([claude, "stop", session], capture_output=True, text=True, timeout=TIMEOUT)
if done.returncode != 0:
raise AgentsError((done.stderr or done.stdout).strip().splitlines()[0] if (done.stderr or done.stdout).strip()
else "claude stop failed")
herdr("pane", "close", pane)
return {"closed": pane, "stopped_session": session}
def main(argv: list[str]) -> int:
if argv[1:] == ["options"]:
result = options()
elif len(argv) == 3 and argv[1] == "start":
result = start(json.loads(argv[2]))
elif len(argv) == 3 and argv[1] == "close":
result = close(argv[2])
else:
raise AgentsError("usage: herdr-agents options|start JSON|close PANE")
print(json.dumps(result, ensure_ascii=False))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main(sys.argv))
except (AgentsError, OSError, ValueError, KeyError, subprocess.TimeoutExpired) as error:
print(str(error)[:4096], file=sys.stderr)
raise SystemExit(1)
@@ -0,0 +1,861 @@
diff --git i/HerdrHud.qml w/HerdrHud.qml
index 018311d..d01449c 100644
--- i/HerdrHud.qml
+++ w/HerdrHud.qml
@@ -7,6 +7,7 @@ import Quickshell.Wayland
import qs.Commons
import qs.Ui as Ui
import "Alerts.js" as Alerts
+import "Roster.js" as Roster
import "."
Item {
@@ -21,12 +22,13 @@ Item {
readonly property string pluginId: (manifest && manifest.id) || "indie.herdr-hud"
readonly property string pluginDir: (manifest && manifest.__sourceDir) || ""
readonly property string bridgePath: decodeURIComponent(String(Qt.resolvedUrl("bin/herdr-monitor")).replace(/^file:\/\//, ""))
+ readonly property string agentsPath: decodeURIComponent(String(Qt.resolvedUrl("bin/herdr-agents")).replace(/^file:\/\//, ""))
readonly property string homeDir: Quickshell.env("HOME")
readonly property string configDir: homeDir + "/.config/herdr-hud-native"
readonly property string statePath: configDir + "/state.json"
property bool opened: false
- onOpenedChanged: if (opened) clearAlerts()
+ onOpenedChanged: if (opened) { clearAlerts(); resort() }
property bool overlayVisible: true
property bool openingRequested: false
property bool demoMode: false
@@ -35,10 +37,136 @@ Item {
property var agents: []
property string selectedPane: ""
property var unread: ({})
- // Keep Herdr's own order (workspace order). Re-sorting by attention moved
- // agents as soon as they were selected or changed status, which sent
- // prompts to the wrong agent; status glyphs and badges show attention.
- readonly property var sortedAgents: agents
+ // Herdr's own order (workspace order) by default. Re-sorting live by
+ // attention moved agents as soon as they were selected or changed status,
+ // which sent prompts to the wrong agent. So other sort orders are worked out
+ // only when the panel opens, the sort changes, or agents come or go; a row
+ // never moves while you look at it. Status glyphs and badges show attention.
+ property string sortMode: "herdr"
+ property var rosterOrder: []
+ property var activity: ({})
+ readonly property var sortedAgents: sortMode === "herdr" ? agents : Roster.arrange(agents, rosterOrder)
+
+ function resort() { rosterOrder = Roster.order(agents, sortMode, activity) }
+
+ function cycleSort(step) {
+ var index = Roster.SORTS.indexOf(sortMode)
+ sortMode = Roster.SORTS[(index + step + Roster.SORTS.length) % Roster.SORTS.length]
+ resort()
+ saveState()
+ }
+
+ // Starting and closing agents goes through bin/herdr-agents.
+ property bool newAgentOpen: false
+ property var spawnOptions: null
+ property string newKind: "claude"
+ property string newFolder: ""
+ property string newWorkspace: ""
+ property bool newAgentBusy: false
+ property string newAgentError: ""
+ property string lastKind: ""
+ property string lastFolder: ""
+ property string pendingPane: ""
+ property string confirmClosePane: ""
+ property string closingPane: ""
+
+ function tildePath(path) {
+ return homeDir && String(path).indexOf(homeDir) === 0 ? "~" + String(path).slice(homeDir.length) : String(path)
+ }
+
+ function expandPath(path) {
+ var text = String(path || "").trim()
+ return text === "~" || text.indexOf("~/") === 0 ? homeDir + text.slice(1) : text
+ }
+
+ // IPC entry point too: omarchy-shell shell call indie.herdr-hud newAgent '{}'
+ function newAgent(_arg) {
+ if (!(opened && overlayVisible)) open("{}")
+ openNewAgent()
+ }
+
+ function openNewAgent() {
+ if (demoMode || optionsProc.running) return
+ newAgentOpen = true
+ newAgentError = ""
+ spawnOptions = null
+ optionsProc.exec(["python3", agentsPath, "options"])
+ }
+
+ function applySpawnOptions(raw, error, exitCode) {
+ if (exitCode !== 0) {
+ newAgentError = String(error || "Couldn't list the agents you can start.").trim()
+ return
+ }
+ try {
+ var options = JSON.parse(String(raw || "{}"))
+ var kinds = options.kinds || []
+ newKind = kinds.some(function(k) { return k.kind === lastKind }) ? lastKind
+ : kinds.some(function(k) { return k.kind === "claude" }) ? "claude" : (kinds.length ? kinds[0].kind : "")
+ // Pick the folder before publishing the options; the form fills in from both.
+ setNewFolder(lastFolder || (options.folders || [])[0] || homeDir, options)
+ spawnOptions = options
+ } catch (parseError) {
+ newAgentError = "Herdr returned an unreadable response."
+ }
+ }
+
+ // Default to the workspace already working in that folder, else a new one.
+ function setNewFolder(path, options) {
+ newFolder = path
+ var dir = expandPath(path)
+ var known = options || spawnOptions
+ var match = (known ? known.workspaces : []).find(function(w) { return w.cwd === dir })
+ newWorkspace = match ? String(match.id) : ""
+ }
+
+ function folderSuggestions(text) {
+ if (!spawnOptions) return []
+ var dir = expandPath(text)
+ var query = dir.toLowerCase()
+ return spawnOptions.folders.filter(function(f) { return f !== dir && f.toLowerCase().indexOf(query) >= 0 }).slice(0, 6)
+ }
+
+ function startAgent(name, prompt) {
+ if (agentsProc.running || !newKind) return
+ if (!expandPath(newFolder)) { newAgentError = "Pick a folder."; return }
+ newAgentBusy = true
+ newAgentError = ""
+ lastKind = newKind
+ lastFolder = expandPath(newFolder)
+ saveState()
+ agentsProc.action = "start"
+ agentsProc.exec(["python3", agentsPath, "start", JSON.stringify({
+ kind: newKind, cwd: expandPath(newFolder), workspace: newWorkspace, name: String(name || ""), prompt: String(prompt || "")
+ })])
+ }
+
+ function closeAgent(pane) {
+ if (agentsProc.running) return
+ confirmClosePane = ""
+ closingPane = pane
+ agentsProc.action = "close"
+ agentsProc.exec(["python3", agentsPath, "close", pane])
+ }
+
+ function applyAgentsAction(action, raw, error, exitCode) {
+ var result = ({})
+ try { result = JSON.parse(String(raw || "{}")) } catch (parseError) { result = ({}) }
+ var message = String(error || "").trim()
+ if (action === "start") {
+ newAgentBusy = false
+ if (exitCode !== 0) { newAgentError = message || "The agent didn't start."; return }
+ newAgentOpen = false
+ pendingPane = String(result.pane_id || "")
+ noticeText = result.blocked ? "It's asking something before it starts. Answer it in the terminal."
+ : result.prompt_error ? "Started, but the first message didn't go through: " + result.prompt_error : ""
+ } else {
+ closingPane = ""
+ if (exitCode !== 0) noticeText = message || "Couldn't close that agent."
+ else noticeText = result.stopped_session ? "Stopped the Claude background session. Its conversation is kept." : ""
+ }
+ refreshRoster()
+ }
property var lastSequence: ({})
property var alertQueue: []
property bool alertsEnabled: true
@@ -190,6 +318,9 @@ Item {
if (Number(parsed.rosterWidth) > 0) rosterWidth = clamp(Number(parsed.rosterWidth), 150, 330)
if (Number(parsed.panelWidth) > 0) panelWidth = Math.max(minPanelWidth, Number(parsed.panelWidth))
if (Number(parsed.panelHeight) > 0) panelHeight = Math.max(minPanelHeight, Number(parsed.panelHeight))
+ if (Roster.SORTS.indexOf(parsed.sortMode) >= 0) sortMode = parsed.sortMode
+ if (typeof parsed.lastKind === "string") lastKind = parsed.lastKind
+ if (typeof parsed.lastFolder === "string") lastFolder = parsed.lastFolder
}
} catch (error) {
positions = ({})
@@ -209,7 +340,10 @@ Item {
positions: positions,
rosterWidth: Math.round(rosterWidth),
panelWidth: Math.round(panelWidth),
- panelHeight: Math.round(panelHeight)
+ panelHeight: Math.round(panelHeight),
+ sortMode: sortMode,
+ lastKind: lastKind,
+ lastFolder: lastFolder
}, null, 2) + "\n")
}
@@ -486,9 +620,10 @@ Item {
return muted
}
- function agentName(agent) {
- if (!agent) return "Agent"
- var type = String(agent.agent || "agent")
+ function agentName(agent) { return Roster.displayName(agent) }
+
+ function agentKind(agent) {
+ var type = String(agent ? agent.agent || "agent" : "agent")
return type.charAt(0).toUpperCase() + type.slice(1)
}
@@ -581,6 +716,7 @@ Item {
var nextUnread = cloneObject(unread)
var nextSequence = ({})
var nextWorkingSince = ({})
+ var nextActivity = ({})
var live = ({})
var previousByPane = ({})
for (var p = 0; p < agents.length; p++) previousByPane[String(agents[p].pane_id || "")] = agents[p]
@@ -595,13 +731,20 @@ Item {
nextWorkingSince[identity] = workingSince[identity] || Date.now()
}
live[pane] = true
+ // Last activity: the session store's last write, else the last status change we saw.
+ var statusChanged = !!previousByPane[pane] && String(previousByPane[pane].agent_status || "") !== status
+ nextActivity[pane] = Math.max(Number(row.active_at) || 0, activity[pane] || 0, statusChanged ? Date.now() : 0)
nextSequence[pane] = sequence === null ? (previousSequence === undefined ? 0 : previousSequence) : sequence
if (status === "working" || (opened && pane === selectedPane)) delete nextUnread[pane]
else if (isUnreadTransition(previousByPane[pane], row, previousSequence, sequence)) nextUnread[pane] = true
}
for (var unreadPane in nextUnread) if (!live[unreadPane]) delete nextUnread[unreadPane]
var previousAgent = agentForPane(selectedPane)
+ var paneSetChanged = rows.length !== agents.length
+ || rows.some(function(row) { return !previousByPane[String(row.pane_id || "")] })
agents = rows
+ activity = nextActivity
+ if (!opened || paneSetChanged) resort()
workingSince = nextWorkingSince
activityNow = Date.now()
var currentAgent = agentForPane(selectedPane)
@@ -616,7 +759,12 @@ Item {
}
completed.forEach(function(agent) { root.queueAlert(agent) })
errorText = ""
- if (!agentForPane(selectedPane)) selectAgent(rows.length ? String(rows[0].pane_id || "") : "")
+ if (pendingPane && live[pendingPane]) {
+ selectAgent(pendingPane)
+ pendingPane = ""
+ }
+ if (confirmClosePane && !live[confirmClosePane]) confirmClosePane = ""
+ if (!agentForPane(selectedPane)) selectAgent(rows.length ? String(sortedAgents[0].pane_id || "") : "")
dataRevision++
} catch (parseError) {
alertBaseline = false
@@ -627,6 +775,86 @@ Item {
}
}
+ // A small text button for the roster and the new-agent form.
+ component HudChip: Rectangle {
+ id: chip
+ required property var hud
+ property string text: ""
+ property bool selected: false
+ property bool primary: false
+ property bool danger: false
+ signal clicked()
+ signal rightClicked()
+ implicitWidth: chipLabel.implicitWidth + 18
+ implicitHeight: 24
+ radius: hud.cornerRadius
+ opacity: enabled ? 1 : 0.5
+ readonly property color tone: danger ? hud.urgent : hud.accent
+ color: primary ? tone : selected ? hud.alpha(tone, 0.22)
+ : hud.alpha(hud.foreground, chipMouse.containsMouse ? 0.1 : 0.04)
+ border.width: 1
+ border.color: primary || selected || chipMouse.containsMouse ? tone : hud.alpha(hud.foreground, 0.16)
+ Text {
+ id: chipLabel
+ anchors.centerIn: parent
+ text: chip.text
+ color: chip.primary ? hud.contrastColor(chip.tone, hud.foreground, hud.background) : chip.selected || chip.danger ? chip.tone : hud.foreground
+ font.family: hud.chromeFont
+ font.pixelSize: 11
+ font.bold: chip.selected || chip.primary
+ }
+ MouseArea {
+ id: chipMouse
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onClicked: function(mouse) { mouse.button === Qt.RightButton ? chip.rightClicked() : chip.clicked() }
+ }
+ }
+
+ component HudField: Rectangle {
+ id: field
+ required property var hud
+ property alias text: input.text
+ property string placeholder: ""
+ property bool multiline: false
+ signal edited()
+ signal cancelled()
+ signal submitted()
+ implicitHeight: multiline ? 70 : 30
+ radius: hud.cornerRadius
+ color: hud.alpha(hud.foreground, 0.05)
+ border.width: 1
+ border.color: input.activeFocus ? hud.accent : hud.alpha(hud.foreground, 0.18)
+ clip: true
+ function focusInput() { input.forceActiveFocus() }
+ TextEdit {
+ id: input
+ anchors.fill: parent
+ anchors.margins: 7
+ color: hud.foreground
+ selectionColor: hud.alpha(hud.accent, 0.4)
+ font.family: hud.chromeFont
+ font.pixelSize: 13
+ wrapMode: field.multiline ? TextEdit.Wrap : TextEdit.NoWrap
+ selectByMouse: true
+ onTextChanged: field.edited()
+ Keys.onEscapePressed: field.cancelled()
+ // One-line fields submit on Enter; the message field takes newlines.
+ Keys.onReturnPressed: function(event) { if (field.multiline) event.accepted = false; else field.submitted() }
+ Keys.onEnterPressed: function(event) { if (field.multiline) event.accepted = false; else field.submitted() }
+ Text {
+ anchors.fill: parent
+ visible: !input.text && !input.activeFocus
+ text: field.placeholder
+ color: hud.muted
+ font: input.font
+ elide: Text.ElideRight
+ }
+ }
+ }
+
Component.onCompleted: {
mkdirProc.running = true
Qt.callLater(root.refreshRoster)
@@ -689,6 +917,21 @@ Item {
onExited: function(exitCode) { root.applyRoster(rosterOut.text, rosterErr.text, exitCode) }
}
+ Process {
+ id: optionsProc
+ stdout: StdioCollector { id: optionsOut; waitForEnd: true }
+ stderr: StdioCollector { id: optionsErr; waitForEnd: true }
+ onExited: function(exitCode) { root.applySpawnOptions(optionsOut.text, optionsErr.text, exitCode) }
+ }
+
+ Process {
+ id: agentsProc
+ property string action: ""
+ stdout: StdioCollector { id: agentsOut; waitForEnd: true }
+ stderr: StdioCollector { id: agentsErr; waitForEnd: true }
+ onExited: function(exitCode) { root.applyAgentsAction(action, agentsOut.text, agentsErr.text, exitCode) }
+ }
+
property string previewIdentity: ""
Process {
id: alertPreviewProc
@@ -1034,6 +1277,186 @@ Item {
Layout.fillWidth: true
Layout.fillHeight: true
+ // New agent: covers the roster and terminal until started or cancelled.
+ Rectangle {
+ id: newAgentPanel
+ anchors.fill: parent
+ z: 3
+ visible: root.newAgentOpen
+ color: root.panelFill
+ radius: root.cornerRadius
+ border.width: 1
+ border.color: root.alpha(root.foreground, 0.12)
+ onVisibleChanged: if (visible) Qt.callLater(function() { nameField.focusInput() })
+
+ Connections {
+ target: root
+ function onSpawnOptionsChanged() {
+ if (!root.spawnOptions) return
+ folderField.text = root.tildePath(root.newFolder)
+ nameField.text = ""
+ promptField.text = ""
+ }
+ }
+
+ MouseArea { anchors.fill: parent } // keep clicks off the terminal underneath
+
+ Flickable {
+ anchors.fill: parent
+ anchors.margins: 14
+ contentHeight: newAgentForm.implicitHeight
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+ ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
+
+ ColumnLayout {
+ id: newAgentForm
+ width: parent.width
+ spacing: 8
+
+ Text {
+ text: "New agent"
+ color: root.accent
+ font.family: root.chromeFont
+ font.pixelSize: 15
+ font.bold: true
+ }
+ Text {
+ Layout.fillWidth: true
+ visible: !root.spawnOptions
+ text: root.newAgentError || "Loading…"
+ color: root.newAgentError ? root.urgent : root.muted
+ wrapMode: Text.Wrap
+ font.family: root.chromeFont
+ font.pixelSize: 12
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ visible: !!root.spawnOptions
+ spacing: 8
+
+ Text { text: "AGENT"; color: root.muted; font.family: root.chromeFont; font.pixelSize: 10; font.bold: true }
+ Flow {
+ Layout.fillWidth: true
+ spacing: 6
+ Repeater {
+ model: root.spawnOptions ? root.spawnOptions.kinds : []
+ delegate: HudChip {
+ required property var modelData
+ hud: root
+ text: modelData.label
+ selected: root.newKind === modelData.kind
+ onClicked: root.newKind = modelData.kind
+ }
+ }
+ }
+
+ Text { text: "FOLDER"; color: root.muted; font.family: root.chromeFont; font.pixelSize: 10; font.bold: true }
+ HudField {
+ id: folderField
+ hud: root
+ Layout.fillWidth: true
+ placeholder: "~/Work/project"
+ onEdited: root.setNewFolder(text)
+ onCancelled: root.newAgentOpen = false
+ onSubmitted: nameField.focusInput()
+ }
+ Flow {
+ Layout.fillWidth: true
+ spacing: 6
+ Repeater {
+ model: root.folderSuggestions(folderField.text)
+ delegate: HudChip {
+ required property var modelData
+ hud: root
+ text: root.tildePath(modelData)
+ onClicked: {
+ root.setNewFolder(modelData)
+ folderField.text = root.tildePath(modelData)
+ }
+ }
+ }
+ }
+
+ Text { text: "WORKSPACE"; color: root.muted; font.family: root.chromeFont; font.pixelSize: 10; font.bold: true }
+ Flow {
+ Layout.fillWidth: true
+ spacing: 6
+ HudChip {
+ hud: root
+ text: "New workspace"
+ selected: root.newWorkspace === ""
+ onClicked: root.newWorkspace = ""
+ }
+ Repeater {
+ model: root.spawnOptions ? root.spawnOptions.workspaces : []
+ delegate: HudChip {
+ required property var modelData
+ hud: root
+ text: String(modelData.label)
+ selected: root.newWorkspace === String(modelData.id)
+ onClicked: root.newWorkspace = String(modelData.id)
+ }
+ }
+ }
+
+ Text { text: "NAME · OPTIONAL"; color: root.muted; font.family: root.chromeFont; font.pixelSize: 10; font.bold: true }
+ HudField {
+ id: nameField
+ hud: root
+ Layout.fillWidth: true
+ placeholder: "e.g. Fix login bug"
+ onCancelled: root.newAgentOpen = false
+ onSubmitted: promptField.focusInput()
+ }
+
+ Text { text: "FIRST MESSAGE · OPTIONAL"; color: root.muted; font.family: root.chromeFont; font.pixelSize: 10; font.bold: true }
+ HudField {
+ id: promptField
+ hud: root
+ Layout.fillWidth: true
+ multiline: true
+ placeholder: "What should it do?"
+ onCancelled: root.newAgentOpen = false
+ }
+
+ Text {
+ Layout.fillWidth: true
+ visible: !!root.newAgentError
+ text: root.newAgentError
+ color: root.urgent
+ wrapMode: Text.Wrap
+ font.family: root.chromeFont
+ font.pixelSize: 12
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.topMargin: 4
+ spacing: 8
+ Text {
+ Layout.fillWidth: true
+ text: "Opens as a HERDR tab."
+ color: root.muted
+ font.family: root.chromeFont
+ font.pixelSize: 11
+ elide: Text.ElideRight
+ }
+ HudChip { hud: root; text: "Cancel"; onClicked: root.newAgentOpen = false }
+ HudChip {
+ hud: root
+ primary: true
+ text: root.newAgentBusy ? "Starting…" : "Start agent"
+ enabled: !root.newAgentBusy && !!root.newKind
+ onClicked: root.startAgent(nameField.text, promptField.text)
+ }
+ }
+ }
+ }
+ }
+ }
+
RowLayout {
anchors.fill: parent
spacing: 0
@@ -1051,15 +1474,36 @@ Item {
anchors.margins: 8
spacing: 8
- Text {
+ RowLayout {
Layout.fillWidth: true
- text: root.demoMode ? "HERDR · PREVIEW" : root.connected
- ? "HERDR · " + root.agents.length + " AGENTS" : "HERDR · OFFLINE"
- color: root.muted
- font.family: root.chromeFont
- font.pixelSize: 11
- font.bold: true
- leftPadding: 5
+ spacing: 6
+ Text {
+ Layout.fillWidth: true
+ text: root.demoMode ? "HERDR · PREVIEW" : root.connected
+ ? "HERDR · " + root.agents.length + " AGENTS" : "HERDR · OFFLINE"
+ color: root.muted
+ font.family: root.chromeFont
+ font.pixelSize: 11
+ font.bold: true
+ leftPadding: 5
+ elide: Text.ElideRight
+ }
+ HudChip {
+ hud: root
+ text: "+ New"
+ enabled: root.connected && !root.demoMode
+ selected: root.newAgentOpen
+ onClicked: root.newAgentOpen ? root.newAgentOpen = false : root.openNewAgent()
+ }
+ }
+
+ HudChip {
+ // Click for the next order, right-click for the previous one.
+ hud: root
+ Layout.leftMargin: 4
+ text: "Sort: " + Roster.SORT_LABELS[root.sortMode] + " ⇅"
+ onClicked: root.cycleSort(1)
+ onRightClicked: root.cycleSort(-1)
}
ListView {
@@ -1093,12 +1537,16 @@ Item {
z: 1
}
+ readonly property string pane: String(modelData.pane_id || "")
+ readonly property bool confirming: root.confirmClosePane === pane
+
MouseArea {
id: agentMouse
anchors.fill: parent
hoverEnabled: true
- onClicked: root.selectAgent(String(agentRow.modelData.pane_id || ""))
+ onClicked: root.selectAgent(agentRow.pane)
}
+ HoverHandler { id: rowHover }
RowLayout {
anchors.fill: parent
@@ -1123,7 +1571,7 @@ Item {
Text {
Layout.fillWidth: true
- text: String(agentRow.modelData.workspace_label || "Untitled space")
+ text: root.agentName(agentRow.modelData)
color: root.accent
font.family: root.chromeFont
font.pixelSize: 15
@@ -1132,32 +1580,59 @@ Item {
}
Text {
Layout.fillWidth: true
- // Short line, so the context size isn't elided in a narrow roster.
+ // Context size first, so a narrow roster elides the workspace instead.
textFormat: Text.StyledText
- text: root.escapeHtml(root.agentName(agentRow.modelData))
- + (root.contextLabel(agentRow.modelData)
- ? " · <font color=\"" + root.contextColor(agentRow.modelData) + "\"><b>"
- + root.contextLabel(agentRow.modelData) + "</b></font>"
+ text: (root.contextLabel(agentRow.modelData)
+ ? "<font color=\"" + root.contextColor(agentRow.modelData) + "\"><b>"
+ + root.contextLabel(agentRow.modelData) + "</b></font> · "
: "")
+ + root.escapeHtml(String(agentRow.modelData.workspace_label || "Untitled space"))
+ + (root.agentName(agentRow.modelData) !== root.agentKind(agentRow.modelData)
+ ? " · " + root.escapeHtml(root.agentKind(agentRow.modelData)) : "")
color: root.muted
font.family: root.chromeFont
font.pixelSize: 12
elide: Text.ElideRight
}
Text {
+ visible: !agentRow.confirming
Layout.fillWidth: true
- text: (root.unread[String(agentRow.modelData.pane_id || "")] ? "Unseen update · " : "")
- + root.statusLabel(agentRow.modelData)
- color: root.unread[String(agentRow.modelData.pane_id || "")] ? root.success : root.muted
+ text: root.closingPane === agentRow.pane ? "Closing…"
+ : (root.unread[agentRow.pane] ? "Unseen update · " : "") + root.statusLabel(agentRow.modelData)
+ color: root.unread[agentRow.pane] ? root.success : root.muted
font.family: root.chromeFont
font.pixelSize: 11
elide: Text.ElideRight
}
+ RowLayout {
+ visible: agentRow.confirming
+ spacing: 6
+ HudChip { hud: root; text: "Close agent"; primary: true; danger: true; onClicked: root.closeAgent(agentRow.pane) }
+ HudChip { hud: root; text: "Keep"; onClicked: root.confirmClosePane = "" }
+ }
}
+ Text {
+ // Close, shown on hover; asks before closing.
+ visible: (rowHover.hovered || agentRow.confirming) && !root.demoMode && root.closingPane !== agentRow.pane
+ Layout.alignment: Qt.AlignTop
+ text: "\u00d7"
+ color: closeMouse.containsMouse || agentRow.confirming ? root.urgent : root.muted
+ font.family: root.chromeFont
+ font.pixelSize: 16
+ font.bold: true
+ MouseArea {
+ id: closeMouse
+ anchors.fill: parent
+ anchors.margins: -4
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.confirmClosePane = agentRow.confirming ? "" : agentRow.pane
+ }
+ }
Text {
// Matches the Alt+1-9 shortcut for this agent.
- visible: agentRow.index < 9
+ visible: agentRow.index < 9 && !rowHover.hovered && !agentRow.confirming
Layout.alignment: Qt.AlignTop
Layout.topMargin: 2
text: String(agentRow.index + 1)
diff --git i/README.md w/README.md
index 685096b..37df3e5 100644
--- i/README.md
+++ w/README.md
@@ -44,6 +44,14 @@ rm -rf ~/.config/herdr-hud-native
- Approval-dialog keyboard input and TUI rendering
- Omarchy and WoW designs
- Agent switching, launcher, alerts, hidden-panel notifications
+- Agent cards lead with the agent's name (Claude's session name, else the Herdr agent name)
+- Sort the roster by Herdr order, status, recent activity, name, or workspace (click to cycle,
+ right-click to go back). Orders other than Herdr's are worked out when the panel opens, the sort
+ changes, or agents come or go, so a row never moves while you are looking at it
+- **+ New** starts any installed agent Herdr supports in a new tab: pick the agent, folder, and
+ workspace, with an optional name and first message. Also `omarchy-shell shell call indie.herdr-hud newAgent '{}'`
+- Hover a card and click × to close its agent (asks first). A `claude attach` pane's background
+ session is stopped too, keeping its conversation
- Safe reconnect, disconnect, bounded read-only monitoring, and dependency errors
The plugin supports Omarchy only. It does not provide an HTML renderer, prompt bridge, legacy plugin migration, or independent chat history. Herdr owns terminal controller, resize, scroll, and alternate-screen behavior.
diff --git i/Roster.js w/Roster.js
index f88af7c..51ba497 100644
--- i/Roster.js
+++ w/Roster.js
@@ -13,3 +13,53 @@ function sorted(agents, unread) {
return a.priority - b.priority || a.index - b.index
}).map(function(item) { return item.agent })
}
+
+// The agent's own name: Claude's session name (its terminal title, or the
+// claude_name token herdr-web reports), else the Herdr agent name, else its kind.
+function displayName(agent) {
+ if (!agent) return "Agent"
+ var token = agent.tokens && agent.tokens.claude_name
+ if (token) return String(token)
+ var title = String(agent.terminal_title_stripped || "").trim()
+ if (agent.agent === "claude" && title && !/^[^\s@]+@[^\s:]+:/.test(title) && title !== "Claude Code") return title
+ if (agent.name) return String(agent.name)
+ var type = String(agent.agent || "agent")
+ return type.charAt(0).toUpperCase() + type.slice(1)
+}
+
+var SORTS = ["herdr", "status", "recent", "name", "workspace"]
+var SORT_LABELS = { herdr: "Herdr", status: "Status", recent: "Recent", name: "Name", workspace: "Workspace" }
+var STATUS_RANK = { blocked: 0, working: 1, done: 2, idle: 3 }
+
+// Pane ids in display order. activity maps pane id -> last-activity time (ms).
+// Ties keep Herdr's order.
+function order(agents, mode, activity) {
+ var rows = agents.map(function(agent, index) { return { agent: agent, index: index } })
+ var name = function(row) { return displayName(row.agent).toLowerCase() }
+ var compare = {
+ status: function(a, b) {
+ var ra = STATUS_RANK[a.agent.agent_status], rb = STATUS_RANK[b.agent.agent_status]
+ return (ra === undefined ? 4 : ra) - (rb === undefined ? 4 : rb)
+ },
+ recent: function(a, b) {
+ return (activity[b.agent.pane_id] || 0) - (activity[a.agent.pane_id] || 0)
+ },
+ name: function(a, b) { return name(a) < name(b) ? -1 : name(a) > name(b) ? 1 : 0 },
+ workspace: function(a, b) {
+ var wa = String(a.agent.workspace_label || "").toLowerCase(), wb = String(b.agent.workspace_label || "").toLowerCase()
+ return wa < wb ? -1 : wa > wb ? 1 : 0
+ },
+ }[mode]
+ if (compare) rows.sort(function(a, b) { return compare(a, b) || a.index - b.index })
+ return rows.map(function(row) { return String(row.agent.pane_id || "") })
+}
+
+// Agents in a saved order; agents the order doesn't know yet go last, in Herdr order.
+function arrange(agents, paneOrder) {
+ var position = {}
+ paneOrder.forEach(function(pane, index) { position[pane] = index })
+ return agents.map(function(agent, index) {
+ var at = position[String(agent.pane_id || "")]
+ return { agent: agent, key: at === undefined ? paneOrder.length + index : at }
+ }).sort(function(a, b) { return a.key - b.key }).map(function(row) { return row.agent })
+}
diff --git i/bin/herdr-monitor w/bin/herdr-monitor
index 1944808..aac81b3 100755
--- i/bin/herdr-monitor
+++ w/bin/herdr-monitor
@@ -113,6 +113,7 @@ def roster() -> None:
item["workspace_label"] = labels.get(workspace_id, workspace_id or "Untitled space")
item["tab_label"] = tab_labels.get(str(row.get("tab_id", "")), str(row.get("tab_id") or ""))
item["context_tokens"], item["context_limit"] = context_usage(row)
+ item["active_at"] = last_active(row)
merged.append(item)
response({"ok": True, "agents": merged})
@@ -328,6 +329,18 @@ CONTEXT_READERS = {
}
+def last_active(row: dict) -> float:
+ """When the agent last wrote to its session store (epoch ms); 0 when unknown."""
+ session = (row.get("agent_session") or {}).get("value")
+ if row.get("agent") != "claude" or not session:
+ return 0
+ try:
+ transcript = claude_transcript(str(session))
+ return transcript.stat().st_mtime * 1000 if transcript else 0
+ except OSError:
+ return 0
+
+
def context_usage(row: dict) -> tuple[int, int]:
"""(context tokens, context window) for an agent; zeros when unknown."""
reader = CONTEXT_READERS.get(str(row.get("agent", "")))
diff --git i/tests/test-alerts.mjs w/tests/test-alerts.mjs
index 9a98741..988cf9c 100644
--- i/tests/test-alerts.mjs
+++ w/tests/test-alerts.mjs
@@ -72,7 +72,8 @@ function unreadContext() {
Alerts: alerts, demoMode: false, alertBaseline: false, agents: [], unread: {},
lastSequence: {}, workingSince: {}, opened: false, selectedPane: '',
activeAlert: null, activeAlertNeedsInput: false, connected: false, errorText: '',
- dataRevision: 0,
+ dataRevision: 0, activity: {}, pendingPane: '', confirmClosePane: '', resort() {},
+ get sortedAgents() { return context.agents; },
agentForPane(pane) {
for (const item of context.agents) if (String(item.pane_id || '') === pane) return item;
return null;
diff --git i/tests/test-roster.mjs w/tests/test-roster.mjs
index 9ea8eef..b92cdb2 100644
--- i/tests/test-roster.mjs
+++ w/tests/test-roster.mjs
@@ -22,3 +22,30 @@ test('attention first, then working, then read; stable within groups', () => {
test('reading a reply moves it down; blocked agents still need attention', () => {
assert.deepEqual(order({}), ['blocked', 'busy', 'read', 'unread', 'idle']);
});
+
+const crew = [
+ { pane_id: 'a', agent: 'codex', name: 'zeta', agent_status: 'idle', workspace_label: 'web' },
+ { pane_id: 'b', agent: 'claude', terminal_title_stripped: 'Alpha task', agent_status: 'working', workspace_label: 'api' },
+ { pane_id: 'c', agent: 'claude', terminal_title_stripped: 'me@host:~/x', tokens: { claude_name: 'Mid' }, agent_status: 'blocked', workspace_label: 'web' },
+ { pane_id: 'd', agent: 'hermes', agent_status: 'done', workspace_label: 'api' },
+];
+const by = (mode, activity = {}) => Array.from(roster.order(crew, mode, activity));
+
+test('display names prefer the Claude session name, then the Herdr name, then the kind', () => {
+ assert.deepEqual(crew.map(a => roster.displayName(a)), ['zeta', 'Alpha task', 'Mid', 'Hermes']);
+ assert.equal(roster.displayName({ agent: 'claude', terminal_title_stripped: 'me@host:~/w' }), 'Claude');
+});
+
+test('sort modes; ties keep Herdr order', () => {
+ assert.deepEqual(by('herdr'), ['a', 'b', 'c', 'd']);
+ assert.deepEqual(by('status'), ['c', 'b', 'd', 'a']);
+ assert.deepEqual(by('name'), ['b', 'd', 'c', 'a']);
+ assert.deepEqual(by('workspace'), ['b', 'd', 'a', 'c']);
+ assert.deepEqual(by('recent', { d: 30, a: 20, c: 10 }), ['d', 'a', 'c', 'b']);
+});
+
+test('arrange keeps a saved order and appends new agents', () => {
+ const ids = rows => Array.from(rows, a => a.pane_id);
+ assert.deepEqual(ids(roster.arrange(crew, ['d', 'b'])), ['d', 'b', 'a', 'c']);
+ assert.deepEqual(ids(roster.arrange(crew.slice(0, 2), ['d', 'b', 'a'])), ['b', 'a']);
+});
diff --git i/tests/test_native.py w/tests/test_native.py
index baa9589..bd36bab 100644
--- i/tests/test_native.py
+++ w/tests/test_native.py
@@ -260,9 +260,13 @@ class NativeTests(unittest.TestCase):
self.assertNotIn("QMLTermWidget", source)
def test_roster_keeps_herdr_order(self):
- # Re-sorting by attention moved agents under the cursor on selection.
+ # Re-sorting live by attention moved agents under the cursor on
+ # selection. Herdr's order is the default, and other sorts use a saved
+ # order that only changes on open, a sort change, or agents coming or going.
source = (NATIVE / "HerdrHud.qml").read_text()
- self.assertIn("readonly property var sortedAgents: agents\n", source)
+ self.assertIn('property string sortMode: "herdr"\n', source)
+ self.assertIn('sortedAgents: sortMode === "herdr" ? agents : Roster.arrange(agents, rosterOrder)\n', source)
+ self.assertIn("if (!opened || paneSetChanged) resort()", source)
self.assertNotIn("Roster.sorted(", source)
def test_release_structure(self):
Binary file not shown.
@@ -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]
+12
View File
@@ -0,0 +1,12 @@
# Omarchy shell plugins. Fields: id<TAB>repo<TAB>pinned-sha
# install.sh clones each into ~/.config/omarchy/plugins/<id> and checks out the sha.
akshar.radio-atlas https://github.com/AksharP5/omarchy-radio-atlas.git ffa7186b53e00be624b56503716c479bb97f5ff4
crmne.hyprmoncfg https://github.com/crmne/omarchy-hyprmoncfg.git 193e5611f934eec42460ef75f3f46c2a8bd0252a
ilyazar.btop https://github.com/omarchy-QOL/omarchy-btop-activity.git 1e896f6f37bf880dee3e55e36ff04f967745c527
indie.herdr-hud https://github.com/kt3v/omarchy-herdr-hud-native.git 75bef0f8012304a4edfec90c690b9f45fe65a04e
io.github.tallsam.navbar-cat https://github.com/tallsam/omarchy-navbar-cat.git 6ddd61a6744bab19b04d131ff4f2e087a504ce20
io.github.thisisgm.omapods https://github.com/thisisgm/omarchy-pods fff7fec600a5b9a61cdb40e93eccbcceb4b8f824
kb2uka.port-doctor https://github.com/Kb2uka/omarchy-port-doctor.git baa13451bfc27d59e8c831c217852279a20901aa
kokd.remote-desktop https://github.com/kostadinkadiev/omarchy-remote-desktop.git 7913b89521f88fac40620f495d1e07835bbb55b4
omaplug https://github.com/fross100/omaplug.git 9da73fc13fc2027be5b44915b4ab89440beb74ab
vstoms.netbird https://github.com/vstoms/omarchy-netbird.git d530c76b13324b26dafd79934529b4395e18a11c
+1
View File
@@ -0,0 +1 @@
options hid_apple fnmode=2
@@ -0,0 +1,9 @@
# Two NICs (Wi-Fi wlo1, dock ethernet enp0s13f0u3u4u3) share 192.168.1.0/24.
# With the defaults (arp_ignore=0/arp_announce=0) the host answers ARP for any
# local address on any interface, so the subnet's mapping collapses onto
# whichever NIC replied last -- traffic aimed at the Wi-Fi IP lands on the dock,
# and the Wi-Fi path silently never gets exercised.
# 1 = only answer ARP for addresses configured on the receiving interface.
# 2 = always use the best local address for the target subnet when announcing.
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
+21
View File
@@ -0,0 +1,21 @@
[Unit]
Description=lan-mouse input sharing daemon (omarchy-handoff)
PartOf=graphical-session.target
After=graphical-session.target xdg-desktop-portal-hyprland.service
Requisite=graphical-session.target
[Service]
Type=simple
# LAN_MOUSE_LOG_LEVEL (NOT RUST_LOG -- lan-mouse ignores that) is what makes auth rejections visible at all; lan-mouse logs the
# offending fingerprint only at debug level. Drop to "info" once paired.
Environment=LAN_MOUSE_LOG_LEVEL=debug
ExecStart=/usr/bin/lan-mouse daemon
Restart=on-failure
RestartSec=2
# Output goes to the journal on purpose. The previous autostart.lua path sent
# both streams to /dev/null, which hid every pairing failure for hours.
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=graphical-session.target
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=Voxtype push-to-talk voice-to-text daemon
Documentation=https://voxtype.io
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/voxtype daemon
Restart=on-failure
RestartSec=5
# Ensure we have access to the display
Environment=XDG_RUNTIME_DIR=%t
[Install]
WantedBy=graphical-session.target
+5
View File
@@ -0,0 +1,5 @@
Backgrounds from Wikimedia Commons (cropped to 3840x2160):
1-giallo-studio.jpg "2009 Ferrari 458 Italia Museo Nazionale dell'Automobile Torino 01" by Rahil Rupawala — CC BY 2.0
2-london-nights.jpg "Ferarri F458 Version 2 (8138264334)" by Ben — CC BY-SA 2.0
3-light-trails.jpg "Ferarri White 458 (8157299104)" by Ben — CC BY-SA 2.0
4-at-speed.jpg "Ferrari 458 (8925562883)" by emperornie — CC BY-SA 2.0
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+35
View File
@@ -0,0 +1,35 @@
# Ferrari 458 Italia — Nero Daytona body, Crema leather interior, Rosso Corsa
# accents, Giallo Modena shield, Tricolore green, Azzurro California and Blu Tour de France.
mode = "dark"
accent = "#d40000"
selection = "#3a2a28"
muted = "#665b57"
background = "#141110"
dark_background = "#0d0b0a"
darker_background = "#070606"
lighter_background = "#221c1a"
foreground = "#ece2d0"
dark_foreground = "#857a6c"
light_foreground = "#d6cab6"
bright_foreground = "#f8f1e4"
red = "#d40000"
yellow = "#fcd116"
orange = "#ff5a1f"
green = "#009246"
cyan = "#3aa0c8"
blue = "#5b7fc7"
magenta = "#c2185b"
brown = "#a0522d"
bright_red = "#ff2800"
bright_yellow = "#ffe45c"
bright_green = "#3cc47c"
bright_cyan = "#6fcbea"
bright_blue = "#8fa9e0"
bright_magenta = "#e8578f"
hyprland_active_border = "rgba(d40000ee) rgba(fcd116ee) 45deg"
+1
View File
@@ -0,0 +1 @@
Yaru-red-dark
+6
View File
@@ -0,0 +1,6 @@
Backgrounds from Wikimedia Commons:
1-rainy-evening.jpg "Porsche 911 on a rainy evening (Unsplash)" — CC0
2-gulf-917.jpg "Gulf Porsche 917" — CC BY 2.0
3-night.jpg "Porsche Porsche at night (8169422529)" — CC BY-SA 2.0
4-carrera-rs.jpg "Porsche 911 Carrera RS, Bj. 1972-73 (2016-07-02 01 Sp)" — CC BY-SA 2.0 de
0-718-cayman-s.jpg "Porsche 718 Cayman S back IMG 0704" by Alexander Migl — CC BY-SA 4.0 (floor extended to 16:9)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+35
View File
@@ -0,0 +1,35 @@
# Porsche — Jet Black Metallic body, Chalk interior, Guards Red accents,
# with Racing Yellow, Lava Orange, Python Green, Miami Blue and Gulf livery blue.
mode = "dark"
accent = "#d5001c"
selection = "#33333a"
muted = "#5c5c64"
background = "#121214"
dark_background = "#0c0c0e"
darker_background = "#070708"
lighter_background = "#1f1f23"
foreground = "#e6e1d8"
dark_foreground = "#7a766f"
light_foreground = "#cfc9bd"
bright_foreground = "#f5f1ea"
red = "#d5001c"
yellow = "#f7c600"
orange = "#ff6a13"
green = "#3fae49"
cyan = "#00a3c7"
blue = "#7fb2dd"
magenta = "#c7346a"
brown = "#8b5a2b"
bright_red = "#ff3343"
bright_yellow = "#ffdd4d"
bright_green = "#6fd36f"
bright_cyan = "#4fd2ef"
bright_blue = "#a9cdee"
bright_magenta = "#e9659a"
hyprland_active_border = "rgba(d5001cee) rgba(ff6a13ee) 45deg"
+1
View File
@@ -0,0 +1 @@
Yaru-red-dark
+14
View File
@@ -0,0 +1,14 @@
# The Simpsons theme — real wallpapers pulled from the internet (wallhaven.cc),
# palette derived from them.
Backgrounds:
1-springfield-wasteland.jpg Homer & Bart, desert/RV scene (wallhaven 43pr6n,
source: thomasjakeross.deviantart.com, SFW)
2-bart-sunset.jpg Bart mid-air skateboard trick, purple-pink sunset
sky over Springfield (wallhaven 4vm36m)
3-duff-wall.jpg Homer surrounded by a wall of Duff, donuts and
Krusty burgers (wallhaven o4d1m4)
All images fetched via the wallhaven.cc public API, SFW-rated, kept at native
resolution (1920x1200 / 1920x1080). Preview is the wasteland shot plus a
palette swatch strip.
Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

+93
View File
@@ -0,0 +1,93 @@
# Main background
theme[main_bg]="#1c1712"
# Main text color
theme[main_fg]="#efe6d4"
# Title color for boxes
theme[title]="#ffd12e"
# Highlight color for keyboard shortcuts
theme[hi_fg]="#56b6c2"
# Background color of selected item in processes box
theme[selected_bg]="#3a3148"
# Foreground color of selected item in processes box
theme[selected_fg]="#ffd12e"
# Color of inactive/disabled text
theme[inactive_fg]="#6b6478"
# Color of text appearing on top of graphs
theme[graph_text]="#efe6d4"
# Background color of the percentage meters
theme[meter_bg]="#2a241c"
# Misc colors for processes box
theme[proc_misc]="#efe6d4"
# CPU, Memory, Network, Proc box outline colors
theme[cpu_box]="#6b6478"
theme[mem_box]="#6b6478"
theme[net_box]="#6b6478"
theme[proc_box]="#6b6478"
# Box divider line and small boxes line color
theme[div_line]="#96705a"
# Temperature graph color (Green -> Yellow -> Red)
theme[temp_start]="#6db567"
theme[temp_mid]="#ffd12e"
theme[temp_end]="#e2584a"
# CPU graph colors (Blue -> Cyan -> Magenta)
theme[cpu_start]="#5c8fd6"
theme[cpu_mid]="#56b6c2"
theme[cpu_end]="#c76ba0"
# Mem/Disk free meter (Reversed "Available" colors: Cyan -> Blue -> Purple)
theme[free_start]="#56b6c2"
theme[free_mid]="#5c8fd6"
theme[free_end]="#c76ba0"
# Mem/Disk cached meter
theme[cached_start]="#5c8fd6"
theme[cached_mid]="#56b6c2"
theme[cached_end]="#efe6d4"
# Mem/Disk available meter (Purple -> Blue -> Cyan)
theme[available_start]="#c76ba0"
theme[available_mid]="#5c8fd6"
theme[available_end]="#56b6c2"
# Mem/Disk used meter
theme[used_start]="#e2584a"
theme[used_mid]="#96705a"
theme[used_end]="#6b6478"
# Download graph colors
theme[download_start]="#6db567"
theme[download_mid]="#56b6c2"
theme[download_end]="#efe6d4"
# Upload graph colors
theme[upload_start]="#5c8fd6"
theme[upload_mid]="#56b6c2"
theme[upload_end]="#efe6d4"
# Process box color gradient
theme[process_start]="#56b6c2"
theme[process_mid]="#5c8fd6"
theme[process_end]="#e2584a"
# Graph gradient colors
theme[gradient_color_0]="#1c1712"
theme[gradient_color_1]="#6b6478"
theme[gradient_color_2]="#96705a"
theme[gradient_color_3]="#e2584a"
theme[gradient_color_4]="#6db567"
theme[gradient_color_5]="#5c8fd6"
theme[gradient_color_6]="#56b6c2"
theme[gradient_color_7]="#ffe45e"
+1
View File
@@ -0,0 +1 @@
28,23,18
+37
View File
@@ -0,0 +1,37 @@
# The Simpsons — Springfield sky. Signature cartoon yellow over desert sand
# and blue sky (breaking-bad frame), Bart's sunset sky, and the loud
# Duff-pink of a junk-food wall.
mode = "dark"
accent = "#ffd12e"
selection = "#3a3148"
muted = "#6b6478"
background = "#1c1712"
dark_background = "#14100c"
darker_background = "#0c0a07"
lighter_background = "#2a241c"
foreground = "#efe6d4"
dark_foreground = "#8a8294"
light_foreground = "#d8cfbd"
bright_foreground = "#f8f2e2"
red = "#e2584a"
yellow = "#ffd12e"
orange = "#f2a04e"
green = "#6db567"
cyan = "#56b6c2"
blue = "#5c8fd6"
magenta = "#c76ba0"
brown = "#96705a"
bright_red = "#ff7a6b"
bright_yellow = "#ffe45e"
bright_green = "#8fd48a"
bright_cyan = "#7cd6e0"
bright_blue = "#7fabef"
bright_magenta = "#e291c0"
hyprland_active_border = "rgba(ffd12eee) rgba(56b6c2ee) 45deg"
hyprland_inactive_border = "rgba(3a3148aa)"
+17
View File
@@ -0,0 +1,17 @@
local active_border_color = { colors = { "rgba(ffd12eee)", "rgba(56b6c2ee)" }, angle = 45 }
local inactive_border_color = "rgba(3a3148aa)"
hl.config({
general = {
col = {
active_border = active_border_color,
inactive_border = inactive_border_color,
},
},
group = {
col = {
border_active = active_border_color,
border_inactive = inactive_border_color,
},
},
})
+1
View File
@@ -0,0 +1 @@
Yaru-yellow-dark
+1
View File
@@ -0,0 +1 @@
ffd12e
+13
View File
@@ -0,0 +1,13 @@
return {
{
"catppuccin/nvim",
name = "catppuccin",
priority = 1000,
},
{
"LazyVim/LazyVim",
opts = {
colorscheme = "catppuccin-mocha",
},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

+6
View File
@@ -0,0 +1,6 @@
text = "#efe6d4"
placeholder = "#8a8294"
text-error = "#efe6d4"
border = "#efe6d4"
border-active = "#ffd12e"
border-error = "#e2584a"