app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
rust: kicad_rescan_libraries (tree refresh first, verified by loading; restart only with force plus confirmRestart)
1 file changed
+105
rust/crates/kicad-bridge/src/verbs_windows.rs+105@@ -241,6 +241,16 @@ pub static VERBS: &[Verb] = &[ related: &["kicad_dismiss_dialogs", "kicad_upgrade"], pitfalls: &["a real GPU / Hyper-V GPU-P passthrough is far better; this is CPU rendering, slow", "writing KiCad's bin needs an elevated Bridge"], },+ Verb {+ name: "kicad_rescan_libraries",+ summary: "Make newly installed parts visible ONCE: reload every open editor's library tree in place (seconds, nothing closed), then verify each listed footprint by loading it in the Footprint Editor. force:true is the process restart, the last resort.",+ mechanism: Mechanism::Window, risk: "write", timeout_sec: 200,+ input: "{\"footprints\"?: [\"Lib:Name\", ...], \"force\"?: bool, \"confirmRestart\"?: bool}",+ example: "kicad_rescan_libraries {\"footprints\":[\"Adom:LQFP-48_STM32G431\"]}",+ hint: "Wiki #51: install N parts, call this once, then show them all. Pass footprints so the reply can say what KiCad can load now; without the list nothing is verified. A restart (force) refuses while any KiCad runs unless confirmRestart:true, because the native build cannot yet tell a user-launched KiCad from its own (the ownership ledger arrives with phase 4).",+ related: &["kicad_install_footprint", "kicad_show_footprint", "kicad_list_footprints"],+ pitfalls: &["KiCad 10 caches footprint libraries per process; the tree refresh reveals parts installed after the editor started (measured 2026-09-07), a full restart is only for the cases it does not."],+ }, ]; /// Verbs that may spawn or raise a KiCad window: they get the 5-round settle sweep and@@ -261,6 +271,7 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> let fg_before = if gui { native().foreground().ok().filter(|h| *h != 0) } else { None }; let mut out = match command { "kicad_launch" => launch(state, args),+ "kicad_rescan_libraries" => rescan_libraries(state, args), "kicad_open_board" => open_file(state, args, WindowKind::PcbEditor), "kicad_open_schematic" => open_file(state, args, WindowKind::SchematicEditor), "kicad_open_symbol_editor" => open_symbol_editor(state, args),@@ -2705,3 +2716,97 @@ mod tests { } } }+++/// kicad_rescan_libraries: tree refresh first (#51), restart only on force plus confirmation.+fn rescan_libraries(state: &mut State, args: &Value) -> Value {+ let force = args.get("force").and_then(Value::as_bool).unwrap_or(false);+ if !force {+ let mut refreshed: Vec<u64> = Vec::new();+ for kind in [WindowKind::FootprintEditor, WindowKind::SymbolEditor] {+ if let Some(h) = first_of_kind(kind) {+ if let Ok(Some(_)) = menu_invoke_by_text(h, "refresh") {+ refreshed.push(h);+ }+ }+ }+ if !refreshed.is_empty() {+ // The tree reloads asynchronously; the Python waited 3 s before probing.+ std::thread::sleep(std::time::Duration::from_secs(3));+ invalidate();+ let mut wanted: Vec<(String, String)> = Vec::new();+ for spec in args.get("footprints").and_then(Value::as_array).cloned().unwrap_or_default() {+ match spec {+ Value::String(s) if s.contains(':') => {+ let (lib, name) = s.split_once(':').unwrap();+ wanted.push((lib.to_string(), name.to_string()));+ }+ Value::Object(o) => {+ if let Some(name) = o.get("name").and_then(Value::as_str) {+ wanted.push((o.get("library").and_then(Value::as_str).unwrap_or("Adom").to_string(), name.to_string()));+ }+ }+ _ => {}+ }+ }+ let mut probed: Vec<Value> = Vec::new();+ if !wanted.is_empty() {+ let fp = first_of_kind(WindowKind::FootprintEditor);+ for (lib, name) in &wanted {+ let (ok, trace, _mech) = match fp {+ Some(h) => navigate_tree(h, &format!("{lib} {name}"), Verify::Footprint { name, library: lib }),+ None => (false, json!({}), "none"),+ };+ probed.push(json!({+ "library": lib, "name": name, "visible": ok,+ "error": if ok { Value::Null } else if fp.is_none() { json!("no Footprint Editor open") } else { json!("editor tree did not load it") },+ "navigation": trace,+ }));+ }+ }+ let verified = !wanted.is_empty() && probed.iter().all(|p| p["visible"].as_bool().unwrap_or(false));+ let mut out = json!({"success": true, "method": "tree-refresh", "refreshedEditors": refreshed, "verified": verified, "probed": probed});+ out["_hint"] = json!(if !wanted.is_empty() && !verified {+ format!("Tree refreshed but KiCad still cannot load: {}. Check the name with kicad_list_footprints; if it is on disk, call again with force:true and confirmRestart:true for the process restart (about a minute).",+ probed.iter().filter(|p| !p["visible"].as_bool().unwrap_or(false)).map(|p| format!("{}:{}", p["library"].as_str().unwrap_or(""), p["name"].as_str().unwrap_or(""))).collect::<Vec<_>>().join(", "))+ } else if !wanted.is_empty() {+ "Verified in the editor: every listed footprint loaded after the refresh, no restart. The last one is now shown in the Footprint Editor.".to_string()+ } else {+ "Refresh invoked on the open editor tree(s). Pass footprints:[\"Lib:Name\"] to have each loaded in the editor as proof; without that list nothing is verified. force:true is the process restart, the last resort.".to_string()+ });+ post_verb(state, "kicad_rescan_libraries", args, &mut out, None);+ return out;+ }+ }+ // Restart path: the native build has no ownership ledger yet (phase 4), so a running+ // KiCad is treated as the user's unless the caller confirms.+ let running = native().processes(&["kicad.exe", "pcbnew.exe", "eeschema.exe"]).unwrap_or_default();+ let confirm = args.get("confirmRestart").and_then(Value::as_bool).unwrap_or(false);+ if !running.is_empty() && !confirm {+ return json!({"success": false, "errorCode": "rescan_refused",+ "reason": "KiCad is running and the native build cannot yet tell a user-launched KiCad from its own",+ "runningPids": running.iter().map(|p| p.pid).collect::<Vec<_>>(),+ "_hint": "Ask the user whether KiCad may be restarted, then call again with force:true and confirmRestart:true. Without force the verb refreshes the open editors' trees in place, which is enough for footprints installed after the editor started."});+ }+ if !running.is_empty() {+ let closed = dispatch(state, "kicad_close", &json!({"force": true})).unwrap_or_else(|| json!({}));+ if !closed["success"].as_bool().unwrap_or(false) {+ return json!({"success": false, "errorCode": "restart_close_failed", "close": closed});+ }+ std::thread::sleep(std::time::Duration::from_secs(4));+ }+ let launched = launch(state, &json!({}));+ if !launched["success"].as_bool().unwrap_or(false) {+ return json!({"success": false, "errorCode": "restart_launch_failed", "launch": launched});+ }+ for _ in 0..80 {+ std::thread::sleep(std::time::Duration::from_millis(750));+ invalidate();+ if !kicad_windows().is_empty() {+ return json!({"success": true, "restarted": true, "ready": true, "method": "process-restart",+ "_hint": "Library caches rebuilt. Every part installed before this call is now visible; show verbs will not trigger another restart until the next install-while-open."});+ }+ }+ json!({"success": false, "restarted": true, "ready": false, "errorCode": "restart_no_window",+ "_hint": "KiCad was restarted but no window appeared within 60 s; kicad_window_info shows what is up."})+}