← Commit history

v0.9.152: Adom overlay badges on bridge-driven KiCad taskbar buttons (ITaskbarList3, pup pattern) + kicad_get_settings/kicad_set_settings (overlayBadges toggle, dashboard backend)

John Lauer ·b3eb86f26d ·1mo ago ·parent ac1ac92
9 files changed +240−8
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.151+0.9.152
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.151",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.151/adom-bridge-kicad-v0.9.151.zip",-  "sha256": "a5e38ba6cfc2b1f15a2d3a5cdbdd633937d6be683ee2c49a3c337647b87fdb38",-  "size": 420496,+  "version": "0.9.152",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.152/adom-bridge-kicad-v0.9.152.zip",+  "sha256": "b1e0e81e561c111003e2f0798b484473a03952f86aefa1937bc196d5a7a73e03",+  "size": 427311,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-17T15:01:11Z",+  "released_at": "2026-08-17T15:05:24Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.152.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.151",+  "version": "0.9.152",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
handlers/settings.pyadded+78
@@ -0,0 +1,78 @@+"""settings.py — kicad_get_settings / kicad_set_settings: the bridge's+user-visible preferences, persisted per box.++Born 2026-08-17 (John): "place adom favicon as overlay icons into all your+kicad windows taskbar icons ... but this should be a Setting in your settings+window in your dashboard." These verbs are the settings BACKEND; the dashboard+(kicad launch page, modeled on fusion-dashboard) renders them.++Settings live in %USERPROFILE%/.adom/kicad-bridge-settings.json.++Current settings:+  overlayBadges (bool, default true) — stamp the Adom favicon as a taskbar+    overlay on every bridge-created KiCad window, so the user can see at a+    glance which windows Adom is driving.+"""+import json+import os+from pathlib import Path++_DEFAULTS = {+    "overlayBadges": True,+}+++def _path() -> Path:+    base = os.environ.get("USERPROFILE") or str(Path.home())+    d = Path(base) / ".adom"+    d.mkdir(parents=True, exist_ok=True)+    return d / "kicad-bridge-settings.json"+++def get_settings() -> dict:+    out = dict(_DEFAULTS)+    try:+        out.update(json.loads(_path().read_text(encoding="utf-8")))+    except Exception:+        pass+    return out+++def handle_get_settings(kicad_info: dict, args: dict) -> dict:+    s = get_settings()+    return {+        "success": True,+        "settings": s,+        "defaults": dict(_DEFAULTS),+        "path": str(_path()),+        "_hint": ("kicad_set_settings {\"overlayBadges\": false} to change; changes apply "+                  "immediately (overlay badges are stamped/cleared on live windows)."),+    }+++def handle_set_settings(kicad_info: dict, args: dict) -> dict:+    s = get_settings()+    changed = {}+    for k in _DEFAULTS:+        if k in args:+            v = bool(args[k])+            if s.get(k) != v:+                changed[k] = v+            s[k] = v+    if not changed:+        return {"success": True, "settings": s, "changed": {},+                "output": "no settings changed",+                "_hint": f"Settable keys: {', '.join(_DEFAULTS)}."}+    try:+        _path().write_text(json.dumps(s, indent=2), encoding="utf-8")+    except Exception as e:+        return {"success": False, "error": f"could not persist settings: {e}"}+    applied = {}+    if "overlayBadges" in changed:+        try:+            from handlers.win_taskbar import rebadge_all+            applied["rebadgedWindows"] = rebadge_all(changed["overlayBadges"])+        except Exception:+            pass+    return {"success": True, "settings": s, "changed": changed, **applied,+            "output": f"settings updated: {changed}"}
handlers/win_focus.py+5
@@ -416,6 +416,11 @@ def _start_park_sweep() -> None:                             _bridge_windows.add(h)                             suppress_activation(h)                             _park_offscreen(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
handlers/win_taskbar.pyadded+139
@@ -0,0 +1,139 @@+"""win_taskbar.py — taskbar overlay badges via ITaskbarList3::SetOverlayIcon.++John's design (2026-08-17, after seeing pup's overlay badges): stamp the Adom+favicon as an OVERLAY on every bridge-created 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).++Mechanics mirror pup's: Windows' ITaskbarList3 COM interface takes a per-window+HICON overlay drawn in the button's corner. The COM call is made from the+parking sweep thread (CoInitialize'd once per thread).+"""+import os+import sys+from pathlib import Path++IS_WINDOWS = sys.platform == "win32"++if IS_WINDOWS:+    import ctypes+    import ctypes.wintypes+    from ctypes import POINTER, byref, c_void_p++_ICON_PATH = str(Path(__file__).resolve().parent.parent / "resources" / "adom-overlay.ico")++_taskbar = None          # ITaskbarList3 pointer (per process, sweep thread)+_overlay_icon = None     # HICON, loaded once+_com_ready = False+_badged: set = set()+++def _guid(s: str):+    class GUID(ctypes.Structure):+        _fields_ = [("Data1", ctypes.c_uint32), ("Data2", ctypes.c_uint16),+                    ("Data3", ctypes.c_uint16), ("Data4", ctypes.c_ubyte * 8)]+    import uuid+    u = uuid.UUID(s)+    g = GUID()+    g.Data1, g.Data2, g.Data3 = u.time_low, u.time_mid, u.time_hi_version+    rest = u.bytes[8:]+    for i in range(8):+        g.Data4[i] = rest[i]+    return g+++def _ensure_taskbar() -> bool:+    """CoCreate ITaskbarList3 on the CALLING thread (call from one thread only)."""+    global _taskbar, _overlay_icon, _com_ready+    if not IS_WINDOWS:+        return False+    if _taskbar is not None:+        return True+    try:+        ole32 = ctypes.windll.ole32+        if not _com_ready:+            ole32.CoInitialize(None)+            _com_ready = True+        CLSID_TaskbarList = _guid("56FDF344-FD6D-11d0-958A-006097C9A090")+        IID_ITaskbarList3 = _guid("EA1AFB91-9E28-4B86-90E9-9E9F8A5EECAF")+        p = c_void_p()+        CLSCTX_INPROC_SERVER = 1+        hr = ole32.CoCreateInstance(byref(CLSID_TaskbarList), None,+                                    CLSCTX_INPROC_SERVER,+                                    byref(IID_ITaskbarList3), byref(p))+        if hr != 0 or not p.value:+            return False+        # vtable: [0]QI [1]AddRef [2]Release [3]HrInit ... [18]SetOverlayIcon+        vtbl = ctypes.cast(p, POINTER(POINTER(c_void_p))).contents+        HrInit = ctypes.WINFUNCTYPE(ctypes.c_long, c_void_p)(vtbl[3])+        if HrInit(p) != 0:+            return False+        _taskbar = p+        # load the overlay icon once (16px is what the badge renders at)+        if os.path.isfile(_ICON_PATH):+            LR_LOADFROMFILE = 0x0010+            IMAGE_ICON = 1+            _overlay_icon = ctypes.windll.user32.LoadImageW(+                None, _ICON_PATH, IMAGE_ICON, 16, 16, LR_LOADFROMFILE)+        return True+    except Exception:+        return False+++def _set_overlay(hwnd, hicon, description: str) -> bool:+    try:+        vtbl = ctypes.cast(_taskbar, POINTER(POINTER(c_void_p))).contents+        SetOverlayIcon = ctypes.WINFUNCTYPE(+            ctypes.c_long, c_void_p, ctypes.wintypes.HWND,+            c_void_p, ctypes.wintypes.LPCWSTR)(vtbl[18])+        return SetOverlayIcon(_taskbar, hwnd, hicon, description) == 0+    except Exception:+        return False+++def apply_adom_badge(hwnd) -> bool:+    """Stamp the Adom overlay on a window's taskbar button (idempotent)."""+    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+    if not _ensure_taskbar() or not _overlay_icon:+        return False+    ok = _set_overlay(hwnd, _overlay_icon, "Adom is driving this KiCad window")+    if ok:+        _badged.add(hwnd)+    return ok+++def clear_adom_badge(hwnd) -> None:+    try:+        if hwnd in _badged and _ensure_taskbar():+            _set_overlay(hwnd, None, "")+        _badged.discard(hwnd)+    except Exception:+        pass+++def rebadge_all(enable: bool) -> int:+    """Setting flipped: apply/clear the badge on every bridge-created window."""+    n = 0+    try:+        from handlers.win_focus import _bridge_windows+        import ctypes as _ct+        for h in list(_bridge_windows):+            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
resources/adom-overlay.icoadded
⋯ 1 unchanged line ⋯
server.py+11−1
@@ -68,6 +68,7 @@ from handlers.export_molecule import handle_export_molecule from handlers.demo import handle_demo from handlers.make_part_project import handle_make_part_project from handlers.export_part import handle_export_part+from handlers.settings import handle_get_settings, handle_set_settings # v0.9.10+: check/upgrade KiCad via the OFFICIAL downloads.kicad.org installer # (NOT winget, which lags the real release). check_for_updates is read-only; # upgrade silent-installs the official signed NSIS build.@@ -367,6 +368,12 @@ _VERB_CATALOG = {     "export_step": {"summary": "Export a STEP 3D model from a board.", "long": False,         "hint": "Absolute board path + output; exports a STEP assembly via kicad-cli.",         "related": ["kicad_open_3d_viewer", "kicad_export_gerber"], "pitfalls": ["only components with 3D models attached appear in the STEP"]},+    "get_settings": {"summary": "Read the bridge's user-visible settings (overlay badges, ...).", "long": False,+        "hint": "kicad_get_settings {} returns settings+defaults. Currently: overlayBadges (Adom favicon stamped on bridge-driven KiCad taskbar buttons, default on).",+        "related": ["kicad_set_settings"], "pitfalls": []},+    "set_settings": {"summary": "Change bridge settings; applies immediately (e.g. overlay badges stamped/cleared on live windows).", "long": False,+        "hint": "kicad_set_settings {\"overlayBadges\": false}.",+        "related": ["kicad_get_settings"], "pitfalls": []},     "export_part": {"summary": "One-off single-part library exports: a .kicad_sym with ONLY that symbol, a .pretty with ONLY that footprint, the bare 3D model file — the reverse of install.", "long": False,         "hint": "kicad_export_part {\"partName\":\"IN-S42ATA\"} (optional what:[\"symbol\",\"footprint\",\"model\"]). Writes to Documents/adom-part-exports/<part>/; pull with ab pull_file. Proves per-asset round-trip.",         "related": ["kicad_install_symbol", "kicad_install_footprint", "kicad_make_part_project"],@@ -549,6 +556,8 @@ COMMAND_HANDLERS = {     "demo": handle_demo,     "make_part_project": handle_make_part_project,     "export_part": handle_export_part,+    "get_settings": handle_get_settings,+    "set_settings": handle_set_settings,     "export_bom_csv": handle_export_bom_csv,     # v0.9.10+: KiCad version check + upgrade via the official downloads.kicad.org     # installer (read-only check; silent /S install). Exposed as kicad_check_for_updates@@ -614,7 +623,8 @@ def dispatch_command(command: str, args: dict) -> dict:     #   diagnostics            — self-inspection; MOST needed when KiCad is undetected     # None of these need the reverse-bridge plugin (there may be no KiCad to load     # it into), so they also skip ensure_plugin_installed_once.-    if command in ("readiness", "describe", "check_for_updates", "diagnostics"):+    if command in ("readiness", "describe", "check_for_updates", "diagnostics",+                   "get_settings", "set_settings"):         return handler(kicad_info, args)      # upgrade INSTALLS KiCad from scratch — the whole point is it runs when KiCad