app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
tour/tour_cli.pydeleted−436@@ -1,436 +0,0 @@-"""Unified CLI for the Gallia KiCad tour — used by Claude Code.--Subcommands:- preflight Check all prerequisites- screenshot [path] Capture desktop to PNG- highlight x y w h Show highlight overlay (non-blocking)- bridge <cmd> [k=v ...] Send bridge command, print JSON- ipc-test [--section N] Run IPC test suite- ipc-layer <id> Set active PCB layer via IPC- window <action> <title> Window management (find/close/front/position/wait)--Zero pip deps — only stdlib.-"""--import argparse-import json-import os-import subprocess-import sys-import time--# Ensure tour package is importable-TOUR_DIR = os.path.dirname(os.path.abspath(__file__))-sys.path.insert(0, TOUR_DIR)--PYTHON = sys.executable---# ═══════════════════════════════════════════════════════════════════-# Subcommands-# ═══════════════════════════════════════════════════════════════════--def cmd_preflight(_args):- """Check all tour prerequisites."""- from bridge_client import health_check- from ipc_client import has_test_binary, has_layer_binary- import win_manager-- print("=== Gallia Tour Preflight ===\n")-- # Bridge server- health = health_check()- status = "ONLINE" if health else "OFFLINE"- print(f"Bridge server: {status}")- if health:- print(f" URL: http://127.0.0.1:8772")-- # IPC binaries- print(f"IPC test binary: {'FOUND' if has_test_binary() else 'MISSING'}")- print(f"IPC layer binary: {'FOUND' if has_layer_binary() else 'MISSING'}")-- # Screen size- sw, sh = win_manager.get_screen_size()- print(f"Screen size: {sw} x {sh}")-- # KiCad windows- found_kicad = False- for name in ["KiCad", "Schematic Editor", "PCB Editor",- "Symbol Editor", "Footprint Editor"]:- wins = win_manager.find_windows_by_title(name)- if wins:- title_ascii = wins[0][1].encode("ascii", "replace").decode()- print(f"Window [{name}]: OPEN - {title_ascii}")- found_kicad = True- if not found_kicad:- print("KiCad windows: none open")-- # Demo project- demo = os.path.join(TOUR_DIR, "demo-project")- sch = os.path.join(demo, "demo-project.kicad_sch")- pcb = os.path.join(demo, "demo-project.kicad_pcb")- print(f"\nDemo schematic: {'EXISTS' if os.path.exists(sch) else 'MISSING'}")- print(f"Demo PCB: {'EXISTS' if os.path.exists(pcb) else 'MISSING'}")- print(f"Demo dir: {demo}")-- # Print paths for Claude to use- if os.path.exists(sch):- print(f"\nSCH_PATH = {sch}")- if os.path.exists(pcb):- print(f"PCB_PATH = {pcb}")---def cmd_screenshot(args):- """Capture the desktop to a PNG file."""- from screenshot import capture_screen-- path = args.output- if not path:- temp = os.environ.get("TEMP", os.environ.get("TMP", "."))- path = os.path.join(temp, "gallia_tour_screenshot.png")-- capture_screen(path)- # Print path so Claude can read it- print(path)---def cmd_highlight(args):- """Show a highlight overlay. Spawns a separate process (non-blocking)."""- script = os.path.join(TOUR_DIR, "highlight.py")- cmd = [- PYTHON, script,- str(args.x), str(args.y), str(args.width), str(args.height),- ]- if args.color:- cmd.extend(["--color", args.color])- if args.duration:- cmd.extend(["--duration", str(args.duration)])- if args.label:- cmd.extend(["--label", args.label])- if args.thickness:- cmd.extend(["--thickness", str(args.thickness)])-- # Spawn without a console window on Windows- flags = 0- if os.name == "nt":- flags = subprocess.CREATE_NO_WINDOW- subprocess.Popen(cmd, creationflags=flags)- print(f"Highlight: ({args.x}, {args.y}) {args.width}x{args.height}"- f" color={args.color} duration={args.duration}ms"- + (f' label="{args.label}"' if args.label else ""))---def cmd_bridge(args):- """Send a command to the KiCad bridge server."""- from bridge_client import send_command, health_check-- if args.command == "health":- result = health_check()- if result:- print(json.dumps(result, indent=2))- else:- print('{"status": "offline"}')- return-- # Parse key=value argument pairs- cmd_args = {}- for item in (args.args or []):- key, _, val = item.partition("=")- if not val:- print(f"Warning: skipping malformed arg '{item}' (expected key=value)")- continue- cmd_args[key] = val-- result = send_command(args.command, cmd_args)- print(json.dumps(result, indent=2, default=str))---def cmd_ipc_test(args):- """Run the IPC test suite and print output."""- from ipc_client import run_ipc_test, has_test_binary-- if not has_test_binary():- print("ERROR: test_kicad_ipc.exe not found")- print("Build with: cargo build --release --example test_kicad_ipc")- sys.exit(1)-- sections = run_ipc_test()- if args.section is not None:- text = sections.get(args.section)- if text:- print(text)- else:- print(f"Section [{args.section}] not found in output")- print(f"Available sections: {sorted(sections.keys())}")- else:- for k in sorted(sections.keys()):- print(sections[k])- print()---def cmd_ipc_layer(args):- """Set KiCad's active PCB layer via IPC."""- from ipc_client import set_active_layer, has_layer_binary-- if not has_layer_binary():- print("ERROR: ipc_set_layer.exe not found")- print("Build with: cargo build --release --example ipc_set_layer")- sys.exit(1)-- # Layer name map for user-friendly output- layer_names = {3: "F.Cu", 4: "In1.Cu", 5: "In2.Cu", 34: "B.Cu"}- name = layer_names.get(args.layer_id, f"layer {args.layer_id}")-- result = set_active_layer(args.layer_id)- if result.get("success"):- print(f"Active layer set to: {name} (id={args.layer_id})")- if result.get("output"):- print(result["output"])- else:- print(f"ERROR: Failed to set layer to {name}")- print(result.get("output", "Unknown error"))- sys.exit(1)---def cmd_window(args):- """Window management: find, close, front, position, wait."""- import win_manager-- if args.action == "find":- windows = win_manager.find_windows_by_title(args.title)- if windows:- for hwnd, title in windows:- safe = title.encode("ascii", "replace").decode()- print(f" hwnd={hwnd} title={safe}")- else:- print(f"No windows matching '{args.title}'")-- elif args.action == "close":- ok = win_manager.close_window_by_title(args.title)- print("Closed" if ok else f"Window not found: '{args.title}'")-- elif args.action == "front":- windows = win_manager.find_windows_by_title(args.title)- if windows:- win_manager.bring_to_front(windows[0][0])- safe = windows[0][1].encode("ascii", "replace").decode()- print(f"Brought to front: {safe}")- else:- print(f"Window not found: '{args.title}'")-- elif args.action == "position":- if args.x is None or args.y is None or args.w is None or args.h is None:- print("ERROR: position requires x y w h arguments")- sys.exit(1)- windows = win_manager.find_windows_by_title(args.title)- if windows:- win_manager.position_window(windows[0][0], args.x, args.y,- args.w, args.h)- safe = windows[0][1].encode("ascii", "replace").decode()- print(f"Positioned: {safe} at ({args.x},{args.y}) "- f"{args.w}x{args.h}")- else:- print(f"Window not found: '{args.title}'")-- elif args.action == "wait":- timeout = args.timeout or 15- deadline = time.monotonic() + timeout- while time.monotonic() < deadline:- windows = win_manager.find_windows_by_title(args.title)- if windows:- safe = windows[0][1].encode("ascii", "replace").decode()- print(f"Found: {safe}")- return- time.sleep(0.5)- print(f"Timeout ({timeout}s) waiting for: '{args.title}'")- sys.exit(1)-- elif args.action == "screen-size":- sw, sh = win_manager.get_screen_size()- print(f"{sw} {sh}")---def cmd_panel(args):- """Control the narration side panel."""- import urllib.request-- port = args.port or 8799- base = f"http://127.0.0.1:{port}"-- if args.action == "start":- # Launch narration.py as a background process- script = os.path.join(TOUR_DIR, "narration.py")- cmd = [PYTHON, script, "--port", str(port)]- # On Windows, use `pythonw.exe` (windowless Python) to launch- # the tkinter GUI without a console window. Falls back to- # regular python.exe if pythonw doesn't exist.- pythonw = PYTHON.replace("python.exe", "pythonw.exe")- if os.path.exists(pythonw):- cmd[0] = pythonw- proc = subprocess.Popen(cmd)-- # Wait for the HTTP server to come up- for _ in range(30):- try:- req = urllib.request.Request(f"{base}/health")- with urllib.request.urlopen(req, timeout=1) as resp:- if resp.status == 200:- print(f"Panel started (pid={proc.pid}, port={port})")- return- except Exception:- time.sleep(0.3)- print(f"Panel process started (pid={proc.pid}) but health check timed out")-- elif args.action == "update":- payload = {}- if args.step is not None:- payload["step"] = args.step- if args.total is not None:- payload["total"] = args.total- if args.title:- payload["title"] = args.title- if args.description:- payload["description"] = args.description.replace("\\n", "\n")- if args.act:- payload["act"] = args.act- if args.status:- payload["status"] = args.status- if args.status_color:- payload["status_color"] = args.status_color- if args.data:- # Convert literal \n sequences to actual newlines- payload["data"] = args.data.replace("\\n", "\n")- if args.data_color:- payload["data_color"] = args.data_color-- data = json.dumps(payload).encode("utf-8")- req = urllib.request.Request(- f"{base}/update", data=data,- headers={"Content-Type": "application/json"},- method="POST",- )- try:- with urllib.request.urlopen(req, timeout=5):- print("Panel updated")- except Exception as e:- print(f"Panel update failed: {e}")-- elif args.action == "stop":- req = urllib.request.Request(f"{base}/quit", method="POST",- data=b"{}")- req.add_header("Content-Type", "application/json")- try:- with urllib.request.urlopen(req, timeout=5):- print("Panel stopped")- except Exception:- print("Panel may already be stopped")-- elif args.action == "health":- try:- req = urllib.request.Request(f"{base}/health")- with urllib.request.urlopen(req, timeout=3) as resp:- print(json.loads(resp.read().decode()))- except Exception:- print("Panel not running")---# ═══════════════════════════════════════════════════════════════════-# Argument parser-# ═══════════════════════════════════════════════════════════════════--def build_parser():- parser = argparse.ArgumentParser(- description="Gallia KiCad Tour CLI — used by Claude Code",- formatter_class=argparse.RawDescriptionHelpFormatter,- )- sub = parser.add_subparsers(dest="cmd")-- # preflight- sub.add_parser("preflight", help="Check all prerequisites")-- # screenshot- p = sub.add_parser("screenshot", help="Capture desktop screenshot")- p.add_argument("output", nargs="?", help="Output PNG path")-- # highlight- p = sub.add_parser("highlight", help="Show highlight overlay on screen")- p.add_argument("x", type=int, help="X position")- p.add_argument("y", type=int, help="Y position")- p.add_argument("width", type=int, help="Width of highlight")- p.add_argument("height", type=int, help="Height of highlight")- p.add_argument("--color", default="#00b8b1", help="Border color")- p.add_argument("--duration", type=int, default=5000, help="Duration ms")- p.add_argument("--label", default="", help="Label text")- p.add_argument("--thickness", type=int, default=3, help="Border px")-- # bridge- p = sub.add_parser("bridge", help="Send bridge command")- p.add_argument("command", help="Command name (or 'health')")- p.add_argument("args", nargs="*", help="key=value pairs")-- # ipc-test- p = sub.add_parser("ipc-test", help="Run IPC test suite")- p.add_argument("--section", type=int, help="Show only section N")-- # ipc-layer- p = sub.add_parser("ipc-layer", help="Set active PCB layer via IPC")- p.add_argument("layer_id", type=int,- help="Layer ID (F.Cu=3, In1.Cu=4, In2.Cu=5, B.Cu=34)")-- # window- p = sub.add_parser("window", help="Window management")- p.add_argument("action",- choices=["find", "close", "front", "position", "wait",- "screen-size"],- help="Action to perform")- p.add_argument("title", nargs="?", default="",- help="Window title substring")- p.add_argument("x", type=int, nargs="?", help="X position")- p.add_argument("y", type=int, nargs="?", help="Y position")- p.add_argument("w", type=int, nargs="?", help="Width")- p.add_argument("h", type=int, nargs="?", help="Height")- p.add_argument("--timeout", type=int, default=15,- help="Wait timeout in seconds")-- # panel- p = sub.add_parser("panel", help="Control the narration side panel")- p.add_argument("action", choices=["start", "update", "stop", "health"],- help="Panel action")- p.add_argument("--port", type=int, default=8799, help="Panel HTTP port")- p.add_argument("--step", type=int, help="Current step number")- p.add_argument("--total", type=int, help="Total steps")- p.add_argument("--title", help="Step title")- p.add_argument("--description", help="Step description / narration")- p.add_argument("--act", help="Act name")- p.add_argument("--status", help="Status text")- p.add_argument("--status-color", dest="status_color", help="Status color")- p.add_argument("--data", help="Data area content")- p.add_argument("--data-color", dest="data_color", help="Data area color")-- return parser---def main():- parser = build_parser()- args = parser.parse_args()-- handlers = {- "preflight": cmd_preflight,- "screenshot": cmd_screenshot,- "highlight": cmd_highlight,- "bridge": cmd_bridge,- "ipc-test": cmd_ipc_test,- "ipc-layer": cmd_ipc_layer,- "window": cmd_window,- "panel": cmd_panel,- }-- handler = handlers.get(args.cmd)- if handler:- handler(args)- else:- parser.print_help()---if __name__ == "__main__":- main()