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.182: deterministic user-intent detection (WH_MOUSE_LL + WH_KEYBOARD_LL - a taskbar click or alt-tab can no longer fall between polls) and issue #34's cached false no_kicad_installed
7 files changed
+177−13
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.181+0.9.182
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.181",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.181/adom-bridge-kicad-v0.9.181.zip",- "sha256": "5d507ca02975d9470e9b3e2ee7e846fab6fa54087f0f7d54eca862e9eff80f55",- "size": 452249,+ "version": "0.9.182",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.182/adom-bridge-kicad-v0.9.182.zip",+ "sha256": "4933a1adfd550d7ed30460d35e76c97ba2d968f28949bf7c8f68ceb0422553c6",+ "size": 454594, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-19T18:17:15Z",+ "released_at": "2026-08-19T18:26:38Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.182.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.181",+ "version": "0.9.182", "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+135@@ -266,6 +266,135 @@ except Exception: pass +# ── USER-INTENT HOOKS (0.9.182) ──────────────────────────────────────────────+# John's requirement, verbatim: "if you need NOACTIVATE then you HAVE to detect+# if the human is asking for the window to be foregrounded, meaning if they+# click the windows taskbar icon or alt_tab to the window, you MUST know that+# and allow them to see it or you have broken the entire user experience."+#+# The old detector POLLED GetAsyncKeyState every 120ms, so a click that began+# and ended between two polls was invisible - which is exactly how a taskbar+# click ended up treated as a self-raise and bounced. Polling cannot fix that;+# the gap is inherent.+#+# These are LOW-LEVEL HOOKS instead (WH_MOUSE_LL / WH_KEYBOARD_LL). Windows+# delivers every mouse button and every keystroke to them synchronously, before+# any application sees it. There is no sampling window and nothing to miss. On+# any of:+# * a mouse button pressed anywhere on the shell (taskbar / Start / tray)+# * a mouse button pressed on one of OUR KiCad windows+# * Alt+Tab+# we IMMEDIATELY drop every activation block, so the user's click or alt-tab+# lands on a window that can take focus normally. Release is instant: there is+# no hold-off, because a user may want the window up the very next second.+_user_intent_until = 0.0 # monotonic deadline: "the user is driving"+_intent_hooks_started = False+_hook_refs = [] # keep callbacks alive for the process lifetime+++def user_wants_foreground() -> bool:+ """True if the user just asked for a window (click or alt-tab)."""+ try:+ import time as _t+ return _t.monotonic() < _user_intent_until+ except Exception:+ return False+++def _note_user_intent(source: str) -> None:+ """Release EVERY activation block, right now."""+ global _user_intent_until+ try:+ import time as _t+ _user_intent_until = _t.monotonic() + 5.0+ n = allow_activation_all()+ if n:+ print(f"[intent] {source}: released activation on {n} window(s)",+ file=sys.stderr, flush=True)+ if _focus_events is not None:+ _focus_events.append({"t": _t.time(), "action": f"user intent ({source})",+ "title": "", "secondsSinceUserInput": 0.0})+ except Exception:+ pass+++def start_user_intent_hooks() -> None:+ """Low-level mouse + keyboard hooks on a dedicated pumped thread."""+ global _intent_hooks_started+ if not IS_WINDOWS or _intent_hooks_started:+ return+ _intent_hooks_started = True+ import threading++ WH_MOUSE_LL, WH_KEYBOARD_LL = 14, 13+ WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN = 0x0201, 0x0204, 0x0207+ WM_KEYDOWN, WM_SYSKEYDOWN = 0x0100, 0x0104+ VK_TAB, VK_MENU = 0x09, 0x12+ SHELL_CLASSES = {"Shell_TrayWnd", "Shell_SecondaryTrayWnd", "TaskListThumbnailWnd",+ "Windows.UI.Core.CoreWindow", "XamlExplorerHostIslandWindow",+ "MultitaskingViewFrame", "TaskSwitcherWnd", "TaskSwitcherOverlayWnd"}++ HOOKPROC = ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_int,+ ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM)++ def _class_of(hwnd) -> str:+ try:+ buf = ctypes.create_unicode_buffer(96)+ _user32.GetClassNameW(hwnd, buf, 96)+ return buf.value or ""+ except Exception:+ return ""++ def _on_mouse(code, wparam, lparam):+ try:+ if code >= 0 and wparam in (WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN):+ pt = ctypes.wintypes.POINT()+ _user32.GetCursorPos(ctypes.byref(pt))+ under = _user32.WindowFromPoint(pt)+ root = _user32.GetAncestor(under, 2) if under else None # GA_ROOT+ cls = _class_of(root) if root else ""+ if cls in SHELL_CLASSES:+ _note_user_intent(f"click on the shell ({cls})")+ elif root and is_kicad_window(root):+ _note_user_intent("click on a KiCad window")+ except Exception:+ pass+ return _user32.CallNextHookEx(None, code, wparam, lparam)++ def _on_key(code, wparam, lparam):+ try:+ if code >= 0 and wparam in (WM_KEYDOWN, WM_SYSKEYDOWN):+ vk = ctypes.cast(lparam, ctypes.POINTER(ctypes.wintypes.DWORD))[0]+ if vk == VK_TAB and (_user32.GetAsyncKeyState(VK_MENU) & 0x8000):+ _note_user_intent("alt-tab")+ except Exception:+ pass+ return _user32.CallNextHookEx(None, code, wparam, lparam)++ def _pump():+ try:+ mp, kp = HOOKPROC(_on_mouse), HOOKPROC(_on_key)+ _hook_refs.extend([mp, kp])+ _user32.SetWindowsHookExW.restype = ctypes.wintypes.HANDLE+ hm = _user32.SetWindowsHookExW(WH_MOUSE_LL, mp, None, 0)+ hk = _user32.SetWindowsHookExW(WH_KEYBOARD_LL, kp, None, 0)+ print(f"[intent] hooks armed mouse={bool(hm)} keyboard={bool(hk)}",+ file=sys.stderr, flush=True)+ if not (hm or hk):+ 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 as e:+ print(f"[intent] hook thread died: {e}", file=sys.stderr, flush=True)++ try:+ threading.Thread(target=_pump, daemon=True, name="kicad-user-intent").start()+ except Exception:+ pass++ def _push_kicad_to_background(h) -> None: """THE ENTIRE BACKGROUNDING ALGORITHM (0.9.181). John's rules, verbatim: @@ -301,6 +430,12 @@ def _push_kicad_to_background(h) -> None: else: _backgrounded_once.add(h) + if user_wants_foreground():+ # The human just clicked or alt-tabbed. Their intent outranks any+ # background policy - do nothing at all.+ print(f"[bg] {h}: user is driving; leaving it exactly where it is",+ file=sys.stderr, flush=True)+ return HWND_BOTTOM = 1 SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE = 0x0010, 0x0002, 0x0001 _user32.SetWindowPos(h, HWND_BOTTOM, 0, 0, 0, 0,
plugin_install.py+32−6@@ -155,7 +155,6 @@ has finished setting Python up. Everything is logged to %TEMP%/adom-kicad-plugin-loader.log so a failure is one file away. \"\"\" import os-import sys import tempfile import threading import time@@ -637,10 +636,23 @@ def ensure_plugin_installed_once(all_kicad_versions: list[dict]) -> dict: global _install_result # pylint: disable=global-statement with _install_lock: if _install_result is not None:- cached = dict(_install_result)- cached["triggered"] = False- cached["cached"] = True- return cached+ # WIKI ISSUE #34 (Colby Knox): a FAILED result must never be cached+ # for the life of the bridge. On his box the auto-install's detect+ # returned zero installs on a machine with KiCad 10 plainly+ # installed - the explicit kicad_install_plugin found it moments+ # later - and because the false negative was cached, the reverse+ # bridge never came up again until a bridge restart, silently+ # degrading every plugin-dependent verb. A negative is now retried,+ # and re-detected from scratch rather than trusting the list we were+ # handed at boot (which is empty if detection had not finished yet).+ failed = (_install_result.get("errorCode") or+ _install_result.get("summary") in ("no-kicad", "failed"))+ if not failed:+ cached = dict(_install_result)+ cached["triggered"] = False+ cached["cached"] = True+ return cached+ _install_result = None # retry below, with a fresh detection # Honor disable env var. Useful for development/diagnostic. if os.environ.get("ADOM_KICAD_PLUGIN_AUTO_INSTALL_DISABLE") == "1": _install_result = {@@ -654,7 +666,21 @@ def ensure_plugin_installed_once(all_kicad_versions: list[dict]) -> dict: ), } return dict(_install_result)- _install_result = _do_install(all_kicad_versions)+ # Re-detect if we were handed nothing: the boot list can legitimately be+ # empty when detection has not completed, and trusting it is precisely+ # how a "no KiCad installed" verdict got minted on a box with KiCad 10.+ versions = all_kicad_versions+ if not versions:+ try:+ from kicad_detect import detect_all_kicad_versions+ versions = detect_all_kicad_versions() or []+ if versions:+ print(f"[plugin] auto-install was handed an EMPTY version list; "+ f"re-detected {len(versions)} install(s) (issue #34)",+ file=sys.stderr, flush=True)+ except Exception:+ versions = all_kicad_versions+ _install_result = _do_install(versions) return _install_result
server.py+3@@ -1158,6 +1158,9 @@ def main(): # transition (kicad_state.focusEvents + stderr) so quietness is provable. try: from handlers import win_focus+ win_focus.start_user_intent_hooks() # 0.9.182: BEFORE anything else -+ # the user's click must be detectable from the first moment the bridge+ # is alive, not only once a verb happens to run. win_focus.start_focus_sentinel() win_focus.start_focus_event_hook() except Exception: