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.160: THE taskbar-icon root cause - WS_EX_NOACTIVATE removes a window from the taskbar by design, so backgrounded windows vanished from it; now paired with WS_EX_APPWINDOW. Plus guard/park watchdogs (a leaked guard stranded a window parked for 5h), non-blocking icon probes, and a reachability check on unpark
5 files changed
+107−15
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.159+0.9.160
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.159",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.159/adom-bridge-kicad-v0.9.159.zip",- "sha256": "a5a88aa11d4f80ec9ddbe0f3e44ed6d84e19302b8948f18e22c3dfb226407b26",- "size": 439770,+ "version": "0.9.160",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.160/adom-bridge-kicad-v0.9.160.zip",+ "sha256": "0ac61822de115957eb211d363e120502d4e94be6539133a190e22629ac68da56",+ "size": 441328, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-18T10:24:38Z",+ "released_at": "2026-08-18T10:33:41Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.160.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.159",+ "version": "0.9.160", "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+100−8@@ -152,8 +152,18 @@ def is_kicad_window(hwnd) -> bool: # 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+WS_EX_APPWINDOW = 0x00040000 GWL_EXSTYLE = -20 +# 0.9.160 (John: "why do i see no windows taskbar icons after you claim you're+# launched"): WS_EX_NOACTIVATE is DOCUMENTED to remove a window from the taskbar+# ("A window that has this style does not appear on the taskbar by default. To+# force the window to appear on the taskbar, use WS_EX_APPWINDOW"). Every+# suppression therefore pairs NOACTIVATE with APPWINDOW, so a backgrounded KiCad+# window keeps a normal, clickable taskbar button - background must mean BEHIND,+# never INVISIBLE. APPWINDOW is left in place when NOACTIVATE clears: it only+# guarantees the button, and re-toggling styles makes the shell rebuild it.+ _noactivated: set = set() _guard_count = 0 _guard_lock = None@@ -175,11 +185,13 @@ def _set_exstyle(hwnd, style): def suppress_activation(hwnd) -> None:- """Make a KiCad window unable to grab the foreground (WS_EX_NOACTIVATE)."""+ """Make a KiCad window unable to grab the foreground (WS_EX_NOACTIVATE),+ while KEEPING its taskbar button (WS_EX_APPWINDOW - see the note above).""" try: cur = _get_exstyle(hwnd)- if not (cur & WS_EX_NOACTIVATE):- _set_exstyle(hwnd, cur | WS_EX_NOACTIVATE)+ want = cur | WS_EX_NOACTIVATE | WS_EX_APPWINDOW+ if want != cur:+ _set_exstyle(hwnd, want) _noactivated.add(hwnd) except Exception: pass@@ -382,6 +394,10 @@ _park_sweep_started = False # no spawned pid tree -> _is_owned_window() is False -> every focus/park/badge # path leaves it completely alone. There is deliberately NO "adopt what exists" # seeding: unknown windows default to user-owned.+_PARK_MAX_S = 12.0 # a window may never stay parked longer than this+_GUARD_GRACE_S = 30.0 # how far past its deadline a guard may run before reset+_park_since: dict = {} # hwnd -> monotonic when it was first parked+_guard_deadline = 0.0 # latest armed guard's deadline (monotonic) _owned_pids: set = set() _OWNED_EXE_PREFIXES = ("kicad", "eeschema", "pcbnew", "gerbview", "pl_editor", "pcb_calculator", "bitmap2component")@@ -706,6 +722,26 @@ def _start_park_sweep() -> None: quiet_since = None while True: try:+ # HARD CAPS (0.9.160). A leaked guard used to keep the sweep+ # active forever, so a launched window stayed parked (and+ # NOACTIVATE'd, hence taskbar-less) for HOURS - John found one+ # 5h in. Two watchdogs make that impossible: a guard that+ # overran its own deadline is reset, and a window parked past+ # _PARK_MAX_S is released no matter what the bridge is doing.+ now_m = _t.monotonic()+ if _guard_count > 0 and _guard_deadline and now_m > _guard_deadline + _GUARD_GRACE_S:+ print(f"[focus] guard watchdog: {_guard_count} guard(s) overran "+ f"by {int(now_m - _guard_deadline)}s - resetting", file=sys.stderr)+ globals()["_guard_count"] = 0+ unpark_all()+ allow_activation_all()+ for h, since in list(_park_since.items()):+ if now_m - since > _PARK_MAX_S:+ print(f"[focus] park watchdog: releasing {h} after "+ f"{int(now_m - since)}s parked", file=sys.stderr)+ _unpark(h)+ allow_activation(h)+ _park_since.pop(h, None) active = _verb_inflight > 0 or _guard_count > 0 if active: globals()["_sweep_ticks"] = _sweep_ticks + 1@@ -722,6 +758,7 @@ def _start_park_sweep() -> None: _save_ledger() suppress_activation(h) _park_offscreen(h)+ _park_since.setdefault(h, _t.monotonic()) try: from handlers.win_taskbar import apply_adom_badge apply_adom_badge(h) # "adom is driving this window"@@ -826,8 +863,16 @@ def _ensure_window_icon(hwnd) -> None: WM_GETICON, WM_SETICON = 0x007F, 0x0080 GCLP_HICON, GCLP_HICONSM = -14, -34 get_cl = getattr(_user32, "GetClassLongPtrW", None) or _user32.GetClassLongW- has = (_user32.SendMessageW(hwnd, WM_GETICON, 1, 0)- or _user32.SendMessageW(hwnd, WM_GETICON, 0, 0)+ # SendMessageW BLOCKS until the target pumps messages - a loading KiCad+ # would freeze this thread (and with it the park sweep / focus guard),+ # which is exactly how a window got stranded parked for hours+ # (0.9.160). Always use the timeout form with SMTO_ABORTIFHUNG.+ def _get_icon(which):+ res = ctypes.c_ulong(0)+ _user32.SendMessageTimeoutW(hwnd, WM_GETICON, which, 0, 0x0002, 200,+ ctypes.byref(res))+ return res.value+ has = (_get_icon(1) or _get_icon(0) or get_cl(hwnd, GCLP_HICON) or get_cl(hwnd, GCLP_HICONSM)) if has: return@@ -843,10 +888,13 @@ def _ensure_window_icon(hwnd) -> None: icons = (big, small) _exe_icons[exe] = icons # keep HICONs alive for process lifetime big, small = icons+ res = ctypes.c_ulong(0) if big and big.value:- _user32.SendMessageW(hwnd, WM_SETICON, 1, big.value)+ _user32.SendMessageTimeoutW(hwnd, WM_SETICON, 1, big.value, 0x0002, 200,+ ctypes.byref(res)) if small and small.value:- _user32.SendMessageW(hwnd, WM_SETICON, 0, small.value)+ _user32.SendMessageTimeoutW(hwnd, WM_SETICON, 0, small.value, 0x0002, 200,+ ctypes.byref(res)) except Exception: pass @@ -904,6 +952,32 @@ def _park_offscreen(hwnd) -> None: _park_last_error = f"{type(e).__name__}: {e}" +def _sane_position(hwnd) -> None:+ """0.9.160: guarantee an unparked window lands somewhere the user can reach.+ A window parked before KiCad finished placing itself unparks to a garbage+ 'true position' - John got a 406x354 sliver at y=993, mostly under the+ taskbar. Anything off-screen or peeking in by less than 200px is moved to a+ plain cascade slot, size preserved, without activation."""+ try:+ r = ctypes.wintypes.RECT()+ if not _user32.GetWindowRect(hwnd, ctypes.byref(r)):+ return+ sw = _user32.GetSystemMetrics(0)+ sh = _user32.GetSystemMetrics(1)+ w, h = max(r.right - r.left, 640), max(r.bottom - r.top, 480)+ visible_w = min(r.right, sw) - max(r.left, 0)+ visible_h = min(r.bottom, sh) - max(r.top, 0)+ if visible_w >= 200 and visible_h >= 200:+ return # genuinely reachable already+ x, y = 120, 90+ SWP_NOSIZE, SWP_NOACTIVATE, SWP_NOZORDER = 0x0001, 0x0010, 0x0004+ _user32.SetWindowPos(hwnd, 0, x, y, w, h, SWP_NOACTIVATE | SWP_NOZORDER)+ print(f"[focus] repositioned {hwnd} to ({x},{y}) - it unparked "+ f"{visible_w}x{visible_h} on-screen", file=sys.stderr)+ except Exception:+ pass++ def _unpark(hwnd, to_bottom: bool = True) -> None: try: pos = _parked.pop(hwnd, None)@@ -936,10 +1010,25 @@ def _unpark(hwnd, to_bottom: bool = True) -> None: HWND_BOTTOM = 1 _user32.SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, 0x0010 | 0x0001 | 0x0002) # NOACTIVATE|NOSIZE|NOMOVE+ _sane_position(hwnd) # never leave it stranded off-screen+ _park_since.pop(hwnd, None)+ _ensure_window_icon(hwnd) # a fresh button must not read as a blank doc+ try:+ from handlers.win_taskbar import apply_adom_badge+ apply_adom_badge(hwnd)+ except Exception:+ pass except Exception: pass +def _clear_park_clock(hwnd=None) -> None:+ if hwnd is None:+ _park_since.clear()+ else:+ _park_since.pop(hwnd, None)++ def unpark_all() -> int: """Place every parked window back at its true coordinates, bottom-z, no activation. Called when the last guard exits."""@@ -947,6 +1036,7 @@ def unpark_all() -> int: for h in list(_parked.keys()): _unpark(h, to_bottom=True) n += 1+ _clear_park_clock() return n @@ -1186,10 +1276,12 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float = def _loop(): global _guard_count+ deadline = _t.monotonic() + seconds if _guard_lock: with _guard_lock: _guard_count += 1- deadline = _t.monotonic() + seconds+ if deadline > _guard_deadline:+ globals()["_guard_deadline"] = deadline tick = 0 try: _sweep_suppress_all() # pre-suppress what's already up before the verb acts