Profiles: machines, roles and shared layers
The repo was one machine's capture. It is now a set of profiles built from layers under profiles/: machines/ hp-laptop, macbook-air-m2, coder-desktop roles/ developer, devops, ai-agents features/ handoff (lan-mouse) shared base, personal, personal-desktop A profile resolves to its includes (depth first), any --roles, then itself; for a path several layers provide, the last wins. install.sh --show prints the layers and which one owns each file. Hyprland settings no longer share a file: base's hyprland.lua loads ~/.config/hypr/layers/*.lua with Omarchy's own require_all, and bindings.lua stays stock so nothing binds twice. Verified with Hyprland 0.56.2 --verify-config for all three profiles, plus a broken probe layer to prove the directory is loaded. packages.txt gains per-line arch (Arch Linux ARM lacks opencode, codex, mise, shellcheck) and cmd= (skip when dotfiles' ~/.local/bin already provides the command), and an [ignore] section that replaces capture.sh's hard-coded list. Tab-separated records would have collapsed empty fields under read, so package records use the unit separator. hp-laptop: re-captured. monitors.lua catch-all scale is now 1, shell.json has four new plugins, fix-stuck-displays has its connector-name fix, navbar-cat is gone, plugin-manager and omarchy-phone are pinned, the herdr-hud working tree is re-diffed and bin/herdr-web rides along. Its handoff peer is now the Air (192.168.1.49); the old default list was the HP's own addresses, written from the Air's side. macbook-air-m2: captured read-only over SSH as it is (no themes, plugins or bar changes). Its LAN_MOUSE_POINTER_SCALE=0.7 moves from the shared unit into a lan-mouse.service.d drop-in in its own layer. coder-desktop: the Omarchy settings covert-cloud's image used to patch in by hand (Virtual-1 at 1920x1080, no animations, stay-awake, Retro 82 once), for the image to apply at a pinned commit. Roles reach it per workspace via install.sh --roles ... --no-clobber. capture.sh writes each file back into the layer that owns it, and refuses to run before the first layered install unless --force, since the old file shapes would land in shared layers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153egowV1Xw2RNjZnSyZicc
@@ -0,0 +1,33 @@
|
||||
-- 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")
|
||||
|
||||
-- omarchy-setup keeps this file at stock on purpose. Profile layers add their
|
||||
-- bindings as hypr/layers/<NN>-<layer>.lua, so a binding added here AND by a
|
||||
-- layer would fire twice. Put machine-only bindings in that machine's layer.
|
||||
@@ -0,0 +1,38 @@
|
||||
-- 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")
|
||||
|
||||
-- omarchy-setup layers: every profile layer (role, machine, feature) puts its
|
||||
-- Hyprland settings in its own file under hypr/layers/, so layers never fight
|
||||
-- over bindings.lua or looknfeel.lua. Loaded in name order, after the files
|
||||
-- above; reload=true makes `hyprctl reload` re-run them.
|
||||
require("default.hypr.require_all").files(os.getenv("HOME") .. "/.config/hypr/layers", "hypr.layers", { reload = true })
|
||||
|
||||
-- Toggle config flags dynamically.
|
||||
require("default.hypr.toggles")
|
||||
|
||||
-- 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
|
||||
@@ -0,0 +1,4 @@
|
||||
# Every machine, including the Coder desktops. Keeps hyprland.lua and
|
||||
# bindings.lua at stock plus the hypr/layers/ loader, so every other layer adds
|
||||
# Hyprland settings as its own file instead of editing a shared one.
|
||||
description="Shared by every machine: the hypr/layers/ loader"
|
||||
@@ -0,0 +1,513 @@
|
||||
#!/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() {
|
||||
# Prefer the user unit, matching cmd_log's assumption. pkill + relaunch races
|
||||
# the unit's Restart=on-failure and can leave TWO daemons contending for UDP
|
||||
# 4242 and the socket, so only fall back to a hand-start when no unit exists.
|
||||
if systemctl --user --quiet is-enabled lan-mouse.service 2>/dev/null \
|
||||
|| systemctl --user --quiet is-active lan-mouse.service 2>/dev/null; then
|
||||
systemctl --user restart lan-mouse.service
|
||||
else
|
||||
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
|
||||
fi
|
||||
sleep 2
|
||||
[[ -S $SOCKET ]] || die "daemon failed to start (omarchy-handoff 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
|
||||
@@ -0,0 +1,4 @@
|
||||
-- handoff feature: share this keyboard/mouse with the other Omarchy laptop.
|
||||
-- SUPER+K is already Omarchy's "Keybindings", so this takes SUPER+SHIFT+K.
|
||||
-- Same chord on both laptops so muscle memory carries across.
|
||||
o.bind("SUPER + SHIFT + K", "Toggle Handoff (keyboard/mouse sharing)", "omarchy-handoff toggle")
|
||||
@@ -0,0 +1,32 @@
|
||||
# omarchy-air side. Install to ~/.config/lan-mouse/config.toml
|
||||
#
|
||||
# NOTE: the daemon OWNS this file. It rewrites it on cli activate/deactivate and
|
||||
# strips every comment, so treat this copy as the documented source and expect the
|
||||
# live file to lose the comments on first use. `lan-mouse cli list` is the only
|
||||
# trustworthy view of runtime state.
|
||||
|
||||
# input-capture-portal is a SILENT no-op on Hyprland 0.56.2: it initializes and
|
||||
# advertises on D-Bus, then creates ZERO barriers. Never leave this unset -- unset
|
||||
# defaults to the portal. See README.md.
|
||||
capture_backend = "layer-shell"
|
||||
port = 4242
|
||||
|
||||
[[clients]]
|
||||
hostname = "omarchy-laptop"
|
||||
# Both of the peer's paths. .24 is Wi-Fi, .86 is the dock NIC. Keep BOTH: .86
|
||||
# disappears on undock. Expect a ~5s stall on .24 while docked -- the laptop's dock
|
||||
# route wins (metric 100 vs 600) so its replies leave as .86 and we drop the
|
||||
# unknown 4-tuple. Structural, not a fault.
|
||||
ips = ["192.168.1.24", "192.168.1.86"]
|
||||
# air sits UNDERNEATH the LG IPS236, so the peer is off our TOP edge.
|
||||
position = "top"
|
||||
# Ships DISARMED. Arm only after the PEER authorizes this machine's fingerprint
|
||||
# (see README "Pairing"), then: omarchy-handoff on
|
||||
activate_on_startup = false
|
||||
|
||||
[authorized_fingerprints]
|
||||
# Keyed BY FINGERPRINT, value is the description -- the reverse of the intuitive
|
||||
# order. MUST be lowercase: lan-mouse generates lowercase and compares with a plain
|
||||
# map lookup, so an uppercase digest is correct, valid, and never matches.
|
||||
# This is the LAPTOP's cert digest; it stays valid unless the laptop is rebuilt.
|
||||
"7c:ea:b6:0d:5d:12:6b:c8:b3:2f:9b:21:f2:20:41:fc:b5:8a:b0:67:06:d8:f9:89:0b:43:b0:ce:10:f9:b6:1e" = "omarchy-laptop"
|
||||
@@ -0,0 +1,17 @@
|
||||
# omarchy-laptop side. Mirror of config.toml.air.
|
||||
# input-capture-portal is a SILENT no-op on Hyprland 0.56.2 -- see ../../README.md
|
||||
capture_backend = "layer-shell"
|
||||
port = 4242
|
||||
|
||||
[[clients]]
|
||||
hostname = "omarchy-air"
|
||||
ips = ["192.168.1.49"]
|
||||
# air sits UNDERNEATH the LG IPS236, so from the laptop it is off the BOTTOM edge.
|
||||
position = "bottom"
|
||||
# Leave FALSE until this machine holds the peer's fingerprint (see README).
|
||||
activate_on_startup = false
|
||||
|
||||
[authorized_fingerprints]
|
||||
# MUST be lowercase. Fill in with:
|
||||
# omarchy-handoff pair omarchy-air <air's lowercase sha256>
|
||||
# Air's fingerprint CHANGES whenever air's cert is regenerated (e.g. a fresh install).
|
||||
@@ -0,0 +1,59 @@
|
||||
# Patched lan-mouse — forward accelerated pointer deltas
|
||||
|
||||
## Why
|
||||
|
||||
Upstream lan-mouse 0.11.0 (`input-capture/src/layer_shell.rs`) captures pointer motion from
|
||||
`zwp_relative_pointer_v1` and forwards the **unaccelerated** deltas:
|
||||
|
||||
```rust
|
||||
dx_unaccel: dx,
|
||||
dy_unaccel: dy,
|
||||
..
|
||||
```
|
||||
|
||||
The `..` discards the accelerated `dx`/`dy` carried in the same event. The receiving end emulates
|
||||
through `wlr-virtual-pointer`, which is not a libinput device and therefore applies no
|
||||
acceleration curve of its own. Net effect: the sender's acceleration curve is stripped in transit
|
||||
and nothing replaces it, so the pointer is far too fast on the remote machine.
|
||||
|
||||
Critically, this also means `sensitivity` / `accel_profile` are inert for forwarded motion on
|
||||
**both** ends — the receiver because a virtual pointer isn't a libinput device, the sender because
|
||||
unaccelerated deltas bypass the curve by definition. There is no configuration fix; that is why
|
||||
this patch exists.
|
||||
|
||||
## What the patch does
|
||||
|
||||
1. Forwards the accelerated `dx`/`dy` instead. This restores the sending machine's curve, and
|
||||
makes the sender's `sensitivity`/`accel_profile` actually control how the remote pointer feels.
|
||||
2. Adds `LAN_MOUSE_POINTER_SCALE` (float, default `1.0`) as a multiplier, for the residual
|
||||
difference in logical pixel density between the two screens. Unset = upstream scaling.
|
||||
|
||||
## Rebuild
|
||||
|
||||
```sh
|
||||
git clone https://github.com/feschber/lan-mouse.git
|
||||
cd lan-mouse
|
||||
git checkout 0d2190e787db9e482e17d4c16fb7c00ef4841bcb # == Arch extra 0.11.0-1
|
||||
git apply ~/.local/share/lan-mouse-patched/accelerated-deltas.patch
|
||||
CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 \
|
||||
cargo build --release --locked --no-default-features \
|
||||
--features layer_shell_capture,wlroots_emulation
|
||||
install -m755 target/release/lan-mouse ~/.local/bin/lan-mouse-patched
|
||||
systemctl --user restart lan-mouse
|
||||
```
|
||||
|
||||
Build deps: `rust` (installed from Arch `extra`). GTK is deliberately excluded — the daemon does
|
||||
not need the GUI, and the signed package at `/usr/bin/lan-mouse` still provides it.
|
||||
|
||||
## Install layout
|
||||
|
||||
- Patched daemon: `~/.local/bin/lan-mouse-patched` (named so it cannot shadow `lan-mouse` on PATH)
|
||||
- Unit: `~/.config/systemd/user/lan-mouse.service` -> `ExecStart` points at the patched binary
|
||||
- The signed Arch package stays installed and untouched; no pacman divergence, no `IgnorePkg`.
|
||||
- Cert/config path is unchanged (`~/.config/lan-mouse/`), so the DTLS fingerprint is unchanged and
|
||||
the peer's `[authorized_fingerprints]` still matches.
|
||||
|
||||
## Upstreaming
|
||||
|
||||
Worth reporting to https://github.com/feschber/lan-mouse — the unaccel choice is defensible when
|
||||
the receiver applies its own acceleration, but no Wayland emulation backend in lan-mouse does.
|
||||
@@ -0,0 +1,48 @@
|
||||
diff --git i/input-capture/src/layer_shell.rs w/input-capture/src/layer_shell.rs
|
||||
index 698c584..7d81d37 100644
|
||||
--- i/input-capture/src/layer_shell.rs
|
||||
+++ w/input-capture/src/layer_shell.rs
|
||||
@@ -70,6 +70,21 @@ use super::{
|
||||
error::{LayerShellCaptureCreationError, WaylandBindError},
|
||||
};
|
||||
|
||||
+/// Multiplier applied to forwarded pointer motion, from `LAN_MOUSE_POINTER_SCALE`.
|
||||
+///
|
||||
+/// Defaults to 1.0. Useful when the two machines differ in logical pixel density, so the
|
||||
+/// same logical delta covers a different physical distance on each screen.
|
||||
+fn pointer_scale() -> f64 {
|
||||
+ static SCALE: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
|
||||
+ *SCALE.get_or_init(|| {
|
||||
+ env::var("LAN_MOUSE_POINTER_SCALE")
|
||||
+ .ok()
|
||||
+ .and_then(|v| v.parse::<f64>().ok())
|
||||
+ .filter(|v| v.is_finite() && *v > 0.0)
|
||||
+ .unwrap_or(1.0)
|
||||
+ })
|
||||
+}
|
||||
+
|
||||
struct Globals {
|
||||
compositor: wl_compositor::WlCompositor,
|
||||
pointer_constraints: ZwpPointerConstraintsV1,
|
||||
@@ -887,13 +902,19 @@ impl Dispatch<ZwpRelativePointerV1, ()> for State {
|
||||
if let zwp_relative_pointer_v1::Event::RelativeMotion {
|
||||
utime_hi,
|
||||
utime_lo,
|
||||
- dx_unaccel: dx,
|
||||
- dy_unaccel: dy,
|
||||
+ dx,
|
||||
+ dy,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
if let Some(window) = &app.focused {
|
||||
let time = ((((utime_hi as u64) << 32) | utime_lo as u64) / 1000) as u32;
|
||||
+ // Forward the ACCELERATED deltas. The unaccelerated ones bypass the sending
|
||||
+ // compositor's pointer-acceleration curve, and the receiving end emulates via
|
||||
+ // wlr-virtual-pointer, which is not a libinput device and so applies no curve
|
||||
+ // of its own -- the acceleration would be lost entirely in transit.
|
||||
+ let scale = pointer_scale();
|
||||
+ let (dx, dy) = (dx * scale, dy * scale);
|
||||
app.pending_events.push_back((
|
||||
window.pos,
|
||||
CaptureEvent::Input(Event::Pointer(PointerEvent::Motion { time, dx, dy })),
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Build the patched lan-mouse daemon into ~/.local/bin/lan-mouse-patched.
|
||||
# Rationale and the upstream bug are in REBUILD.md. Called by ../install.sh.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Arch extra 0.11.0-1. Pinned because the patch is against these exact line numbers;
|
||||
# a newer upstream may have fixed this, in which case check before repinning.
|
||||
commit=0d2190e787db9e482e17d4c16fb7c00ef4841bcb
|
||||
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
command -v cargo >/dev/null || {
|
||||
echo "cargo not found -- install the 'rust' package first" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build in a throwaway dir: the source tree is a build input, not something to keep.
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
git clone --quiet https://github.com/feschber/lan-mouse.git "$work/lan-mouse"
|
||||
cd "$work/lan-mouse"
|
||||
git checkout --quiet "$commit"
|
||||
git apply "$here/accelerated-deltas.patch"
|
||||
|
||||
# GTK deliberately excluded: the daemon needs no GUI, and the signed Arch package
|
||||
# still provides it at /usr/bin/lan-mouse. LTO off keeps this at ~1 min.
|
||||
CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 \
|
||||
cargo build --release --locked --no-default-features \
|
||||
--features layer_shell_capture,wlroots_emulation
|
||||
|
||||
install -Dm755 target/release/lan-mouse ~/.local/bin/lan-mouse-patched
|
||||
echo "installed ~/.local/bin/lan-mouse-patched"
|
||||
@@ -0,0 +1,4 @@
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
lan-mouse # keyboard+mouse sharing driven by bin/omarchy-handoff; also the unpatched fallback
|
||||
rust # only to compile handoff/patch/; ~250MiB installed, removable after
|
||||
@@ -0,0 +1,4 @@
|
||||
# omarchy-handoff: lan-mouse keyboard/mouse sharing between the two laptops.
|
||||
# Machines that include this set handoff_peer_ips to the OTHER laptop's
|
||||
# addresses; install.sh's handoff step opens 4242/udp in ufw for exactly those.
|
||||
description="Keyboard/mouse sharing between the laptops (lan-mouse, patched)"
|
||||
@@ -0,0 +1,33 @@
|
||||
[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 silently ignores RUST_LOG.
|
||||
# Note there is NO auth-rejection message at any level: rejection surfaces only
|
||||
# as a frontend event, so debug gives connect attempts and DTLS alert text only.
|
||||
# To see WHICH side rejected a handshake you need the wire, not the log.
|
||||
Environment=LAN_MOUSE_LOG_LEVEL=info
|
||||
# Patched daemon: upstream 0.11.0 forwards UNACCELERATED pointer deltas
|
||||
# (input-capture/src/layer_shell.rs discards the accelerated dx/dy in the same
|
||||
# event), and the wlr-virtual-pointer receiver applies no curve of its own, so
|
||||
# the remote pointer is far too fast and sensitivity/accel_profile are inert on
|
||||
# BOTH ends. Built by install.sh from handoff/patch/ -- see its REBUILD.md.
|
||||
# Falls back to nothing: if the build is missing the unit fails loudly rather
|
||||
# than silently running the unpatched binary with the wrong pointer feel.
|
||||
ExecStart=%h/.local/bin/lan-mouse-patched daemon
|
||||
# LAN_MOUSE_POINTER_SCALE (default 1.0) is per-machine, so it is not set here:
|
||||
# a machine that needs it ships lan-mouse.service.d/pointer-scale.conf
|
||||
# (see machines/macbook-air-m2).
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
# Output goes to the journal on purpose. The previous autostart.lua path sent
|
||||
# both streams to /dev/null, which hid every pairing failure for hours.
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
@@ -0,0 +1,5 @@
|
||||
-- coder-desktop: a streamed VM with no GPU. vga=virtio without virgl, so
|
||||
-- Hyprland draws on the CPU (llvmpipe) and every frame is a full-screen copy
|
||||
-- into the scanout buffer. Anything that animates keeps the CPU busy, so no
|
||||
-- window animations.
|
||||
hl.config({ animations = { enabled = false } })
|
||||
@@ -0,0 +1,20 @@
|
||||
-- See https://wiki.hypr.land/Configuring/Basics/Monitors/
|
||||
-- List current monitors and supported resolutions with: hyprctl monitors all
|
||||
|
||||
local omarchy_gdk_scale = 1
|
||||
local omarchy_monitor_scale = "auto"
|
||||
|
||||
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
|
||||
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale })
|
||||
|
||||
-- Configure a specific monitor.
|
||||
-- hl.monitor({ output = "DP-2", mode = "2560x1440@144", position = "0x0", scale = 1 })
|
||||
|
||||
-- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°).
|
||||
-- hl.monitor({ output = "DP-2", mode = "preferred", position = "auto", scale = 1, transform = 1 })
|
||||
|
||||
-- Streamed desktop: the VM's virtio display, fixed to the stream size.
|
||||
-- MUST match --manual-width/--manual-height in the image's selkies.service
|
||||
-- (covert-cloud: platform/workspaces/omarchy-desktop/image/systemd/user/), and
|
||||
-- the image build fails if this line is missing.
|
||||
hl.monitor({ output = "Virtual-1", mode = "1920x1080@60", position = "0x0", scale = 1 })
|
||||
@@ -0,0 +1,15 @@
|
||||
# Coder desktops: Omarchy in a Proxmox VM, streamed by Selkies, reached only
|
||||
# through Coder. Built by covert-cloud's platform/workspaces/omarchy-desktop/
|
||||
# image, which runs `install.sh --profile coder-desktop` at a pinned commit.
|
||||
#
|
||||
# This layer is the desktop's Omarchy look and behaviour. The image keeps the
|
||||
# plumbing: Selkies, the Coder agent, the Super key stand-in, the start page,
|
||||
# autologin and the lockdown (no SSH, no file transfer, locked password).
|
||||
#
|
||||
# Extra roles come from the Coder template's "Roles" parameter, applied at
|
||||
# every workspace start with --roles and --no-clobber. Nothing personal
|
||||
# (personal/, personal-desktop/, handoff) belongs here: other people use these.
|
||||
description="Coder desktop VM (streamed by Selkies): no GPU, fixed 1920x1080"
|
||||
include=(base roles/developer)
|
||||
hostnames=(omarchy-desktop)
|
||||
arch=x86_64
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# coder-desktop setup, run by install.sh's `setup` step after the files are in
|
||||
# place. Everything here is state outside ~/.config. Idempotent; honours
|
||||
# OMARCHY_SETUP_DRY=1.
|
||||
set -uo pipefail
|
||||
|
||||
run() { if [[ ${OMARCHY_SETUP_DRY:-0} == 1 ]]; then echo " ? would: $*"; else "$@"; fi; }
|
||||
state=$HOME/.local/state/omarchy-setup
|
||||
export OMARCHY_PATH=${OMARCHY_PATH:-/usr/share/omarchy}
|
||||
|
||||
# No idle lock or screensaver: Omarchy's stay-awake mode. The desktop user has
|
||||
# no password (the image locks it), so a locked screen could never be unlocked,
|
||||
# and an idle lock would darken the stream. The image build checks for this.
|
||||
marker=$HOME/.local/state/omarchy/indicators/stay-awake
|
||||
if [[ -e $marker ]]; then
|
||||
echo " = stay-awake"
|
||||
else
|
||||
run mkdir -p "${marker%/*}" && run touch "$marker" && echo " + stay-awake"
|
||||
fi
|
||||
|
||||
# Reduced motion, so Chromium reports prefers-reduced-motion and pages that
|
||||
# honour it stop animating (see layers/80-coder-desktop.lua for why). No
|
||||
# session during the image build, hence dbus-run-session.
|
||||
if command -v gsettings >/dev/null; then
|
||||
gs=(gsettings); [[ -n ${DBUS_SESSION_BUS_ADDRESS:-} ]] || gs=(dbus-run-session -- gsettings)
|
||||
if [[ $("${gs[@]}" get org.gnome.desktop.interface enable-animations 2>/dev/null) == false ]]; then
|
||||
echo " = gtk animations off"
|
||||
else
|
||||
run "${gs[@]}" set org.gnome.desktop.interface enable-animations false && echo " + gtk animations off"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Default theme: Omarchy's Retro 82. Only on the first apply, because install.sh
|
||||
# also runs at every workspace start (for the roles parameter) and must not undo
|
||||
# a theme the developer picked. The image also installs the Covert Cloud themes
|
||||
# for anyone who chooses them.
|
||||
if [[ -e $state/coder-desktop-theme ]]; then
|
||||
echo " = default theme already set once (left alone)"
|
||||
else
|
||||
run env OMARCHY_THEME_HEADLESS=1 "$OMARCHY_PATH/bin/omarchy-theme-set" "Retro 82" \
|
||||
&& run mkdir -p "$state" && run touch "$state/coder-desktop-theme" && echo " + theme: Retro 82"
|
||||
fi
|
||||
@@ -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##*/}; out=${out#card*-}
|
||||
log "releasing $out"
|
||||
echo on | sudo -n tee "$d/status" >/dev/null
|
||||
wait_for 5 sh -c "hyprctl monitors all | grep -q '^Monitor $out '" || log "$out never appeared in Hyprland"
|
||||
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
|
||||
@@ -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" }
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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" })
|
||||
@@ -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
|
||||
|
||||
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,
|
||||
})
|
||||
@@ -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
|
||||
@@ -0,0 +1,48 @@
|
||||
# HP laptop (omarchy-laptop) packages beyond the Omarchy 4.x baseline and the
|
||||
# packages its other layers already list.
|
||||
#
|
||||
# Format (every layer's packages.txt): "name [arch...] [cmd=command] # why".
|
||||
# arch only install on these architectures (x86_64, aarch64)
|
||||
# cmd=X skip when command X is already on PATH (e.g. a dotfiles shim)
|
||||
# Sections:
|
||||
# [repo] official repos + the omarchy repo -> omarchy pkg add
|
||||
# [aur] AUR only -> omarchy pkg aur add
|
||||
# [optional] installed here, not required for the desktop to work
|
||||
# [ignore] installed here on purpose but not managed (capture.sh stays quiet)
|
||||
|
||||
[repo]
|
||||
# --- hardware: Intel + an Anker USB-C dock ---
|
||||
linux-omarchy # kernel this install actually boots
|
||||
linux-omarchy-headers # needed to build out-of-tree modules against it
|
||||
intel-ucode # CPU microcode
|
||||
vulkan-intel # Intel Vulkan driver
|
||||
lib32-vulkan-intel # 32-bit Vulkan, for Steam/Proton
|
||||
sof-firmware # Sound Open Firmware - required for this laptop's audio
|
||||
thermald # Intel thermal daemon; without it the CPU throttles hard
|
||||
zram-generator # compressed swap in RAM
|
||||
|
||||
# --- audio: full PipeWire stack with the Pulse/JACK/ALSA shims ---
|
||||
pipewire-pulse
|
||||
pipewire-alsa
|
||||
pipewire-jack
|
||||
libpulse
|
||||
gst-plugin-pipewire
|
||||
|
||||
# --- desktop / input ---
|
||||
python-evdev # input tooling used while debugging lan-mouse (not required by it)
|
||||
wayland-utils # wayland-info, used when debugging outputs
|
||||
ghostty # terminal (alongside Omarchy's default)
|
||||
|
||||
# --- apps ---
|
||||
1password
|
||||
1password-cli # `op`, which the dotfiles repo's secrets module uses
|
||||
bun-bin
|
||||
|
||||
[optional]
|
||||
steam # games; pulls a large 32-bit dependency tree
|
||||
|
||||
[ignore]
|
||||
# toolchain/base, present on any Omarchy install or pulled in as a dependency
|
||||
base base-devel sudo omarchy mkinitcpio linux-firmware efibootmgr devtools cmake ninja
|
||||
# the remote-access stack, out of this repo's scope
|
||||
wayvnc sunshine freerdp remmina omarchy-remote-desktop-backend pipewire-debug
|
||||
@@ -0,0 +1,8 @@
|
||||
# HP laptop (omarchy-laptop): Intel, an Anker USB-C dock, the LG IPS236 on HDMI
|
||||
# and the LG TV through the dock. The machine this repo was first captured from.
|
||||
description="HP laptop (omarchy-laptop): Intel, Anker dock, LG displays"
|
||||
include=(base personal personal-desktop features/handoff roles/developer roles/devops roles/ai-agents)
|
||||
hostnames=(omarchy-laptop)
|
||||
arch=x86_64
|
||||
# The peer for omarchy-handoff is the Air (Wi-Fi only).
|
||||
handoff_peer_ips=(192.168.1.49)
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1,96 @@
|
||||
onboarding = false
|
||||
# Mirrors the Omarchy tmux config in config/tmux/tmux.conf
|
||||
# tmux session -> herdr workspace, tmux window -> herdr tab, tmux pane -> herdr pane
|
||||
|
||||
[theme]
|
||||
# tmux ran on the terminal's own palette (bg=default, fg=default, ANSI blue accents)
|
||||
name = "terminal"
|
||||
|
||||
[theme.custom]
|
||||
# The active tab is drawn as panel_bg text on an accent background, so panel_bg
|
||||
# has to be dark for it to read - same colors as status-left's "#[fg=black,bg=blue]"
|
||||
panel_bg = "black"
|
||||
|
||||
[terminal]
|
||||
# Matches -c "#{pane_current_path}" on every split, window, and session
|
||||
new_cwd = "follow"
|
||||
|
||||
[keys]
|
||||
prefix = "ctrl+space"
|
||||
|
||||
# Config and help
|
||||
reload_config = "prefix+q"
|
||||
help = "prefix+?"
|
||||
detach = "prefix+d"
|
||||
|
||||
# Copy mode
|
||||
copy_mode = "prefix+["
|
||||
|
||||
# Panes
|
||||
split_horizontal = ["prefix+h", "alt+enter"]
|
||||
split_vertical = ["prefix+v", "alt+shift+enter"]
|
||||
close_pane = ["prefix+x", "alt+esc"]
|
||||
zoom = "prefix+z"
|
||||
last_pane = "prefix+;"
|
||||
|
||||
focus_pane_left = "ctrl+alt+left"
|
||||
focus_pane_down = "ctrl+alt+down"
|
||||
focus_pane_up = "ctrl+alt+up"
|
||||
focus_pane_right = "ctrl+alt+right"
|
||||
|
||||
resize_mode = ["prefix+ctrl+left", "prefix+ctrl+down", "prefix+ctrl+up", "prefix+ctrl+right"]
|
||||
|
||||
# Like resize-pane on C-M-S-arrows
|
||||
resize_pane_left = "ctrl+alt+shift+left"
|
||||
resize_pane_down = "ctrl+alt+shift+down"
|
||||
resize_pane_up = "ctrl+alt+shift+up"
|
||||
resize_pane_right = "ctrl+alt+shift+right"
|
||||
|
||||
# No tmux equivalent; herdr's default prefix+shift+p is taken by previous session
|
||||
rename_pane = "prefix+shift+o"
|
||||
|
||||
# Windows -> tabs
|
||||
new_tab = "prefix+c"
|
||||
rename_tab = "prefix+r"
|
||||
close_tab = "prefix+k"
|
||||
switch_tab = ["prefix+1..9", "alt+1..9"]
|
||||
previous_tab = ["prefix+p", "alt+left"]
|
||||
next_tab = ["prefix+n", "alt+right"]
|
||||
|
||||
# Like swap-window -t -1/+1 on M-S-Left/Right
|
||||
move_tab_previous = "alt+shift+left"
|
||||
move_tab_next = "alt+shift+right"
|
||||
|
||||
# Sessions -> workspaces
|
||||
new_workspace = "prefix+shift+c"
|
||||
rename_workspace = "prefix+shift+r"
|
||||
close_workspace = "prefix+shift+k"
|
||||
previous_workspace = ["prefix+shift+p", "alt+up"]
|
||||
next_workspace = ["prefix+shift+n", "alt+down"]
|
||||
|
||||
[ui]
|
||||
accent = "blue"
|
||||
|
||||
# tmux drew single-line dividers between adjacent panes and no outer frame
|
||||
pane_gaps = false
|
||||
pane_outer_borders = false
|
||||
|
||||
# tmux had no scrollbar column beside its panes
|
||||
pane_scrollbars = false
|
||||
|
||||
# kill-window and kill-session never asked
|
||||
confirm_close = false
|
||||
|
||||
# automatic-rename gave windows a name without prompting
|
||||
prompt_new_tab_name = false
|
||||
|
||||
# set -g mouse on
|
||||
mouse_capture = true
|
||||
|
||||
# status-right had the zoom flag followed by #h
|
||||
tab_bar_right = [{ type = "zoom" }, { type = "hostname" }]
|
||||
|
||||
# set -g set-titles on / set -g set-titles-string '#h:#W', where tmux's #W was
|
||||
# the basename of the pane cwd. This is what Hyprland shows in the group bar,
|
||||
# and it resolves on the server so remote sessions name the remote host.
|
||||
window_title = "{hostname}: {workspace}"
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Keep only your personal input overrides here. Uncommented settings below
|
||||
-- replace Omarchy's defaults.
|
||||
|
||||
-- Keyboard layout and options.
|
||||
-- See https://wiki.hypr.land/Configuring/Basics/Variables/#input
|
||||
-- hl.config({
|
||||
-- input = {
|
||||
-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt.
|
||||
-- kb_layout = "us,dk,eu",
|
||||
-- kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle",
|
||||
--
|
||||
-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards).
|
||||
-- kb_variant = "intl",
|
||||
--
|
||||
-- -- Change speed of keyboard repeat.
|
||||
-- repeat_rate = 40,
|
||||
-- repeat_delay = 250,
|
||||
--
|
||||
-- -- Start with numlock on by default.
|
||||
-- numlock_by_default = true,
|
||||
--
|
||||
-- -- Increase sensitivity for mouse/trackpad (default: 0).
|
||||
-- sensitivity = 0.35,
|
||||
--
|
||||
-- -- Turn off mouse acceleration (default: adaptive).
|
||||
-- accel_profile = "flat",
|
||||
--
|
||||
-- touchpad = {
|
||||
-- -- Use traditional (non-inverse) scrolling.
|
||||
-- natural_scroll = false,
|
||||
--
|
||||
-- -- Re-enable tap-to-click (one-finger tap = left, two-finger = right).
|
||||
-- tap_to_click = true,
|
||||
--
|
||||
-- -- Use two-finger clicks for right-click instead of lower-right corner.
|
||||
-- clickfinger_behavior = true,
|
||||
--
|
||||
-- -- Control the speed of your scrolling.
|
||||
-- scroll_factor = 0.4,
|
||||
--
|
||||
-- -- Enable the touchpad while typing.
|
||||
-- disable_while_typing = false,
|
||||
--
|
||||
-- -- Left-click-and-drag with three fingers.
|
||||
-- drag_3fg = 1,
|
||||
-- },
|
||||
-- },
|
||||
-- })
|
||||
|
||||
-- App-specific touchpad scroll speeds.
|
||||
-- o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 })
|
||||
-- o.window("com.mitchellh.ghostty", { scroll_touchpad = 0.2 })
|
||||
|
||||
-- Enable touchpad gestures for changing workspaces.
|
||||
-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Gestures/
|
||||
-- hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
|
||||
|
||||
-- Enable touchpad gestures for moving focus (helpful on scrolling layout).
|
||||
-- hl.gesture({ fingers = 3, direction = "left", action = function() hl.dispatch(hl.dsp.focus({ direction = "l" })) end })
|
||||
-- hl.gesture({ fingers = 3, direction = "right", action = function() hl.dispatch(hl.dsp.focus({ direction = "r" })) end })
|
||||
|
||||
-- lan-mouse (handoff) virtual pointer: DO NOT add sensitivity/accel_profile rules here.
|
||||
--
|
||||
-- Tried and measured as INERT on 2026-10-10. `hl.device({name="unknown-device", ...})` does
|
||||
-- match the device (proven by toggling scroll_factor and watching hyprctl report it), but
|
||||
-- `sensitivity` and `accel_profile` are libinput pointer-acceleration settings and a
|
||||
-- wlr-virtual-pointer is not a libinput device, so they have nothing to act on. Measured
|
||||
-- travel with `lan-mouse test-emulation` + hyprctl cursorpos was 398x198px at sensitivity 0
|
||||
-- AND at -0.9 -- identical, and exactly 1:1 with the raw deltas. Both machines measure the
|
||||
-- same 398x198, so neither end scales.
|
||||
--
|
||||
-- Corollary: global `accel_profile = "flat"` would also be a no-op for the virtual pointer
|
||||
-- while permanently changing how the real trackpad feels. Pure loss -- do not do it.
|
||||
-- The magnitude of forwarded motion originates on the SENDING machine's trackpad.
|
||||
--
|
||||
-- ROOT CAUSE FOUND 2026-10-10: lan-mouse 0.11.0 forwards UNACCELERATED deltas.
|
||||
-- input-capture/src/layer_shell.rs:887 destructures the relative-pointer event as
|
||||
-- `dx_unaccel: dx, dy_unaccel: dy, ..` -- the `..` throws away the accelerated dx/dy that the
|
||||
-- same event carries. So the accel curve is stripped at the SENDER and nothing replaces it at
|
||||
-- the receiver. Consequence: sensitivity/accel_profile are inert for forwarded motion on BOTH
|
||||
-- ends, so "sync the sensitivity between the laptops" is impossible by configuration.
|
||||
-- The only fix is a source patch to use the accelerated dx/dy.
|
||||
-- See memory: lan-mouse-forwards-unaccelerated-deltas.md
|
||||
@@ -0,0 +1,22 @@
|
||||
-- See https://wiki.hypr.land/Configuring/Basics/Monitors/
|
||||
-- List current monitors and supported resolutions with: hyprctl monitors all
|
||||
|
||||
-- Monitor scale is Hyprland's scale for the output. It sizes everything
|
||||
-- Wayland-native, accepts fractions (1.6, 1.75), and applies immediately.
|
||||
-- "auto" lets Hyprland pick per display.
|
||||
local omarchy_monitor_scale = 1.33333
|
||||
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = omarchy_monitor_scale })
|
||||
|
||||
-- Configure a specific monitor.
|
||||
-- hl.monitor({ output = "DP-2", mode = "2560x1440@144", position = "0x0", scale = 1 })
|
||||
|
||||
-- Portrait/rotated secondary monitor (transform: 1 = 90°, 3 = 270°).
|
||||
-- hl.monitor({ output = "DP-2", mode = "preferred", position = "auto", scale = 1, transform = 1 })
|
||||
|
||||
-- GDK scale is GDK_SCALE, the factor GTK draws its own UI at. It's what
|
||||
-- sizes X11/XWayland windows, which Omarchy leaves unscaled so they stay
|
||||
-- crisp instead of being stretched by the compositor. GTK only honors whole
|
||||
-- numbers, so use the nearest integer to the monitor scale, and restart an
|
||||
-- app for a change to reach it.
|
||||
local omarchy_gdk_scale = 1
|
||||
hl.env("GDK_SCALE", tostring(omarchy_gdk_scale))
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"theme": "system",
|
||||
"autoupdate": false
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# Voxtype Configuration
|
||||
#
|
||||
# Location: ~/.config/voxtype/config.toml
|
||||
# All settings can be overridden via CLI flags
|
||||
#
|
||||
# State file for external integrations (Waybar, polybar, etc.)
|
||||
# Use "auto" for default location ($XDG_RUNTIME_DIR/voxtype/state),
|
||||
# a custom path, or "disabled" to turn off. The daemon writes state
|
||||
# ("idle", "recording", "transcribing") to this file whenever it changes.
|
||||
# Required for `voxtype record toggle` and `voxtype status` commands.
|
||||
state_file = "auto"
|
||||
|
||||
[hotkey]
|
||||
# Hotkey is configured in Hyprland. Default is Super + Ctrl + X
|
||||
enabled = false
|
||||
|
||||
[audio]
|
||||
# Audio input device ("default" uses system default)
|
||||
# List devices with: pactl list sources short
|
||||
device = "default"
|
||||
|
||||
# Sample rate in Hz (whisper expects 16000)
|
||||
sample_rate = 16000
|
||||
|
||||
# Maximum recording duration in seconds (safety limit)
|
||||
max_duration_secs = 60
|
||||
|
||||
# Pause MPRIS media players during recording
|
||||
pause_media = true
|
||||
|
||||
# [audio.feedback]
|
||||
# Enable audio feedback sounds (beeps when recording starts/stops)
|
||||
# enabled = true
|
||||
#
|
||||
# Sound theme: "default", "subtle", "mechanical", or path to custom theme directory
|
||||
# theme = "default"
|
||||
#
|
||||
# Volume level (0.0 to 1.0)
|
||||
# volume = 0.7
|
||||
|
||||
[whisper]
|
||||
# Model to use for transcription
|
||||
# Options: tiny, tiny.en, base, base.en, small, small.en, medium, medium.en, large-v3, large-v3-turbo
|
||||
# .en models are English-only but faster and more accurate for English
|
||||
# large-v3-turbo is faster than large-v3 with minimal accuracy loss (recommended for GPU)
|
||||
# Or provide absolute path to a custom .bin model file
|
||||
model = "base.en"
|
||||
|
||||
# Language for transcription
|
||||
# Use "en" for English, "auto" for auto-detection
|
||||
# See: https://github.com/openai/whisper#available-models-and-languages
|
||||
language = "en"
|
||||
|
||||
# Translate non-English speech to English
|
||||
translate = false
|
||||
|
||||
# Number of CPU threads for inference (omit for auto-detection)
|
||||
# threads = 4
|
||||
|
||||
[output]
|
||||
# Primary output mode: "type" or "clipboard"
|
||||
# - type: Simulates keyboard input at cursor position (requires ydotool)
|
||||
# - clipboard: Copies text to clipboard (requires wl-copy)
|
||||
mode = "type"
|
||||
|
||||
# Fall back to clipboard if typing fails
|
||||
fallback_to_clipboard = true
|
||||
|
||||
# Delay between typed characters in milliseconds
|
||||
# 0 = fastest possible, increase if characters are dropped
|
||||
type_delay_ms = 1
|
||||
|
||||
# Post-processing command (optional)
|
||||
# Pipe transcribed text through an external command for cleanup before output.
|
||||
# The command receives text on stdin and outputs processed text on stdout.
|
||||
# Useful for LLM-based text cleanup, grammar correction, filler word removal.
|
||||
# On any failure (timeout, error), falls back to original transcription.
|
||||
#
|
||||
# [output.post_process]
|
||||
# command = "ollama run llama3.2:1b 'Clean up this dictation. Fix grammar, remove filler words. Output only the cleaned text:'"
|
||||
# timeout_ms = 30000 # 30 second timeout (generous for LLM)
|
||||
|
||||
[output.notification]
|
||||
# Show notification when recording starts (hotkey pressed)
|
||||
on_recording_start = false
|
||||
|
||||
# Show notification when recording stops (transcription beginning)
|
||||
on_recording_stop = false
|
||||
|
||||
# Show notification with transcribed text after transcription completes
|
||||
on_transcription = false
|
||||
|
||||
# [text]
|
||||
# Text processing options (word replacements, spoken punctuation)
|
||||
#
|
||||
# Enable spoken punctuation conversion (e.g., say "period" to get ".")
|
||||
# spoken_punctuation = false
|
||||
#
|
||||
# Custom word replacements (case-insensitive)
|
||||
# replacements = { "hyperwhisper" = "hyprwhspr" }
|
||||
@@ -0,0 +1,35 @@
|
||||
# MacBook Air M2 packages beyond the Omarchy baseline and the other layers.
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
|
||||
[repo]
|
||||
# --- Asahi platform: installed by the Asahi installer, listed so a check catches drift ---
|
||||
linux-asahi
|
||||
asahi-meta
|
||||
asahi-audio
|
||||
asahi-scripts
|
||||
asahi-fwextract
|
||||
m1n1
|
||||
uboot-asahi
|
||||
speakersafetyd # speaker protection; without it the amps are capped
|
||||
vulkan-asahi
|
||||
libva-v4l2_request-avd # hardware video decode (omarchy-aarch64)
|
||||
pipewire-pulse
|
||||
pipewire-alsa
|
||||
|
||||
# --- access ---
|
||||
openssh # sshd is enabled: the HP reaches it as `ssh air`
|
||||
|
||||
# --- apps ---
|
||||
obsidian-appimage # omarchy-aarch64
|
||||
wf-recorder # screen recording
|
||||
|
||||
[optional]
|
||||
steam # asahi-alarm build, runs x86 games through FEX
|
||||
omarchy-steam-fex
|
||||
|
||||
[ignore]
|
||||
# Arch Linux ARM base system and keyrings
|
||||
base base-devel sudo omarchy linux-firmware btrfs-progs grub dhcpcd netctl net-tools nano which
|
||||
ex-vi-compat terminus-font archlinuxarm-keyring asahi-alarm-keyring hyprtoolkit
|
||||
neovim # Omarchy ships nvim; this is the ALARM name for it
|
||||
dotnet-runtime-2.1 # explicit, required by nothing; unclear why it is here
|
||||
@@ -0,0 +1,11 @@
|
||||
# MacBook Air M2 (omarchy-air): Asahi Linux on Arch Linux ARM, aarch64.
|
||||
# Captured as it is on 2026-10-16 (Omarchy 4.0.3rc4): no hand-built themes, no
|
||||
# shell plugins, stock bar, stock terminal fonts. Add personal-desktop to
|
||||
# include= to give it the HP's desktop.
|
||||
description="MacBook Air M2 (omarchy-air): Asahi, aarch64"
|
||||
include=(base personal features/handoff roles/developer roles/devops roles/ai-agents)
|
||||
hostnames=(omarchy-air)
|
||||
arch=aarch64
|
||||
# The peer for omarchy-handoff is the HP: Wi-Fi .24 and the dock NIC .86, which
|
||||
# disappears on undock, so both.
|
||||
handoff_peer_ips=(192.168.1.24 192.168.1.86)
|
||||
@@ -0,0 +1 @@
|
||||
options appledrm show_notch=1
|
||||
@@ -0,0 +1 @@
|
||||
options hid_apple fnmode=1
|
||||
@@ -0,0 +1,6 @@
|
||||
# omarchy-air only. Multiplier for the residual logical-ppi difference between
|
||||
# the two screens (handoff/patch adds it; default 1.0 = upstream). The Air is
|
||||
# 140.1 logical ppi against ~95.8 for the 23" LG, so 0.7 makes forwarded motion
|
||||
# cover the same PHYSICAL distance.
|
||||
[Service]
|
||||
Environment=LAN_MOUSE_POINTER_SCALE=0.7
|
||||
@@ -0,0 +1,11 @@
|
||||
-- personal-desktop layer: the bindings that go with this layer's plugins.
|
||||
|
||||
-- Open Claude instead of the stock ChatGPT webapp.
|
||||
hl.unbind("SUPER + SHIFT + A")
|
||||
o.bind("SUPER + SHIFT + A", "Claude", { webapp = "https://claude.ai" })
|
||||
|
||||
-- Herdr HUD overlay (indie.herdr-hud native plugin, local branch).
|
||||
o.bind("SUPER + H", "Toggle Herdr HUD", "omarchy-shell shell toggle indie.herdr-hud '{}'")
|
||||
o.bind("SUPER + ALT + H", "Show or hide Herdr HUD button", "omarchy-shell shell call indie.herdr-hud toggleVisibility '{}'")
|
||||
o.bind("SUPER + CTRL + DOWN", "Herdr HUD: next agent", "omarchy-shell shell call indie.herdr-hud nextAgent '{}'")
|
||||
o.bind("SUPER + CTRL + UP", "Herdr HUD: previous agent", "omarchy-shell shell call indie.herdr-hud previousAgent '{}'")
|
||||
@@ -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"
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"version": 1,
|
||||
"idle": {
|
||||
"screensaver": 1200,
|
||||
"lock": 1800
|
||||
},
|
||||
"bar": {
|
||||
"position": "top",
|
||||
"transparent": false,
|
||||
"centerAnchor": "omarchy.clock",
|
||||
"layout": {
|
||||
"left": [
|
||||
{
|
||||
"id": "omarchy.menu"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.workspaces"
|
||||
},
|
||||
{
|
||||
"id": "niccoconnor.mantel"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.active-window"
|
||||
}
|
||||
],
|
||||
"center": [
|
||||
{
|
||||
"id": "omarchy.indicators"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.keyboard-layout"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.clock",
|
||||
"format": "dddd HH:mm",
|
||||
"formatAlt": "d MMMM 'W'ww yyyy",
|
||||
"verticalFormat": "HH\n—\nmm"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.weather"
|
||||
},
|
||||
{
|
||||
"id": "kb2uka.port-doctor"
|
||||
},
|
||||
{
|
||||
"id": "akshar.radio-atlas"
|
||||
},
|
||||
{
|
||||
"id": "omaplug"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.system-update"
|
||||
}
|
||||
],
|
||||
"right": [
|
||||
{
|
||||
"id": "omarchy.tray"
|
||||
},
|
||||
{
|
||||
"id": "izeesoft.omarchy-phone"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.spacer"
|
||||
},
|
||||
{
|
||||
"id": "io.github.juancasanueva.plugin-manager",
|
||||
"tiledExpandedPanel": true
|
||||
},
|
||||
{
|
||||
"id": "io.github.thisisgm.omapods"
|
||||
},
|
||||
{
|
||||
"id": "crmne.hyprmoncfg"
|
||||
},
|
||||
{
|
||||
"id": "ilyazar.btop"
|
||||
},
|
||||
{
|
||||
"id": "vstoms.netbird"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.agents"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.bluetooth"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.network"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.audio"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.monitor"
|
||||
},
|
||||
{
|
||||
"id": "omarchy.power"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"id": "indie.herdr-hud"
|
||||
},
|
||||
{
|
||||
"id": "niccoconnor.keyhints"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[font]
|
||||
base-size = 10
|
||||
@@ -0,0 +1,10 @@
|
||||
# Packages the plugins in plugins.txt need. See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
qmltermwidget # embedded terminal widget; indie.herdr-hud needs it
|
||||
qt6-connectivity # Bluetooth/QML bits for the omapods plugin
|
||||
qt6-tools # qmllint etc. when editing plugin QML
|
||||
openbsd-netcat # nc for kb2uka.port-doctor checks
|
||||
|
||||
[aur]
|
||||
hyprmoncfg-bin # display profile daemon; pairs with crmne.hyprmoncfg plugin
|
||||
netbird-bin # NetBird client for the covert.vpn private network; pairs with vstoms.netbird
|
||||
@@ -0,0 +1,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,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Show a Herdr pane in the herdr web ui (devswha.herdr-web-ui) as one floating app window.
|
||||
|
||||
The shell cannot host a web view (QtWebEngine aborts inside Quickshell), so the HUD
|
||||
hands agents off to a browser app window instead. The HUD keeps a single window:
|
||||
asking for the pane it already shows brings it forward; asking for another pane
|
||||
replaces it in the same place and size. Failures go to a desktop notification,
|
||||
since the HUD has closed by then.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote, urlsplit
|
||||
|
||||
HOME = Path.home()
|
||||
WEB_ENV = HOME / ".config/herdr/plugins/config/devswha.herdr-web-ui/env"
|
||||
CONFIG = HOME / ".config/herdr-hud-native/web.json"
|
||||
STATE = HOME / ".config/herdr-hud-native/web-window.json"
|
||||
WAIT_SECONDS = 10
|
||||
|
||||
|
||||
class WebError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def base_url() -> str:
|
||||
"""HERDR_HUD_WEB_URL, else {"url": ...} in web.json, else the web ui's own listen address."""
|
||||
override = os.environ.get("HERDR_HUD_WEB_URL", "").strip()
|
||||
if not override:
|
||||
try:
|
||||
override = str(json.loads(CONFIG.read_text()).get("url") or "").strip()
|
||||
except (OSError, ValueError, AttributeError):
|
||||
override = ""
|
||||
if override:
|
||||
return override.rstrip("/")
|
||||
values = {}
|
||||
try:
|
||||
for line in WEB_ENV.read_text().splitlines():
|
||||
key, sep, value = line.partition("=")
|
||||
if sep and key.strip() in ("HOST", "PORT"):
|
||||
values[key.strip()] = value.strip().strip("'\"")
|
||||
except OSError:
|
||||
pass
|
||||
host = values.get("HOST") or "127.0.0.1"
|
||||
if host in ("0.0.0.0", "::", "[::]"):
|
||||
host = "127.0.0.1"
|
||||
return f"http://{host}:{values.get('PORT') or '7317'}"
|
||||
|
||||
|
||||
def hyprctl(*args: str) -> str:
|
||||
done = subprocess.run(["hyprctl", *args], capture_output=True, text=True, timeout=5)
|
||||
if done.returncode != 0 or done.stdout.startswith("Couldn't connect"):
|
||||
raise WebError(done.stdout.strip() or done.stderr.strip() or "hyprctl failed")
|
||||
return done.stdout
|
||||
|
||||
|
||||
def fix_hyprland_signature() -> None:
|
||||
"""A shell that outlived a Hyprland restart keeps a dead signature; use the live one."""
|
||||
try:
|
||||
hyprctl("version")
|
||||
return
|
||||
except WebError:
|
||||
pass
|
||||
runtime = Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")) / "hypr"
|
||||
for candidate in sorted(runtime.glob("*/.socket.sock"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||
os.environ["HYPRLAND_INSTANCE_SIGNATURE"] = candidate.parent.name
|
||||
try:
|
||||
hyprctl("version")
|
||||
return
|
||||
except WebError:
|
||||
continue
|
||||
raise WebError("Cannot reach Hyprland")
|
||||
|
||||
|
||||
def dispatch(expression: str) -> None:
|
||||
out = hyprctl("dispatch", expression).strip()
|
||||
if out != "ok":
|
||||
raise WebError(out)
|
||||
|
||||
|
||||
def clients() -> list:
|
||||
return json.loads(hyprctl("clients", "-j"))
|
||||
|
||||
|
||||
def web_windows(host: str) -> dict:
|
||||
# Chromium-family app windows are classed "<browser>-<host>__<path>-<profile>".
|
||||
marker = f"-{host}__"
|
||||
return {c["address"]: c for c in clients() if marker in str(c.get("class", ""))}
|
||||
|
||||
|
||||
def load_state() -> dict:
|
||||
try:
|
||||
state = json.loads(STATE.read_text())
|
||||
return state if isinstance(state, dict) else {}
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def save_state(state: dict) -> None:
|
||||
STATE.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = STATE.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(state, indent=2) + "\n")
|
||||
tmp.replace(STATE)
|
||||
|
||||
|
||||
def active_workspace() -> str:
|
||||
return str(json.loads(hyprctl("activeworkspace", "-j")).get("id", ""))
|
||||
|
||||
|
||||
def bring_forward(address: str) -> None:
|
||||
window = f"address:{address}"
|
||||
workspace = active_workspace()
|
||||
if workspace:
|
||||
dispatch(f"hl.dsp.window.move({{ workspace = '{workspace}', window = '{window}' }})")
|
||||
dispatch(f"hl.dsp.focus({{ window = '{window}' }})")
|
||||
|
||||
|
||||
def default_size() -> tuple:
|
||||
for monitor in json.loads(hyprctl("monitors", "-j")):
|
||||
if monitor.get("focused"):
|
||||
scale = float(monitor.get("scale") or 1)
|
||||
width, height = monitor["width"] / scale, monitor["height"] / scale
|
||||
return round(width * 0.72), round(height * 0.82)
|
||||
return 1280, 860
|
||||
|
||||
|
||||
def place(address: str, previous: dict | None) -> None:
|
||||
window = f"address:{address}"
|
||||
dispatch(f"hl.dsp.window.float({{ action = 'enable', window = '{window}' }})")
|
||||
if previous and previous.get("floating"):
|
||||
width, height = previous["size"]
|
||||
x, y = previous["at"]
|
||||
dispatch(f"hl.dsp.window.resize({{ x = {width}, y = {height}, window = '{window}' }})")
|
||||
dispatch(f"hl.dsp.window.move({{ x = {x}, y = {y}, window = '{window}' }})")
|
||||
else:
|
||||
width, height = default_size()
|
||||
dispatch(f"hl.dsp.window.resize({{ x = {width}, y = {height}, window = '{window}' }})")
|
||||
dispatch(f"hl.dsp.window.center({{ window = '{window}' }})")
|
||||
dispatch(f"hl.dsp.focus({{ window = '{window}' }})")
|
||||
|
||||
|
||||
def open_pane(pane: str) -> None:
|
||||
base = base_url()
|
||||
host = urlsplit(base).hostname or ""
|
||||
if not host:
|
||||
raise WebError(f"Bad web ui address: {base}")
|
||||
fix_hyprland_signature()
|
||||
existing = web_windows(host)
|
||||
state = load_state()
|
||||
tracked = existing.get(str(state.get("address", "")))
|
||||
|
||||
if tracked and state.get("pane") == pane:
|
||||
bring_forward(tracked["address"])
|
||||
return
|
||||
|
||||
if tracked:
|
||||
dispatch(f"hl.dsp.window.close({{ window = 'address:{tracked['address']}' }})")
|
||||
|
||||
url = f"{base}/?pane={quote(pane, safe='')}" if pane else f"{base}/"
|
||||
subprocess.Popen(["omarchy-launch-webapp", url], stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
|
||||
deadline = time.monotonic() + WAIT_SECONDS
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(0.15)
|
||||
fresh = [a for a in web_windows(host) if a not in existing]
|
||||
if fresh:
|
||||
place(fresh[0], tracked)
|
||||
save_state({"address": fresh[0], "pane": pane})
|
||||
return
|
||||
raise WebError("The web ui window did not appear")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3 or sys.argv[1] != "open":
|
||||
print("usage: herdr-web open PANE", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
open_pane(sys.argv[2])
|
||||
except (WebError, OSError, ValueError, KeyError, subprocess.TimeoutExpired) as error:
|
||||
subprocess.run(["notify-send", "Herdr HUD", f"Cannot open the web ui: {error}"], check=False)
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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.thisisgm.omapods https://github.com/thisisgm/omarchy-pods fff7fec600a5b9a61cdb40e93eccbcceb4b8f824
|
||||
kb2uka.port-doctor https://github.com/Kb2uka/omarchy-port-doctor.git baa13451bfc27d59e8c831c217852279a20901aa
|
||||
kokd.remote-desktop https://github.com/kostadinkadiev/omarchy-remote-desktop.git 7913b89521f88fac40620f495d1e07835bbb55b4
|
||||
omaplug https://github.com/fross100/omaplug.git 9da73fc13fc2027be5b44915b4ab89440beb74ab
|
||||
vstoms.netbird https://github.com/vstoms/omarchy-netbird.git d530c76b13324b26dafd79934529b4395e18a11c
|
||||
io.github.juancasanueva.plugin-manager https://github.com/juancasanueva/omarchy-plugin-manager.git 0d26a81b528ecb6e2d5580192b20f53d793009fc
|
||||
izeesoft.omarchy-phone https://github.com/AdamMusa/omarchy-phone.git 5b4d275b555abb9e3fed3100ee14af5e83fc0a82
|
||||
@@ -0,0 +1,6 @@
|
||||
# The full personal desktop: hand-built themes, the shell plugins (pinned, with
|
||||
# local plugin work replayed from plugin-patches/), the bar layout and the
|
||||
# bindings that go with them. Only the HP laptop carries this today.
|
||||
description="Nic's desktop: themes, shell plugins, bar layout, Claude/Herdr bindings"
|
||||
# Themes managed by aether (reinstall with `aether`), so not carried here.
|
||||
aether_themes=(pink-gt3rs porsche-cayman porsche-rain)
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
Yaru-red-dark
|
||||
@@ -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)
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
Yaru-red-dark
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
28,23,18
|
||||
@@ -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)"
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
Yaru-yellow-dark
|
||||
@@ -0,0 +1 @@
|
||||
ffd12e
|
||||
@@ -0,0 +1,13 @@
|
||||
return {
|
||||
{
|
||||
"catppuccin/nvim",
|
||||
name = "catppuccin",
|
||||
priority = 1000,
|
||||
},
|
||||
{
|
||||
"LazyVim/LazyVim",
|
||||
opts = {
|
||||
colorscheme = "catppuccin-mocha",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 450 KiB |
@@ -0,0 +1,6 @@
|
||||
text = "#efe6d4"
|
||||
placeholder = "#8a8294"
|
||||
text-error = "#efe6d4"
|
||||
border = "#efe6d4"
|
||||
border-active = "#ffd12e"
|
||||
border-error = "#e2584a"
|
||||
@@ -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 }],
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
██████████████████████████████████████████████████████
|
||||
██████████████████████████████████████████████████████
|
||||
████ ████ ████
|
||||
████ ████ ████
|
||||
████ █████████████████████ ████████ ████
|
||||
████ █████████████████████ ████████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████████████ ████ ████
|
||||
████████████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ████ ████ ████
|
||||
████ ██████████████████████████████████████ ████
|
||||
████ ██████████████████████████████████████ ████
|
||||
████ ████ ████
|
||||
████ ████ ████
|
||||
█████████████████████████████ ████████████████████
|
||||
█████████████████████████████ ████████████████████
|
||||
@@ -0,0 +1,10 @@
|
||||
▄▄▄
|
||||
▄█████▄ ▄███████████▄ ▄███████ ▄███████ ▄███████ ▄█ █▄ ▄█ █▄
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▀ ███ ███ ███ ███
|
||||
███ ███ ███ ███ ███ ▄███▄▄▄███ ▄███▄▄▄██▀ ███ ▄███▄▄▄███▄ ███▄▄▄███
|
||||
███ ███ ███ ███ ███ ▀███▀▀▀███ ▀███▀▀▀▀ ███ ▀▀███▀▀▀███ ▀▀▀▀▀▀███
|
||||
███ ███ ███ ███ ███ ███ ███ ██████████ ███ █▄ ███ ███ ▄██ ███
|
||||
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
|
||||
▀█████▀ ▀█ ███ █▀ ███ █▀ ███ ███ ███████▀ ███ █▀ ▀█████▀
|
||||
███ █▀
|
||||
@@ -0,0 +1 @@
|
||||
claude
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"autoupdate": false
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
voxtype-bin # push-to-talk voice-to-text (voxtype.service); in the omarchy repo on both arches
|
||||
@@ -0,0 +1,3 @@
|
||||
# What both of Nic's laptops share today: branding, default agent, herdr,
|
||||
# opencode and dictation. Not for Coder desktops, which other people use.
|
||||
description="Nic's identity: branding, default agent, herdr, opencode, voxtype"
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,7 @@
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
claude-code cmd=claude # omarchy repo, both arches
|
||||
github-copilot-cli cmd=copilot # omarchy repo, both arches
|
||||
gemini-cli cmd=gemini
|
||||
opencode x86_64 cmd=opencode # not in Arch Linux ARM; dotfiles installs it there
|
||||
openai-codex x86_64 cmd=codex # not in Arch Linux ARM; dotfiles installs it there
|
||||
@@ -0,0 +1,9 @@
|
||||
# Coding agents. Omarchy already ships herdr (the agent multiplexer) and
|
||||
# `omarchy agent`, which launches whichever agent `omarchy-default-agent`
|
||||
# names; this installs the agents themselves. Choosing the default is left to
|
||||
# the person (Omarchy menu > Setup > Agent), or to a layer that ships
|
||||
# config/omarchy/defaults/agent, as personal/ does.
|
||||
description="Coding agents: Claude Code, OpenCode, Codex, Gemini, Copilot"
|
||||
# mcp-config points opencode at the covert-gpt LiteLLM gateway (needs secrets);
|
||||
# hermes is the Hermes agent. Neither belongs on a Coder desktop.
|
||||
dotfiles=(bin mcp-config hermes)
|
||||
@@ -0,0 +1,13 @@
|
||||
# See machines/hp-laptop/packages.txt for the format. cmd= skips a package whose
|
||||
# command is already on PATH, e.g. from dotfiles' ~/.local/bin shims.
|
||||
[repo]
|
||||
github-cli cmd=gh # GitHub CLI
|
||||
git-delta cmd=delta # syntax-highlighted git diffs
|
||||
uv cmd=uv # Python packaging and venvs
|
||||
just cmd=just # command runner
|
||||
direnv cmd=direnv # per-directory env (.envrc)
|
||||
shfmt cmd=shfmt # shell formatter
|
||||
shellcheck x86_64 cmd=shellcheck # not built for aarch64 by Arch Linux ARM
|
||||
httpie cmd=http # HTTP client
|
||||
mise x86_64 cmd=mise # runtime manager Omarchy's dev-env uses; HP has mise-bin, ALARM has no mise
|
||||
visual-studio-code-bin cmd=code # editor (omarchy repo)
|
||||
@@ -0,0 +1,8 @@
|
||||
# General software development. Omarchy already ships git, docker (+compose,
|
||||
# buildx), lazygit, lazydocker, nvim, ripgrep, fd, fzf, clang/llvm and
|
||||
# `omarchy install dev-env <lang>` for language runtimes via mise, so this only
|
||||
# adds what is missing from that.
|
||||
description="Software development: gh, delta, uv, just, direnv, VS Code"
|
||||
# dotfiles modules this role pairs with on Nic's machines (advisory; install.sh
|
||||
# prints them, it never runs dotfiles itself): zsh, git
|
||||
dotfiles=(zsh git)
|
||||
@@ -0,0 +1,28 @@
|
||||
# See machines/hp-laptop/packages.txt for the format.
|
||||
[repo]
|
||||
# --- Kubernetes ---
|
||||
kubectl cmd=kubectl
|
||||
helm cmd=helm
|
||||
k9s cmd=k9s
|
||||
kubectx cmd=kubectx # kubectx + kubens
|
||||
stern cmd=stern # multi-pod log tailing
|
||||
kustomize cmd=kustomize
|
||||
talosctl cmd=talosctl # the Omni-managed cluster runs Talos
|
||||
cilium-cli cmd=cilium-cli,cilium
|
||||
|
||||
# --- infrastructure as code ---
|
||||
opentofu cmd=tofu
|
||||
terragrunt cmd=terragrunt
|
||||
packer cmd=packer # Proxmox desktop images
|
||||
ansible cmd=ansible
|
||||
go-task cmd=go-task,task # Arch names the binary go-task; dotfiles installs it as task
|
||||
go-yq cmd=yq
|
||||
sops cmd=sops # encrypted secrets in git
|
||||
age cmd=age # sops' key backend
|
||||
|
||||
# --- network debugging ---
|
||||
bind cmd=dig # dig, delv, nslookup
|
||||
mtr cmd=mtr
|
||||
whois cmd=whois
|
||||
nmap cmd=nmap
|
||||
tcpdump cmd=tcpdump
|
||||
@@ -0,0 +1,8 @@
|
||||
# Infrastructure and Kubernetes work: the Omni/Talos cluster, OpenTofu,
|
||||
# Packer/Proxmox images, Ansible.
|
||||
#
|
||||
# On Nic's laptops dotfiles' bin/ already puts most of these in ~/.local/bin,
|
||||
# and each line's cmd= skips the package when that shim exists. On a Coder
|
||||
# desktop (no dotfiles, no 1Password) the Arch packages are the tools.
|
||||
description="Infrastructure/Kubernetes: kubectl, helm, k9s, tofu, talosctl, ansible"
|
||||
dotfiles=(bin ansible secrets)
|
||||