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.113: always-on focus sentinel (John saw late self-raises land in the foreground after per-verb guards expired) - bounces any KiCad fg transition not preceded by user input, honors deliberate user clicks via GetLastInputInfo, logs every transition to kicad_state.focusEvents + stderr
7 files changed
+177−51
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.112+0.9.113
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.112",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.112/adom-bridge-kicad-v0.9.112.zip",- "sha256": "a50bc546ef5b47f236626f0dee19126450adf2189e5b89064729f920d523e182",- "size": 407166,+ "version": "0.9.113",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.113/adom-bridge-kicad-v0.9.113.zip",+ "sha256": "4afd53258aa6a175bd6b2a585473bb88c7e4ca111e1708a376aca3127d6af45e",+ "size": 408755, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-16T12:24:34Z",+ "released_at": "2026-08-16T13:20:09Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.113.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.112",+ "version": "0.9.113", "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/state.py+10−1@@ -82,9 +82,17 @@ def handle_state(kicad_info: dict, args: dict) -> dict: except Exception: pass + focus_events = []+ try:+ from handlers import win_focus+ focus_events = win_focus.get_focus_events()[-40:]+ except Exception:+ pass+ return { "success": True, "running": running,+ "focusEvents": focus_events, "activeVersion": kicad_info.get("version"), "windows": [{k: w.get(k) for k in ("hwnd", "title", "className", "rect")} for w in windows],@@ -95,7 +103,8 @@ def handle_state(kicad_info: dict, args: dict) -> dict: "summary": editors.get("summary"), }, "modalDialogs": modal,- "_hint": ("The full state of the union: every KiCad-owned window is in shots[] as inline "+ "_hint": ("focusEvents is the focus sentinel log: a self-raise (bounced) entry is a PREVENTED foreground steal; user-foregrounded means the human chose it. "+ "The full state of the union: every KiCad-owned window is in shots[] as inline " "base64 PNG (render them all — one window is NOT the app). modalDialogs non-empty " "means something is blocking; editors.list is the plugin's logical inventory " "(null while the editor's Python is still binding, issue #21). Poll before/after "
handlers/win_focus.py+149−43@@ -218,6 +218,152 @@ def _demote_below(kicad_hwnd, user_hwnd) -> None: pass +def _sweep_suppress_all() -> None:+ """Pre-suppress EVERY visible KiCad top-level window — so a window's FIRST+ self-activation is refused too, not just its repeat offenses. (v0.9.102: the+ 0.9.101 proof run still showed exactly one visible steal per fresh window —+ the free first offense. This closes it.)"""+ try:+ import ctypes as _ct+ WNDENUMPROC = _ct.WINFUNCTYPE(_ct.c_bool, _ct.wintypes.HWND, _ct.wintypes.LPARAM)+ def cb(h, _l):+ try:+ if _user32.IsWindowVisible(h) and is_kicad_window(h):+ suppress_activation(h)+ except Exception:+ pass+ return True+ cbp = WNDENUMPROC(cb)+ _user32.EnumWindows(cbp, 0)+ except Exception:+ pass+++def _push_kicad_to_background(h) -> None:+ """v0.9.104 (John, verbatim design): "you just always have to only deal with+ the windows you opened from kicad" — every action here targets the KICAD+ window ONLY. If it holds the foreground, MINIMIZE-BOUNCE it: minimizing the+ foreground window makes WINDOWS ITSELF activate the user's previous window+ (we never locate, choose, or raise any user window), then show it again+ unfocused, and drop it to the bottom of the z-order."""+ import time as _t+ try:+ suppress_activation(h)+ if _user32.GetForegroundWindow() == h:+ _user32.ShowWindow(h, 6) # SW_MINIMIZE -> OS returns focus naturally+ _t.sleep(0.05)+ _user32.ShowWindow(h, 4) # SW_SHOWNOACTIVATE -> visible again, unfocused+ HWND_BOTTOM = 1+ SWP = 0x0010 | 0x0002 | 0x0001 # NOACTIVATE|NOMOVE|NOSIZE+ _user32.SetWindowPos(h, HWND_BOTTOM, 0, 0, 0, 0, SWP)+ except Exception:+ pass+++def _seconds_since_user_input() -> float:+ """Seconds since the user's last keyboard/mouse input, system-wide."""+ try:+ import ctypes as _ct++ class LASTINPUTINFO(_ct.Structure):+ _fields_ = [("cbSize", _ct.wintypes.UINT), ("dwTime", _ct.wintypes.DWORD)]++ lii = LASTINPUTINFO()+ lii.cbSize = _ct.sizeof(LASTINPUTINFO)+ if _user32.GetLastInputInfo(_ct.byref(lii)):+ return max(0.0, (_kernel32.GetTickCount() - lii.dwTime) / 1000.0)+ except Exception:+ pass+ return 1e9+++# ---------------------------------------------------------------------------+# The ALWAYS-ON focus sentinel (0.9.113, John 2026-08-16: "i just saw some of+# your windows auto-foreground"). The per-verb guardians below PREVENT steals+# while a verb is in flight, but they expire (and deliberately clear the+# NOACTIVATE styles so the user can click into KiCad). KiCad windows self-raise+# LATE — a 3D render or a footprint-library load finishing minutes after the+# verb — and until now nothing watched that window. The sentinel does, forever:+#+# - fg transition to a KiCad window + NO recent user input => self-raise:+# minimize-bounce it back down (and log it).+# - fg transition to a KiCad window right after user input => the USER chose+# it (John: "always let the user choose to fg a window") — leave it alone.+# - while a guardian is active it only logs; the guardian owns the response.+#+# Every transition lands in a ring buffer (kicad_state.focusEvents) and on+# stderr (=> ~/.adom/bridge-logs/kicad.log), so background-contract compliance+# is PROVABLE from the log, not sampled by callers.+# ---------------------------------------------------------------------------+_USER_INTENT_WINDOW_S = 1.2+_sentinel_started = False+_focus_events = None # collections.deque, lazy+++def get_focus_events() -> list:+ """The sentinel's transition log, oldest first."""+ return list(_focus_events) if _focus_events is not None else []+++def start_focus_sentinel() -> None:+ """Start the process-lifetime sentinel thread (idempotent; called at boot)."""+ global _sentinel_started, _focus_events+ if not IS_WINDOWS or _sentinel_started:+ return+ _sentinel_started = True+ import collections+ import threading+ import time as _t+ _focus_events = collections.deque(maxlen=200)++ def _log(action, hwnd, ago):+ try:+ buf = ctypes.create_unicode_buffer(80)+ _user32.GetWindowTextW(hwnd, buf, 80)+ evt = {"t": _t.time(), "action": action, "title": buf.value,+ "secondsSinceUserInput": round(min(ago, 9999.0), 2)}+ _focus_events.append(evt)+ print(f"[focus-sentinel] {action}: '{buf.value}' "+ f"(user input {evt['secondsSinceUserInput']}s ago)", file=sys.stderr)+ except Exception:+ pass++ def _loop():+ last_fg = None+ blessed = set() # KiCad hwnds the user deliberately foregrounded+ while True:+ try:+ fg = _user32.GetForegroundWindow()+ if fg != last_fg:+ if fg and is_kicad_window(fg):+ ago = _seconds_since_user_input()+ if ago <= _USER_INTENT_WINDOW_S:+ blessed.add(fg)+ _log("user-foregrounded (left alone)", fg, ago)+ elif _guard_count > 0:+ _log("steal-during-guard (guardian handles)", fg, ago)+ else:+ _log("self-raise (bounced to background)", fg, ago)+ _push_kicad_to_background(fg)+ # no guard is active: immediately clear the NOACTIVATE+ # bit the bounce set, so a real user click still works+ allow_activation(fg)+ else:+ blessed.discard(last_fg)+ last_fg = fg+ _t.sleep(0.12)+ except Exception:+ try:+ _t.sleep(1.0)+ except Exception:+ pass++ try:+ threading.Thread(target=_loop, daemon=True, name="kicad-focus-sentinel").start()+ except Exception:+ pass++ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float = 0.1) -> None: """Focus guardian: for `seconds` after a window-spawning verb, make sure NO KiCad window holds the user's foreground. KiCad activates windows@@ -239,46 +385,6 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float = import threading import time as _t - def _sweep_suppress():- """Pre-suppress EVERY visible KiCad top-level window — so a window's- FIRST self-activation is refused too, not just its repeat offenses.- (v0.9.102: the 0.9.101 proof run still showed exactly one visible steal- per fresh window — the free first offense. This closes it.)"""- try:- import ctypes as _ct- WNDENUMPROC = _ct.WINFUNCTYPE(_ct.c_bool, _ct.wintypes.HWND, _ct.wintypes.LPARAM)- def cb(h, _l):- try:- if _user32.IsWindowVisible(h) and is_kicad_window(h):- suppress_activation(h)- except Exception:- pass- return True- cbp = WNDENUMPROC(cb)- _user32.EnumWindows(cbp, 0)- except Exception:- pass-- def _push_to_background(h):- """v0.9.104 (John, verbatim design): "you just always have to only deal- with the windows you opened from kicad" — every action here targets the- KICAD window ONLY. If it holds the foreground, MINIMIZE-BOUNCE it:- minimizing the foreground window makes WINDOWS ITSELF activate the- user's previous window (we never locate, choose, or raise any user- window), then show it again unfocused, and drop it to the bottom of the- z-order. Combined with NOACTIVATE it stays down."""- try:- suppress_activation(h)- if _user32.GetForegroundWindow() == h:- _user32.ShowWindow(h, 6) # SW_MINIMIZE -> OS returns focus naturally- _t.sleep(0.05)- _user32.ShowWindow(h, 4) # SW_SHOWNOACTIVATE -> visible again, unfocused- HWND_BOTTOM = 1- SWP = 0x0010 | 0x0002 | 0x0001 # NOACTIVATE|NOMOVE|NOSIZE- _user32.SetWindowPos(h, HWND_BOTTOM, 0, 0, 0, 0, SWP)- except Exception:- pass- def _loop(): global _guard_count if _guard_lock:@@ -287,15 +393,15 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float = deadline = _t.monotonic() + seconds tick = 0 try:- _sweep_suppress() # pre-suppress what's already up before the verb acts+ _sweep_suppress_all() # pre-suppress what's already up before the verb acts while _t.monotonic() < deadline: try: tick += 1 if tick % 5 == 0:- _sweep_suppress() # catch windows CREATED mid-verb (~every 0.5s)+ _sweep_suppress_all() # catch windows CREATED mid-verb (~every 0.5s) fg = _user32.GetForegroundWindow() if fg and is_kicad_window(fg):- _push_to_background(fg)+ _push_kicad_to_background(fg) # NON-KiCad foreground = the user's business. We never track, # never adopt, never raise it. (v0.9.103's conditional restore # was still user-window surgery — removed entirely.)
server.py+11@@ -1001,6 +1001,17 @@ def main(): except Exception: pass + # 0.9.113: the always-on focus sentinel — after per-verb guards expire,+ # late self-raising KiCad windows (3D render / library-load completion)+ # used to land in the user's foreground unopposed. The sentinel bounces+ # any KiCad fg transition that no user input preceded, and logs every+ # transition (kicad_state.focusEvents + stderr) so quietness is provable.+ try:+ from handlers import win_focus+ win_focus.start_focus_sentinel()+ except Exception:+ pass+ port = DEFAULT_PORT if "--port" in sys.argv: idx = sys.argv.index("--port")