← Commit history

1.0.52: 1.0.52: wiki #117: kicad_prewarm, declared as bridge.json prewarm.verb (requiresHostApp), which Adom Bridge 2.1.153 calls once after each install or update: detects KiCad (warming kicad-cli), seeds the first-run config, marks KiCad's setup wizard done, repairs the taskbar shortcuts; idempotent, config steps skipped while KiCad runs. Wiki #116: the modal-dialog hints Adom Bridge's CLI used to append now come from the bridge itself (_modalHint on the open and install verbs and on kicad_window_info, which says whether a dialog is up). And bridge.json now declares all 119 verbs; it had drifted to 69, and a unit test now fails the build if it drifts again.

John Lauer ·ce54cf9543 ·1mo ago ·parent dadcfe9
5 files changed +83−4
rust/Cargo.lock+3−3
@@ -244,7 +244,7 @@ dependencies = [  [[package]] name = "kicad-bridge"-version = "1.0.51"+version = "1.0.52" dependencies = [  "kicad-core",  "kicad-platform",@@ -255,7 +255,7 @@ dependencies = [  [[package]] name = "kicad-core"-version = "1.0.51"+version = "1.0.52" dependencies = [  "kicad-ipc-rs",  "kicad-platform",@@ -281,7 +281,7 @@ dependencies = [  [[package]] name = "kicad-platform"-version = "1.0.51"+version = "1.0.52" dependencies = [  "serde",  "serde_json",
rust/Cargo.toml+1−1
@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"]  [workspace.package]-version = "1.0.51"+version = "1.0.52" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/catalog.rs+18
@@ -204,3 +204,21 @@ pub fn describe(version: &str) -> Value {         "_hint": "Native bridge: headless, schematic edit and live routing verbs are served here. Window, show and demo verbs arrive with phase 3; until then the Python bridge serves them.",     }) }++#[cfg(test)]+mod manifest_tests {+    /// bridge.json is what Adom Bridge reads to know this bridge's verbs. It had drifted to 69 of+    /// 118 (wiki #116): every verb the catalog serves must be declared, and nothing else.+    #[test]+    fn bridge_json_declares_exactly_the_catalog() {+        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../bridge.json");+        let doc: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();+        let mut declared: Vec<String> = doc["verbs"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect();+        let mut served: Vec<String> = super::all().iter().map(|v| v.name.to_string()).collect();+        declared.sort(); declared.dedup(); served.sort(); served.dedup();+        let missing: Vec<&String> = served.iter().filter(|v| !declared.contains(v)).collect();+        let stale: Vec<&String> = declared.iter().filter(|v| !served.contains(v)).collect();+        assert!(missing.is_empty() && stale.is_empty(), "bridge.json verbs out of sync: missing {missing:?}, not served {stale:?}");+        assert_eq!(doc["prewarm"]["verb"], "kicad_prewarm");+    }+}
rust/crates/kicad-bridge/src/verbs.rs+21
@@ -48,6 +48,7 @@ pub fn dispatch(state: &mut State, command_in: &str, args: &Value, _caller: &Cal     let _driving = crate::taskbar::note_driving(command, verb.mechanism.as_str());     // Verb groups ported later live in their own files; each returns Some when it owns the verb.     if let Some(mut out) = crate::groups::dispatch(state, command, args) {+        reply_hints(command, &mut out);         prog.attach(&mut out);         if let Some(o) = out.as_object_mut() {             o.entry("mechanism").or_insert(json!(verb.mechanism.as_str()));@@ -70,6 +71,7 @@ pub fn dispatch(state: &mut State, command_in: &str, args: &Value, _caller: &Cal         "kicad_uninstall" => { let info = state.kicad_info(); kicad_core::uninstall::handle(&info.installs, args) }         _ => fail("not_implemented", format!("{command} is catalogued but not implemented in this phase"), "Use the Python bridge for this verb until the phase that ports it lands."),     };+    reply_hints(command, &mut out);     prog.attach(&mut out);     if let Some(o) = out.as_object_mut() {         o.entry("mechanism").or_insert(json!(verb.mechanism.as_str()));@@ -77,6 +79,25 @@ pub fn dispatch(state: &mut State, command_in: &str, args: &Value, _caller: &Cal     out } +/// Hints Adom Bridge's CLI used to append to KiCad replies until ab 2.1.145 (wiki #116, adom/adom-bridge#212:+/// ab keeps nothing bridge-specific). They belong to the bridge, which knows it sweeps benign dialogs itself.+pub fn reply_hints(command: &str, out: &mut Value) {+    const OPENS: &[&str] = &["kicad_open_schematic", "kicad_open_board", "kicad_install_symbol", "kicad_install_footprint",+        "kicad_open_footprint_editor", "kicad_open_symbol_editor", "kicad_open_3d_viewer"];+    let ok = out.get("success").and_then(Value::as_bool).unwrap_or(true) && out.get("error").map(|e| e.as_str().unwrap_or("").is_empty()).unwrap_or(true);+    if OPENS.contains(&command) && ok {+        out["_modalHint"] = json!("KiCad can show a modal dialog (save prompt, file warning, format error) that blocks the next command. The bridge closes benign ones itself (see _autoDialogs); for anything else kicad_window_info reports hasModalDialogs, and kicad_screenshot_all captures every window including the dialog so you can read it and decide: kicad_dismiss_dialogs, retry, or close.");+    }+    if command == "kicad_window_info" {+        let modal = out.pointer("/data/hasModalDialogs").and_then(Value::as_bool).unwrap_or(false);+        out["_modalHint"] = json!(if modal {+            "hasModalDialogs is TRUE: something is blocking KiCad. kicad_screenshot_all captures every window including the dialog (or desktop_screenshot_window with the dialog's hwnd from modalDialogs). Read its text, then kicad_dismiss_dialogs, retry, or kicad_close {force:true}."+        } else {+            "No modal dialog is up. If a later verb stalls, call this again: hasModalDialogs turns true while a dialog blocks KiCad."+        });+    }+}+ fn is_sch(args: &Value) -> bool {     arg_any(args, &["filePath", "schematicPath"]).map(|p| p.to_ascii_lowercase().ends_with(".kicad_sch")).unwrap_or(false) }
rust/crates/kicad-bridge/src/verbs_maint.rs+40
@@ -34,6 +34,16 @@ pub static VERBS: &[Verb] = &[         related: &["kicad_readiness", "kicad_open_board", "kicad_launch"],         pitfalls: &["Never stamp Adom identity on KiCad windows: Windows caches the taskbar icon per AUMID string and a bad stamp is permanent (skills/kicad-bridge-dev, THE WHITE-ICON MECHANISM).", "Needs a detected KiCad (the shortcuts are named by major.minor)."],     },+    Verb {+        name: "kicad_prewarm",+        summary: "Get this machine ready for a fast first KiCad call: detect KiCad, seed its first-run config, mark its setup wizard done, repair its taskbar shortcuts. Idempotent.",+        mechanism: Mechanism::File, risk: "write", timeout_sec: 120,+        input: "{}",+        example: "kicad_prewarm {}",+        hint: "Adom Bridge calls this once per installed bridge version, in the background, about 20 s after the first boot that follows an install or update (bridge.json prewarm.verb, ab 2.1.153); bridge_prewarm {\"name\":\"kicad\",\"force\":true} runs it again. Everything it does is what the first real verb would otherwise pay for: kicad-cli is run once (detection), and the config steps only happen while no KiCad is running, because KiCad rewrites its own settings on exit. Never downloads KiCad: kicad_upgrade does that, and asks first.",+        related: &["kicad_readiness", "kicad_taskbar_repair", "kicad_upgrade"],+        pitfalls: &["With KiCad running the config steps are skipped (reported), not forced: run it again with KiCad closed.", "No KiCad installed answers ok with nothing done; bridge.json declares requiresHostApp so ab normally skips such a box."],+    },     Verb {         name: "kicad_model_check",         summary: "Read-only 3D model diagnostics for a footprint or a whole board: which (model ...) references exist, where KiCad looks, whether each file exists and holds geometry.",@@ -139,6 +149,7 @@ pub static VERBS: &[Verb] = &[ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> {     let out = match command {         "kicad_taskbar_repair" => taskbar_repair(&state.kicad_info().version),+        "kicad_prewarm" => prewarm(),         "kicad_3d_viewer_settings" => {             let ctx = Ctx::from_info(&state.kicad_info());             let sets = model_check::VIEWER_FLAGS.iter().any(|(k, _, _)| args.get(*k).map(|v| !v.is_null()).unwrap_or(false));@@ -191,6 +202,35 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value>   /// Wiki: the white taskbar icon. Also run once at boot from main.rs.+/// `kicad_prewarm` (wiki #117): everything a first real verb would otherwise wait for, once.+pub fn prewarm() -> Value {+    let started = std::time::Instant::now();+    let info = kicad_core::detect::detect(); // runs kicad-cli version: the CLI is warm afterwards+    let mut steps = vec![json!({"step": "detect", "ok": true, "installs": info.installs.iter().map(|i| json!({"version": i.version, "cli": i.cli_version})).collect::<Vec<_>>()})];+    if info.installs.is_empty() {+        return json!({"success": true, "ok": true, "warmed": false, "steps": steps, "seconds": started.elapsed().as_secs_f64(),+            "output": "No KiCad on this machine: nothing to prewarm.",+            "_hint": "kicad_upgrade installs KiCad (it asks the user first); prewarm again afterwards."});+    }+    let running = !native().processes(&["kicad.exe", "pcbnew.exe", "eeschema.exe"]).unwrap_or_default().is_empty();+    for inst in &info.installs {+        let Some(cfg) = native().config_dir(&inst.version) else { continue };+        if running {+            steps.push(json!({"step": "config", "version": inst.version, "ok": true, "skipped": "KiCad is running; it rewrites its own settings on exit"}));+            continue;+        }+        let seeded = kicad_core::kicad_common::seed_first_run(&cfg);+        let wizard = kicad_core::kicad_common::mark_first_run_done(&cfg);+        steps.push(json!({"step": "config", "version": inst.version, "ok": seeded.errors.is_empty() && wizard.is_ok(),+            "written": seeded.written, "merged": seeded.merged, "setupWizardMarkedDone": wizard.as_ref().ok(), "errors": seeded.errors, "wizardError": wizard.err()}));+    }+    let tb = taskbar_repair(&info.version);+    steps.push(json!({"step": "taskbar", "ok": tb["success"].as_bool().unwrap_or(false), "written": tb["written"], "already": tb["already"], "missing": tb["missing"]}));+    let ok = steps.iter().all(|s| s["ok"].as_bool().unwrap_or(false));+    json!({"success": ok, "ok": ok, "warmed": true, "steps": steps, "seconds": (started.elapsed().as_secs_f64() * 100.0).round() / 100.0,+        "output": format!("KiCad {} ready: CLI warm, first-run config seeded, setup wizard marked done, taskbar shortcuts checked{}.", info.version, if running { " (config skipped: KiCad is running)" } else { "" })})+}+ pub fn taskbar_repair(version: &str) -> Value {     let mm: String = version.split('.').take(2).collect::<Vec<_>>().join(".");     if mm.is_empty() || !mm.contains('.') {