← 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.

John Lauer ·dfc69bc4b4 ·1mo ago ·parent 39a7e94
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");