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.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 first-run seeding now MERGES into the kicad_common.json that kicad-cli writes during the install's own verify step (it used to skip an existing file, so the startup wizard showed on every blank box and the IPC API stayed off); a file whose first-run prompts are answered is still left alone, backup kept. kicad_taskbar_repair also looks under ProgramData, where an all-users install puts its Start Menu shortcuts (it reported all three missing). The 3D verbs answer canvasRendered:false with a _renderHint when KiCad has reported Could not use OpenGL, instead of ok over a black viewer (wiki #100), and point at kicad_enable_software_opengl. kicad_readiness no longer says window verbs are served by the Python bridge.
6 files changed
+96−5
rust/Cargo.lock+3−3@@ -244,7 +244,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.34"+version = "1.0.35" dependencies = [ "kicad-core", "kicad-platform",@@ -255,7 +255,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.34"+version = "1.0.35" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -281,7 +281,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.34"+version = "1.0.35" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.34"+version = "1.0.35" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs.rs+1−1@@ -111,7 +111,7 @@ fn readiness(state: &mut State) -> Value { v["alternatives"] = alternatives(); v["_hint"] = json!("No KiCad on this box. Pick one of `alternatives`, or call kicad_upgrade to install KiCad silently (asks the user first)."); } else {- v["_hint"] = json!("Ready. Headless verbs need nothing else; window verbs are served by the Python bridge until phase 3.");+ v["_hint"] = json!("Ready. Every verb is served by this native build. Live board verbs (kicad_routing_state and the edits) need KiCad's IPC API server: kicad_ipc_api reads and sets the switch, and KiCad reads it at start."); } v }
rust/crates/kicad-bridge/src/verbs_windows.rs+21@@ -343,6 +343,13 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> /// The server.py dispatch wrapper for a window verb: dialog sweep, foreground etiquette, /// the one-shot focus check, window labelling.+/// Set when KiCad's own "Could not use OpenGL" box was seen; cleared by kicad_enable_software_opengl.+pub(crate) static NO_OPENGL: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);++fn is_3d_verb(command: &str) -> bool {+ matches!(command, "kicad_open_3d_viewer" | "kicad_show_3d_board" | "kicad_show_3d_chip" | "kicad_3d_view")+}+ pub(crate) fn post_verb(state: &mut State, command: &str, args: &Value, out: &mut Value, fg_before: Option<u64>) { let spawns = SPAWNS_WINDOW.contains(&command); let info = state.kicad_info();@@ -357,8 +364,19 @@ pub(crate) fn post_verb(state: &mut State, command: &str, args: &Value, out: &mu "KiCad popped {} dialog(s) during this verb (non-benign: {}). Each was screenshotted (see _autoDialogs[].screenshot + .body) and auto-expired so you are unblocked. If your verb 'failed'/'did not open', this dialog is almost certainly why: read the body and act on it.", swept["count"], titles.join(", "))); }+ // KiCad says this once per process start on a machine without usable OpenGL (a VM with no+ // GPU, RDP): 2D falls back to software, the 3D viewer stays BLACK. Remember it, so the 3D+ // verbs stop answering ok over a black canvas (wiki #100, seen on winvm).+ if swept["dialogs"].as_array().is_some_and(|a| a.iter().any(|d| d["body"].as_str().unwrap_or("").contains("Could not use OpenGL"))) {+ NO_OPENGL.store(true, std::sync::atomic::Ordering::Relaxed);+ } out["_autoDialogs"] = swept; }+ if is_3d_verb(command) && NO_OPENGL.load(std::sync::atomic::Ordering::Relaxed) && out.get("success").and_then(Value::as_bool).unwrap_or(true) {+ out["canvasRendered"] = json!(false);+ out["renderVerified"] = json!(false);+ out["_renderHint"] = json!("KiCad reported \"Could not use OpenGL\" on this machine (no GPU: a VM or a remote session). The PCB and schematic canvases fall back to software, but the 3D viewer needs OpenGL 1.5 and shows a BLACK canvas: do not present its frame as proof. Fix: kicad_close, then kicad_enable_software_opengl (Mesa on the CPU, slow but correct, for GPU-less hosts only), then open again.");+ } // Foreground etiquette (John, 2026-08-14 and 2026-08-16): the ONE sanctioned way a // KiCad window comes to the front is an explicit foreground flag WITH a reason. let wants_fg = user_wants_foreground(args);@@ -3075,6 +3093,9 @@ fn enable_software_opengl(state: &mut State) -> Value { eprintln!("[kicad-bridge] enable_software_opengl: deployed to {}", norm(bd)); } let ok = !enabled.is_empty();+ if !enabled.is_empty() {+ NO_OPENGL.store(false, std::sync::atomic::Ordering::Relaxed); // the next start renders through Mesa+ } json!({ "success": ok, "enabledIn": enabled, "errors": errors, "renderer": "Mesa llvmpipe (CPU software OpenGL)", "_hint": if ok {
rust/crates/kicad-core/src/install_kicad.rs+1@@ -690,6 +690,7 @@ fn seed_new_install(inst: &KicadInstall) -> Value { "version": inst.version, "configDir": norm(&cfg), "written": s.written,+ "merged": s.merged, "skipped": s.skipped, "errors": s.errors, "libTables": lib_tables,
rust/crates/kicad-core/src/kicad_common.rs+69@@ -179,6 +179,8 @@ pub fn now_utc_iso() -> String { /// What first-run seeding did: which files it wrote and which it left alone. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Seeded {+ /// Files that existed but were still KiCad's untouched defaults, so the seed was merged in.+ pub merged: Vec<String>, pub written: Vec<String>, pub skipped: Vec<String>, pub errors: Vec<String>,@@ -227,6 +229,40 @@ pub fn first_run_documents() -> Vec<(&'static str, Value)> { ] } +/// True when the file is KiCad's own untouched default: no first-run prompt answered.+pub fn is_pristine_common(doc: &Value) -> bool {+ let d = &doc["do_not_show_again"];+ d.is_object() && !truthy(&d["data_collection_prompt"]) && !truthy(&d["update_check_prompt"])+}++fn merge_value(into: &mut Value, from: &Value) {+ match (into, from) {+ (Value::Object(a), Value::Object(b)) => {+ for (k, v) in b {+ if k == "meta" { continue; } // KiCad's own schema version stays+ merge_value(a.entry(k.clone()).or_insert(Value::Null), v);+ }+ }+ (slot, v) => *slot = v.clone(),+ }+}++/// Merge the seed into an existing pristine kicad_common.json. Ok(false) = not pristine, untouched.+fn merge_into_pristine(path: &Path, seed: &Value) -> Result<bool, String> {+ let text = std::fs::read_to_string(path).map_err(|e| e.to_string())?;+ let mut doc: Value = serde_json::from_str(&text).map_err(|e| e.to_string())?;+ if !is_pristine_common(&doc) {+ return Ok(false);+ }+ let backup = path.with_file_name(format!("{}.adom-bak", path.file_name().unwrap_or_default().to_string_lossy()));+ if !backup.exists() {+ std::fs::write(&backup, &text).map_err(|e| e.to_string())?;+ }+ merge_value(&mut doc, seed);+ std::fs::write(path, serde_json::to_string_pretty(&doc).unwrap_or_default() + "\n").map_err(|e| e.to_string())?;+ Ok(true)+}+ /// Seed the first-run settings files into a per-version config dir, each written ONLY /// if absent (never on a settings file the user already has). Creates the dir when /// missing. Never panics; failures land in `Seeded.errors`.@@ -239,6 +275,18 @@ pub fn seed_first_run(config_dir: &Path) -> Seeded { for (name, doc) in first_run_documents() { let path = config_dir.join(name); if path.exists() {+ // kicad-cli writes kicad_common.json with defaults the first time it runs, and the+ // installer's own verify step runs kicad-cli. Skipping an existing file therefore+ // skipped EVERY fresh box and the startup wizard showed anyway (winvm, 10.0.6).+ // A file whose first-run prompts are all unanswered has never been through the+ // wizard: nobody's choices live in it, so the seed is merged in (backup kept).+ if name == "kicad_common.json" {+ match merge_into_pristine(&path, &doc) {+ Ok(true) => { out.merged.push(name.to_string()); continue; }+ Ok(false) => {}+ Err(e) => out.errors.push(format!("{name}: {e}")),+ }+ } out.skipped.push(name.to_string()); continue; }@@ -271,6 +319,27 @@ mod tests { serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap() } + #[test]+ fn a_pristine_file_kicad_cli_wrote_is_merged_and_a_users_file_is_left_alone() {+ let t = tmp("pristine");+ let f = t.0.join("kicad_common.json");+ // what kicad-cli leaves behind on a blank box+ std::fs::write(&f, r#"{"meta":{"version":6},"api":{"enable_server":false,"interpreter_path":"x"},"graphics":{"canvas_type":0},"do_not_show_again":{"data_collection_prompt":false,"update_check_prompt":false,"zone_fill_warning":false}}"#).unwrap();+ let s = seed_first_run(&t.0);+ assert_eq!(s.merged, vec!["kicad_common.json".to_string()]);+ let d = read_json(&f);+ assert_eq!(d["do_not_show_again"]["data_collection_prompt"], json!(true));+ assert_eq!(d["api"]["enable_server"], json!(true));+ assert_eq!(d["api"]["interpreter_path"], json!("x"));+ assert_eq!(d["meta"]["version"], json!(6));+ assert!(t.0.join("kicad_common.json.adom-bak").exists());+ // the user has been through the wizard: their file is theirs+ std::fs::write(&f, r#"{"api":{"enable_server":false},"do_not_show_again":{"data_collection_prompt":true,"update_check_prompt":true}}"#).unwrap();+ let s = seed_first_run(&t.0);+ assert!(s.merged.is_empty() && s.skipped.contains(&"kicad_common.json".to_string()));+ assert_eq!(read_json(&f)["api"]["enable_server"], json!(false));+ }+ #[test] fn seeds_only_absent_files() { let t = tmp("seed");