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.106: chords resolve via the menu-bar accelerator table -> WM_COMMAND (background); Alt+3 keystroke fallbacks deleted
8 files changed
+85−65
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.105+0.9.106
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.105",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.105/adom-bridge-kicad-v0.9.105.zip",- "sha256": "20642cf4c35bc8dacb55568f86c0de5fec951cc0d212ddc29b804180b8174a8c",- "size": 396469,+ "version": "0.9.106",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.106/adom-bridge-kicad-v0.9.106.zip",+ "sha256": "c0a6ab963c2544b796b32184f211d8decddb7786df984994ca91f92362253d98",+ "size": 396973, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-15T10:09:52Z",+ "released_at": "2026-08-15T10:18:25Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.106.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.105",+ "version": "0.9.106", "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/kicad_ui.py+20−1@@ -581,9 +581,28 @@ def handle_send_key(kicad_info: dict, args: dict) -> dict: # theft, works while the window is buried. Chords still need the focus path # (posted messages don't set global modifier state), so those are gated. combo_probe = _parse_modifier_combo(key)+ if combo_probe is not None:+ # v0.9.106 (John): a CHORD usually IS a menu accelerator — resolve it from+ # the window's Win32 menu bar and fire PostMessage(WM_COMMAND). Fully+ # background: no foreground, no real keystrokes, no fragility. Only when+ # no menu item carries the accelerator do we fall to the foreground path.+ try:+ from handlers import win_menu+ cid, label = win_menu.find_by_accelerator(target_hwnd, key)+ if cid:+ win_menu.invoke_menu_command(target_hwnd, cid)+ return {"success": True,+ "output": f"Chord '{key}' delivered as menu command '{label}' (background)",+ "data": {"method": "menu-command", "cmdId": cid, "menuItem": label,+ "hwnd": target_hwnd, "title": title, "foreground": False},+ "_hint": ("Resolved via the window's menu-bar accelerator table and fired "+ "with WM_COMMAND — no focus was touched. This is the preferred "+ "path for every chord that maps to a menu item.")}+ except Exception:+ pass if not allow_fg: if combo_probe is not None:- return _foreground_warning(f"send the key chord '{key}' to KiCad")+ return _foreground_warning(f"send the key chord '{key}' to KiCad — no menu item carries this accelerator, so only real (focused) keystrokes can deliver it") vk = VK_MAP.get(key) ch = key if (vk is None and len(key) == 1) else None if vk is None and ch:
handlers/open_footprint_editor.py+7−56@@ -944,35 +944,8 @@ def handle_open_3d_viewer(kicad_info: dict, args: dict) -> dict: except Exception as e: # pylint: disable=broad-except fp_pathways.append(f"uia-error:{type(e).__name__}") - # Opt-in disruptive fallback: Alt+3 keystroke (needs the FP window foregrounded,- # so it steals focus) — only when the caller explicitly allows it.- if args.get("allowFocusSteal"):- try:- from handlers import win_focus as _wf- _wf.announce_foreground("open the 3D viewer via Alt+3")- except Exception:- pass- VK_MENU_LOCAL = 0x12- VK_3 = 0x33- user32.SetForegroundWindow(fp_hwnd)- time.sleep(0.2)- try:- user32.keybd_event(VK_MENU_LOCAL, 0, 0, 0)- user32.keybd_event(VK_3, 0, 0, 0)- user32.keybd_event(VK_3, 0, KEYEVENTF_KEYUP, 0)- finally:- user32.keybd_event(VK_MENU_LOCAL, 0, KEYEVENTF_KEYUP, 0)- fp_pathways.append("alt3_focussteal")- time.sleep(2.5)- windows = _find_windows_by_title("3D Viewer")- if windows:- return {- "success": True,- "output": f"3D Viewer opened from Footprint Editor: {windows[0][1]}",- "pathway": "fp_alt3",- "pathwaysTried": fp_pathways,- }-+ # (v0.9.106: the Alt+3 keystroke fallback is DELETED — the Win32 menu+ # Tier 0 above delivers the same command by PostMessage, background.) # FP path failed (plugin frame absent AND no focus-steal fallback). # If caller asked for 'auto' AND a PCB editor is also open, fall through # to PCB attempt rather than dead-ending here.@@ -1060,33 +1033,11 @@ def handle_open_3d_viewer(kicad_info: dict, args: dict) -> dict: # keystroke lands there (a global Alt+3 to the user's own window could fire # their app's Alt+3). That steals focus, so it's OPT-IN only # ({"allowFocusSteal": true}); by default we fail to the background path.- if args.get("allowFocusSteal"):- try:- from handlers import win_focus as _wf- _wf.announce_foreground("open the board 3D viewer via Alt+3")- except Exception:- pass- user32.SetForegroundWindow(pcb_hwnd)- time.sleep(0.3)- VK_MENU = 0x12 # Alt key- VK_3 = 0x33 # '3' key- KEYEVENTF_KEYUP = 0x0002- user32.keybd_event(VK_MENU, 0, 0, 0) # Alt down- user32.keybd_event(VK_3, 0, 0, 0) # 3 down- user32.keybd_event(VK_3, 0, KEYEVENTF_KEYUP, 0) # 3 up- user32.keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0) # Alt up- pathways_tried.append("alt_keystroke")- time.sleep(3.0)- windows = _find_windows_by_title("3D Viewer")- if windows:- return {- "success": True,- "output": f"3D Viewer opened from PCB Editor: {windows[0][1]}",- "pathway": "alt_keystroke",- "pathwaysTried": pathways_tried,- }- else:- pathways_tried.append("alt_keystroke_skipped_background_mode")+ # (v0.9.106: pcb Alt+3 keystroke fallback DELETED — win32-menu Tier 0+ # delivers 3D Viewer via WM_COMMAND, background.)+ if False:+ pass+ return { "success": False,
handlers/win_menu.py+50@@ -79,3 +79,53 @@ def open_via_menu(hwnd: int, *contains: str): return {"ok": False, "reason": info} ok = invoke_menu_command(hwnd, cid) return {"ok": ok, "cmdId": cid, "menuText": info}+++def _norm_accel(text: str) -> str:+ """Normalize an accelerator for comparison: 'Ctrl+Shift+S' -> 'ctrl+shift+s'."""+ return "".join((text or "").lower().split())+++def find_by_accelerator(hwnd: int, chord: str, max_depth: int = 4):+ """Resolve a keyboard CHORD (e.g. 'ctrl+s', 'alt+3') to the menu command that+ carries it as an accelerator — wx menu items render as 'Save\tCtrl+S', so the+ menu bar doubles as a chord->command table. Returns (cmd_id, menu_text) or+ (None, reason). This lets send_key deliver chords via PostMessage(WM_COMMAND)+ — fully background — instead of real keystrokes that need the foreground+ (John 2026-08-15: use menu navigation instead of send_key)."""+ if not IS_WINDOWS or not hwnd:+ return None, "no-win32"+ user32.GetMenu.restype = ctypes.c_void_p+ user32.GetSubMenu.restype = ctypes.c_void_p+ hmenu = user32.GetMenu(ctypes.c_void_p(hwnd))+ if not hmenu:+ return None, "no-menu"+ want = _norm_accel(chord)++ def _text(menu, i):+ buf = ctypes.create_unicode_buffer(256)+ user32.GetMenuStringW(ctypes.c_void_p(menu), i, buf, 256, MF_BYPOSITION)+ return buf.value or ""++ def _walk(menu, depth):+ n = user32.GetMenuItemCount(ctypes.c_void_p(menu))+ for i in range(max(0, n)):+ raw = _text(menu, i)+ sub = user32.GetSubMenu(ctypes.c_void_p(menu), i)+ if sub:+ if depth < max_depth:+ hit = _walk(sub, depth + 1)+ if hit is not None:+ return hit+ elif "\t" in raw:+ label, accel = raw.split("\t", 1)+ if _norm_accel(accel) == want:+ cid = user32.GetMenuItemID(ctypes.c_void_p(menu), i)+ if cid and cid != -1:+ return cid, label.replace("&", "")+ return None++ hit = _walk(hmenu, 0)+ if hit is None:+ return None, "no-accelerator-match"+ return hit
server.py+1−1@@ -310,7 +310,7 @@ _VERB_CATALOG = { "screenshot_all": {"summary": "Screenshot every open KiCad window.", "long": False, "hint": "Captures all KiCad windows; the go-to way to VERIFY what actually happened after an open/edit.", "related": ["kicad_open_editors", "kicad_window_info", "kicad_open_board"], "pitfalls": ["a window still rendering (e.g. 3D viewer) shows blank — wait a beat first"]},- "send_key": {"summary": "Send a keystroke to an EXACT KiCad window by hwnd (SendMessage; not for chords).", "long": False,+ "send_key": {"summary": "Send a key to KiCad — chords auto-resolve to menu commands (background)", "long": False, "hint": "For modifier CHORDS ('ctrl+s', 'alt+3', 'ctrl+shift+p') use Bridge's desktop_press_key — it owns full chord parsing; don't route chords here. Use this verb only for the case Bridge's focus-first model can't cover: a single key delivered to an EXACT hwnd (e.g. Enter/Escape to a specific dialog from window_info).", "related": ["kicad_window_info", "kicad_click", "kicad_fix_keyboard"], "pitfalls": ["not for chords — use desktop_press_key", "needs a target hwnd (from window_info)"]}, "click": {"summary": "Click at coordinates inside a KiCad window.", "long": False,