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
1.0.36: 1.0.36: three more fixes from the full verb battery on a from-scratch Windows VM (KiCad 10.0.6). Stock libraries resolve on a fresh KiCad 10 install: its user sym-lib-table and fp-lib-table hold ONE row of type Table pointing at KiCad's template table, and the bridge only read the top level, so Device and every other stock library looked unregistered (kicad_list_symbols, kicad_sch_place_symbol and kicad_list_footprints now follow the nested table; the writers still never touch that row). kicad_open_symbol_editor and the other tree navigations wait for an editor that is busy loading libraries instead of giving up after 1.5 s. kicad_rescan_libraries gets the same foreground guard as the open verbs, so a footprint that finishes loading late no longer leaves the editor on top of the user's window.
2 files changed
+57−2
rust/crates/kicad-core/src/libraries.rs+56−1@@ -221,6 +221,44 @@ impl LibTable { LibTable::parse_str(&text).map_err(|e| format!("{}: {e}", path.display())) } + /// READ-ONLY view with nested tables expanded. A fresh KiCad 10 install does not list the stock+ /// libraries in the user's table: it writes ONE row of type "Table" whose uri is KiCad's own+ /// template table. Looking only at the top level made every stock library (Device, ...) look+ /// unregistered on a blank box (winvm, 10.0.6). Never write this view back: it would flatten+ /// KiCad's file. Rows of the outer table win over nested rows of the same name.+ pub fn parse_file_resolved(path: &Path) -> Result<LibTable, String> {+ let mut table = LibTable::parse_file(path)?;+ let mut seen = vec![path.to_path_buf()];+ let mut depth = 0;+ while depth < 4 && table.entries.iter().any(|e| e.lib_type.eq_ignore_ascii_case("Table")) {+ depth += 1;+ let mut flat: Vec<LibEntry> = Vec::new();+ let mut nested: Vec<LibEntry> = Vec::new();+ for e in std::mem::take(&mut table.entries) {+ if !e.lib_type.eq_ignore_ascii_case("Table") {+ flat.push(e);+ continue;+ }+ let inner = PathBuf::from(e.uri.replace('\\', "/"));+ if e.disabled || seen.contains(&inner) || !inner.is_file() {+ continue;+ }+ seen.push(inner.clone());+ if let Ok(t) = LibTable::parse_file(&inner) {+ nested.extend(t.entries);+ }+ }+ for e in nested {+ if !flat.iter().any(|f| f.name == e.name) {+ flat.push(e);+ }+ }+ table.entries = flat;+ }+ table.entries.retain(|e| !e.lib_type.eq_ignore_ascii_case("Table"));+ Ok(table)+ }+ pub fn parse_str(text: &str) -> Result<LibTable, String> { let root = parse_sexp(text).map_err(|e| format!("Cannot parse lib table: {e}"))?; let head = root.head().ok_or_else(|| "Cannot parse lib table: no root expression found".to_string())?;@@ -939,7 +977,7 @@ pub fn list_footprints(ctx: &LibCtx, want: Option<&str>) -> (Vec<(String, Pretty }; if let Some(table) = ctx.fp_lib_table() { if table.is_file() {- if let Ok(t) = LibTable::parse_file(&table) {+ if let Ok(t) = LibTable::parse_file_resolved(&table) { for e in t.entries.iter().filter(|e| !e.disabled) { if let Some(resolved) = expand_kicad_uri(&e.uri, ctx) { add(&e.name, Path::new(&resolved), &mut libs);@@ -1054,6 +1092,23 @@ mod tests { assert!(t.to_text().ends_with("(descr \"Adom Bridge - custom footprints\"))\n)\n")); } + #[test]+ fn a_fresh_kicad10_nested_table_row_is_followed_for_reads_only() {+ let d = std::env::temp_dir().join(format!("kicad-core-nested-{}", std::process::id()));+ let _ = std::fs::remove_dir_all(&d);+ std::fs::create_dir_all(&d).unwrap();+ let stock = d.join("stock-sym-lib-table");+ std::fs::write(&stock, "(sym_lib_table\n (version 7)\n (lib (name \"Device\")(type \"KiCad\")(uri \"${KICAD10_SYMBOL_DIR}/Device.kicad_sym\")(options \"\")(descr \"\"))\n (lib (name \"Adom\")(type \"KiCad\")(uri \"stock-adom\")(options \"\")(descr \"\"))\n)\n").unwrap();+ let user = d.join("sym-lib-table");+ std::fs::write(&user, format!("(sym_lib_table\n (version 7)\n (lib (name \"KiCad\")(type \"Table\")(uri \"{}\")(options \"\")(descr \"KiCad Default Libraries\"))\n (lib (name \"Adom\")(type \"KiCad\")(uri \"user-adom\")(options \"\")(descr \"\"))\n)\n", norm_path(&stock))).unwrap();+ let plain = LibTable::parse_file(&user).unwrap();+ assert!(!plain.has_library("Device") && plain.has_library("KiCad")); // the writers' view is untouched+ let r = LibTable::parse_file_resolved(&user).unwrap();+ assert!(r.has_library("Device") && !r.has_library("KiCad"));+ assert_eq!(r.get("Adom").unwrap().uri, "user-adom"); // the outer row wins+ let _ = std::fs::remove_dir_all(&d);+ }+ #[test] fn missing_table_file_is_empty() { let t = LibTable::parse_file(Path::new("/nonexistent/dir/sym-lib-table")).unwrap();
rust/crates/kicad-core/src/schematic.rs+1−1@@ -1014,7 +1014,7 @@ pub fn resolve_lib_symbol(ctx: &LibCtx, lib_id: &str, project_dir: Option<&Path> continue; } checked.push(libraries::norm_path(t));- let table = LibTable::parse_file(t)?;+ let table = LibTable::parse_file_resolved(t)?; let Some(entry) = table.get(nick) else { continue }; let uri = match project_dir { Some(d) => entry.uri.replace("${KIPRJMOD}", &libraries::norm_path(d)),