app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
0.9.348: Focus guardian identity survives hwnd reuse; editors spawn without activation (pre-promotion run findings)
9c005c0
2mo ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696
"""win_focus.py — keep KiCad windows in the BACKGROUND so the bridge never
steals the user's focus / interrupts their work.
The rule (enforced centrally by the dispatcher): capture whatever window the USER
has in the foreground BEFORE a window-spawning/interacting verb runs, then RESTORE
it AFTER. KiCad still opens and renders (visible behind), but the user's active
window stays active — their typing, their focus, undisturbed.
Why a helper: a plain SetForegroundWindow from a background process is blocked by
Windows' foreground-lock unless the caller owns the foreground. We use the
documented AttachThreadInput technique (attach our thread to both the current-fg
and target threads) so the restore reliably works cross-process.
"""
import ctypes
import ctypes.wintypes
import json
import os
import sys
import time
IS_WINDOWS = sys.platform == "win32"
if IS_WINDOWS:
import ctypes
import ctypes.wintypes
_user32 = ctypes.windll.user32
_kernel32 = ctypes.windll.kernel32
SW_SHOWNOACTIVATE = 4 # show a window WITHOUT activating it (no focus theft)
def capture_foreground():
"""Return the user's current foreground window handle (or None)."""
if not IS_WINDOWS:
return None
try:
hwnd = _user32.GetForegroundWindow()
return hwnd or None
except Exception:
return None
def _set_foreground_robust(hwnd) -> bool:
"""SetForegroundWindow that actually works from a background process, via the
AttachThreadInput trick. Returns True on best-effort success."""
try:
fg = _user32.GetForegroundWindow()
if fg == hwnd:
return True
cur = _kernel32.GetCurrentThreadId()
tid_target = _user32.GetWindowThreadProcessId(hwnd, None)
tid_fg = _user32.GetWindowThreadProcessId(fg, None) if fg else 0
attached = []
for tid in (tid_fg, tid_target):
if tid and tid != cur and _user32.AttachThreadInput(cur, tid, True):
attached.append(tid)
_user32.BringWindowToTop(hwnd)
_user32.SetForegroundWindow(hwnd)
for tid in attached:
_user32.AttachThreadInput(cur, tid, False)
return True
except Exception:
return False
def restore_foreground(hwnd) -> bool:
"""Put the user's window (captured earlier) back in front — but ONLY if a
KiCad window is currently holding the foreground.
v0.9.103 (John: "do you keep bringing this browser window to the fg? that's
rude."): the captured hwnd is a SNAPSHOT from verb START. During a long verb
the user may deliberately switch windows; blindly re-raising the snapshot at
verb END yanked them out of their new window and back into the old one (his
browser). Rule — identical to the guardian loop's: if the current foreground
is any NON-KiCad window, the user is where they chose to be; touch NOTHING.
Only when KiCad itself holds the foreground do we hand focus back."""
if not IS_WINDOWS or not hwnd:
return False
try:
if not _user32.IsWindow(hwnd) or not _user32.IsWindowVisible(hwnd):
return False
fg = _user32.GetForegroundWindow()
if fg == hwnd:
return True
if fg and not is_kicad_window(fg):
return False # user moved on — never fight their choice
return _set_foreground_robust(hwnd)
except Exception:
return False
def show_without_activating(hwnd) -> None:
"""Make a window visible but do NOT activate it — the gentlest way to surface a
KiCad window in the background (use instead of ShowWindow(SW_RESTORE)+SetForeground)."""
if not IS_WINDOWS or not hwnd:
return
try:
_user32.ShowWindow(hwnd, SW_SHOWNOACTIVATE)
except Exception:
pass
def background_startupinfo():
"""A subprocess STARTUPINFO that asks a freshly-launched GUI (KiCad) to show its
window WITHOUT activating it (SW_SHOWNOACTIVATE) — so a new pcbnew/eeschema opens
behind the user's work instead of grabbing foreground. Best-effort: an app can
override nCmdShow, which is why the dispatcher ALSO runs a persistent restore."""
if not IS_WINDOWS:
return None
try:
import subprocess
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.wShowWindow = SW_SHOWNOACTIVATE # 4
return si
except Exception:
return None
_KICAD_EXES = {"kicad.exe", "eeschema.exe", "pcbnew.exe", "kicad-cli.exe",
"pl_editor.exe", "bitmap2component.exe", "pcb_calculator.exe"}
def _hwnd_exe_name(hwnd) -> str:
"""Executable basename (lowercase) owning a window, '' on failure."""
try:
import ctypes
import ctypes.wintypes
pid = ctypes.wintypes.DWORD()
_user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
h = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid.value)
if not h:
return ""
try:
buf = ctypes.create_unicode_buffer(512)
size = ctypes.wintypes.DWORD(512)
if ctypes.windll.kernel32.QueryFullProcessImageNameW(h, 0, buf, ctypes.byref(size)):
return buf.value.replace("\\", "/").rsplit("/", 1)[-1].lower()
finally:
ctypes.windll.kernel32.CloseHandle(h)
except Exception:
pass
return ""
def is_kicad_window(hwnd) -> bool:
return _hwnd_exe_name(hwnd) in _KICAD_EXES
# ── PREVENTION, not reaction (v0.9.101, John: "i sure see a lot of windows
# coming to fg"). The polling guardian REACTED: KiCad took focus, we yanked it
# back 250ms later — the user saw every steal as a flash and lost keystrokes.
# WS_EX_NOACTIVATE makes Windows REFUSE the window's self-activation outright:
# no flash, no focus churn. We apply it to a KiCad window the first time it
# misbehaves (or when a guardian arms over it), and CLEAR it when the last
# 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
try:
import threading as _thr
_guard_lock = _thr.Lock()
except Exception:
pass
def _window_responsive(hwnd, ms: int = 150) -> bool:
"""Is this window's thread pumping messages? SetWindowLongPtr SENDS
WM_STYLECHANGING/WM_STYLECHANGED cross-process, so touching the style of a
HUNG window blocks the caller forever (0.9.165: that deadlock, on the boot
path, took the whole bridge down). Probe first, with a timeout."""
if not IS_WINDOWS:
return False
try:
res = ctypes.c_ulong(0)
SMTO_ABORTIFHUNG = 0x0002
WM_NULL = 0x0000
ok = _user32.SendMessageTimeoutW(hwnd, WM_NULL, 0, 0, SMTO_ABORTIFHUNG,
ms, ctypes.byref(res))
return bool(ok)
except Exception:
return False
def _get_exstyle(hwnd):
fn = getattr(_user32, "GetWindowLongPtrW", None) or _user32.GetWindowLongW
return fn(hwnd, GWL_EXSTYLE)
def _set_exstyle(hwnd, style):
fn = getattr(_user32, "SetWindowLongPtrW", None) or _user32.SetWindowLongW
return fn(hwnd, GWL_EXSTYLE, style)
def suppress_activation(hwnd) -> None:
"""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)
want = cur | WS_EX_NOACTIVATE | WS_EX_APPWINDOW
if want != cur:
_set_exstyle(hwnd, want)
_noactivated.add(hwnd)
except Exception:
pass
def allow_activation(hwnd) -> None:
"""Undo suppress_activation for one window (user wants it / bridge done)."""
try:
if _user32.IsWindow(hwnd):
cur = _get_exstyle(hwnd)
if cur & WS_EX_NOACTIVATE:
_set_exstyle(hwnd, cur & ~WS_EX_NOACTIVATE)
except Exception:
pass
_noactivated.discard(hwnd)
def allow_activation_all() -> int:
"""Clear NOACTIVATE from every window we altered. Returns count cleared."""
n = 0
for h in list(_noactivated):
allow_activation(h)
n += 1
return n
def _demote_below(kicad_hwnd, user_hwnd) -> None:
"""Slide a KiCad window directly UNDER the user's window without activating
either — the window stays visible in the taskbar and fully screenshotable,
but never covers what the user is doing."""
try:
SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE = 0x0010, 0x0002, 0x0001
_user32.SetWindowPos(kicad_hwnd, user_hwnd, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)
except Exception:
pass
def _sweep_suppress_all() -> None:
"""REMOVED (0.9.181). This used to stamp WS_EX_NOACTIVATE on every visible
KiCad window so none could self-activate. That style also blocks the USER's
click, which John called out as rude: they may want the window in front the
very next second. Prevention is not worth taking the user's click away."""
return
class _NullLock:
def __enter__(self):
return self
def __exit__(self, *exc):
return False
_backgrounded_once: dict = {} # (hwnd, pid) -> (pushes, title at last push)
_bg_lock = None
try:
import threading as _thr2
_bg_lock = _thr2.Lock()
except Exception:
pass
# ── NO SYSTEM-WIDE HOOKS. EVER. (0.9.184) ───────────────────────────────────
# 0.9.182 installed machine-wide low-level input hooks to detect a taskbar
# click or alt-tab. Those are MACHINE-WIDE input hooks: every mouse event and
# keystroke on the computer passes through this bridge's callback. That is a
# grossly disproportionate thing for an EDA bridge to install, I did it without
# asking, and it degraded John's cursor within minutes.
#
# They are removed, not repaired. The rule, permanently:
# * NEVER install a system-wide hook (SetWindowsHookEx of any kind), a global
# keyboard/mouse listener, or anything else that sits in the OS input path.
# * If a future problem seems to need one, STOP and ask first, with the
# tradeoff spelled out. Do not ship it and see.
#
# The bridge does not need intent detection anyway, because it no longer blocks
# activation: WS_EX_NOACTIVATE is gone, so a click or alt-tab simply works, the
# way it does for every other window on the machine. Nothing to detect.
def user_wants_foreground() -> bool:
"""Always False: the bridge never blocks the user's activation any more, so
there is nothing to detect and nothing to override."""
return False
def start_user_intent_hooks() -> None:
"""Removed - see the note above. Kept as a no-op so no call site breaks."""
return
def _push_kicad_to_background(h) -> None:
"""THE ENTIRE BACKGROUNDING ALGORITHM (0.9.181). John's rules, verbatim:
"never fucking minimize it, cuz then you can't take screenshots of the
window when it's in the background"
"ONLY background the window once IF IT APPEARS IN THE FOREGROUND. then do
nothing else"
"if you are waiting 2 seconds to let the user activate the window that's
rude as fuck. a user sometimes wants to immediately foreground it"
So, for a window that actually took the foreground, exactly one action:
SetWindowPos(hwnd, HWND_BOTTOM, SWP_NOACTIVATE|SWP_NOMOVE|SWP_NOSIZE)
and nothing else, ever. In particular:
- NEVER minimize. A minimized window cannot be screenshotted, which breaks
every screenshot-based verb in this bridge.
- NEVER move it (no off-screen parking - removed entirely, see below).
- NEVER set WS_EX_NOACTIVATE. It blocks the user's own click, and a user
may want the window in front the very next second. Their click must work
immediately, with no hold-off window.
- NEVER act on the same window twice. One z-order change is invisible;
repeating anything is what produced the strobing.
"""
try:
# "Once per window" needs a window identity that survives two things seen
# on ConfRoomROG (2026-09-08, the pre-promotion run): Windows RECYCLES
# hwnd values, so a set of bare hwnds that never expires silently refused
# to push a NEW editor that inherited an old number; and an editor frame
# raises itself twice per load (once as "untitled" while it opens, once
# more when the document title lands), so the single push happened while
# the frame still said "untitled" and the finished editor sat on the
# user's browser. Identity is (hwnd, pid), closed windows expire from the
# table, and the push stays ONE per window. Still no minimize, no move,
# no NOACTIVATE, no strobing.
try:
buf = ctypes.create_unicode_buffer(128)
_user32.GetWindowTextW(h, buf, 128)
title = buf.value
except Exception:
title = ""
key = (int(h), _pid_of_hwnd(h))
with (_bg_lock or _NullLock()):
for k in [k for k in _backgrounded_once if not _user32.IsWindow(k[0])]:
_backgrounded_once.pop(k, None)
seen = _backgrounded_once.get(key)
if seen is not None:
# John's rule stands: ONE push per window, then hands off. (A second
# push after the title landed was tried on ConfRoomROG 2026-09-08 and
# changed nothing: the editor re-raises itself more than twice while
# it loads, so the outcome is Windows' own foreground rule, not ours.)
return
_backgrounded_once[key] = (1, title)
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,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE)
print(f"[bg] {h}: sent behind once (z-order only). Hands off from here.",
file=sys.stderr, flush=True)
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
_expected_fg_until = 0.0 # sanctioned bridge-initiated foreground window
def expect_foreground(seconds: float = 20.0) -> None:
"""The bridge is ABOUT to foreground a KiCad window legitimately (user asked:
foreground:true / allowForeground / bring_to_user — always announced first).
The sentinel must not fight it: exempt bounces for `seconds`."""
global _expected_fg_until
import time as _t
_expected_fg_until = max(_expected_fg_until, _t.monotonic() + seconds)
def get_focus_debug() -> dict:
"""Introspection for the parking machinery (kicad_state.focusDebug)."""
return {
"sweepStarted": _park_sweep_started,
"hookStarted": _hook_started,
"sentinelStarted": _sentinel_started,
"verbInflight": _verb_inflight,
"guardCount": _guard_count,
"parkedCount": len(_parked),
"baselineCount": len(_park_baseline),
"bridgeWindows": len(_bridge_windows),
"parkAttempts": _park_attempts,
"parkLastError": _park_last_error,
"sweepTicks": _sweep_ticks,
"ownedPids": sorted(_owned_pids),
# How hard did we fight KiCad, and where did we concede? (John asked for
# exactly this: "log what happens on how often you blink the window".)
"parkFights": {str(k): v for k, v in list(_park_fights.items())[-20:]},
"parkGaveUp": len(_park_gaveup),
"guardianDemotes": {str(k): v for k, v in list(_demote_counts.items())[-20:]},
"guardianDemoteLimit": _DEMOTE_LIMIT,
"backgroundedOnce": len(_backgrounded_once),
"rule": "each window is backgrounded EXACTLY once, then never touched again",
"parkFightLog": list(_park_fight_log)[-10:],
"parkFightLimit": _PARK_FIGHT_LIMIT,
}
def get_focus_events() -> list:
"""The sentinel's transition log, oldest first."""
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.
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# OFF-SCREEN PARKING (0.9.138, John's design, 2026-08-17 verbatim: "open them
# at xy screen coordinates that are way off in a non-visible part of the xy
# layout of the screen, then background those windows, then place them into
# the correct xy position of my screen in the background"). NOACTIVATE stops
# focus theft, but a freshly CREATED window still PAINTS near the top of the
# z-order — visible over the user's work even unfocused. Parking closes that:
# the SHOW hook teleports a new KiCad window to (-32000,-32000) before its
# first paint lands on the visible desktop; when bridge work ends (or the
# window must be shown deliberately) it is placed back at its real coordinates
# AT THE BOTTOM of the z-order. Taskbar buttons keep working; PrintWindow
# capture works off-screen; the user sees nothing appear.
# ---------------------------------------------------------------------------
_parked: dict = {} # hwnd -> (x, y, was_max)
_park_fights: dict = {} # hwnd -> how many times we re-parked it
_park_gaveup: set = set() # windows we STOPPED fighting (see _PARK_FIGHT_LIMIT)
_park_fight_log: list = [] # rolling record: what we fought, how hard, and why
_PARK_FIGHT_LIMIT = 3 # John, 2026-08-19: "you blink them 20 or 30 times"
_demote_counts: dict = {} # hwnd -> demotes by the GUARDIAN (wiki issue #33)
_DEMOTE_LIMIT = 3 # then hold it down once and stop re-demoting
_park_attempts = 0
_park_last_error = ""
_sweep_ticks = 0
_verb_inflight = 0
_park_baseline: set = set()
_bridge_windows: set = set() # windows the BRIDGE created (persists across verbs)
_park_sweep_started = False
# ---- Ownership ledger (John, 2026-08-17: "what happens if the user opens their
# own kicad? i don't want us ruining that") -------------------------------------
# The bridge may only park/bounce/badge windows it OWNS. Ownership is decided by
# PROCESS: every kicad-family pid the bridge spawns is registered here (children
# count via parent-chain walk — the PM spawns eeschema.exe/pcbnew.exe). The set
# persists to %USERPROFILE%/.adom/kicad-bridge-owned.json so a bridge restart
# does not orphan (or worse, mis-adopt) windows. A KiCad the USER launched is in
# 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")
def _ledger_path():
import os as _os
from pathlib import Path as _P
base = _os.environ.get("USERPROFILE") or str(_P.home())
d = _P(base) / ".adom"
try:
d.mkdir(parents=True, exist_ok=True)
except Exception:
pass
return d / "kicad-bridge-owned.json"
def _pid_of_hwnd(hwnd) -> int:
pid = ctypes.wintypes.DWORD()
_user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
return int(pid.value)
def _pid_exe_base(pid) -> str:
try:
h = _kernel32.OpenProcess(0x1000, False, int(pid))
if not h:
return ""
try:
buf = ctypes.create_unicode_buffer(512)
size = ctypes.wintypes.DWORD(512)
if _kernel32.QueryFullProcessImageNameW(h, 0, buf, ctypes.byref(size)):
import os as _os
return _os.path.basename(buf.value).lower()
finally:
_kernel32.CloseHandle(h)
except Exception:
pass
return ""
def _pid_is_kicad(pid) -> bool:
base = _pid_exe_base(pid)
return any(base.startswith(e) for e in _OWNED_EXE_PREFIXES)
def _save_ledger() -> None:
try:
import json as _json
live = {int(p) for p in _owned_pids if _pid_is_kicad(p)}
wins = []
for h in list(_bridge_windows):
try:
if _user32.IsWindow(h):
wins.append({"hwnd": int(h), "pid": _pid_of_hwnd(h)})
except Exception:
pass
_ledger_path().write_text(_json.dumps({"pids": sorted(live), "windows": wins,
"projects": sorted(_owned_projects)}))
except Exception:
pass
def load_owned_ledger() -> None:
"""Boot: re-own only what provably survived — a ledger hwnd whose pid still
matches, and ledger pids still running a kicad-family exe. Everything else
(including anything the user launched while the bridge was down) stays
user-owned."""
try:
import json as _json
data = _json.loads(_ledger_path().read_text())
except Exception:
return
for e in data.get("windows", []):
try:
h, p = int(e.get("hwnd") or 0), int(e.get("pid") or 0)
if h and _user32.IsWindow(h) and _pid_of_hwnd(h) == p and is_kicad_window(h):
_bridge_windows.add(h)
_owned_pids.add(p)
except Exception:
pass
for p in data.get("pids", []):
try:
if _pid_is_kicad(p):
_owned_pids.add(int(p))
except Exception:
pass
for p in data.get("projects", []):
try:
_owned_projects.add(str(p))
except Exception:
pass
_spawn_watch_until = 0.0 # we are responsible for windows appearing before this
def spawn_watch_active() -> bool:
try:
import time as _t
return _t.monotonic() < _spawn_watch_until
except Exception:
return False
def register_owned_pid(pid) -> None:
"""Spawn sites call this for every kicad-family process the bridge starts."""
global _spawn_watch_until
try:
if pid:
_owned_pids.add(int(pid))
_save_ledger()
# 0.9.185 (John, live: "you opened the kicad main menu and it came to
# the foreground - you did nothing to even attempt once to put it in
# the background"). A spawned app can take many seconds to show its
# first window, by which time the verb has returned and the idle rule
# correctly says "hands off". That rule is for windows the USER
# opened; for 60s after WE spawn something, a window that appears in
# front is ours to put behind - exactly once, as always.
import time as _t
_spawn_watch_until = _t.monotonic() + 60.0
except Exception:
pass
def _parent_pid_map() -> dict:
"""{pid: ppid} via toolhelp snapshot (cheap, ~ms)."""
out = {}
try:
TH32CS_SNAPPROCESS = 0x02
class PE32(ctypes.Structure):
_fields_ = [("dwSize", ctypes.wintypes.DWORD),
("cntUsage", ctypes.wintypes.DWORD),
("th32ProcessID", ctypes.wintypes.DWORD),
("th32DefaultHeapID", ctypes.POINTER(ctypes.c_ulong)),
("th32ModuleID", ctypes.wintypes.DWORD),
("cntThreads", ctypes.wintypes.DWORD),
("th32ParentProcessID", ctypes.wintypes.DWORD),
("pcPriClassBase", ctypes.c_long),
("dwFlags", ctypes.wintypes.DWORD),
("szExeFile", ctypes.c_wchar * 260)]
snap = _kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
if snap == ctypes.wintypes.HANDLE(-1).value:
return out
try:
pe = PE32()
pe.dwSize = ctypes.sizeof(PE32)
if _kernel32.Process32FirstW(snap, ctypes.byref(pe)):
while True:
out[int(pe.th32ProcessID)] = int(pe.th32ParentProcessID)
if not _kernel32.Process32NextW(snap, ctypes.byref(pe)):
break
finally:
_kernel32.CloseHandle(snap)
except Exception:
pass
return out
def _is_owned_window(hwnd) -> bool:
"""Is this window the bridge's (spawned by us, or child of our spawn)?"""
if hwnd in _bridge_windows:
return True
try:
pid = _pid_of_hwnd(hwnd)
except Exception:
return False
if pid in _owned_pids:
return True
try:
pm = _parent_pid_map()
p, depth = pid, 0
while p in pm and depth < 6:
p = pm[p]
depth += 1
if p in _owned_pids:
_owned_pids.add(pid) # child of our spawn: ours too
_save_ledger()
return True
except Exception:
pass
return False
def is_bridge_window(hwnd) -> bool:
"""Public ownership probe (kicad_state, taskbar badges)."""
try:
return _is_owned_window(hwnd)
except Exception:
return False
_owned_projects: set = set()
_last_sanitize = 0.0
def register_owned_project(path) -> None:
"""Project-creating verbs (make_part_project, demo prep) record their
.kicad_pro here so session sanitizing knows exactly what is OURS."""
try:
if path:
_owned_projects.add(str(path))
_save_ledger()
except Exception:
pass
def _any_kicad_process_running() -> bool:
try:
pm = _parent_pid_map()
for pid in pm:
if _pid_is_kicad(pid):
return True
except Exception:
pass
return False
def _is_bridge_project_path(p: str) -> bool:
q = (p or "").replace("/", "\\").lower()
if any(q == str(o).replace("/", "\\").lower() for o in _owned_projects):
return True
# legacy pollution predating the project ledger: the bridge's own demo
# naming (adom-* and in-<part>-demo under Documents)
import re as _re
return bool(_re.search(r"\\(adom-[a-z0-9-]+|in-[a-z0-9]+-demo)\\", q))
def sanitize_kicad_session() -> int:
"""Remove BRIDGE projects from KiCad's restore-on-launch list (John,
2026-08-17: every launch of HIS KiCad auto-opened OUR adom-demo and hit the
project lock warning). kicad.json system.open_projects is what the PM
restores at startup; KiCad rewrites it at exit, so edits only stick while
NO KiCad runs — the sweep retries until that window opens. file_history is
left alone (honest recents). Returns entries removed."""
global _last_sanitize
import time as _tm
if not IS_WINDOWS:
return 0
_last_sanitize = _tm.monotonic()
if _any_kicad_process_running():
return 0
removed = 0
try:
import glob as _glob
import json as _json
import os as _os
appdata = _os.environ.get("APPDATA") or ""
for cfg in _glob.glob(_os.path.join(appdata, "kicad", "*", "kicad.json")):
try:
data = _json.loads(open(cfg, encoding="utf-8").read())
sysd = data.get("system") or {}
op = sysd.get("open_projects") or []
keep = [p for p in op if not _is_bridge_project_path(p)]
if len(keep) != len(op):
sysd["open_projects"] = keep
data["system"] = sysd
open(cfg, "w", encoding="utf-8").write(_json.dumps(data, indent=2))
removed += len(op) - len(keep)
print(f"[session] scrubbed {len(op)-len(keep)} bridge project(s) "
f"from {cfg}", file=sys.stderr)
except Exception:
pass
except Exception:
pass
return removed
_last_usability_heal = 0.0
def heal_window_usability() -> int:
"""Make every KiCad window the bridge is NOT actively working with fully
usable again: clickable (no stale WS_EX_NOACTIVATE), present in the taskbar
(WS_EX_APPWINDOW), iconed, and on-screen.
0.9.164 - John: "why is it that when i try to click to see one of your kicad
windows it doesn't work?" Exactly right, and the gap is subtler than the
0.9.160 fix: suppression is cleared through _noactivated, which lives in the
bridge PROCESS. Any window suppressed by an earlier bridge (or one that died
mid-verb) is unknown to the new process, so nothing ever clears it - the
window stays permanently un-clickable and taskbar-less no matter how many
fixes ship afterwards. So we heal by INSPECTION, not by memory: read the
style off every KiCad window and clear what should not be there."""
global _last_usability_heal
import time as _t
_last_usability_heal = _t.monotonic()
healed = 0
if not IS_WINDOWS:
return healed
try:
busy = _verb_inflight > 0 or _guard_count > 0
for h in _enum_kicad_hwnds():
if busy and h in _parked:
continue # mid-verb: this window is legitimately suppressed
try:
if not _window_responsive(h):
continue # hung window: touching its style would block us too
cur = _get_exstyle(h)
want = (cur & ~WS_EX_NOACTIVATE) | WS_EX_APPWINDOW
if want != cur:
_set_exstyle(h, want)
_noactivated.discard(h)
_ensure_window_icon(h)
healed += 1
print(f"[focus] usability heal: cleared stale suppression on {h}",
file=sys.stderr)
_sane_position(h)
except Exception:
pass
except Exception:
pass
return healed
def heal_offscreen_windows() -> int:
"""Boot repair (John, 2026-08-17: "when the user goes to open kicad normally
... it starts way offscreen and kicad doesn't fix itself"): if a parked
window was closed (or the bridge died) before unparking, KiCad saved the
off-screen geometry, and the next launch restores there. Nothing but our
parking puts a KiCad window at <= -20000, so any window found there — OURS
OR THE USER'S — is our poison to heal: move it to a sane on-screen spot,
no activation, size preserved."""
healed = 0
if not IS_WINDOWS:
return healed
try:
for i, h in enumerate(sorted(_enum_kicad_hwnds())):
try:
if _user32.IsIconic(h):
continue
wp = _WINDOWPLACEMENT()
wp.length = ctypes.sizeof(_WINDOWPLACEMENT)
if not _user32.GetWindowPlacement(h, ctypes.byref(wp)):
continue
if wp.rcNormalPosition.left > -20000:
continue
width = wp.rcNormalPosition.right - wp.rcNormalPosition.left
height = wp.rcNormalPosition.bottom - wp.rcNormalPosition.top
x, y = 120 + 48 * (i % 5), 90 + 36 * (i % 5)
wp.rcNormalPosition.left = x
wp.rcNormalPosition.top = y
wp.rcNormalPosition.right = x + max(width, 400)
wp.rcNormalPosition.bottom = y + max(height, 300)
wp.showCmd = 8 # SW_SHOWNA — visible, no activation
_user32.SetWindowPlacement(h, ctypes.byref(wp))
healed += 1
print(f"[focus] healed off-screen window {h} -> ({x},{y})", file=sys.stderr)
except Exception:
pass
except Exception:
pass
return healed
def _enum_kicad_hwnds() -> set:
out = set()
try:
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND,
ctypes.wintypes.LPARAM)
def cb(h, _l):
try:
if _user32.IsWindowVisible(h) and is_kicad_window(h):
out.add(h)
except Exception:
pass
return True
cbp = WNDENUMPROC(cb)
_user32.EnumWindows(cbp, 0)
except Exception:
pass
return out
def _start_park_sweep() -> None:
"""50ms parking sweep, active only while a GUI verb is in flight or a guard
is armed: any KiCad window NOT in the verb-start baseline is suppressed and
parked off-screen the moment it enumerates. Deterministic complement to the
SHOW hook (which arms but receives no events on some sessions)."""
global _park_sweep_started
if _park_sweep_started or not IS_WINDOWS:
return
_park_sweep_started = True
import threading
import time as _t
def _loop():
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
quiet_since = None
if _t.monotonic() >= _expected_fg_until:
for h in _enum_kicad_hwnds() - _park_baseline:
# Ownership gate: a new window from a process tree we
# did NOT spawn is the USER's (e.g. they launched
# KiCad mid-verb) — never adopt, park, or badge it.
if h not in _bridge_windows and not _is_owned_window(h):
continue
if h not in _bridge_windows:
_bridge_windows.add(h)
_save_ledger()
# ONLY act if this window actually took the
# foreground. A window that opened quietly behind
# the user's work needs nothing from us (John:
# "ONLY background the window once IF IT APPEARS IN
# THE FOREGROUND. then do nothing else").
if _user32.GetForegroundWindow() == h:
_push_kicad_to_background(h)
try:
from handlers.win_taskbar import apply_adom_badge
apply_adom_badge(h) # "adom is driving this window"
except Exception:
pass
_t.sleep(0.05)
elif _parked:
# bridge quiet but windows still parked: keep re-parking
# fighters briefly, then place everything at its true
# position (bottom-z) once things have settled for 2s.
if quiet_since is None:
quiet_since = _t.monotonic()
if _t.monotonic() - quiet_since >= 2.0:
unpark_all()
quiet_since = None
else:
# 0.9.181: this used to RE-PARK every quiet window on a
# 50ms tick. Repetition is exactly what strobed. Windows
# are handled once, by _push_kicad_to_background, and
# then left alone.
_t.sleep(0.05)
continue
_t.sleep(0.1)
else:
if _t.monotonic() - _last_usability_heal > 20:
try:
heal_window_usability()
except Exception:
pass
if _t.monotonic() - _last_sanitize > 60:
try:
sanitize_kicad_session()
except Exception:
pass
_t.sleep(0.25)
except Exception:
try:
_t.sleep(0.5)
except Exception:
pass
try:
threading.Thread(target=_loop, daemon=True, name="kicad-park-sweep").start()
except Exception:
pass
def begin_gui_verb() -> None:
"""Dispatcher marks a GUI verb in flight. The baseline is every window that
ALREADY EXISTS as the verb starts; only windows that appear DURING the verb
get parked, because parking exists for exactly one purpose: stopping a
brand-new window from flashing into the user's face while it is created.
0.9.161 (John: "it is really weird how you open kicad in the background"):
the baseline used to subtract _bridge_windows, so every window the bridge
had EVER created stayed permanently 'new' and got re-parked off-screen on
EVERY subsequent verb - a launched KiCad kept getting yanked off the screen
for the rest of its life, every time any verb ran. A window that has already
finished its park/unpark cycle is settled; it belongs in the baseline like
any of the user's own."""
global _verb_inflight
if _verb_inflight == 0 and _guard_count == 0:
for h in list(_bridge_windows):
if not _user32.IsWindow(h):
_bridge_windows.discard(h)
_park_baseline.clear()
_park_baseline.update(_enum_kicad_hwnds() - set(_parked.keys()))
_verb_inflight += 1
_start_park_sweep()
def end_gui_verb() -> None:
"""0.9.180: the moment the bridge is idle, hand every window back. A stale
WS_EX_NOACTIVATE is what makes a taskbar click do nothing (John hit exactly
that), so it must never outlive the work that justified it."""
global _verb_inflight
_verb_inflight = max(0, _verb_inflight - 1)
if _verb_inflight == 0 and _guard_count == 0:
try:
allow_activation_all()
except Exception:
pass
class _WINDOWPLACEMENT(ctypes.Structure if IS_WINDOWS else object):
if IS_WINDOWS:
_fields_ = [("length", ctypes.wintypes.UINT),
("flags", ctypes.wintypes.UINT),
("showCmd", ctypes.wintypes.UINT),
("ptMinPosition", ctypes.wintypes.POINT),
("ptMaxPosition", ctypes.wintypes.POINT),
("rcNormalPosition", ctypes.wintypes.RECT)]
_exe_icons: dict = {} # exe path -> (hicon_big, hicon_small), kept alive
def _hwnd_exe_path(hwnd) -> str:
try:
pid = ctypes.wintypes.DWORD()
_user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
h = _kernel32.OpenProcess(0x1000, False, pid.value)
if not h:
return ""
try:
buf = ctypes.create_unicode_buffer(512)
size = ctypes.wintypes.DWORD(512)
if _kernel32.QueryFullProcessImageNameW(h, 0, buf, ctypes.byref(size)):
return buf.value
finally:
_kernel32.CloseHandle(h)
except Exception:
pass
return ""
def _ensure_window_icon(hwnd) -> None:
"""Minimize-parking freezes a window into the taskbar BEFORE KiCad assigns
its icon (wx sets it a beat after frame creation), so the shell shows a
blank document icon (John, 2026-08-17: "why are my kicad icons fucked
up"). If the window has no icon, stamp it with its own exe's icon."""
try:
WM_GETICON, WM_SETICON = 0x007F, 0x0080
GCLP_HICON, GCLP_HICONSM = -14, -34
get_cl = getattr(_user32, "GetClassLongPtrW", None) or _user32.GetClassLongW
# 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
exe = _hwnd_exe_path(hwnd)
if not exe:
return
icons = _exe_icons.get(exe)
if icons is None:
big = ctypes.wintypes.HICON()
small = ctypes.wintypes.HICON()
ctypes.windll.shell32.ExtractIconExW(exe, 0, ctypes.byref(big),
ctypes.byref(small), 1)
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.SendMessageTimeoutW(hwnd, WM_SETICON, 1, big.value, 0x0002, 200,
ctypes.byref(res))
if small and small.value:
_user32.SendMessageTimeoutW(hwnd, WM_SETICON, 0, small.value, 0x0002, 200,
ctypes.byref(res))
except Exception:
pass
def _park_offscreen(hwnd) -> None:
"""REMOVED (0.9.181). Off-screen parking moved a window to (-32000,-32000)
and later moved it back; John asked for it originally, it never delivered,
and every move is a visible frame. Kept only as a name so older call sites
do not break - it now does the ONE sanctioned thing, or nothing."""
_push_kicad_to_background(hwnd)
class _MONITORINFO(ctypes.Structure):
_fields_ = [("cbSize", ctypes.wintypes.DWORD), ("rcMonitor", ctypes.wintypes.RECT),
("rcWork", ctypes.wintypes.RECT), ("dwFlags", ctypes.wintypes.DWORD)]
def _visible_on_any_monitor(r) -> tuple:
"""(visible_w, visible_h) of window rect r on the monitor it overlaps most;
(0, 0) when it is off every monitor. MONITOR_DEFAULTTONULL, so a second or
third display counts exactly like the primary."""
try:
_user32.MonitorFromRect.restype = ctypes.wintypes.HANDLE # 64-bit handle, never truncated
_user32.MonitorFromRect.argtypes = [ctypes.POINTER(ctypes.wintypes.RECT), ctypes.wintypes.DWORD]
_user32.GetMonitorInfoW.argtypes = [ctypes.wintypes.HANDLE, ctypes.POINTER(_MONITORINFO)]
hmon = _user32.MonitorFromRect(ctypes.byref(r), 0)
if not hmon:
return (0, 0)
mi = _MONITORINFO(); mi.cbSize = ctypes.sizeof(_MONITORINFO)
if not _user32.GetMonitorInfoW(hmon, ctypes.byref(mi)):
return (0, 0)
m = mi.rcMonitor
return (min(r.right, m.right) - max(r.left, m.left), min(r.bottom, m.bottom) - max(r.top, m.top))
except Exception:
# Unknown: report reachable so the healer never moves a window on a guess.
return (10**6, 10**6)
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
w, h = max(r.right - r.left, 640), max(r.bottom - r.top, 480)
# Wiki #48 (AdityaAngajala, 2026-09-04): "when I drag KiCad to another
# monitor it gets dragged back to my main one after some time". This
# ran every 20 s from the idle healer and measured reachability against
# the PRIMARY monitor only (GetSystemMetrics 0/1), so a window fully on
# a second monitor counted as off-screen and was moved to (120,90).
# Measure against the monitor the window actually overlaps most.
visible_w, visible_h = _visible_on_any_monitor(r)
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:
"""0.9.180: nothing is moved any more, so "unparking" only means handing the
window back to the user - clear the activation block, make sure it has a
taskbar button and an icon, and leave its geometry exactly where KiCad put
it. No SetWindowPlacement, no restore, no motion."""
try:
_parked.pop(hwnd, None)
_park_fights.pop(hwnd, None)
_park_since.pop(hwnd, None)
if not _user32.IsWindow(hwnd):
return
allow_activation(hwnd)
_ensure_window_icon(hwnd)
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."""
n = 0
for h in list(_parked.keys()):
_unpark(h, to_bottom=True)
n += 1
_clear_park_clock()
return n
_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 and _verb_inflight <= 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 not _is_owned_window(hwnd):
return # the user's own KiCad: not ours to park or bounce
print(f"[focus-hook] event=0x{event:04x} hwnd={hwnd} inflight={_verb_inflight} guards={_guard_count}", file=sys.stderr)
if event == EVENT_OBJECT_SHOW:
# A window appearing is NOT a foreground steal. Only act if it
# actually took the foreground (John: "ONLY background the
# window once IF IT APPEARS IN THE FOREGROUND").
if _user32.GetForegroundWindow() == hwnd:
_push_kicad_to_background(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:
_user32.SetWinEventHook.restype = ctypes.wintypes.HANDLE
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)
print(f"[focus-hook] armed fg={bool(h1)} show={bool(h2)}", file=sys.stderr)
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
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 _cursor_over(hwnd) -> bool:
"""Is the cursor currently over this window (top-level hit test)?"""
try:
import ctypes as _ct
pt = _ct.wintypes.POINT()
_user32.GetCursorPos(_ct.byref(pt))
under = _user32.WindowFromPoint(pt)
GA_ROOT = 2
return bool(under) and _user32.GetAncestor(under, GA_ROOT) == hwnd
except Exception:
return False
# 0.9.155 (caught live: John launched his own KiCad and the sentinel bounced
# him 8 times, including a File Open Warning he never saw): a click on the
# TASKBAR / Start is the canonical "bring this window up" gesture, but the
# cursor is over the shell — not the raised window — when the transition
# lands, so click-on-window missed it and the raise read as a self-raise.
_SHELL_CLASSES = ("Shell_TrayWnd", "Shell_SecondaryTrayWnd",
"Windows.UI.Core.CoreWindow", "XamlExplorerHostIslandWindow",
"TopLevelWindowForOverflowXamlIsland", "Progman", "WorkerW")
def _cursor_over_shell() -> bool:
try:
import ctypes as _ct
pt = _ct.wintypes.POINT()
_user32.GetCursorPos(_ct.byref(pt))
under = _user32.WindowFromPoint(pt)
GA_ROOT = 2
root = _user32.GetAncestor(under, GA_ROOT) if under else None
if not root:
return False
buf = _ct.create_unicode_buffer(64)
_user32.GetClassNameW(root, buf, 64)
return buf.value in _SHELL_CLASSES
except Exception:
return False
def _loop():
# v0.9.115: "recent user input" was the wrong bless test — the user
# typing in ANOTHER app is exactly when a self-raise is most disruptive,
# yet it counted as user intent (caught live by John over RustDesk).
# A foreground is user-initiated only if a mouse CLICK landed (and the
# cursor sits on the new window) or an alt-tab was in progress.
last_fg = None
last_click_t = 0.0
last_alt_t = 0.0
bounce_hold = {} # hwnd -> monotonic deadline to clear NOACTIVATE
VK_LBUTTON, VK_RBUTTON, VK_MENU = 0x01, 0x02, 0x12
while True:
try:
now = _t.monotonic()
try:
if (_user32.GetAsyncKeyState(VK_LBUTTON) & 0x8001) or \
(_user32.GetAsyncKeyState(VK_RBUTTON) & 0x8001):
last_click_t = now
if _user32.GetAsyncKeyState(VK_MENU) & 0x8000:
last_alt_t = now
except Exception:
pass
fg = _user32.GetForegroundWindow()
if fg != last_fg:
if fg and is_kicad_window(fg):
ago = _seconds_since_user_input()
clicked_here = (now - last_click_t) <= 1.2 and \
(_cursor_over(fg) or _cursor_over_shell())
alt_tabbed = (now - last_alt_t) <= 2.0
if now < _expected_fg_until:
_log("sanctioned-foreground (allowed)", fg, ago)
elif clicked_here or alt_tabbed:
_log("user-foregrounded (left alone)", fg, ago)
elif not _is_owned_window(fg):
# Not our spawn tree: the USER's own KiCad. The
# bridge has no authority over it — ever.
_log("user-owned window (left alone)", fg, ago)
elif _guard_count > 0:
_log("steal-during-guard (guardian handles)", fg, ago)
elif (_verb_inflight <= 0 and _guard_count <= 0
and spawn_watch_active() and _is_owned_window(fg)):
# We spawned this within the last minute: it is ours
# to background, once, even though the verb returned.
_log("our freshly spawned window took the foreground", fg, ago)
_push_kicad_to_background(fg)
elif _verb_inflight <= 0 and _guard_count <= 0:
# 0.9.172 - THE IDLE RULE (John, 2026-08-19: "when I
# click the taskbar icon to bring the app to the fg,
# you don't let me"). Bouncing only exists to stop a
# window flashing into his face WHILE the bridge is
# working. With no verb in flight and no guard armed,
# the bridge is not opening anything - so a KiCad
# window reaching the foreground is the USER, whether
# or not we managed to observe the click that did it
# (a fast taskbar click can land entirely between two
# 120ms sentinel polls). Leave it alone, always.
_log("foreground while bridge idle (left alone)", fg, ago)
else:
_log("self-raise (bounced to background)", fg, ago)
_push_kicad_to_background(fg)
# 10.0.5: NOACTIVATE no longer PREVENTS the steal, it
# only helps after the bounce — hold the suppression
# for 10s so a repeat-raising window is refused
# silently instead of flickering, THEN clear it so
# user clicks work again.
bounce_hold[fg] = now + 3.0 # was 10s: too long to
# leave a window unclickable once the verb has moved on
for h, until in list(bounce_hold.items()):
if now >= until:
allow_activation(h)
del bounce_hold[h]
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
ASYNCHRONOUSLY (a 3D viewer self-raises when its render completes — sometimes
30s+ after the verb returned), so a one-shot restore misses.
v0.9.79 rules (John, 2026-08-14 — "ALWAYS open in the background"):
- Only act when the foreground thief IS a KiCad window. The old loop
restored a FIXED hwnd against any change, which fought the USER when they
deliberately switched apps mid-guard.
- Follow the user: whenever a non-KiCad window legitimately holds the
foreground, adopt it as the new window-to-protect.
- Don't just steal focus back — also DEMOTE the offending KiCad window to
directly below the user's window, so it doesn't sit painted over their
work even unfocused. Screenshots/PrintWindow keep working on it there.
Non-blocking: returns immediately; the response isn't delayed."""
# 2026-08-17: hwnd is VESTIGIAL post-0.9.104 (the guardian touches only
# KiCad windows, never the user's) — but `not hwnd` still gated the whole
# arm, so whenever foreground capture returned None NO guard ever armed
# and the parking sweep went idle mid-editor-load. Arm regardless.
if not IS_WINDOWS:
return
import threading
import time as _t
def _loop():
global _guard_count
deadline = _t.monotonic() + seconds
if _guard_lock:
with _guard_lock:
_guard_count += 1
if deadline > _guard_deadline:
globals()["_guard_deadline"] = deadline
tick = 0
try:
_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_all() # catch windows CREATED mid-verb (~every 0.5s)
fg = _user32.GetForegroundWindow()
if fg and is_kicad_window(fg):
# WIKI ISSUE #33 (Colby Knox, independently of John's
# report of the same class of bug): a just-launched wx
# frame Raise()es itself repeatedly while it loads, and
# this loop demoted it EVERY tick with no debounce - so
# KiCad raised, we demoted, KiCad raised... at 10x/sec.
# That is the visible "epileptic" flicker and the CPU
# burn. One demote that STICKS is enough: after a few
# rounds we hold the window down (bottom of z-order,
# activation suppressed) and stop re-demoting it, which
# also keeps the loop off the UI frame rate.
# One funnel, one action per window, for its whole
# life. No counting, no escalation, no second chance.
_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.)
except Exception:
pass
_t.sleep(interval)
finally:
# last guardian out clears the NOACTIVATE styles so the user can
# click into KiCad normally once bridge work has quieted down.
if _guard_lock:
with _guard_lock:
_guard_count -= 1
last = _guard_count <= 0
else:
last = True
if last:
unpark_all()
allow_activation_all()
try:
threading.Thread(target=_loop, daemon=True).start()
except Exception:
pass
def user_wants_foreground(args) -> bool:
"""True only when the CALLER explicitly asked to SEE the window — the user's
own choice to foreground (John: 'always let the user choose to fg a window').
Never inferred; must be an explicit foreground/showToUser/bringToFront flag."""
if not isinstance(args, dict):
return False
return bool(args.get("foreground") or args.get("showToUser") or args.get("bringToFront"))
def bring_to_user(hwnd, reason: str = "") -> bool:
"""Explicit, user-requested foreground of a window (the ONLY sanctioned way a
KiCad window is brought to the front for the user — everything else stays
background). John's contract (2026-08-16): NO foreground without a stated
reason, and every sanctioned foreground shows an ab caption with that reason
for 3 seconds first. A call without a reason is REFUSED."""
if not IS_WINDOWS or not hwnd:
return False
if not (reason or "").strip():
try:
print("[focus] bring_to_user REFUSED: no reason given", file=sys.stderr)
except Exception:
pass
return False
try:
announce_foreground(reason, seconds=3)
import time as _t
_t.sleep(1.2) # let the caption render BEFORE the window moves
expect_foreground(20.0) # tell the sentinel this foreground is sanctioned
_unpark(hwnd, to_bottom=False) # if parked off-screen, put it back first
allow_activation(hwnd) # a suppressed window can't be activated
_user32.ShowWindow(hwnd, 9) # SW_RESTORE (in case minimized)
return _set_foreground_robust(hwnd)
except Exception:
return False
def announce_foreground(action: str, seconds: int = 3) -> None:
"""MANDATORY courtesy caption before any forced foreground (John 2026-08-15:
"when you HAVE to foreground cuz you have no other choice, show an ab caption
telling the user why"). Fires a native ab toast — branded, and attributed to
the calling AI thread via the forwarded caller headers — so the user sees WHY
their screen is about to be taken, BEFORE it happens. Fire-and-forget in a
daemon thread: the toast failing must never fail (or delay) the verb."""
expect_foreground(max(20.0, float(seconds) + 15.0)) # sentinel: sanctioned fg incoming
def _fire():
try:
from caller_identity import ad_callback
ad_callback("notify_user", {
"message": f"KiCad bridge needs your screen for ~{seconds}s: {action}. "
"Background methods were exhausted — this was explicitly allowed "
"by the calling AI (allowFocusSteal/allowForeground).",
"title": "Taking the foreground briefly",
"level": "warning",
}, timeout=8)
except Exception:
pass
try:
import threading
threading.Thread(target=_fire, daemon=True).start()
import time as _t
_t.sleep(1.2) # give the toast a beat to render BEFORE the grab
except Exception:
pass
# ── PLACEMENT MEMORY (wiki #48, Colby/Caleb, 2026-08-20) ────────────────────
# A bridge-initiated restart used to lose where the user had put their windows.
# Caleb drags KiCad to his external monitor, the bridge restarts it for the
# footprint-cache remedy, and Windows places the fresh process on the PRIMARY
# display: "it keeps moving and annoying." Restoring placement is not focus
# steal, so it belongs inside the background-first contract, not outside it.
_PLACEMENT_FILE = os.path.join(
os.environ.get("USERPROFILE") or os.path.expanduser("~"),
".adom", "kicad-bridge-placement.json")
def capture_placement() -> dict:
"""Where every KiCad window is RIGHT NOW, keyed by window kind.
Keyed by kind ("symbol editor", "pcb editor", ...) rather than hwnd,
because the whole point is to survive a restart, and hwnds do not."""
out = {}
try:
for h in _enum_kicad_hwnds():
try:
if h in _parked:
continue # never memorise the park position
_b = ctypes.create_unicode_buffer(160)
_user32.GetWindowTextW(h, _b, 160)
title = (_b.value or "").lower()
kind = next((k for k in ("symbol editor", "footprint editor", "pcb editor",
"schematic editor", "3d viewer", "kicad")
if k in title), None)
if not kind:
continue
r = ctypes.wintypes.RECT()
_user32.GetWindowRect(h, ctypes.byref(r))
if r.right - r.left < 200 or r.bottom - r.top < 150:
continue # a minimised/parked stub is not a placement
out[kind] = {"x": r.left, "y": r.top,
"w": r.right - r.left, "h": r.bottom - r.top,
"maximized": bool(_user32.IsZoomed(h))}
except Exception:
continue
except Exception:
pass
if out:
try:
os.makedirs(os.path.dirname(_PLACEMENT_FILE), exist_ok=True)
prev = {}
try:
with open(_PLACEMENT_FILE) as f:
prev = json.load(f)
except Exception:
prev = {}
prev.update(out)
with open(_PLACEMENT_FILE, "w") as f:
json.dump(prev, f, indent=2)
except Exception:
pass
return out
def restore_placement(saved: dict = None, waits: int = 40) -> dict:
"""Put fresh KiCad windows back where the user had them.
Uses the on-disk memory when no live capture was taken, so even a COLD
spawn lands on the monitor they last used rather than the primary."""
if saved is None:
try:
with open(_PLACEMENT_FILE) as f:
saved = json.load(f)
except Exception:
saved = {}
if not saved:
return {"restored": 0, "reason": "no remembered placement"}
SWP_NOACTIVATE, SWP_NOZORDER = 0x0010, 0x0004
SW_SHOWNOACTIVATE, SW_SHOWMAXIMIZED = 4, 3
done, seen = 0, set()
for _ in range(waits):
for h in _enum_kicad_hwnds():
try:
if h in seen:
continue
_b = ctypes.create_unicode_buffer(160)
_user32.GetWindowTextW(h, _b, 160)
title = (_b.value or "").lower()
kind = next((k for k in saved if k in title), None)
if not kind:
continue
p = saved[kind]
# NEVER leave it minimised: a parked window blanks PrintWindow,
# which silently breaks the evidence/shot pipeline (#48 ask 4).
_user32.ShowWindow(h, SW_SHOWMAXIMIZED if p.get("maximized")
else SW_SHOWNOACTIVATE)
if not p.get("maximized"):
_user32.SetWindowPos(h, 0, int(p["x"]), int(p["y"]),
int(p["w"]), int(p["h"]),
SWP_NOACTIVATE | SWP_NOZORDER)
seen.add(h)
done += 1
except Exception:
continue
if done >= len(saved):
break
time.sleep(0.5)
return {"restored": done, "kinds": sorted(saved.keys())}