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.301: Taskbar hygiene (John: frozen progress bars on every KiCad window, no Adom badge): the boot sweep that clears…
0d3d7f6
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
"""win_taskbar.py — taskbar identity + Adom overlay badges for KiCad windows.
John's design (2026-08-17, after seeing pup's overlay badges): stamp the Adom
favicon as an OVERLAY on every bridge-driven KiCad window's taskbar button, so
the user sees at a glance "adom is controlling my kicad". Gated by the
`overlayBadges` setting (handlers/settings.py, default on).
THE LESSON (paid for by pup first, then fusion, now us — src/server.js in pup,
handlers/taskbar_overlay.py in fusion): an in-bridge ITaskbarList3::SetOverlayIcon
is IN-PROCESS-ONLY. The COM call returns S_OK cross-process and the shell silently
ignores it (verified live here 2026-08-17: badges never rendered on kicad.exe's
buttons). The working path is AD-core's `desktop_taskbar` verb: hand it the badge
PNG as base64 (resources/adom-overlay-32.png — mark anchored TOP-RIGHT of a
transparent 32px canvas, matching how pup places its favicon badge; John,
2026-08-21: "put the favicon in the upper right corner instead of the lower
right". Windows renders the overlay from that canvas, so the anchor in the
asset is what decides the corner)
and AD paints the overlay natively, cross-process.
set: desktop_taskbar {hwnd, overlay: {icon: <b64 png>, tooltip}}
clear: desktop_taskbar {hwnd, overlay: {badge: "none"}}
SECOND LESSON (same day): eeschema.exe/pcbnew.exe taskbar buttons showed blank
white-document icons even though both exes carry icon resources — the shell's
icon cache for those exe paths was poisoned (buttons first appeared during the
pre-0.9.151 parking era, before wx assigned window icons, and Windows cached the
blank). Rebuilding the shell icon cache means killing Explorer; instead we
BYPASS the poisoned cache with pup's identity machinery: stamp each eeschema/
pcbnew window with a per-exe AUMID (`desktop_set_window_identity`) whose
iconPath is a real .ico extracted from the exe itself — a fresh identity gets a
fresh icon lookup. kicad.exe's cache entry is healthy; its windows stay native.
"""
import os
import sys
import threading
from pathlib import Path
IS_WINDOWS = sys.platform == "win32"
_BADGE_PATH = str(Path(__file__).resolve().parent.parent / "resources" / "adom-overlay-32.png")
# ⛔ THIS BRIDGE STAMPS NO AUMIDs. EVER. (John, 2026-08-23: "i just want
# default kicad behavior.")
#
# History, so nobody rebuilds this: weeks ago an AI stamped per-window AUMIDs
# on eeschema/pcbnew windows and at least one stamp ran before icon extraction
# succeeded - Windows caches the icon PER AUMID STRING, so those buttons became
# a white generic box that survived KiCad reinstalls. The first "fix" (.v2
# suffix, 0.9.240-242) replaced the poison with Adom-flavoured identity, which
# was still an override of default KiCad behaviour. The real fix is REMOVAL:
# `_identity_repair()` below clears the per-window AppUserModelID property on
# every eeschema/pcbnew window and deletes our Adom.KiCad.* registry
# registrations, so buttons fall back to the exes' own native icons.
#
# The overlay badge and the progress bar DO NOT need an AUMID - they ride
# ITaskbarList3 (SetOverlayIcon/SetProgressValue) against the hwnd's button
# directly. Activity indication keeps working on fully native buttons.
_ONCE_STAMPED_EXES = {"eeschema.exe", "pcbnew.exe"}
_AUMID_PREFIX = "Adom.KiCad."
_badged: set = set()
_B64 = None
_LOCK = threading.Lock()
def _badge_b64():
global _B64
if _B64 is not None:
return _B64
try:
import base64
_B64 = base64.b64encode(open(_BADGE_PATH, "rb").read()).decode()
except Exception:
_B64 = ""
return _B64
def _ad(verb: str, args: dict) -> bool:
try:
from handlers import ad_client
r = ad_client.call(verb, args, timeout=8)
raw = str(r)
return bool(r) and ("Taskbar updated" in raw
or (isinstance(r, dict) and r.get("success") is not False
and not r.get("error")))
except Exception:
return False
def _hwnd_exe_base(hwnd) -> str:
try:
from handlers.win_focus import _hwnd_exe_path
return os.path.basename(_hwnd_exe_path(hwnd) or "").lower()
except Exception:
return ""
def _clear_window_aumid(hwnd) -> bool:
"""Blank the per-window PKEY_AppUserModel_ID (VT_EMPTY) so the button
falls back to the exe's own identity. ctypes COM because the bridge has
no pywin32; AD's desktop_set_window_identity has no clear path (an empty
appId no-ops, verified live)."""
import ctypes
from ctypes import wintypes
class GUID(ctypes.Structure):
_fields_ = [("d1", ctypes.c_uint32), ("d2", ctypes.c_uint16),
("d3", ctypes.c_uint16), ("d4", ctypes.c_ubyte * 8)]
class PROPERTYKEY(ctypes.Structure):
_fields_ = [("fmtid", GUID), ("pid", ctypes.c_uint32)]
class PROPVARIANT(ctypes.Structure):
_fields_ = [("vt", ctypes.c_uint16), ("r1", ctypes.c_uint16),
("r2", ctypes.c_uint16), ("r3", ctypes.c_uint16),
("data", ctypes.c_byte * 16)]
try:
iid_ips = GUID(0x886D8EEB, 0x8CF2, 0x4446, (ctypes.c_ubyte * 8)(
0x8D, 0x02, 0xCD, 0xBA, 0x1D, 0xBD, 0xCF, 0x99))
pkey = PROPERTYKEY(GUID(0x9F4C2855, 0x9F79, 0x4B39, (ctypes.c_ubyte * 8)(
0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3)), 5)
ps = ctypes.c_void_p()
hr = ctypes.windll.shell32.SHGetPropertyStoreForWindow(
wintypes.HWND(hwnd), ctypes.byref(iid_ips), ctypes.byref(ps))
if hr != 0 or not ps.value:
return False
# IPropertyStore vtable: QI(0) AddRef(1) Release(2) GetCount(3)
# GetAt(4) GetValue(5) SetValue(6) Commit(7)
vtbl = ctypes.cast(ps.value,
ctypes.POINTER(ctypes.POINTER(ctypes.c_void_p))).contents
set_value = ctypes.WINFUNCTYPE(
ctypes.c_long, ctypes.c_void_p, ctypes.POINTER(PROPERTYKEY),
ctypes.POINTER(PROPVARIANT))(vtbl[6])
commit = ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_void_p)(vtbl[7])
release = ctypes.WINFUNCTYPE(ctypes.c_long, ctypes.c_void_p)(vtbl[2])
empty = PROPVARIANT() # vt == VT_EMPTY
ok = set_value(ps, ctypes.byref(pkey), ctypes.byref(empty)) == 0
ok = (commit(ps) == 0) and ok
release(ps)
return ok
except Exception:
return False
def _delete_adom_aumid_registrations() -> int:
"""Delete every HKCU AppUserModelId registration we ever created
(Adom.KiCad.*): icon paths, display names, the poisoned strings, all of
it. Best-effort; returns how many keys went."""
n = 0
try:
import winreg
root = r"Software\Classes\AppUserModelId"
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, root) as k:
subs, i = [], 0
while True:
try:
subs.append(winreg.EnumKey(k, i))
i += 1
except OSError:
break
for s in subs:
if s.startswith(_AUMID_PREFIX):
try:
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, root + "\\" + s)
n += 1
except OSError:
pass
except Exception:
pass
return n
def _rebuild_button(hwnd) -> None:
"""Win11 bakes the button's tile at creation; a cleared AUMID only shows
after the button is recreated. Brief hide + re-show (no activation) forces
that - except on the foreground window, which we never blink."""
try:
import ctypes
u = ctypes.windll.user32
if u.GetForegroundWindow() == hwnd:
return
if u.IsIconic(hwnd):
u.ShowWindow(hwnd, 0) # SW_HIDE
u.ShowWindow(hwnd, 7) # SW_SHOWMINNOACTIVE
else:
u.ShowWindow(hwnd, 0)
u.ShowWindow(hwnd, 8) # SW_SHOWNA
except Exception:
pass
def _identity_repair() -> None:
"""One-shot per boot: remove every taskbar-identity override this bridge
(or any earlier AI experiment) ever applied, restoring default KiCad
taskbar behaviour. Idempotent - a clean box is a fast no-op."""
cleared = _delete_adom_aumid_registrations()
fixed = 0
for h in _enum_all_kicad_hwnds():
if _hwnd_exe_base(h) not in _ONCE_STAMPED_EXES:
continue
if _clear_window_aumid(h):
_rebuild_button(h)
fixed += 1
if cleared or fixed:
print("[taskbar] identity repair: %d registry key(s) removed, "
"%d window(s) restored to native identity" % (cleared, fixed))
_repair_kicad_shortcut_aumids()
def _repair_kicad_shortcut_aumids() -> None:
"""Fill the AppUserModelID field KiCad's OWN installer forgot (solved live
on John's taskbar, 2026-08-23).
Standalone editor processes present AUMIDs like Kicad.Kicad.pcbnew.10.0
(built at runtime; the pattern is confirmed by reading the taskbar buttons'
AutomationIds). The installer writes that AUMID onto the MAIN shortcut only
and leaves it blank on the standalone editor shortcuts, so the shell has no
shortcut to resolve the editor group's icon from - on some machines it
falls back to the window's icon (ConfRoomROG), on others it paints the
generic white page (AdomLapper; why the fallback differs is unsolved).
Writing KiCad's own AUMID onto KiCad's own user-level shortcut makes the
icon resolution deterministic - the exact mechanism that keeps the main
KiCad button correct. NO Adom identity involved.
The mapping takes effect when explorer next (re)builds its Start Menu
AUMID index (logon / explorer restart); we never restart explorer
ourselves. Idempotent: shortcuts already carrying an AUMID are skipped."""
try:
import subprocess
from kicad_detect import detect_kicad
info = detect_kicad() or {}
ver = str(info.get("version") or info.get("kicadVersion") or "")
ver = ".".join(ver.split(".")[:2]) if ver else ""
if not ver:
return
ps = r'''
$cs = @"
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
public class LnkAumid {
[StructLayout(LayoutKind.Sequential, Pack=4)] public struct PROPERTYKEY { public Guid fmtid; public uint pid; }
[StructLayout(LayoutKind.Explicit)] public struct PropVariant {
[FieldOffset(0)] public ushort vt; [FieldOffset(8)] public IntPtr p;
}
[ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPropertyStore {
int GetCount(out uint c); int GetAt(uint i, out PROPERTYKEY k);
int GetValue(ref PROPERTYKEY k, out PropVariant v);
int SetValue(ref PROPERTYKEY k, ref PropVariant v); int Commit();
}
[ComImport, Guid("00021401-0000-0000-C000-000000000046")] class CShellLink {}
public static bool Set(string path, string aumid) {
var link = (object)new CShellLink();
var pf = (IPersistFile)link;
pf.Load(path, 2);
var ps = (IPropertyStore)link;
var k = new PROPERTYKEY { fmtid = new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), pid = 5 };
PropVariant cur; ps.GetValue(ref k, out cur);
if (cur.vt == 31 && Marshal.PtrToStringUni(cur.p) != "") return false;
var v = new PropVariant { vt = 31, p = Marshal.StringToCoTaskMemUni(aumid) };
bool ok = ps.SetValue(ref k, ref v) == 0 && ps.Commit() == 0;
pf.Save(path, true);
Marshal.FreeCoTaskMem(v.p);
return ok;
}
}
"@
Add-Type -TypeDefinition $cs
$v = "%VER%"
$map = @{
("PCB Editor " + $v + " (standalone).lnk") = ("Kicad.Kicad.pcbnew." + $v)
("Schematic Editor " + $v + " (standalone).lnk") = ("Kicad.Kicad.eeschema." + $v)
("Gerber Viewer " + $v + ".lnk") = ("Kicad.Kicad.gerbview." + $v)
}
$n = 0
$root = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\KiCad " + $v
foreach ($name in $map.Keys) {
$p = Join-Path $root $name
if (Test-Path $p) { try { if ([LnkAumid]::Set($p, $map[$name])) { $n++ } } catch {} }
}
Write-Output $n
'''.replace("%VER%", ver)
cnw = getattr(subprocess, "CREATE_NO_WINDOW", 0)
r = subprocess.run(["powershell", "-NoProfile", "-Command", ps],
capture_output=True, timeout=60, creationflags=cnw)
n = (r.stdout or b"").decode(errors="replace").strip()
if n and n != "0":
print("[taskbar] wrote KiCad's own AUMID onto %s standalone "
"shortcut(s) (installer gap); effective at next explorer "
"restart/logon" % n)
except Exception:
pass
def apply_adom_badge(hwnd) -> bool:
"""Stamp the Adom overlay on the window's taskbar button (idempotent per
hwnd). ITaskbarList3 by hwnd - touches no AUMID, no base icon. Best-effort:
never raises, never blocks a verb."""
try:
from handlers.settings import get_settings
if not get_settings().get("overlayBadges", True):
return False
except Exception:
pass
if hwnd in _badged:
return True
with _LOCK:
icon = _badge_b64()
if not icon:
return False
ok = _ad("desktop_taskbar", {"hwnd": int(hwnd), "overlay": {
"icon": icon, "tooltip": "Adom is driving this KiCad window"}})
if ok:
_badged.add(hwnd)
return ok
def clear_adom_badge(hwnd) -> None:
try:
if hwnd in _badged:
_ad("desktop_taskbar", {"hwnd": int(hwnd), "overlay": {"badge": "none"}})
_badged.discard(hwnd)
except Exception:
pass
def _all_kicad_hwnds() -> set:
"""Only windows the bridge OWNS (tracked live + revived from the persisted
ownership ledger at boot). 0.9.154 briefly badged EVERY enumerated KiCad
window to survive restarts — wrong the moment the user opens their own
KiCad (John, 2026-08-17). The ledger solves restart amnesia the right way;
user-owned windows are never touched."""
try:
from handlers.win_focus import _bridge_windows
return set(_bridge_windows)
except Exception:
return set()
def rebadge_all(enable: bool) -> int:
"""Setting flipped (or bridge booted): apply/clear on every KiCad window."""
n = 0
try:
import ctypes as _ct
for h in _all_kicad_hwnds():
if not _ct.windll.user32.IsWindow(h):
continue
if enable:
_badged.discard(h) # force reapply
if apply_adom_badge(h):
n += 1
else:
clear_adom_badge(h)
n += 1
except Exception:
pass
return n
def badge_existing_on_boot() -> None:
"""Server boot: badge KiCad windows that survived a bridge restart, off the
boot path (AD calls + enumeration can stall; never block serving)."""
if not IS_WINDOWS:
return
import time
# INVERTED (2026-08-22, twice in one day and each move follows from the
# badge's one-sentence meaning). First: boot re-BADGING was retired - a
# freshly booted bridge is driving nothing, so stamping at boot lied.
# Then the live idle-clear proof caught the mirror-image gap: a badge
# stamped by the PREVIOUS bridge process survived the restart, because the
# fresh process had no _badged memory and an unarmed watchdog - overlays
# outlived a deploy by design. Same sentence, same conclusion: at boot,
# Adom is driving nothing, so any overlay found on a KiCad window is stale.
# CLEAR them, off the boot path.
def _boot_clear():
import time as _t
# John, 2026-09-03: twelve KiCad buttons kept frozen green bars for
# hours after a night of bridge_install restarts. This sweep ran ONCE at
# +6s, while ab itself was still restarting, and every call failed
# silently. Retry across the first two minutes until at least one
# window answers ok, then keep a slow idle reaper alive (below).
for delay in (6, 15, 30, 60, 120):
_t.sleep(delay)
cleared = 0
try:
for h in _enum_all_kicad_hwnds():
r = _ad("desktop_taskbar", {"hwnd": int(h), "overlay": {"badge": "none"},
"progress": {"state": "none"}})
if r:
cleared += 1
except Exception:
cleared = 0
if cleared:
print(f"[taskbar] boot sweep cleared {cleared} KiCad button(s)")
break
_idle_reaper()
def _idle_reaper():
"""Fail-safe, the way ab reaps captions: every 5 minutes while the
bridge is driving nothing, clear progress and badge on every KiCad
window. A bar that nobody is painting is a lie, whoever painted it."""
import time as _t
while True:
_t.sleep(300)
try:
with _inflight_lock:
busy = _inflight > 0
if busy or (_last_drive and (_t.time() - _last_drive) < 60):
continue
for h in _enum_all_kicad_hwnds():
_ad("desktop_taskbar", {"hwnd": int(h), "overlay": {"badge": "none"},
"progress": {"state": "none"}})
except Exception:
pass
threading.Thread(target=_boot_clear, daemon=True,
name="kicad-boot-badge-clear").start()
def _repair():
import time as _t
_t.sleep(8) # off the boot path
try:
_identity_repair()
except Exception:
pass
threading.Thread(target=_repair, daemon=True,
name="kicad-identity-repair").start()
return
# ── ACTIVITY-DRIVEN overlay (John, 2026-08-22) ──────────────────────────────
# THE HIGH-LEVEL GOAL of the Adom overlay: tell the user "Adom is driving
# KiCad for you, right now". Which means it must track ACTIVITY, not ownership:
#
# * John drove his own KiCad via web control and saw NO badge, because the
# old code stamped only windows the bridge had itself spawned. But "who
# launched it" is not what the badge claims - "who is driving it" is, and
# a verb navigating the user's own KiCad is Adom driving it.
# * The badge must also COME OFF when the driving stops, or it decays into
# wallpaper. Five minutes with no KiCad verb -> cleared -> "it's yours".
#
# The 0.9.154 lesson still holds in its correct scope: never badge a user's
# KiCad that Adom is NOT touching (boot-time re-badging of enumerated windows
# was wrong). Activity semantics satisfy both: badge while verbs run against
# KiCad, whoever launched it; clear on idle.
_IDLE_CLEAR_SEC = 300 # "adom is no longer driving kicad, it's yours"
_last_drive = 0.0
_watchdog_on = False
# Verbs currently IN FLIGHT. The first ticker keyed "driving" off the
# note_driving timestamp alone, which is stamped once at dispatch START - so a
# 23s verb had its bar cleared at ~4s, mid-work (caught on a live screenshot:
# the direct paint showed, the ticker's did not). A counter cannot lie about
# in-flight the way a timestamp does.
_inflight = 0
_inflight_lock = threading.Lock()
def _enum_all_kicad_hwnds() -> set:
"""EVERY live KiCad window, not just bridge-owned - during activity the
badge belongs on whatever Adom is actually driving."""
try:
from handlers.win_focus import _enum_kicad_hwnds
return _enum_kicad_hwnds()
except Exception:
return set()
def note_driving() -> None:
"""A KiCad verb is running: stamp the overlay everywhere and reset the
idle clock. Called from the dispatch chokepoint; best-effort, never
blocks a verb."""
global _last_drive, _watchdog_on, _inflight
import time as _t
_last_drive = _t.time()
with _inflight_lock:
_inflight += 1
try:
from handlers.settings import get_settings
if not get_settings().get("overlayBadges", True):
return
except Exception:
pass
try:
for h in _enum_all_kicad_hwnds():
apply_adom_badge(h)
except Exception:
pass
if not _watchdog_on:
_watchdog_on = True
threading.Thread(target=_idle_watchdog, daemon=True,
name="kicad-badge-idle").start()
global _prog_ticker_on
if not _prog_ticker_on:
_prog_ticker_on = True
print("[taskbar] progress ticker starting")
threading.Thread(target=_progress_ticker, daemon=True,
name="kicad-taskbar-progress").start()
def note_done() -> None:
"""The verb finished (success, failure, or raise): its driving is over.
Paired with note_driving at the dispatch chokepoint's finally."""
global _inflight, _last_drive
import time as _t
with _inflight_lock:
_inflight = max(0, _inflight - 1)
_last_drive = _t.time() # idle clock starts when the LAST verb ends
def _idle_watchdog() -> None:
"""Clear the overlay after _IDLE_CLEAR_SEC with no KiCad verb activity.
Checks every 30s; a new verb simply re-stamps, so flapping costs nothing."""
import time as _t
while True:
_t.sleep(30)
try:
if _last_drive and (_t.time() - _last_drive) >= _IDLE_CLEAR_SEC:
# Sweep every enumerated window, not just _badged: process
# restarts (deploys, the footprint-cache remedy) orphan badges
# this set has never heard of.
for h in (_enum_all_kicad_hwnds() | set(_badged)):
clear_adom_badge(h) if h in _badged else _ad(
"desktop_taskbar", {"hwnd": int(h), "overlay": {"badge": "none"}})
except Exception:
pass
# ── Taskbar PROGRESS instead of the orange flash (John, 2026-08-22) ────────
# The orange flash was never ours: this bridge calls FlashWindowEx nowhere.
# Windows itself flashes the button when a backgrounded KiCad's self-raise is
# denied by the focus sentinel - a side effect of the background-first
# contract. And a flash NAGS: it sits there until the user clicks it away, so
# it converts "Adom is working" into "you must do something". A progress bar
# says the same thing and then removes itself, which is the difference between
# informing and demanding. Adopted from the fusion bridge, toggle
# `activityIndicator` (progress|flash|both|none, default progress).
_FLASHW_STOP = 0
def _flash_stop(hwnd) -> None:
"""Cancel an active OS attention-flash on a window (FLASHW_STOP)."""
try:
import ctypes
class FLASHWINFO(ctypes.Structure):
_fields_ = [("cbSize", ctypes.c_uint), ("hwnd", ctypes.c_void_p),
("dwFlags", ctypes.c_uint), ("uCount", ctypes.c_uint),
("dwTimeout", ctypes.c_uint)]
fi = FLASHWINFO(ctypes.sizeof(FLASHWINFO), int(hwnd), _FLASHW_STOP, 0, 0)
ctypes.windll.user32.FlashWindowEx(ctypes.byref(fi))
except Exception:
pass
def _paint_progress(hwnd, percent) -> None:
"""Paint (or clear) the taskbar progress bar on one KiCad button."""
if percent is None:
_ad("desktop_taskbar", {"hwnd": int(hwnd), "progress": {"state": "none"}})
elif percent < 0:
_ad("desktop_taskbar", {"hwnd": int(hwnd),
"progress": {"state": "indeterminate"}})
else:
_ad("desktop_taskbar", {"hwnd": int(hwnd), "progress": {
"state": "normal", "value": max(1, min(100, int(percent)))}})
_prog_ticker_on = False
def _progress_ticker() -> None:
"""While verbs run, mirror the bridge's MEASURED live progress frames onto
every KiCad taskbar button; clear the bars the moment activity stops.
2s cadence with a cached hwnd set - deliberately NOT per-paint enumeration
at 0.7s, which the fusion code review measured at ~780 subprocess spawns
per long verb."""
import time as _t
global _prog_ticker_on
idle_polls = 0
hwnds = set()
try:
while True:
_t.sleep(2)
mode = "progress"
try:
from handlers.settings import get_settings
mode = get_settings().get("activityIndicator", "progress")
except Exception:
pass
with _inflight_lock:
_busy = _inflight > 0
driving = _busy or (_last_drive and (_t.time() - _last_drive) < 3.5)
if driving:
idle_polls = 0
hwnds = _enum_all_kicad_hwnds() or hwnds
pct = None
try:
from handlers import progress as _pg
frames = _pg.live(mine_only=False)
if frames:
pct = min((f.get("percent") for f in frames
if f.get("percent") is not None), default=-1)
pct = -1 if pct is None else pct
except Exception:
pct = -1
for h in hwnds:
if mode in ("progress", "both"):
_paint_progress(h, pct if pct is not None else -1)
if mode in ("progress", "none"):
_flash_stop(h) # the OS flash contradicts the bar
else:
idle_polls += 1
if hwnds:
for h in hwnds:
_paint_progress(h, None) # bars vanish WITH the work
hwnds = set()
if idle_polls > 5:
_prog_ticker_on = False
return # nothing to do; next verb restarts us
except Exception as e:
print(f"[taskbar] ticker DIED: {type(e).__name__}: {e}")
_prog_ticker_on = False