From 14856ec9d6019ea1cbd13522cda6fff925207d01 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:23:41 +0800 Subject: [PATCH] feat(common): add SaveCopyOfDocument API and CLI command --- README.md | 7 ++-- docs/TEST_CLI.md | 6 +++ src/client.rs | 34 +++++++++++++++ test-scripts/kicad-ipc-cli.rs | 79 ++++++++++++++++++++++++++++++++++- 4 files changed, 122 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2489be7..d6cce97 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `GetKiCadBinaryPath` - `GetPluginSettingsPath` - `SaveDocument` +- `SaveCopyOfDocument` ## KiCad v10 RC1.1 API Completion Matrix @@ -56,11 +57,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | -| Common editor/document | 23 | 16 | 70% | +| Common editor/document | 23 | 17 | 74% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **45** | **80%** | +| **Total** | **56** | **46** | **82%** | ### Common (base) @@ -80,7 +81,7 @@ Legend: | `RefreshEditor` | Implemented | `KiCadClient::refresh_editor` | | `GetOpenDocuments` | Implemented | `KiCadClient::get_open_documents`, `KiCadClient::get_current_project_path`, `KiCadClient::has_open_board` | | `SaveDocument` | Implemented | `KiCadClient::save_document_raw`, `KiCadClient::save_document` | -| `SaveCopyOfDocument` | Not yet | - | +| `SaveCopyOfDocument` | Implemented | `KiCadClient::save_copy_of_document_raw`, `KiCadClient::save_copy_of_document` | | `RevertDocument` | Not yet | - | | `RunAction` | Not yet | - | | `BeginCommit` | Implemented | `KiCadClient::begin_commit_raw`, `KiCadClient::begin_commit` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 8bb3300..038d458 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -169,6 +169,12 @@ Save current board document: cargo run --bin kicad-ipc-cli -- save-doc ``` +Save a copy of current board document: + +```bash +cargo run --bin kicad-ipc-cli -- save-copy --path /tmp/example.kicad_pcb --overwrite --include-project +``` + Show summary of current PCB selection by item type: ```bash diff --git a/src/client.rs b/src/client.rs index 052a101..4541bb4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -81,6 +81,7 @@ const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox"; const CMD_HIT_TEST: &str = "kiapi.common.commands.HitTest"; const CMD_GET_TITLE_BLOCK_INFO: &str = "kiapi.common.commands.GetTitleBlockInfo"; const CMD_SAVE_DOCUMENT: &str = "kiapi.common.commands.SaveDocument"; +const CMD_SAVE_COPY_OF_DOCUMENT: &str = "kiapi.common.commands.SaveCopyOfDocument"; const CMD_SAVE_DOCUMENT_TO_STRING: &str = "kiapi.common.commands.SaveDocumentToString"; const CMD_SAVE_SELECTION_TO_STRING: &str = "kiapi.common.commands.SaveSelectionToString"; @@ -1391,6 +1392,39 @@ impl KiCadClient { Ok(()) } + pub async fn save_copy_of_document_raw( + &self, + path: impl Into, + overwrite: bool, + include_project: bool, + ) -> Result { + let command = common_commands::SaveCopyOfDocument { + document: Some(self.current_board_document_proto().await?), + path: path.into(), + options: Some(common_commands::SaveOptions { + overwrite, + include_project, + }), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_SAVE_COPY_OF_DOCUMENT)) + .await?; + response_payload_as_any(response, RES_PROTOBUF_EMPTY) + } + + pub async fn save_copy_of_document( + &self, + path: impl Into, + overwrite: bool, + include_project: bool, + ) -> Result<(), KiCadError> { + let _ = self + .save_copy_of_document_raw(path, overwrite, include_project) + .await?; + Ok(()) + } + pub async fn get_board_as_string(&self) -> Result { let command = common_commands::SaveDocumentToString { document: Some(self.current_board_document_proto().await?), diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index ed00a76..61671fa 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -91,6 +91,11 @@ enum Command { message: String, }, SaveDoc, + SaveCopy { + path: String, + overwrite: bool, + include_project: bool, + }, AddToSelection { item_ids: Vec, }, @@ -444,6 +449,19 @@ async fn run() -> Result<(), KiCadError> { client.save_document().await?; println!("save_document=ok"); } + Command::SaveCopy { + path, + overwrite, + include_project, + } => { + client + .save_copy_of_document(path, overwrite, include_project) + .await?; + println!( + "save_copy_of_document=ok overwrite={} include_project={}", + overwrite, include_project + ); + } Command::AddToSelection { item_ids } => { let summary = client.add_to_selection(item_ids).await?; println!("selection_total={}", summary.total_items); @@ -1249,6 +1267,40 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE } } "save-doc" => Command::SaveDoc, + "save-copy" => { + let mut path = None; + let mut overwrite = false; + let mut include_project = false; + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--path" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for save-copy --path".to_string(), + })?; + path = Some(value.clone()); + i += 2; + } + "--overwrite" => { + overwrite = true; + i += 1; + } + "--include-project" => { + include_project = true; + i += 1; + } + _ => i += 1, + } + } + + Command::SaveCopy { + path: path.ok_or_else(|| KiCadError::Config { + reason: "save-copy requires `--path `".to_string(), + })?, + overwrite, + include_project, + } + } "add-to-selection" => { let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; Command::AddToSelection { item_ids } @@ -1723,7 +1775,7 @@ fn default_config() -> CliConfig { fn print_help() { println!( - "kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--client-name NAME] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n kicad-binary-path [--binary-name ]\n Resolve absolute path for a KiCad binary (default: kicad-cli)\n plugin-settings-path [--identifier ]\n Resolve writeable plugin settings directory (default: kicad-ipc-rust)\n open-docs [--type ] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n net-classes List project netclass definitions\n text-variables List text variables for current board document\n expand-text-variables Expand variables in provided text values\n Options: --text (repeatable)\n text-extents Measure text bounding box\n Options: --text \n text-as-shapes Convert text to rendered shapes\n Options: --text (repeatable)\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n items-by-id --id ... Show parsed details for specific item IDs\n item-bbox --id ... Show bounding boxes for item IDs\n hit-test --id --x-nm --y-nm [--tolerance-nm ]\n Hit-test one item at a point\n types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n pad-shape-polygon --pad-id ... --layer-id [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id ... --layer-id ... [--debug]\n Check padstack shape presence matrix across layers\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup Show typed board stackup\n graphics-defaults Show typed graphics defaults\n appearance Show typed editor appearance settings\n set-appearance --inactive-layer-display \n --net-color-display \n --board-flip \n --ratsnest-display \n Set editor appearance settings\n inject-drc-error --severity --message [--x-nm --y-nm ] [--item-id ...]\n Inject a DRC marker (severity: warning|error|exclusion|ignore|info|action|debug|undefined)\n refill-zones [--zone-id ...]\n Refill all zones or a provided subset\n netclass Show typed netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n set-enabled-layers --copper-layer-count [--layer-id ...]\n Set enabled board layer set\n active-layer Show active board layer\n set-active-layer --layer-id \n Set active board layer\n visible-layers Show currently visible board layers\n set-visible-layers --layer-id ...\n Set visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\n set-board-origin --type --x-nm --y-nm \n Set board origin (`grid` or `drill`)\n refresh-editor [--frame ] Refresh a specific editor frame (default: pcb)\n begin-commit Start staged commit and print commit ID\n end-commit --id [--action ] [--message ]\n End staged commit with commit/drop action\n save-doc Save current board document\n add-to-selection --id ...\n Add items to current selection\n remove-from-selection --id ...\n Remove items from current selection\n clear-selection Clear current item selection\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n" + "kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--client-name NAME] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n kicad-binary-path [--binary-name ]\n Resolve absolute path for a KiCad binary (default: kicad-cli)\n plugin-settings-path [--identifier ]\n Resolve writeable plugin settings directory (default: kicad-ipc-rust)\n open-docs [--type ] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n net-classes List project netclass definitions\n text-variables List text variables for current board document\n expand-text-variables Expand variables in provided text values\n Options: --text (repeatable)\n text-extents Measure text bounding box\n Options: --text \n text-as-shapes Convert text to rendered shapes\n Options: --text (repeatable)\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n items-by-id --id ... Show parsed details for specific item IDs\n item-bbox --id ... Show bounding boxes for item IDs\n hit-test --id --x-nm --y-nm [--tolerance-nm ]\n Hit-test one item at a point\n types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n pad-shape-polygon --pad-id ... --layer-id [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id ... --layer-id ... [--debug]\n Check padstack shape presence matrix across layers\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup Show typed board stackup\n graphics-defaults Show typed graphics defaults\n appearance Show typed editor appearance settings\n set-appearance --inactive-layer-display \n --net-color-display \n --board-flip \n --ratsnest-display \n Set editor appearance settings\n inject-drc-error --severity --message [--x-nm --y-nm ] [--item-id ...]\n Inject a DRC marker (severity: warning|error|exclusion|ignore|info|action|debug|undefined)\n refill-zones [--zone-id ...]\n Refill all zones or a provided subset\n netclass Show typed netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n set-enabled-layers --copper-layer-count [--layer-id ...]\n Set enabled board layer set\n active-layer Show active board layer\n set-active-layer --layer-id \n Set active board layer\n visible-layers Show currently visible board layers\n set-visible-layers --layer-id ...\n Set visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\n set-board-origin --type --x-nm --y-nm \n Set board origin (`grid` or `drill`)\n refresh-editor [--frame ] Refresh a specific editor frame (default: pcb)\n begin-commit Start staged commit and print commit ID\n end-commit --id [--action ] [--message ]\n End staged commit with commit/drop action\n save-doc Save current board document\n save-copy --path [--overwrite] [--include-project]\n Save current board document to a new location\n add-to-selection --id ...\n Add items to current selection\n remove-from-selection --id ...\n Remove items from current selection\n clear-selection Clear current item selection\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n" ); } @@ -2437,6 +2489,31 @@ mod tests { assert!(matches!(command, Command::SaveDoc)); } + #[test] + fn parse_args_parses_save_copy() { + let (_, command) = parse_args_from(vec![ + "save-copy".to_string(), + "--path".to_string(), + "/tmp/example.kicad_pcb".to_string(), + "--overwrite".to_string(), + "--include-project".to_string(), + ]) + .expect("save-copy args should parse"); + + match command { + Command::SaveCopy { + path, + overwrite, + include_project, + } => { + assert_eq!(path, "/tmp/example.kicad_pcb"); + assert!(overwrite); + assert!(include_project); + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![