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
Update 40 file(s)
4 files changed
+143−7
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.264+0.9.265
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.264",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.264/adom-bridge-kicad-v0.9.264.zip",- "sha256": "6f7179349c1454ad103f2de90b8ad4f87492207b03d960b854e9de7007b8f39b",- "size": 1030598,+ "version": "0.9.265",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.265/adom-bridge-kicad-v0.9.265.zip",+ "sha256": "743ae641dec347f38e814dee3027008ffff1b070418e930d4564e6e058b2a408",+ "size": 1033495, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-09-01T01:48:44Z",+ "released_at": "2026-09-01T02:23:49Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.264",+ "version": "0.9.265", "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.", "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.",
handlers/foreground_guard.pyadded+136@@ -0,0 +1,136 @@+"""Foreground etiquette for actions that raise a KiCad window.++Two helpers, `warn_before_foreground` and `guarded_foreground`, that every verb+which is KNOWN to raise a window should use. Added 2026-09-01 on John's+instruction, after he watched this bridge repeatedly steal his foreground window.++WHY THIS EXISTS AT ALL+----------------------+KiCad is wxWidgets, and wx ACTIVATES ITS OWN TOP-LEVEL WINDOW whenever its UI+state changes. Measured on AdomLapper against KiCad 10.0 by comparing z-order+before and after each call (see skills/kicad-uia):++ desktop_ui_tree (read-only) z unchanged+ desktop_ui_expand (uia-pattern) KiCad z 4 -> 0+ desktop_ui_select (uia-pattern) editor z 1 -> 0+ desktop_ui_click (Invoke) editor z 1 -> 0++Note the middle two took the pure UIA pattern path — Adom Bridge never touched+SendInput — and the window STILL came forward. So this is not something a+different ab verb or code path can avoid: you cannot change a wx control's state+without the app raising itself. UI Automation is a way to READ KiCad precisely+and to act without coordinates. It is NOT a way to act invisibly, and it must+never be described to a user as if it were.++THE 200ms RULE (the important part)+-----------------------------------+When we cause a raise, we hand the user's window straight back, so the raise is a+blip instead of a theft. But the guard gets EXACTLY ONE LOOK, within ~200ms, and+then stops forever for that action.++That bound is the safety property, not a performance tweak. A KiCad window+arriving 50ms after our call is ours. A KiCad window arriving two seconds later+is THE USER — they clicked its taskbar button or alt-tabbed to it. A guard that+keeps watching would fight them: they foreground KiCad, we shove it back, and the+machine feels broken. John's words, 2026-09-01: "after that you need to be hands+off in case the adom user wants to click the windows taskbar icon to foreground+that window or alt-tab to it."++So restoring requires BOTH:+ 1. we are still inside the window (default 200ms), AND+ 2. the window now on top is the one WE raised — not merely "not the user's".+Fail either and do nothing at all.++CAPTION FIRST+-------------+Anything known to foreground announces itself on screen first, via+desktop_caption, so a window appearing is explained rather than surprising. ab+requires a `reason` (>=10 chars) which it prints on the caption beside the thread+name, so the user can always see who is drawing on their screen. Keep the text to+one clause: ab measures the rendered box and warns `too_wide_for_screen`.+"""++from __future__ import annotations++import time++GUARD_WINDOW_MS = 200+++def _top_window(ab_call) -> int | None:+ """hwnd currently at the top of the z-order, or None if unreadable."""+ try:+ res = ab_call("desktop_list_windows", {})+ wins = (res or {}).get("windows") or []+ if not wins:+ return None+ return sorted(wins, key=lambda w: w.get("z", 10**9))[0].get("hwnd")+ except Exception: # pylint: disable=broad-except+ return None+++def warn_before_foreground(ab_call, text: str, reason: str, seconds: int = 5) -> None:+ """Tell the user on screen WHY a window is about to jump to the front.++ Best-effort: a caption failure must never block the real work, and must never+ turn into an error the user sees twice."""+ try:+ ab_call("desktop_caption", {"text": text[:120], "seconds": seconds,+ "reason": reason})+ except Exception: # pylint: disable=broad-except+ pass+++def guarded_foreground(ab_call, action, *, caption: str | None = None,+ reason: str = "", guard_ms: int = GUARD_WINDOW_MS) -> dict:+ """Run `action` (which is expected to raise a KiCad window) and give the+ user's window back — once, immediately, and only if we are the cause.++ Returns the action's result with a `_foreground` report attached, so a caller+ can SAY what happened instead of guessing. Never raises on guard failure: the+ user's window placement is a courtesy, not a correctness requirement."""+ if caption:+ warn_before_foreground(ab_call, caption,+ reason or "Warn before an action that raises a KiCad window")++ user_win = _top_window(ab_call)+ t0 = time.monotonic()+ result = action()+ elapsed_ms = (time.monotonic() - t0) * 1000.0++ report = {"userWindowBefore": user_win, "elapsedMs": round(elapsed_ms),+ "restored": False, "why": ""}++ # Condition 1: are we still inside the guard window? If the action itself+ # took longer than the guard, we have ALREADY missed our chance — anything+ # on top now is as likely to be the user as us, so we do not touch it.+ if elapsed_ms > guard_ms:+ report["why"] = (f"action took {report['elapsedMs']}ms (> {guard_ms}ms guard); "+ "too late to tell our raise from a deliberate user switch")+ result["_foreground"] = report+ return result++ now_top = _top_window(ab_call)+ report["topAfter"] = now_top++ # Condition 2: is the window on top the one WE raised? "Not the user's+ # window" is NOT good enough — a third app may have come forward on its own,+ # and shoving it aside would be the same rudeness in a different direction.+ if user_win is None or now_top is None:+ report["why"] = "could not read z-order; leaving the user's screen alone"+ elif now_top == user_win:+ report["why"] = "nothing was raised"+ else:+ try:+ ab_call("desktop_raise_window", {+ "hwnd": user_win,+ "reason": "Restore the window that was in front before this thread raised a KiCad window"})+ report["restored"] = True+ report["why"] = "restored the user's window within the guard window"+ except Exception as e: # pylint: disable=broad-except+ report["why"] = f"restore failed: {type(e).__name__}"++ # No second look. Ever. From here the foreground belongs to the user: if they+ # alt-tab to KiCad a moment from now, that is their choice and we leave it.+ result["_foreground"] = report+ return result