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.
← Commit history
Merge PR #416: 1.0.101: kicad_show_library shows the library instead of walking its parts
4 files changed
+90−19
rust/Cargo.lock+3−3@@ -319,7 +319,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.100"+version = "1.0.101" dependencies = [ "adom-eda-checks", "kicad-core",@@ -331,7 +331,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.100"+version = "1.0.101" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -358,7 +358,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.100"+version = "1.0.101" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.100"+version = "1.0.101" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs_show.rs+1−1@@ -123,7 +123,7 @@ pub static VERBS: &[Verb] = &[ mechanism: Mechanism::Window, risk: "read", timeout_sec: 130, input: "{\"libraryName\": \"Device\", \"kind\"?: \"symbol\"|\"footprint\", \"symbolName\"?: \"...\", \"footprintName\"?: \"...\", \"capture\"?: bool, \"foreground\"?: bool, \"foregroundReason\"?: \"...\"}", example: "kicad_show_library {\"libraryName\":\"Device\"}",- hint: "kicad_show_library {\"libraryName\":\"Device\"} (add kind:\"footprint\" for the fp side). Filters the tree to the library. A symbolName/footprintName passed alongside is honoured, not dropped (#49 follow-up).",+ hint: "kicad_show_library {\"libraryName\":\"Device\"} (add kind:\"footprint\" for the fp side). Lists the whole library in the tree and loads nothing, in the background. A warm editor's canvas keeps the part it had: canvasPart names it and canvasFromOtherLibrary says if a proof image shows another library's part. A symbolName/footprintName passed alongside is honoured, not dropped (#49 follow-up).", related: &["kicad_show_symbol", "kicad_show_footprint", "kicad_list_symbols"], pitfalls: &["the library must be registered in the sym-/fp-lib-table"], },
rust/crates/kicad-bridge/src/verbs_windows.rs+85−14@@ -1435,6 +1435,39 @@ fn verify_title(title: &str, v: Verify<'_>) -> bool { /// LIB_TREE filter, Down through the filter box, Enter posted to the tree candidates, /// Down x1..3 then Enter, a posted double-click on the first row. Zero focus. /// Returns (confirmed, trace, mechanism).+/// The tree filter that lists one library whole. KiCad reads "Lib:" as "every part of Lib" and+/// nothing else; the bare name is a text search, which drops parts that don't say it and pulls in+/// other libraries' parts that do. KiCad splits the filter on spaces, so a name with one stays bare.+fn library_filter(library: &str) -> String {+ let name = library.trim().trim_end_matches(':');+ if name.chars().any(char::is_whitespace) { name.to_string() } else { format!("{name}:") }+}++/// Whether a navigation goes on to load a part, or only filters the tree.+fn loads_a_part(verify: Verify<'_>) -> bool {+ !matches!(verify, Verify::Never)+}++/// The reply of a library browse. The canvas is left alone, so on a warm editor it still shows+/// whatever was there: say so, because a proof image of it shows that part and not the library's.+fn library_browse_reply(ok: bool, title: &str, editor_word: &str, library: &str, nothing: &str) -> Value {+ if !ok {+ return json!({"success": false, "errorCode": "library_filter_failed", "error": format!("The {editor_word} is open but its library filter could not be set to '{library}'"),+ "_hint": "Read navigation.steps. A hidden library tree pane is the usual cause: show it from the editor's View menu."});+ }+ let (lib, part) = wm::parse_editor_title(title, editor_word);+ let mut out = json!({"success": true, "libraryShown": library});+ if part.is_empty() {+ out["output"] = json!(format!("{editor_word} open with the '{library}' library filtered. {nothing}"));+ } else {+ let id = if lib.is_empty() { part.clone() } else { format!("{lib}:{part}") };+ out["canvasPart"] = json!(id);+ out["canvasFromOtherLibrary"] = json!(!lib.eq_ignore_ascii_case(library));+ out["output"] = json!(format!("{editor_word} open with the '{library}' library filtered. This call loaded nothing; the canvas still shows {id} from before."));+ }+ out+}+ fn navigate_tree(frame: u64, filter: &str, verify: Verify<'_>) -> (bool, Value, &'static str) { let mut steps: Vec<String> = Vec::new(); let mut trace = json!({"filter": filter, "editorHwnd": frame, "titleBefore": title_of(frame)});@@ -1472,6 +1505,13 @@ fn navigate_tree(frame: u64, filter: &str, verify: Verify<'_>) -> (bool, Value, } } sleep_ms(1400); // LIB_TREE search debounce on the UI thread + tree rebuild+ // A library browse asks for no part: everything below this exists to load one, and with no+ // part to confirm it would walk the library row by row and leave the last one on the canvas.+ if !loads_a_part(verify) {+ steps.push("library browse: filter set, no part loaded".into());+ trace["steps"] = json!(steps);+ return (true, trace, mechanism);+ } // Down THROUGH the filter box: KiCad forwards nav keys to the tree. post_vk(edit.hwnd, wm::VK_DOWN); sleep_ms(500);@@ -2120,13 +2160,13 @@ fn open_symbol_editor_inner(info: &KicadInfo, args: &Value, symbol_name: &str, l } // Library-only browse mode (0.9.92): filter the tree to the library, no part load. if symbol_name.is_empty() {- let (_, trace, nav_mech) = navigate_tree(frame, library_name, Verify::Never);- return json!({- "success": true, "hwnd": frame, "libraryShown": library_name, "mechanism": nav_mech, "openedVia": mechanism, "navigation": trace,- "output": format!("Symbol Editor open with the '{library_name}' library filtered and selected. No symbol was requested, so none was loaded into the canvas."),- "via": "win32-library-browse",- "_hint": "Library browse mode: the tree is filtered to the library. To load a part into the canvas, call again with symbolName too.",- });+ let (ok, trace, nav_mech) = navigate_tree(frame, &library_filter(library_name), Verify::Never);+ let mut v = library_browse_reply(ok, &title_of(frame), "Symbol Editor", library_name, "No symbol was requested, so none was loaded into the canvas.");+ merge(&mut v, json!({"hwnd": frame, "mechanism": nav_mech, "openedVia": mechanism, "navigation": trace, "via": "win32-library-browse"}));+ if ok {+ v["_hint"] = json!("Library browse mode: the tree is filtered to the library. To load a part into the canvas, call again with symbolName too.");+ }+ return v; } let filter = if library_name.is_empty() { symbol_name.to_string() } else { format!("{library_name} {symbol_name}") }; let verify = Verify::Symbol { name: symbol_name, library: library_name };@@ -2262,13 +2302,13 @@ pub(crate) fn open_footprint_editor(state: &mut State, args: &Value) -> Value { return json!({"success": true, "hwnd": frame, "mechanism": mechanism, "pathwaysTried": pathways, "output": if mechanism == "existing" { "Footprint Editor is already open.".to_string() } else { format!("Footprint Editor opened successfully ({mechanism}).") }}); } if footprint_name.is_empty() {- let (_, trace, nav_mech) = navigate_tree(frame, &library_name, Verify::Never);- return json!({- "success": true, "hwnd": frame, "libraryShown": library_name, "mechanism": nav_mech, "openedVia": mechanism, "navigation": trace,- "output": format!("Footprint Editor open with the '{library_name}' library filtered and selected. No footprint was requested, so none was loaded into the canvas."),- "via": "win32-library-browse",- "_hint": "Library browse mode: the tree is filtered to the library. To load a part, call again with footprintName too.",- });+ let (ok, trace, nav_mech) = navigate_tree(frame, &library_filter(&library_name), Verify::Never);+ let mut v = library_browse_reply(ok, &title_of(frame), "Footprint Editor", &library_name, "No footprint was requested, so none was loaded into the canvas.");+ merge(&mut v, json!({"hwnd": frame, "mechanism": nav_mech, "openedVia": mechanism, "navigation": trace, "via": "win32-library-browse"}));+ if ok {+ v["_hint"] = json!("Library browse mode: the tree is filtered to the library. To load a part, call again with footprintName too.");+ }+ return v; } let filter = if library_name.is_empty() { footprint_name.clone() } else { format!("{library_name} {footprint_name}") }; let verify = Verify::Footprint { name: &footprint_name, library: &library_name };@@ -3440,6 +3480,37 @@ fn enable_software_opengl(state: &mut State) -> Value { #[cfg(test)] mod tests {+ #[test]+ fn a_library_browse_filters_on_the_library_itself() {+ assert_eq!(super::library_filter("Timer"), "Timer:");+ assert_eq!(super::library_filter(" Timer: "), "Timer:");+ assert_eq!(super::library_filter("My Parts"), "My Parts", "KiCad splits the filter on spaces");+ }++ #[test]+ fn a_library_browse_never_loads_a_part() {+ assert!(!super::loads_a_part(super::Verify::Never), "a browse with no part must stop at the filter");+ assert!(super::loads_a_part(super::Verify::Symbol { name: "R", library: "Device" }));+ }++ #[test]+ fn a_library_browse_says_what_the_canvas_still_shows() {+ let e = "Symbol Editor";+ let empty = super::library_browse_reply(true, "[no symbol loaded] \u{2014} Symbol Editor", e, "Tester", "None was loaded.");+ assert_eq!(empty["libraryShown"], "Tester");+ assert!(empty.get("canvasPart").is_none(), "{empty}");+ assert!(empty["output"].as_str().unwrap().contains("None was loaded."));+ let other = super::library_browse_reply(true, "MCU_Module:Flipper_Zero \u{2014} Symbol Editor", e, "Tester", "None was loaded.");+ assert_eq!(other["canvasPart"], "MCU_Module:Flipper_Zero");+ assert_eq!(other["canvasFromOtherLibrary"], true);+ assert!(other["output"].as_str().unwrap().contains("from before"), "{other}");+ let same = super::library_browse_reply(true, "Tester:BZT52C18Q \u{2014} Symbol Editor", e, "tester", "");+ assert_eq!(same["canvasFromOtherLibrary"], false);+ let bad = super::library_browse_reply(false, "", e, "Tester", "");+ assert_eq!(bad["success"], false);+ assert_eq!(bad["errorCode"], "library_filter_failed");+ }+ use super::*; #[test] fn viewer_selection_is_bound_to_source_and_refuses_ambiguity(){