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
1.0.35: 1.0.35: three fixes from a from-scratch install on a clean Windows VM (KiCad 10.0.6, no GPU). kicad_upgrade's f…
2a70935
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
//! KiCad's OWN AppUserModelID onto KiCad's OWN standalone Start Menu shortcuts.
//!
//! The white taskbar icon, solved end to end on John's taskbar on 2026-08-23 and recorded
//! in skills/kicad-bridge-dev/SKILL.md ("THE WHITE-ICON MECHANISM"): a standalone editor
//! process, which is how this bridge launches pcbnew, presents a runtime-built AUMID such
//! as `Kicad.Kicad.pcbnew.10.0`. KiCad's 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. Some machines fall back to the
//! window's own icon; others paint the generic white page. Writing KiCad's own AUMID onto
//! KiCad's own user-level shortcut makes the resolution deterministic, the exact mechanism
//! that keeps the main KiCad button correct. No Adom identity is involved anywhere: this
//! bridge stamps NO AUMIDs of its own, ever.
//!
//! The Python bridge did this through inline C# in PowerShell (`handlers/win_taskbar.py`,
//! `_repair_kicad_shortcut_aumids`); this is the same operation in process. Idempotent: a
//! shortcut that already carries any AUMID is left alone. The shell reads the mapping when
//! it next rebuilds its Start Menu index (logon or an explorer restart); `SHChangeNotify`
//! is sent for each written file to invite an earlier refresh, and explorer is never
//! restarted from here.
use std::path::{Path, PathBuf};
use windows::core::{Interface, GUID, HSTRING, PWSTR};
use windows::Win32::Foundation::{PROPERTYKEY, RPC_E_CHANGED_MODE};
use windows::Win32::System::Com::StructuredStorage::{PropVariantClear, PROPVARIANT};
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CoTaskMemAlloc, CoTaskMemFree, CoUninitialize, IPersistFile,
CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, STGM_READWRITE,
};
use windows::Win32::System::Variant::VT_LPWSTR;
use windows::Win32::UI::Shell::PropertiesSystem::IPropertyStore;
use windows::Win32::UI::Shell::{IShellLinkW, SHChangeNotify, ShellLink, SHCNE_UPDATEITEM, SHCNF_FLUSHNOWAIT, SHCNF_PATHW};
/// `System.AppUserModel.ID` (propkey.h): {9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}, 5.
const PKEY_APPUSERMODEL_ID: PROPERTYKEY = PROPERTYKEY { fmtid: GUID::from_u128(0x9F4C2855_9F79_4B39_A8D0_E1D42DE1D5F3), pid: 5 };
/// One shortcut's fate.
#[derive(Debug, Clone)]
pub struct ShortcutRepair {
pub path: String,
pub aumid: String,
/// `written`, `already` (carried an AUMID, untouched), `missing` (no such shortcut), `error`.
pub outcome: &'static str,
pub detail: Option<String>,
}
/// The standalone shortcuts KiCad's installer writes without an AUMID, with the AUMID each
/// running process actually presents (read from the taskbar buttons' AutomationIds).
/// `version` is major.minor ("10.0"): KiCad names both the folder and the shortcuts by it.
pub fn shortcuts_for(version: &str) -> Vec<(PathBuf, String)> {
// A per-user install puts its Start Menu folder under %APPDATA%; an all-users install (what
// kicad_upgrade does when the bridge runs elevated) puts it under %ProgramData%. Looking only
// at the first reported all three shortcuts missing on a fresh all-users box (winvm, 10.0.6).
let folder = |base: std::ffi::OsString| Path::new(&base).join("Microsoft").join("Windows").join("Start Menu").join("Programs").join(format!("KiCad {version}"));
let roots: Vec<PathBuf> = ["APPDATA", "ProgramData"].iter().filter_map(|v| std::env::var_os(v)).map(folder).collect();
if roots.is_empty() {
return Vec::new();
}
[
(format!("PCB Editor {version} (standalone).lnk"), format!("Kicad.Kicad.pcbnew.{version}")),
(format!("Schematic Editor {version} (standalone).lnk"), format!("Kicad.Kicad.eeschema.{version}")),
(format!("Gerber Viewer {version}.lnk"), format!("Kicad.Kicad.gerbview.{version}")),
]
.into_iter()
.map(|(name, aumid)| {
let found = roots.iter().map(|r| r.join(&name)).find(|p| p.is_file());
(found.unwrap_or_else(|| roots[0].join(&name)), aumid)
})
.collect()
}
/// COM apartment for this thread, released on drop; a thread already in an apartment of
/// the other kind keeps it (RPC_E_CHANGED_MODE is not a failure for us).
struct Com(bool);
impl Com {
fn init() -> Result<Com, String> {
// SAFETY: CoInitializeEx on the current thread; balanced in Drop only when this
// call took the reference.
let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
if hr.is_ok() {
Ok(Com(true))
} else if hr == RPC_E_CHANGED_MODE {
Ok(Com(false))
} else {
Err(format!("CoInitializeEx: {hr}"))
}
}
}
impl Drop for Com {
fn drop(&mut self) {
if self.0 {
// SAFETY: matches the successful CoInitializeEx; every COM object made in
// between is dropped before this runs (they live inside repair_one).
unsafe { CoUninitialize() };
}
}
}
/// Fill the AUMID on every standalone shortcut that lacks one. Never fails as a whole:
/// each shortcut reports its own outcome, and a missing shortcut is `missing`, not an error.
pub fn repair_kicad_shortcut_aumids(version: &str) -> Result<Vec<ShortcutRepair>, String> {
let _com = Com::init()?;
let mut out = Vec::new();
for (path, aumid) in shortcuts_for(version) {
let shown = path.to_string_lossy().replace('\\', "/");
if !path.is_file() {
out.push(ShortcutRepair { path: shown, aumid, outcome: "missing", detail: None });
continue;
}
match repair_one(&path, &aumid) {
Ok(true) => out.push(ShortcutRepair { path: shown, aumid, outcome: "written", detail: None }),
Ok(false) => out.push(ShortcutRepair { path: shown, aumid, outcome: "already", detail: None }),
Err(e) => out.push(ShortcutRepair { path: shown, aumid, outcome: "error", detail: Some(e) }),
}
}
Ok(out)
}
/// Ok(true) written, Ok(false) already carried an AUMID.
fn repair_one(path: &Path, aumid: &str) -> Result<bool, String> {
let wide = HSTRING::from(path.as_os_str());
// SAFETY: standard COM object creation and QueryInterface on a live object; every
// pointer handed to the shell outlives the call that reads it.
unsafe {
let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER).map_err(|e| format!("CoCreateInstance(ShellLink): {e}"))?;
let file: IPersistFile = link.cast().map_err(|e| format!("IPersistFile: {e}"))?;
file.Load(&wide, STGM_READWRITE).map_err(|e| format!("Load: {e}"))?;
let store: IPropertyStore = link.cast().map_err(|e| format!("IPropertyStore: {e}"))?;
let mut current = store.GetValue(&PKEY_APPUSERMODEL_ID).map_err(|e| format!("GetValue: {e}"))?;
let existing = read_lpwstr(¤t);
let _ = PropVariantClear(&mut current);
if existing.map(|s| !s.is_empty()).unwrap_or(false) {
return Ok(false);
}
let mut value = lpwstr_variant(aumid);
let set = store.SetValue(&PKEY_APPUSERMODEL_ID, &value).map_err(|e| format!("SetValue: {e}"));
let commit = set.and_then(|_| store.Commit().map_err(|e| format!("Commit: {e}")));
free_lpwstr_variant(&mut value);
commit?;
file.Save(&wide, true).map_err(|e| format!("Save: {e}"))?;
// Invite explorer to refresh its view of this item now rather than at next logon.
SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATHW | SHCNF_FLUSHNOWAIT, Some(wide.as_ptr() as *const _), None);
}
Ok(true)
}
/// The string in a VT_LPWSTR PROPVARIANT, None for any other type (VT_EMPTY on a shortcut
/// that has no AUMID).
unsafe fn read_lpwstr(pv: &PROPVARIANT) -> Option<String> {
let inner = &pv.Anonymous.Anonymous;
if inner.vt != VT_LPWSTR {
return None;
}
let p: PWSTR = inner.Anonymous.pwszVal;
if p.is_null() {
return Some(String::new());
}
Some(p.to_string().unwrap_or_default())
}
/// A VT_LPWSTR PROPVARIANT owning a CoTaskMem copy of `s` (what IPropertyStore expects).
unsafe fn lpwstr_variant(s: &str) -> PROPVARIANT {
let mut units: Vec<u16> = s.encode_utf16().collect();
units.push(0);
let bytes = units.len() * 2;
let mem = CoTaskMemAlloc(bytes) as *mut u16;
std::ptr::copy_nonoverlapping(units.as_ptr(), mem, units.len());
let mut pv = PROPVARIANT::default();
let inner = &mut *(&mut pv.Anonymous.Anonymous as *mut _ as *mut windows::Win32::System::Com::StructuredStorage::PROPVARIANT_0_0);
inner.vt = VT_LPWSTR;
inner.Anonymous.pwszVal = PWSTR(mem);
pv
}
unsafe fn free_lpwstr_variant(pv: &mut PROPVARIANT) {
let inner = &pv.Anonymous.Anonymous;
let p = inner.Anonymous.pwszVal;
if !p.is_null() {
CoTaskMemFree(Some(p.0 as *const _));
}
}