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())}