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,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"
|
||||