← 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

John Lauer ·5115713f9c ·1mo ago ·parent 8b1dcdc
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