← Commit history

rust: kicad_rescan_libraries (tree refresh first, verified by loading; restart only with force plus confirmRestart)

John Lauer ·012b0387ab ·28d ago ·parent 46c748c
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."})+}