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
v0.9.101: PREVENTION guardian — WS_EX_NOACTIVATE on misbehaving KiCad windows (no more fg flashes/churn), cleared on guard-exit/bring_to_user/close; 100ms poll; runner w-shadowing crash fix
8 files changed
+130−27
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.100+0.9.101
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.100",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.100/adom-bridge-kicad-v0.9.100.zip",- "sha256": "9b1e10da5e79e8f24c8a1317602fd7af790f5c2484b4a8c37fd0fd779cd37b6b",- "size": 391976,+ "version": "0.9.101",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.101/adom-bridge-kicad-v0.9.101.zip",+ "sha256": "54488db05f7494046db51ae454cf333861dcc2502adc7c44652647fdd7e2027e",+ "size": 392971, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-15T09:44:48Z",+ "released_at": "2026-08-15T09:53:07Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.101.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.100",+ "version": "0.9.101", "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/close_windows.py+7@@ -1054,6 +1054,13 @@ def handle_close_3d_viewer(kicad_info: dict, args: dict) -> dict: def handle_close_kicad(kicad_info: dict, args: dict) -> dict:+ # Safety: clear any WS_EX_NOACTIVATE the focus guardian applied, so nothing+ # un-clickable lingers regardless of how the guard threads ended.+ try:+ from handlers import win_focus+ win_focus.allow_activation_all()+ except Exception: # pylint: disable=broad-except+ pass """Close all KiCad windows (editors first, then the project manager). Args:
handlers/win_focus.py+98−16@@ -133,6 +133,69 @@ def is_kicad_window(hwnd) -> bool: return _hwnd_exe_name(hwnd) in _KICAD_EXES +# ── PREVENTION, not reaction (v0.9.101, John: "i sure see a lot of windows+# coming to fg"). The polling guardian REACTED: KiCad took focus, we yanked it+# back 250ms later — the user saw every steal as a flash and lost keystrokes.+# WS_EX_NOACTIVATE makes Windows REFUSE the window's self-activation outright:+# no flash, no focus churn. We apply it to a KiCad window the first time it+# misbehaves (or when a guardian arms over it), and CLEAR it when the last+# guardian exits / on bring_to_user / on close — so after bridge work ends the+# user can click into KiCad normally again.+WS_EX_NOACTIVATE = 0x08000000+GWL_EXSTYLE = -20++_noactivated: set = set()+_guard_count = 0+_guard_lock = None+try:+ import threading as _thr+ _guard_lock = _thr.Lock()+except Exception:+ pass+++def _get_exstyle(hwnd):+ fn = getattr(_user32, "GetWindowLongPtrW", None) or _user32.GetWindowLongW+ return fn(hwnd, GWL_EXSTYLE)+++def _set_exstyle(hwnd, style):+ fn = getattr(_user32, "SetWindowLongPtrW", None) or _user32.SetWindowLongW+ return fn(hwnd, GWL_EXSTYLE, style)+++def suppress_activation(hwnd) -> None:+ """Make a KiCad window unable to grab the foreground (WS_EX_NOACTIVATE)."""+ try:+ cur = _get_exstyle(hwnd)+ if not (cur & WS_EX_NOACTIVATE):+ _set_exstyle(hwnd, cur | WS_EX_NOACTIVATE)+ _noactivated.add(hwnd)+ except Exception:+ pass+++def allow_activation(hwnd) -> None:+ """Undo suppress_activation for one window (user wants it / bridge done)."""+ try:+ if _user32.IsWindow(hwnd):+ cur = _get_exstyle(hwnd)+ if cur & WS_EX_NOACTIVATE:+ _set_exstyle(hwnd, cur & ~WS_EX_NOACTIVATE)+ except Exception:+ pass+ _noactivated.discard(hwnd)+++def allow_activation_all() -> int:+ """Clear NOACTIVATE from every window we altered. Returns count cleared."""+ n = 0+ for h in list(_noactivated):+ allow_activation(h)+ n += 1+ return n++ def _demote_below(kicad_hwnd, user_hwnd) -> None: """Slide a KiCad window directly UNDER the user's window without activating either — the window stays visible in the taskbar and fully screenshotable,@@ -145,7 +208,7 @@ def _demote_below(kicad_hwnd, user_hwnd) -> None: pass -def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float = 0.25) -> None:+def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float = 0.1) -> None: """Focus guardian: for `seconds` after a window-spawning verb, make sure NO KiCad window holds the user's foreground. KiCad activates windows ASYNCHRONOUSLY (a 3D viewer self-raises when its render completes — sometimes@@ -167,23 +230,41 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float = import time as _t def _loop():+ global _guard_count+ if _guard_lock:+ with _guard_lock:+ _guard_count += 1 protect = hwnd deadline = _t.monotonic() + seconds- while _t.monotonic() < deadline:- try:- fg = _user32.GetForegroundWindow()- if fg and fg != protect:- if is_kicad_window(fg):- # KiCad stole the foreground: user back on top, thief tucked under.- if _user32.IsWindow(protect):- _set_foreground_robust(protect)- _demote_below(fg, protect)- else:- # The USER moved to another window — protect that instead.- protect = fg- except Exception:- pass- _t.sleep(interval)+ try:+ while _t.monotonic() < deadline:+ try:+ fg = _user32.GetForegroundWindow()+ if fg and fg != protect:+ if is_kicad_window(fg):+ # PREVENT the next steal (NOACTIVATE), then repair this+ # one: user back on top, thief tucked under.+ suppress_activation(fg)+ if _user32.IsWindow(protect):+ _set_foreground_robust(protect)+ _demote_below(fg, protect)+ else:+ # The USER moved to another window — protect that instead.+ protect = fg+ except Exception:+ pass+ _t.sleep(interval)+ finally:+ # last guardian out clears the NOACTIVATE styles so the user can+ # click into KiCad normally once bridge work has quieted down.+ if _guard_lock:+ with _guard_lock:+ _guard_count -= 1+ last = _guard_count <= 0+ else:+ last = True+ if last:+ allow_activation_all() try: threading.Thread(target=_loop, daemon=True).start()@@ -207,6 +288,7 @@ def bring_to_user(hwnd) -> bool: if not IS_WINDOWS or not hwnd: return False try:+ allow_activation(hwnd) # a suppressed window can't be activated _user32.ShowWindow(hwnd, 9) # SW_RESTORE (in case minimized) return _set_foreground_robust(hwnd) except Exception:
skills/kicad-bridge-background/SKILL.md+14@@ -63,6 +63,20 @@ An ungated one is a focus-steal bug. dialog auto-dismiss that pokes a window, `_force_foreground` calls left in old handlers. New code copies old code; old code steals focus. +## The mechanism (v0.9.101: PREVENT, don't react)++v0.9.79's guardian only REACTED — poll 250ms, yank focus back after KiCad had+already taken it. John watched a suite run: "i sure see a lot of windows coming+to fg." Every steal was a visible flash + lost keystrokes, and the+SetForegroundWindow restore can silently fail from a background process. The+v0.9.101 guardian PREVENTS: the first time a KiCad window grabs the foreground+(or is caught by a guard), it gets **WS_EX_NOACTIVATE** — Windows then refuses+its self-activation outright (no flash, no churn; wx Raise() becomes a no-op for+focus). The style is CLEARED when the last guardian exits, on+`bring_to_user` (foreground:true — a suppressed window can't be activated), and+by `kicad_close` — so the user can always click into KiCad once bridge work+quiets down. Poll tightened to 100ms for the first-steal repair.+ ## The mechanism (v0.9.79) `handlers/win_focus.py`:
skills/kicad-bridge-test/run_verb_tests.py+4−4@@ -300,10 +300,10 @@ def main(): "Footprint Editor", "3D Viewer") _user_win = {} if not a.skip_gui and not a.no_stealth:- w = _current_top()- # only adopt a NON-KiCad window as the one to protect- if w and not any(k in (w.get("title") or "") for k in _kw):- _user_win = w+ _tw = _current_top() # NB: do NOT name this `w` — it shadowed the paths+ # dict and crashed phase 4 with KeyError 'templib' (2026-08-15 run).+ if _tw and not any(k in (_tw.get("title") or "") for k in _kw):+ _user_win = _tw print(f"[stealth] keeping KiCad behind your window: {(_user_win.get('title') or '')[:50]}") def _keep_background():