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/narration.pydeleted−348@@ -1,348 +0,0 @@-"""Narration panel for the KiCad tour.--Styled to match the Adom Desktop Tauri app exactly:- Colors: desktop/src/style.css CSS custom properties- Fonts: Segoe UI (body) + Cascadia Code (mono)- Layout: bg-primary page, bg-secondary panels, bg-tertiary hover--Runs standalone with an HTTP server so Claude can push updates:- python narration.py [--port 8799]--HTTP API:- POST /update {"step":1,"total":19,"title":"...","description":"...",- "act":"...","status":"...","status_color":"#3fb950",- "data":"...","data_color":"#3fb950"}- POST /quit Close the panel- GET /health {"status":"ok"}-"""--import argparse-import ctypes-import json-import sys-import threading-import tkinter as tk-from http.server import HTTPServer, BaseHTTPRequestHandler-from tkinter import ttk---# ── Colors — exact match to desktop/src/style.css :root vars ─────-BG_PRIMARY = "#0d1117" # --bg-primary (page background)-BG_SECONDARY = "#161b22" # --bg-secondary (panels, toolbars)-BG_TERTIARY = "#21262d" # --bg-tertiary (hover, elevated)-BORDER = "#30363d" # --border-TEXT_PRIMARY = "#e6edf3" # --text-primary-TEXT_SECONDARY = "#8b949e" # --text-secondary-TEXT_MUTED = "#6e7681" # --text-muted-ACCENT = "#58a6ff" # --accent-GREEN = "#3fb950" # --green-RED = "#f85149" # --red-YELLOW = "#d29922" # --yellow-ORANGE = "#db6d28" # --orange--# Brand accent-TEAL = "#00b8b1"-TEAL_BRIGHT = "#00e6dc"-PURPLE = "#8C6BF7"-PURPLE_LIGHT = "#C5B3FF"-BLUE = ACCENT-BLUE_LIGHT = "#64ABFF"--# Backward-compat aliases-FG = TEXT_PRIMARY-FG_DIM = TEXT_MUTED-FG_SECONDARY = TEXT_SECONDARY--# ── Typography ──────────────────────────────────────────────────-FONT_TITLE = ("Segoe UI", 18, "bold")-FONT_H2 = ("Segoe UI", 15, "bold")-FONT_ACT = ("Segoe UI", 11, "bold")-FONT_DESC = ("Segoe UI", 13)-FONT_MONO = ("Cascadia Code", 12)-FONT_STATUS = ("Segoe UI", 13)-FONT_BTN = ("Segoe UI", 13)-FONT_SMALL = ("Segoe UI", 12)-FONT_TAG = ("Segoe UI", 11, "bold")---class NarrationPanel:- """Always-on-top narration panel — styled identical to Adom Desktop."""-- def __init__(self, width: int = 480, on_quit=None):- self.width = width- self._on_quit = on_quit-- self.root = tk.Tk()- self.root.title("Gallia Tour")- self.root.configure(bg=BG_PRIMARY)- self.root.attributes("-topmost", True)-- # Position on the right side of the screen, accounting for taskbar.- # Use tkinter's own screen dimensions (logical pixels) so geometry- # values are consistent regardless of DPI awareness.- screen_w = self.root.winfo_screenwidth()- screen_h = self.root.winfo_screenheight()- # Subtract space for the taskbar (~50 logical px) and titlebar (~32 px)- # to guarantee the Quit button is fully visible.- panel_h = screen_h - 80- x = screen_w - width- self.root.geometry(f"{width}x{panel_h}+{x}+0")- self.root.resizable(False, False)-- self.root.protocol("WM_DELETE_WINDOW", self._quit)-- self._build_ui()-- # Apply dark mode to the native Windows titlebar via DWM API.- # This gives us a dark titlebar that matches the app without- # removing it (which caused the invisible-window bug).- self.root.update_idletasks()- self._apply_dark_titlebar()-- def _apply_dark_titlebar(self):- """Use DWM API to render the native titlebar in dark mode (Win10 1809+)."""- try:- hwnd = ctypes.windll.user32.GetParent(self.root.winfo_id())- # DWMWA_USE_IMMERSIVE_DARK_MODE = 20 (Windows 10 1809+)- DWMWA_USE_IMMERSIVE_DARK_MODE = 20- value = ctypes.c_int(1) # TRUE = dark mode- ctypes.windll.dwmapi.DwmSetWindowAttribute(- hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,- ctypes.byref(value), ctypes.sizeof(value),- )- except Exception:- pass # Fall back to default titlebar if DWM not available-- def _build_ui(self):- # ── Status bar (matches #top-bar status group) ───- status_bar = tk.Frame(self.root, bg=BG_SECONDARY, padx=16, pady=8)- status_bar.pack(fill="x")-- # Green dot + step counter (like the Connected indicator)- dot_canvas = tk.Canvas(- status_bar, width=14, height=14,- bg=BG_SECONDARY, highlightthickness=0,- )- dot_canvas.pack(side="left", padx=(0, 8))- self._dot = dot_canvas- self._dot_id = dot_canvas.create_oval(2, 2, 12, 12, fill=GREEN, outline="")-- self._step_label = tk.Label(- status_bar, text="Gallia Feature Tour", font=FONT_SMALL,- bg=BG_SECONDARY, fg=TEXT_SECONDARY,- )- self._step_label.pack(side="left")-- self._act_tag = tk.Label(- status_bar, text="", font=FONT_TAG,- bg="#1a1a33", fg="#a78bfa",- padx=6, pady=1,- )- self._act_tag.pack(side="right")-- # Border- tk.Frame(self.root, bg=BORDER, height=1).pack(fill="x")-- # ── Progress bar ─────────────────────────────- prog_frame = tk.Frame(self.root, bg=BG_PRIMARY, padx=16)- prog_frame.pack(fill="x", pady=(8, 0))-- style = ttk.Style()- style.theme_use("default")- style.configure(- "Tour.Horizontal.TProgressbar",- troughcolor=BG_TERTIARY,- background=TEAL,- thickness=4,- )- self._progress = ttk.Progressbar(- prog_frame, style="Tour.Horizontal.TProgressbar",- length=self.width - 32, mode="determinate",- )- self._progress.pack(fill="x")-- # ── Content area ─────────────────────────────- content_frame = tk.Frame(self.root, bg=BG_PRIMARY, padx=16, pady=12)- content_frame.pack(fill="x")-- self._section_label = tk.Label(- content_frame, text="FEATURE TOUR", font=FONT_H2,- bg=BG_PRIMARY, fg=TEXT_SECONDARY,- )- self._section_label.pack(anchor="w")-- self._title_label = tk.Label(- content_frame, text="Waiting for Claude...", font=FONT_TITLE,- bg=BG_PRIMARY, fg=TEXT_PRIMARY, anchor="w",- wraplength=self.width - 32, justify="left",- )- self._title_label.pack(fill="x", pady=(8, 0))-- tk.Frame(self.root, bg=BORDER, height=1).pack(fill="x", padx=16, pady=8)-- self._desc_label = tk.Label(- self.root, text="The tour will begin shortly.\nClaude is analyzing your setup...",- font=FONT_DESC,- bg=BG_PRIMARY, fg=TEXT_SECONDARY, padx=16, anchor="nw",- wraplength=self.width - 32, justify="left",- )- self._desc_label.pack(fill="x")-- # ── Status line ──────────────────────────────- self._status_label = tk.Label(- self.root, text="", font=FONT_STATUS,- bg=BG_PRIMARY, fg=GREEN, padx=16, anchor="w",- )- self._status_label.pack(fill="x", pady=(8, 0))-- # ── Data area (matches #activity-log) ────────- data_outer = tk.Frame(self.root, bg=BG_PRIMARY, padx=16, pady=8)- data_outer.pack(fill="both", expand=True)-- data_border = tk.Frame(data_outer, bg=BORDER, padx=1, pady=1)- data_border.pack(fill="both", expand=True)-- self._data_text = tk.Text(- data_border, font=FONT_MONO,- bg=BG_SECONDARY, fg=TEXT_PRIMARY,- insertbackground=GREEN,- selectbackground=BG_TERTIARY,- relief="flat", wrap="word",- padx=8, pady=8,- borderwidth=0, highlightthickness=0,- )- self._data_text.pack(fill="both", expand=True)- self._data_text.config(state="disabled")-- # ── Button bar (matches #stats-footer) ───────- tk.Frame(self.root, bg=BORDER, height=1).pack(fill="x", side="bottom")-- btn_frame = tk.Frame(self.root, bg=BG_SECONDARY, pady=8, padx=16)- btn_frame.pack(fill="x", side="bottom")-- tk.Button(- btn_frame, text="Quit Tour", command=self._quit,- font=FONT_BTN, bg="#da3633", fg="#ffffff",- activebackground="#b62324", activeforeground="#ffffff",- relief="flat", padx=14, pady=6, cursor="hand2",- borderwidth=1, highlightthickness=0,- ).pack(side="right")-- # "Powered by Claude" label- tk.Label(- btn_frame, text="Powered by Claude",- font=FONT_SMALL, bg=BG_SECONDARY, fg=TEXT_MUTED,- ).pack(side="left")-- # ── Public API (thread-safe via root.after) ───────────────────-- def set_step(self, number: int, total: int, title: str, description: str,- act: str = ""):- self._step_label.config(text=f"Step {number} of {total}")- self._progress["maximum"] = total- self._progress["value"] = number- if act:- self._act_tag.config(text=act.upper())- self._title_label.config(text=title)- self._desc_label.config(text=description)- self._status_label.config(text="")- self._data_text.config(state="normal")- self._data_text.delete("1.0", "end")- self._data_text.config(state="disabled")-- def set_status(self, text: str, color: str = GREEN):- self._status_label.config(text=text, fg=color)-- def set_data(self, text: str):- self._data_text.config(state="normal")- self._data_text.delete("1.0", "end")- self._data_text.insert("1.0", text)- self._data_text.config(state="disabled")-- def set_data_color(self, color: str):- self._data_text.config(fg=color)-- def apply_update(self, data: dict):- """Apply a dict of updates from the HTTP API."""- if "step" in data and "total" in data:- self.set_step(- data["step"], data["total"],- data.get("title", ""), data.get("description", ""),- data.get("act", ""),- )- else:- if "title" in data:- self._title_label.config(text=data["title"])- if "description" in data:- self._desc_label.config(text=data["description"])- if "act" in data:- self._act_tag.config(text=data["act"].upper())- if "status" in data:- color = data.get("status_color", GREEN)- self.set_status(data["status"], color)- if "data" in data:- self.set_data(data["data"])- if "data_color" in data:- self.set_data_color(data["data_color"])-- # ── HTTP server for Claude to push updates ────────────────────-- def start_server(self, port: int = 8799):- """Start the HTTP control server on a background thread."""- panel = self-- class Handler(BaseHTTPRequestHandler):- def do_POST(self):- if self.path == "/update":- length = int(self.headers.get("Content-Length", 0))- body = json.loads(self.rfile.read(length)) if length else {}- panel.root.after(0, lambda: panel.apply_update(body))- self._respond(200, {"ok": True})- elif self.path == "/quit":- self._respond(200, {"ok": True})- panel.root.after(100, panel.root.destroy)- else:- self._respond(404, {"error": "not found"})-- def do_GET(self):- if self.path == "/health":- self._respond(200, {"status": "ok", "port": port})- else:- self._respond(404, {"error": "not found"})-- def _respond(self, code, data):- self.send_response(code)- self.send_header("Content-Type", "application/json")- self.end_headers()- self.wfile.write(json.dumps(data).encode())-- def log_message(self, fmt, *args):- pass # Suppress request logging-- server = HTTPServer(("127.0.0.1", port), Handler)- t = threading.Thread(target=server.serve_forever, daemon=True)- t.start()- return server-- # ── Internal ──────────────────────────────────────────────────-- def _quit(self):- if self._on_quit:- self._on_quit()- self.root.destroy()---# ── Standalone mode ──────────────────────────────────────────────--if __name__ == "__main__":- parser = argparse.ArgumentParser(description="Gallia Tour narration panel")- parser.add_argument("--port", type=int, default=8799,- help="HTTP server port (default: 8799)")- parser.add_argument("--width", type=int, default=480,- help="Panel width in pixels (default: 480)")- args = parser.parse_args()-- panel = NarrationPanel(width=args.width)- panel.start_server(port=args.port)- print(f"Panel server listening on http://127.0.0.1:{args.port}")- panel.root.mainloop()