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.137: event-driven focus enforcement (SetWinEventHook on SHOW/FOREGROUND) - during bridge work a KiCad window is suppressed the instant it appears and bounced within milliseconds if it still grabs fg; polling was the visible flash on 10.0.5
6 files changed
+99−7
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.136+0.9.137
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.136",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.136/adom-bridge-kicad-v0.9.136.zip",- "sha256": "d7b3825a5814703bf003a2215dca9263a6ca1431dc42a09ba991db28ad5bc0da",- "size": 415573,+ "version": "0.9.137",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.137/adom-bridge-kicad-v0.9.137.zip",+ "sha256": "47efeabe8f26f2e15720a68c9b3eac912e6dc160347bf06676ad89b373d8891e",+ "size": 416523, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-17T14:03:27Z",+ "released_at": "2026-08-17T14:11:48Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.137.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.136",+ "version": "0.9.137", "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/win_focus.py+91@@ -315,6 +315,97 @@ def get_focus_events() -> list: return list(_focus_events) if _focus_events is not None else [] +# ---------------------------------------------------------------------------+# EVENT-DRIVEN enforcement (0.9.137, John 2026-08-17: "prove to me you don't+# foreground kicad windows"). The polling sentinel reacts in ~120ms and the+# guardian sweeps at 500ms — on 10.0.5, where WS_EX_NOACTIVATE no longer+# prevents a first steal, that reaction latency IS the visible flash. This+# hook makes Windows deliver the events synchronously instead:+# EVENT_OBJECT_SHOW -> a KiCad window appearing during bridge work is+# suppressed BEFORE it can take the foreground.+# EVENT_SYSTEM_FOREGROUND-> a KiCad window that still grabs fg during bridge+# work is bounced within milliseconds.+# Scope: only while guards are active (bridge work in flight) and never inside+# an expect_foreground() window — outside bridge work the click-aware polling+# sentinel owns classification, so user clicks into KiCad always work.+# ---------------------------------------------------------------------------+_hook_started = False+++def start_focus_event_hook() -> None:+ global _hook_started+ if not IS_WINDOWS or _hook_started:+ return+ _hook_started = True+ import threading+ import time as _t++ EVENT_SYSTEM_FOREGROUND = 0x0003+ EVENT_OBJECT_SHOW = 0x8002+ WINEVENT_OUTOFCONTEXT = 0x0000+ WINEVENT_SKIPOWNPROCESS = 0x0002+ OBJID_WINDOW = 0++ WinEventProc = ctypes.WINFUNCTYPE(+ None, ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.wintypes.HWND,+ ctypes.wintypes.LONG, ctypes.wintypes.LONG, ctypes.wintypes.DWORD,+ ctypes.wintypes.DWORD)++ def _on_event(_hook, event, hwnd, id_object, _id_child, _tid, _time):+ try:+ if id_object != OBJID_WINDOW or not hwnd:+ return+ if _guard_count <= 0:+ return # bridge idle: the polling sentinel owns policy+ if _t.monotonic() < _expected_fg_until:+ return # sanctioned foreground in progress+ if not is_kicad_window(hwnd):+ return+ if event == EVENT_OBJECT_SHOW:+ suppress_activation(hwnd)+ elif event == EVENT_SYSTEM_FOREGROUND:+ _push_kicad_to_background(hwnd)+ try:+ if _focus_events is not None:+ buf = ctypes.create_unicode_buffer(80)+ _user32.GetWindowTextW(hwnd, buf, 80)+ _focus_events.append({"t": __import__("time").time(),+ "action": "guarded-steal (hook-bounced, ms)",+ "title": buf.value,+ "secondsSinceUserInput": round(min(_seconds_since_user_input(), 9999.0), 2)})+ except Exception:+ pass+ except Exception:+ pass++ proc = WinEventProc(_on_event)++ def _pump():+ try:+ h1 = _user32.SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,+ None, proc, 0, 0,+ WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS)+ h2 = _user32.SetWinEventHook(EVENT_OBJECT_SHOW, EVENT_OBJECT_SHOW,+ None, proc, 0, 0,+ WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS)+ if not (h1 or h2):+ return+ msg = ctypes.wintypes.MSG()+ while _user32.GetMessageW(ctypes.byref(msg), None, 0, 0) != 0:+ _user32.TranslateMessage(ctypes.byref(msg))+ _user32.DispatchMessageW(ctypes.byref(msg))+ except Exception:+ pass++ try:+ t = threading.Thread(target=_pump, daemon=True, name="kicad-focus-event-hook")+ t.start()+ # keep the callback alive for the process lifetime+ globals()["_hook_proc_ref"] = proc+ except Exception:+ pass++ def start_focus_sentinel() -> None: """Start the process-lifetime sentinel thread (idempotent; called at boot).""" global _sentinel_started, _focus_events
server.py+1@@ -1048,6 +1048,7 @@ def main(): try: from handlers import win_focus win_focus.start_focus_sentinel()+ win_focus.start_focus_event_hook() except Exception: pass