refactor: restructure project into muxplex/ subdir with brand integration

- Move coordinator/, frontend/, Caddyfile, pyproject.toml, requirements.txt,
  docs/ into muxplex/ subdir in prep for packaging/sharing
- Add brand assets to frontend/: favicon.ico, pwa-192/512.png,
  apple-touch-icon.png, wordmark-on-dark.svg
- Update app: title → muxplex, header → wordmark SVG, brand color tokens
  in style.css, manifest.json updated with muxplex name and brand icons
- Add design system: assets/branding/tokens.css (101 CSS custom properties),
  tokens.json (127 tokens), DESIGN-SYSTEM.md (856-line spec)
- Add assets/branding/: SVG sources, rendered PNGs (icons, favicons, PWA, OG)
- Add scripts/render-brand-assets.py for reproducible PNG generation
- Add muxplex/README.md
This commit is contained in:
Brian Krabach
2026-03-27 15:06:00 -07:00
commit 8234e2ec05
76 changed files with 17006 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
"""
ttyd process lifecycle management for the tmux-web coordinator.
Constants:
TTYD_PID_DIR — directory for the PID file (default: ~/.local/share/tmux-web/)
TTYD_PID_PATH — full path to the PID file (TTYD_PID_DIR / 'ttyd.pid')
TTYD_PORT — port ttyd listens on (7682)
Module state:
_active_process — the currently running ttyd subprocess (or None)
Public API:
spawn_ttyd(session_name) — spawn ttyd attached to a tmux session, write PID file
kill_ttyd() — kill the running ttyd process, clean up PID file
kill_orphan_ttyd() — kill any orphaned ttyd from a previous coordinator run
"""
import asyncio
import os
import signal
import time
from pathlib import Path
# ---------------------------------------------------------------------------
# Paths and constants
# ---------------------------------------------------------------------------
_default_ttyd_pid_dir = Path.home() / ".local" / "share" / "tmux-web"
TTYD_PID_DIR: Path = Path(os.environ.get("TMUX_WEB_STATE_DIR", _default_ttyd_pid_dir))
TTYD_PID_PATH: Path = TTYD_PID_DIR / "ttyd.pid"
TTYD_PORT: int = 7682
# ---------------------------------------------------------------------------
# Module state
# ---------------------------------------------------------------------------
_active_process: asyncio.subprocess.Process | None = None
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
async def kill_ttyd() -> bool:
"""Kill the running ttyd process and clean up the PID file.
Reads the PID from TTYD_PID_PATH. If no PID file exists, returns False.
If the file content is not a valid integer, removes the file and returns False.
Checks whether the process is alive via ``os.kill(pid, 0)``. If the
process is already gone (ProcessLookupError), cleans up and returns True.
Otherwise sends SIGTERM and polls every 0.1 s for up to 2 s waiting for
the process to exit. The PID file and ``_active_process`` are cleared in
all cases before returning.
Uses ``asyncio.sleep`` for polling to avoid blocking the event loop.
Returns:
True — process was killed or was already dead.
False — no PID file found, or PID file contained invalid content.
"""
global _active_process
if not TTYD_PID_PATH.exists():
return False
try:
pid = int(TTYD_PID_PATH.read_text().strip())
except ValueError:
TTYD_PID_PATH.unlink(missing_ok=True)
return False
# Check whether the process is still alive.
try:
os.kill(pid, 0)
except ProcessLookupError:
# Already dead — clean up and report success.
TTYD_PID_PATH.unlink(missing_ok=True)
_active_process = None
return True
# Process is alive — ask it to terminate.
os.kill(pid, signal.SIGTERM)
# Poll up to 2 s for the process to exit, yielding to the event loop each iteration.
deadline = time.time() + 2.0
while time.time() < deadline:
try:
os.kill(pid, 0)
except (ProcessLookupError, PermissionError):
break
await asyncio.sleep(0.1)
# Always clean up regardless of whether the process exited in time.
TTYD_PID_PATH.unlink(missing_ok=True)
_active_process = None
return True
async def kill_orphan_ttyd() -> bool:
"""Kill any orphaned ttyd process left over from a previous coordinator run.
On coordinator startup, checks for a stale PID file from a previous run.
If found, kills the process (if still running) and removes the PID file.
Prevents two ttyd instances running simultaneously after a coordinator
restart or crash.
Delegates to kill_ttyd() for all process management and PID file cleanup.
Returns:
True — an orphan was found (process was dead or alive).
False — no PID file found, or PID file contained invalid content.
"""
return await kill_ttyd()
async def spawn_ttyd(session_name: str) -> asyncio.subprocess.Process:
"""Spawn a ttyd process attached to *session_name* via ``tmux attach``.
Runs::
ttyd -W -m 3 -p 7682 tmux attach -t <session_name>
stdout and stderr are discarded (DEVNULL). The PID is written to
TTYD_PID_PATH. The process handle is stored in ``_active_process``.
Args:
session_name: The tmux session name to attach to.
Returns:
The asyncio.subprocess.Process object for the spawned ttyd.
"""
global _active_process
proc = await asyncio.create_subprocess_exec(
"ttyd",
"-W",
"-m",
"3",
"-p",
str(TTYD_PORT),
"tmux",
"attach",
"-t",
session_name,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
# Write PID file (create parent dirs if needed)
TTYD_PID_DIR.mkdir(parents=True, exist_ok=True)
TTYD_PID_PATH.write_text(str(proc.pid))
_active_process = proc
return proc