8b0eb42fec
First developer tool for the XDJ-100SX project. Connects Claude Code directly to the Pi over SSH — push skin files, take screenshots, restart Mixxx, flash Pico firmware, and more without leaving the editor. Available MCP tools: - run_command, read_file, write_file, list_files - push_skin, pull_skin, push_skin_file, pull_skin_file - push_midi, pull_midi - take_screenshot, navigate_panel - restart_mixxx - check (preflight: SSH, Mixxx, Pico, audio) - pico_bootloader, pico_flash - discover_units — scan network for all reachable XDJ Pi units - select_unit — switch active connection mid-session (multi-unit support) Also adds --about flag and TUI About modal with authors and credits, and fixes scrolling/close behavior on Help and About modals. By: Jeancarlo Cardoso de Faria Filho (jaianlab) <jaianlabworks@gmail.com>
159 lines
4.8 KiB
Python
159 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
"""
|
|
Shared terminal UI helpers.
|
|
|
|
Extracted from xdj-pi-dev.py so all sub-modules can import consistent
|
|
colour, spinner, and logging primitives without circular dependencies.
|
|
"""
|
|
|
|
import itertools
|
|
import shutil
|
|
import sys
|
|
import threading
|
|
from typing import Any
|
|
|
|
|
|
# ─── ANSI colour ─────────────────────────────────────────────────────────────
|
|
|
|
def _ansi_enable() -> bool:
|
|
"""Enable ANSI on Windows; return True if terminal supports colour."""
|
|
if not sys.stdout.isatty():
|
|
return False
|
|
if sys.platform == "win32":
|
|
try:
|
|
import ctypes
|
|
k = ctypes.windll.kernel32
|
|
k.SetConsoleMode(k.GetStdHandle(-11), 7)
|
|
except Exception:
|
|
return False
|
|
return True
|
|
|
|
|
|
_COLOR = _ansi_enable()
|
|
|
|
|
|
class _C:
|
|
RESET = "\033[0m" if _COLOR else ""
|
|
BOLD = "\033[1m" if _COLOR else ""
|
|
DIM = "\033[2m" if _COLOR else ""
|
|
GREEN = "\033[32m" if _COLOR else ""
|
|
YELLOW = "\033[33m" if _COLOR else ""
|
|
RED = "\033[31m" if _COLOR else ""
|
|
CYAN = "\033[36m" if _COLOR else ""
|
|
GRAY = "\033[90m" if _COLOR else ""
|
|
ERASE = "\r\033[K" if _COLOR else "\r"
|
|
|
|
|
|
_SPIN_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
|
|
|
|
# ─── Spinner step ─────────────────────────────────────────────────────────────
|
|
|
|
class Step:
|
|
"""
|
|
Context manager for a named operation with a live spinner.
|
|
|
|
with Step("Stopping Mixxx"):
|
|
pi_client.exec("killall mixxx")
|
|
|
|
Prints ⠹ Stopping Mixxx... while running,
|
|
then ✓ Stopping Mixxx on success,
|
|
or ✗ Stopping Mixxx on exception.
|
|
"""
|
|
|
|
def __init__(self, label: str, indent: int = 2) -> None:
|
|
self.label = label
|
|
self.indent = " " * indent
|
|
self._stop = threading.Event()
|
|
self._thread: threading.Thread | None = None
|
|
|
|
def __enter__(self) -> "Step":
|
|
self._stop.clear()
|
|
self._thread = threading.Thread(target=self._spin, daemon=True)
|
|
self._thread.start()
|
|
return self
|
|
|
|
def _spin(self) -> None:
|
|
for frame in itertools.cycle(_SPIN_FRAMES):
|
|
sys.stdout.write(
|
|
f"{_C.ERASE}{self.indent}{_C.CYAN}{frame}{_C.RESET}"
|
|
f" {self.label}…"
|
|
)
|
|
sys.stdout.flush()
|
|
if self._stop.wait(0.08):
|
|
break
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
self._stop.set()
|
|
if self._thread:
|
|
self._thread.join()
|
|
if exc_type is None:
|
|
sys.stdout.write(
|
|
f"{_C.ERASE}{self.indent}{_C.GREEN}✓{_C.RESET}"
|
|
f" {self.label}\n"
|
|
)
|
|
else:
|
|
sys.stdout.write(
|
|
f"{_C.ERASE}{self.indent}{_C.RED}✗{_C.RESET}"
|
|
f" {self.label} {_C.DIM}({exc_val}){_C.RESET}\n"
|
|
)
|
|
sys.stdout.flush()
|
|
return False # don't suppress exceptions
|
|
|
|
|
|
# ─── Log routing ──────────────────────────────────────────────────────────────
|
|
|
|
# Set by XDJApp when TUI is active; (level, msg) -> None where level in ok/warn/fail/info/head
|
|
_tui_log_fn: Any = None
|
|
|
|
|
|
def section(title: str) -> None:
|
|
if _tui_log_fn:
|
|
_tui_log_fn("head", title)
|
|
return
|
|
w = shutil.get_terminal_size((72, 24)).columns
|
|
bar = "─" * min(w, 72)
|
|
print(f"\n{_C.BOLD}{bar}{_C.RESET}")
|
|
print(f" {_C.BOLD}{title}{_C.RESET}")
|
|
print(f"{_C.BOLD}{bar}{_C.RESET}")
|
|
|
|
|
|
def ok(msg: str) -> None:
|
|
if _tui_log_fn:
|
|
_tui_log_fn("ok", msg)
|
|
return
|
|
print(f" {_C.GREEN}✓{_C.RESET} {msg}")
|
|
|
|
|
|
def warn(msg: str) -> None:
|
|
if _tui_log_fn:
|
|
_tui_log_fn("warn", msg)
|
|
return
|
|
print(f" {_C.YELLOW}⚠{_C.RESET} {msg}")
|
|
|
|
|
|
def fail(msg: str) -> None:
|
|
if _tui_log_fn:
|
|
_tui_log_fn("fail", msg)
|
|
return
|
|
print(f" {_C.RED}✗{_C.RESET} {msg}")
|
|
|
|
|
|
def _log_line(line: str) -> None:
|
|
lo = line.lower()
|
|
if _tui_log_fn:
|
|
if any(x in lo for x in ("error:", "fatal error:", " error ")):
|
|
_tui_log_fn("fail", line)
|
|
elif "warning:" in lo:
|
|
_tui_log_fn("warn", line)
|
|
elif line.strip():
|
|
_tui_log_fn("info", line)
|
|
return
|
|
if any(x in lo for x in ("error:", "fatal error:", " error ")):
|
|
print(f" {_C.RED}│{_C.RESET} {_C.RED}{line}{_C.RESET}")
|
|
elif "warning:" in lo:
|
|
print(f" {_C.YELLOW}│{_C.RESET} {_C.YELLOW}{line}{_C.RESET}")
|
|
elif line.strip():
|
|
print(f" {_C.GRAY}│{_C.RESET} {line}")
|