From 8e8033f07cfc407f54537e7120cb65fd11394292 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 16:45:22 +0800 Subject: [PATCH 01/28] feat(client): add BeginCommit API and CLI command --- README.md | 6 +-- docs/TEST_CLI.md | 6 +++ src/client.rs | 89 +++++++++++++++++++++++++++++++---- src/lib.rs | 9 ++-- src/model/common.rs | 5 ++ test-scripts/kicad-ipc-cli.rs | 8 +++- 6 files changed, 105 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 8e205b0..882e512 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 4 | 67% | -| Common editor/document | 23 | 9 | 39% | +| Common editor/document | 23 | 10 | 43% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 13 | 59% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **29** | **52%** | +| **Total** | **56** | **30** | **54%** | ### Common (base) @@ -66,7 +66,7 @@ Legend: | `SaveCopyOfDocument` | Not yet | - | | `RevertDocument` | Not yet | - | | `RunAction` | Not yet | - | -| `BeginCommit` | Not yet | - | +| `BeginCommit` | Implemented | `KiCadClient::begin_commit_raw`, `KiCadClient::begin_commit` | | `EndCommit` | Not yet | - | | `CreateItems` | Not yet | - | | `GetItems` | Implemented | `KiCadClient::get_items_raw_by_type_codes`, `KiCadClient::get_items_by_type_codes`, `KiCadClient::get_items_details_by_type_codes`, `KiCadClient::get_all_pcb_items_raw`, `KiCadClient::get_all_pcb_items`, `KiCadClient::get_all_pcb_items_details`, `KiCadClient::get_pad_netlist` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 204131d..5dfb17b 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -107,6 +107,12 @@ Show drill origin: cargo run --bin kicad-ipc-cli -- board-origin --type drill ``` +Start a staged commit and print commit ID: + +```bash +cargo run --bin kicad-ipc-cli -- begin-commit +``` + Show summary of current PCB selection by item type: ```bash diff --git a/src/client.rs b/src/client.rs index e238c32..3e7f818 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,10 +18,11 @@ use crate::model::board::{ Vector2Nm, }; use crate::model::common::{ - DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode, - ProjectInfo, SelectionItemDetail, SelectionSummary, SelectionTypeCount, TextAsShapesEntry, - TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, TextObjectSpec, - TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, VersionInfo, + CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, + PcbObjectTypeCode, ProjectInfo, SelectionItemDetail, SelectionSummary, SelectionTypeCount, + TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, + TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, + VersionInfo, }; use crate::proto::kiapi::board as board_proto; use crate::proto::kiapi::board::commands as board_commands; @@ -58,6 +59,7 @@ const CMD_GET_PAD_SHAPE_AS_POLYGON: &str = "kiapi.board.commands.GetPadShapeAsPo const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str = "kiapi.board.commands.CheckPadstackPresenceOnLayers"; const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection"; +const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems"; const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById"; const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox"; @@ -87,6 +89,7 @@ const RES_PAD_SHAPE_AS_POLYGON_RESPONSE: &str = "kiapi.board.commands.PadShapeAs const RES_PADSTACK_PRESENCE_RESPONSE: &str = "kiapi.board.commands.PadstackPresenceResponse"; const RES_VECTOR2: &str = "kiapi.common.types.Vector2"; const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse"; +const RES_BEGIN_COMMIT_RESPONSE: &str = "kiapi.common.commands.BeginCommitResponse"; const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse"; const RES_GET_BOUNDING_BOX_RESPONSE: &str = "kiapi.common.commands.GetBoundingBoxResponse"; const RES_HIT_TEST_RESPONSE: &str = "kiapi.common.commands.HitTestResponse"; @@ -462,6 +465,21 @@ impl KiCadClient { Ok(!docs.is_empty()) } + pub async fn begin_commit_raw(&self) -> Result { + let command = common_commands::BeginCommit {}; + let response = self + .send_command(envelope::pack_any(&command, CMD_BEGIN_COMMIT)) + .await?; + response_payload_as_any(response, RES_BEGIN_COMMIT_RESPONSE) + } + + pub async fn begin_commit(&self) -> Result { + let payload = self.begin_commit_raw().await?; + let response: common_commands::BeginCommitResponse = + decode_any(&payload, RES_BEGIN_COMMIT_RESPONSE)?; + map_commit_session(response) + } + pub async fn get_nets(&self) -> Result, KiCadError> { let board = self.current_board_document_proto().await?; let command = board_commands::GetNets { @@ -1551,6 +1569,22 @@ fn summarize_item_details( Ok(details) } +fn map_commit_session( + response: common_commands::BeginCommitResponse, +) -> Result { + let id = response.id.ok_or_else(|| KiCadError::InvalidResponse { + reason: "BeginCommit response missing commit id".to_string(), + })?; + + if id.value.is_empty() { + return Err(KiCadError::InvalidResponse { + reason: "BeginCommit response returned empty commit id".to_string(), + }); + } + + Ok(CommitSession { id: id.value }) +} + fn ensure_item_request_ok(status: i32) -> Result<(), KiCadError> { let request_status = common_types::ItemRequestStatus::try_from(status) .unwrap_or(common_types::ItemRequestStatus::IrsUnknown); @@ -2617,12 +2651,12 @@ fn default_client_name() -> String { #[cfg(test)] mod tests { use super::{ - any_to_pretty_debug, ensure_item_request_ok, layer_to_model, map_hit_test_result, - map_item_bounding_boxes, map_polygon_with_holes, model_document_to_proto, - normalize_socket_uri, pad_netlist_from_footprint_items, select_single_board_document, - select_single_project_path, selection_item_detail, summarize_item_details, - summarize_selection, text_horizontal_alignment_to_proto, text_spec_to_proto, - PCB_OBJECT_TYPES, + any_to_pretty_debug, ensure_item_request_ok, layer_to_model, map_commit_session, + map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, + model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, + response_payload_as_any, select_single_board_document, select_single_project_path, + selection_item_detail, summarize_item_details, summarize_selection, + text_horizontal_alignment_to_proto, text_spec_to_proto, PCB_OBJECT_TYPES, }; use crate::error::KiCadError; use crate::model::common::{ @@ -2759,6 +2793,41 @@ mod tests { assert_eq!(project.path, "/tmp/demo"); } + #[test] + fn map_commit_session_maps_commit_id() { + let response = crate::proto::kiapi::common::commands::BeginCommitResponse { + id: Some(crate::proto::kiapi::common::types::Kiid { + value: "commit-123".to_string(), + }), + }; + + let session = map_commit_session(response).expect("commit id should map"); + assert_eq!(session.id, "commit-123"); + } + + #[test] + fn map_commit_session_requires_commit_id() { + let response = crate::proto::kiapi::common::commands::BeginCommitResponse { id: None }; + let err = map_commit_session(response).expect_err("missing id must fail"); + assert!(matches!(err, KiCadError::InvalidResponse { .. })); + } + + #[test] + fn response_payload_as_any_validates_type_url() { + let response = crate::proto::kiapi::common::ApiResponse { + header: None, + status: None, + message: Some(prost_types::Any { + type_url: super::envelope::type_url("kiapi.common.commands.GetVersionResponse"), + value: Vec::new(), + }), + }; + + let err = response_payload_as_any(response, "kiapi.common.commands.BeginCommitResponse") + .expect_err("wrong type_url must fail"); + assert!(matches!(err, KiCadError::UnexpectedPayloadType { .. })); + } + #[test] fn summarize_selection_counts_payload_types() { let items = vec![ diff --git a/src/lib.rs b/src/lib.rs index 3b11e53..645ff19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,8 +33,9 @@ pub use crate::model::board::{ Vector2Nm, }; pub use crate::model::common::{ - DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode, - SelectionItemDetail, SelectionSummary, SelectionTypeCount, TextAsShapesEntry, - TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, TextObjectSpec, - TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, VersionInfo, + CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, + PcbObjectTypeCode, SelectionItemDetail, SelectionSummary, SelectionTypeCount, + TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, + TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, + VersionInfo, }; diff --git a/src/model/common.rs b/src/model/common.rs index 6bfd6c5..0a0291a 100644 --- a/src/model/common.rs +++ b/src/model/common.rs @@ -113,6 +113,11 @@ pub struct SelectionItemDetail { pub raw_len: usize, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CommitSession { + pub id: String, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct TitleBlockInfo { pub title: String, diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 6d20915..44bb3c1 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -50,6 +50,7 @@ enum Command { BoardOrigin { kind: BoardOriginKind, }, + BeginCommit, SelectionSummary, SelectionDetails, SelectionRaw, @@ -309,6 +310,10 @@ async fn run() -> Result<(), KiCadError> { kind, origin.x_nm, origin.y_nm ); } + Command::BeginCommit => { + let session = client.begin_commit().await?; + println!("commit_id={}", session.id); + } Command::SelectionSummary => { let summary = client.get_selection_summary().await?; println!("selection_total={}", summary.total_items); @@ -761,6 +766,7 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> { } Command::BoardOrigin { kind } } + "begin-commit" => Command::BeginCommit, "selection-summary" => Command::SelectionSummary, "selection-details" => Command::SelectionDetails, "selection-raw" => Command::SelectionRaw, @@ -1075,7 +1081,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] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\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 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 active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\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] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\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 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 active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\n begin-commit Start staged commit and print commit ID\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" ); } From aa406927a5111df9d088b34479be50b9d8b8b8df Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 16:51:27 +0800 Subject: [PATCH 02/28] feat(client): add EndCommit API and CLI command --- README.md | 6 +- docs/TEST_CLI.md | 14 +++- src/client.rs | 72 ++++++++++++++++--- src/lib.rs | 10 +-- src/model/common.rs | 46 ++++++++++++ test-scripts/kicad-ipc-cli.rs | 127 ++++++++++++++++++++++++++++++++-- 6 files changed, 253 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 882e512..381e177 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 4 | 67% | -| Common editor/document | 23 | 10 | 43% | +| Common editor/document | 23 | 11 | 48% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 13 | 59% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **30** | **54%** | +| **Total** | **56** | **31** | **55%** | ### Common (base) @@ -67,7 +67,7 @@ Legend: | `RevertDocument` | Not yet | - | | `RunAction` | Not yet | - | | `BeginCommit` | Implemented | `KiCadClient::begin_commit_raw`, `KiCadClient::begin_commit` | -| `EndCommit` | Not yet | - | +| `EndCommit` | Implemented | `KiCadClient::end_commit_raw`, `KiCadClient::end_commit` | | `CreateItems` | Not yet | - | | `GetItems` | Implemented | `KiCadClient::get_items_raw_by_type_codes`, `KiCadClient::get_items_by_type_codes`, `KiCadClient::get_items_details_by_type_codes`, `KiCadClient::get_all_pcb_items_raw`, `KiCadClient::get_all_pcb_items`, `KiCadClient::get_all_pcb_items_details`, `KiCadClient::get_pad_netlist` | | `GetItemsById` | Implemented | `KiCadClient::get_items_by_id_raw`, `KiCadClient::get_items_by_id`, `KiCadClient::get_items_by_id_details` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 5dfb17b..47ba650 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -110,7 +110,13 @@ cargo run --bin kicad-ipc-cli -- board-origin --type drill Start a staged commit and print commit ID: ```bash -cargo run --bin kicad-ipc-cli -- begin-commit +cargo run --bin kicad-ipc-cli -- --client-name write-test begin-commit +``` + +End a staged commit: + +```bash +cargo run --bin kicad-ipc-cli -- --client-name write-test end-commit --id --action drop --message "cli test cleanup" ``` Show summary of current PCB selection by item type: @@ -265,6 +271,12 @@ Custom token: cargo run --bin kicad-ipc-cli -- --token "$KICAD_API_TOKEN" version ``` +Stable client name (needed when pairing `begin-commit` and `end-commit` across separate CLI runs): + +```bash +cargo run --bin kicad-ipc-cli -- --client-name write-test begin-commit +``` + Custom timeout: ```bash diff --git a/src/client.rs b/src/client.rs index 3e7f818..3c0e0c0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,11 +18,11 @@ use crate::model::board::{ Vector2Nm, }; use crate::model::common::{ - CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, - PcbObjectTypeCode, ProjectInfo, SelectionItemDetail, SelectionSummary, SelectionTypeCount, - TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, - TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, - VersionInfo, + CommitAction, CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, + ItemHitTestResult, PcbObjectTypeCode, ProjectInfo, SelectionItemDetail, SelectionSummary, + SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, + TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, + TextVerticalAlignment, TitleBlockInfo, VersionInfo, }; use crate::proto::kiapi::board as board_proto; use crate::proto::kiapi::board::commands as board_commands; @@ -60,6 +60,7 @@ const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str = "kiapi.board.commands.CheckPadstackPresenceOnLayers"; const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection"; const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; +const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems"; const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById"; const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox"; @@ -90,6 +91,7 @@ const RES_PADSTACK_PRESENCE_RESPONSE: &str = "kiapi.board.commands.PadstackPrese const RES_VECTOR2: &str = "kiapi.common.types.Vector2"; const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse"; const RES_BEGIN_COMMIT_RESPONSE: &str = "kiapi.common.commands.BeginCommitResponse"; +const RES_END_COMMIT_RESPONSE: &str = "kiapi.common.commands.EndCommitResponse"; const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse"; const RES_GET_BOUNDING_BOX_RESPONSE: &str = "kiapi.common.commands.GetBoundingBoxResponse"; const RES_HIT_TEST_RESPONSE: &str = "kiapi.common.commands.HitTestResponse"; @@ -480,6 +482,39 @@ impl KiCadClient { map_commit_session(response) } + pub async fn end_commit_raw( + &self, + session: CommitSession, + action: CommitAction, + message: impl Into, + ) -> Result { + if session.id.is_empty() { + return Err(KiCadError::Config { + reason: "end_commit_raw requires a non-empty commit session id".to_string(), + }); + } + + let command = common_commands::EndCommit { + id: Some(common_types::Kiid { value: session.id }), + action: commit_action_to_proto(action), + message: message.into(), + }; + let response = self + .send_command(envelope::pack_any(&command, CMD_END_COMMIT)) + .await?; + response_payload_as_any(response, RES_END_COMMIT_RESPONSE) + } + + pub async fn end_commit( + &self, + session: CommitSession, + action: CommitAction, + message: impl Into, + ) -> Result<(), KiCadError> { + self.end_commit_raw(session, action, message).await?; + Ok(()) + } + pub async fn get_nets(&self) -> Result, KiCadError> { let board = self.current_board_document_proto().await?; let command = board_commands::GetNets { @@ -1534,6 +1569,13 @@ fn board_origin_kind_to_proto(kind: BoardOriginKind) -> i32 { } } +fn commit_action_to_proto(action: CommitAction) -> i32 { + match action { + CommitAction::Commit => common_commands::CommitAction::CmaCommit as i32, + CommitAction::Drop => common_commands::CommitAction::CmaDrop as i32, + } +} + fn summarize_selection(items: Vec) -> SelectionSummary { let mut counts = BTreeMap::::new(); @@ -2651,8 +2693,8 @@ fn default_client_name() -> String { #[cfg(test)] mod tests { use super::{ - any_to_pretty_debug, ensure_item_request_ok, layer_to_model, map_commit_session, - map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, + any_to_pretty_debug, commit_action_to_proto, ensure_item_request_ok, layer_to_model, + map_commit_session, map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, response_payload_as_any, select_single_board_document, select_single_project_path, selection_item_detail, summarize_item_details, summarize_selection, @@ -2660,8 +2702,8 @@ mod tests { }; use crate::error::KiCadError; use crate::model::common::{ - DocumentSpecifier, DocumentType, ProjectInfo, TextAttributesSpec, TextHorizontalAlignment, - TextSpec, + CommitAction, DocumentSpecifier, DocumentType, ProjectInfo, TextAttributesSpec, + TextHorizontalAlignment, TextSpec, }; use prost::Message; use std::path::PathBuf; @@ -2812,6 +2854,18 @@ mod tests { assert!(matches!(err, KiCadError::InvalidResponse { .. })); } + #[test] + fn commit_action_to_proto_maps_known_variants() { + assert_eq!( + commit_action_to_proto(CommitAction::Commit), + crate::proto::kiapi::common::commands::CommitAction::CmaCommit as i32 + ); + assert_eq!( + commit_action_to_proto(CommitAction::Drop), + crate::proto::kiapi::common::commands::CommitAction::CmaDrop as i32 + ); + } + #[test] fn response_payload_as_any_validates_type_url() { let response = crate::proto::kiapi::common::ApiResponse { diff --git a/src/lib.rs b/src/lib.rs index 645ff19..b919033 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,9 +33,9 @@ pub use crate::model::board::{ Vector2Nm, }; pub use crate::model::common::{ - CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, - PcbObjectTypeCode, SelectionItemDetail, SelectionSummary, SelectionTypeCount, - TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, - TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, - VersionInfo, + CommitAction, CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, + ItemHitTestResult, PcbObjectTypeCode, SelectionItemDetail, SelectionSummary, + SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, + TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, + TextVerticalAlignment, TitleBlockInfo, VersionInfo, }; diff --git a/src/model/common.rs b/src/model/common.rs index 0a0291a..89f7dc0 100644 --- a/src/model/common.rs +++ b/src/model/common.rs @@ -118,6 +118,35 @@ pub struct CommitSession { pub id: String, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CommitAction { + Commit, + Drop, +} + +impl std::fmt::Display for CommitAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Commit => write!(f, "commit"), + Self::Drop => write!(f, "drop"), + } + } +} + +impl FromStr for CommitAction { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "commit" => Ok(Self::Commit), + "drop" => Ok(Self::Drop), + _ => Err(format!( + "unknown commit action `{value}`; expected `commit` or `drop`" + )), + } + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct TitleBlockInfo { pub title: String, @@ -304,3 +333,20 @@ impl std::fmt::Display for ItemHitTestResult { write!(f, "{value}") } } + +#[cfg(test)] +mod tests { + use super::CommitAction; + use std::str::FromStr; + + #[test] + fn commit_action_parses_known_values() { + assert_eq!(CommitAction::from_str("commit"), Ok(CommitAction::Commit)); + assert_eq!(CommitAction::from_str("drop"), Ok(CommitAction::Drop)); + } + + #[test] + fn commit_action_rejects_unknown_values() { + assert!(CommitAction::from_str("rollback").is_err()); + } +} diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 44bb3c1..4c434d0 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -6,8 +6,9 @@ use std::str::FromStr; use std::time::Duration; use kicad_ipc::{ - BoardOriginKind, ClientBuilder, DocumentType, KiCadClient, KiCadError, PadstackPresenceState, - PcbObjectTypeCode, TextObjectSpec, TextShapeGeometry, TextSpec, Vector2Nm, + BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, KiCadClient, + KiCadError, PadstackPresenceState, PcbObjectTypeCode, TextObjectSpec, TextShapeGeometry, + TextSpec, Vector2Nm, }; const REPORT_MAX_PAD_NET_ROWS: usize = 2_000; @@ -20,6 +21,7 @@ const REPORT_MAX_BOARD_SNAPSHOT_CHARS: usize = 750_000; struct CliConfig { socket: Option, token: Option, + client_name: Option, timeout_ms: u64, } @@ -51,6 +53,11 @@ enum Command { kind: BoardOriginKind, }, BeginCommit, + EndCommit { + id: String, + action: CommitAction, + message: String, + }, SelectionSummary, SelectionDetails, SelectionRaw, @@ -142,6 +149,9 @@ async fn run() -> Result<(), KiCadError> { if let Some(token) = config.token { builder = builder.token(token); } + if let Some(client_name) = config.client_name { + builder = builder.client_name(client_name); + } let client = builder.connect().await?; @@ -314,6 +324,16 @@ async fn run() -> Result<(), KiCadError> { let session = client.begin_commit().await?; println!("commit_id={}", session.id); } + Command::EndCommit { + id, + action, + message, + } => { + client + .end_commit(CommitSession { id }, action, message) + .await?; + println!("end_commit=ok action={}", action); + } Command::SelectionSummary => { let summary = client.get_selection_summary().await?; println!("selection_total={}", summary.total_items); @@ -615,8 +635,10 @@ async fn run() -> Result<(), KiCadError> { } fn parse_args() -> Result<(CliConfig, Command), KiCadError> { - let mut args: Vec = std::env::args().skip(1).collect(); + parse_args_from(std::env::args().skip(1).collect()) +} +fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadError> { if args.is_empty() { return Ok((default_config(), Command::Help)); } @@ -640,6 +662,13 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> { config.token = Some(value.clone()); args.drain(index..=index + 1); } + "--client-name" => { + let value = args.get(index + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for --client-name".to_string(), + })?; + config.client_name = Some(value.clone()); + args.drain(index..=index + 1); + } "--timeout-ms" => { let value = args.get(index + 1).ok_or_else(|| KiCadError::Config { reason: "missing value for --timeout-ms".to_string(), @@ -767,6 +796,49 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> { Command::BoardOrigin { kind } } "begin-commit" => Command::BeginCommit, + "end-commit" => { + let mut id = None; + let mut action = CommitAction::Commit; + let mut message = String::new(); + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--id" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for end-commit --id".to_string(), + })?; + id = Some(value.clone()); + i += 2; + } + "--action" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for end-commit --action".to_string(), + })?; + action = CommitAction::from_str(value) + .map_err(|err| KiCadError::Config { reason: err })?; + i += 2; + } + "--message" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for end-commit --message".to_string(), + })?; + message = value.clone(); + i += 2; + } + _ => { + i += 1; + } + } + } + + Command::EndCommit { + id: id.ok_or_else(|| KiCadError::Config { + reason: "end-commit requires `--id `".to_string(), + })?, + action, + message, + } + } "selection-summary" => Command::SelectionSummary, "selection-details" => Command::SelectionDetails, "selection-raw" => Command::SelectionRaw, @@ -1075,13 +1147,14 @@ fn default_config() -> CliConfig { CliConfig { socket: None, token: None, + client_name: None, timeout_ms: 15_000, } } fn print_help() { println!( - "kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\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 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 active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\n begin-commit Start staged commit and print commit ID\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 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 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 active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\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 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" ); } @@ -1630,3 +1703,49 @@ fn hex_char(value: u8) -> char { _ => '?', } } + +#[cfg(test)] +mod tests { + use super::{parse_args_from, Command}; + use kicad_ipc::CommitAction; + + #[test] + fn parse_args_accepts_client_name_for_commit_flow() { + let (config, command) = parse_args_from(vec![ + "--client-name".to_string(), + "write-test".to_string(), + "begin-commit".to_string(), + ]) + .expect("client-name + begin-commit should parse"); + + assert_eq!(config.client_name.as_deref(), Some("write-test")); + assert!(matches!(command, Command::BeginCommit)); + } + + #[test] + fn parse_args_parses_end_commit_flags() { + let (_, command) = parse_args_from(vec![ + "end-commit".to_string(), + "--id".to_string(), + "commit-1".to_string(), + "--action".to_string(), + "drop".to_string(), + "--message".to_string(), + "cleanup".to_string(), + ]) + .expect("end-commit args should parse"); + + match command { + Command::EndCommit { + id, + action, + message, + } => { + assert_eq!(id, "commit-1"); + assert_eq!(action, CommitAction::Drop); + assert_eq!(message, "cleanup"); + } + other => panic!("unexpected command variant: {other:?}"), + } + } +} From 331910444dd724bc4f6b049e70eab58a09fcfee1 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 16:54:54 +0800 Subject: [PATCH 03/28] feat(client): add RefreshEditor API and CLI command --- README.md | 6 +-- docs/TEST_CLI.md | 8 ++++ src/client.rs | 14 ++++++- src/lib.rs | 2 +- src/model/common.rs | 78 ++++++++++++++++++++++++++++++++++- test-scripts/kicad-ipc-cli.rs | 49 ++++++++++++++++++++-- 6 files changed, 147 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 381e177..ba1254b 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 4 | 67% | -| Common editor/document | 23 | 11 | 48% | +| Common editor/document | 23 | 12 | 52% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 13 | 59% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **31** | **55%** | +| **Total** | **56** | **32** | **57%** | ### Common (base) @@ -60,7 +60,7 @@ Legend: | KiCad Command | Status | Rust API | | --- | --- | --- | -| `RefreshEditor` | Not yet | - | +| `RefreshEditor` | Implemented | `KiCadClient::refresh_editor` | | `GetOpenDocuments` | Implemented | `KiCadClient::get_open_documents`, `KiCadClient::get_current_project_path`, `KiCadClient::has_open_board` | | `SaveDocument` | Not yet | - | | `SaveCopyOfDocument` | Not yet | - | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 47ba650..bf8d70e 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -107,6 +107,14 @@ Show drill origin: cargo run --bin kicad-ipc-cli -- board-origin --type drill ``` +Refresh PCB editor: + +```bash +cargo run --bin kicad-ipc-cli -- refresh-editor --frame pcb +``` + +If your KiCad build does not expose this handler yet, this call may return `AS_UNHANDLED`. + Start a staged commit and print commit ID: ```bash diff --git a/src/client.rs b/src/client.rs index 3c0e0c0..5cca2da 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,7 +18,7 @@ use crate::model::board::{ Vector2Nm, }; use crate::model::common::{ - CommitAction, CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, + CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode, ProjectInfo, SelectionItemDetail, SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, @@ -42,6 +42,7 @@ const CMD_GET_TEXT_VARIABLES: &str = "kiapi.common.commands.GetTextVariables"; const CMD_EXPAND_TEXT_VARIABLES: &str = "kiapi.common.commands.ExpandTextVariables"; const CMD_GET_TEXT_EXTENTS: &str = "kiapi.common.commands.GetTextExtents"; const CMD_GET_TEXT_AS_SHAPES: &str = "kiapi.common.commands.GetTextAsShapes"; +const CMD_REFRESH_EDITOR: &str = "kiapi.common.commands.RefreshEditor"; const CMD_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocuments"; const CMD_GET_NETS: &str = "kiapi.board.commands.GetNets"; const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabledLayers"; @@ -293,6 +294,17 @@ impl KiCadClient { Ok(()) } + pub async fn refresh_editor(&self, frame: EditorFrameType) -> Result<(), KiCadError> { + let command = envelope::pack_any( + &common_commands::RefreshEditor { + frame: frame.to_proto(), + }, + CMD_REFRESH_EDITOR, + ); + self.send_command(command).await?; + Ok(()) + } + pub async fn get_version(&self) -> Result { let command = envelope::pack_any(&common_commands::GetVersion {}, CMD_GET_VERSION); let response = self.send_command(command).await?; diff --git a/src/lib.rs b/src/lib.rs index b919033..23bf66e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ pub use crate::model::board::{ Vector2Nm, }; pub use crate::model::common::{ - CommitAction, CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, + CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode, SelectionItemDetail, SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, diff --git a/src/model/common.rs b/src/model/common.rs index 89f7dc0..d8d4e09 100644 --- a/src/model/common.rs +++ b/src/model/common.rs @@ -12,6 +12,65 @@ pub struct VersionInfo { pub full_version: String, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EditorFrameType { + ProjectManager, + SchematicEditor, + PcbEditor, + SpiceSimulator, + SymbolEditor, + FootprintEditor, + DrawingSheetEditor, +} + +impl EditorFrameType { + pub(crate) fn to_proto(self) -> i32 { + match self { + Self::ProjectManager => common_types::FrameType::FtProjectManager as i32, + Self::SchematicEditor => common_types::FrameType::FtSchematicEditor as i32, + Self::PcbEditor => common_types::FrameType::FtPcbEditor as i32, + Self::SpiceSimulator => common_types::FrameType::FtSpiceSimulator as i32, + Self::SymbolEditor => common_types::FrameType::FtSymbolEditor as i32, + Self::FootprintEditor => common_types::FrameType::FtFootprintEditor as i32, + Self::DrawingSheetEditor => common_types::FrameType::FtDrawingSheetEditor as i32, + } + } +} + +impl std::fmt::Display for EditorFrameType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = match self { + Self::ProjectManager => "project-manager", + Self::SchematicEditor => "schematic", + Self::PcbEditor => "pcb", + Self::SpiceSimulator => "spice", + Self::SymbolEditor => "symbol", + Self::FootprintEditor => "footprint", + Self::DrawingSheetEditor => "drawing-sheet", + }; + write!(f, "{value}") + } +} + +impl FromStr for EditorFrameType { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "project-manager" => Ok(Self::ProjectManager), + "schematic" => Ok(Self::SchematicEditor), + "pcb" => Ok(Self::PcbEditor), + "spice" => Ok(Self::SpiceSimulator), + "symbol" => Ok(Self::SymbolEditor), + "footprint" => Ok(Self::FootprintEditor), + "drawing-sheet" => Ok(Self::DrawingSheetEditor), + _ => Err(format!( + "unknown frame `{value}`; expected one of: project-manager, schematic, pcb, spice, symbol, footprint, drawing-sheet" + )), + } + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DocumentType { Schematic, @@ -336,7 +395,7 @@ impl std::fmt::Display for ItemHitTestResult { #[cfg(test)] mod tests { - use super::CommitAction; + use super::{CommitAction, EditorFrameType}; use std::str::FromStr; #[test] @@ -349,4 +408,21 @@ mod tests { fn commit_action_rejects_unknown_values() { assert!(CommitAction::from_str("rollback").is_err()); } + + #[test] + fn editor_frame_type_parses_known_values() { + assert_eq!( + EditorFrameType::from_str("pcb"), + Ok(EditorFrameType::PcbEditor) + ); + assert_eq!( + EditorFrameType::from_str("project-manager"), + Ok(EditorFrameType::ProjectManager) + ); + } + + #[test] + fn editor_frame_type_rejects_unknown_values() { + assert!(EditorFrameType::from_str("layout").is_err()); + } } diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 4c434d0..7e1a531 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -6,9 +6,9 @@ use std::str::FromStr; use std::time::Duration; use kicad_ipc::{ - BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, KiCadClient, - KiCadError, PadstackPresenceState, PcbObjectTypeCode, TextObjectSpec, TextShapeGeometry, - TextSpec, Vector2Nm, + BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, EditorFrameType, + KiCadClient, KiCadError, PadstackPresenceState, PcbObjectTypeCode, TextObjectSpec, + TextShapeGeometry, TextSpec, Vector2Nm, }; const REPORT_MAX_PAD_NET_ROWS: usize = 2_000; @@ -52,6 +52,9 @@ enum Command { BoardOrigin { kind: BoardOriginKind, }, + RefreshEditor { + frame: EditorFrameType, + }, BeginCommit, EndCommit { id: String, @@ -320,6 +323,10 @@ async fn run() -> Result<(), KiCadError> { kind, origin.x_nm, origin.y_nm ); } + Command::RefreshEditor { frame } => { + client.refresh_editor(frame).await?; + println!("refresh_editor=ok frame={}", frame); + } Command::BeginCommit => { let session = client.begin_commit().await?; println!("commit_id={}", session.id); @@ -795,6 +802,23 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE } Command::BoardOrigin { kind } } + "refresh-editor" => { + let mut frame = EditorFrameType::PcbEditor; + let mut i = 1; + while i < args.len() { + if args[i] == "--frame" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for refresh-editor --frame".to_string(), + })?; + frame = EditorFrameType::from_str(value) + .map_err(|err| KiCadError::Config { reason: err })?; + i += 2; + continue; + } + i += 1; + } + Command::RefreshEditor { frame } + } "begin-commit" => Command::BeginCommit, "end-commit" => { let mut id = None; @@ -1154,7 +1178,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 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 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 active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\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 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 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 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 active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type ] Show board origin (`grid` default, 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 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" ); } @@ -1748,4 +1772,21 @@ mod tests { other => panic!("unexpected command variant: {other:?}"), } } + + #[test] + fn parse_args_parses_refresh_editor_frame() { + let (_, command) = parse_args_from(vec![ + "refresh-editor".to_string(), + "--frame".to_string(), + "schematic".to_string(), + ]) + .expect("refresh-editor args should parse"); + + match command { + Command::RefreshEditor { frame } => { + assert_eq!(frame.to_string(), "schematic"); + } + other => panic!("unexpected command variant: {other:?}"), + } + } } From 35f6773b115518ea5f6038e1b4a6575321434396 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 16:58:11 +0800 Subject: [PATCH 04/28] feat(client): add SetActiveLayer API and CLI command --- README.md | 6 ++--- docs/TEST_CLI.md | 6 +++++ src/client.rs | 13 ++++++++++ test-scripts/kicad-ipc-cli.rs | 46 ++++++++++++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ba1254b..b98d88a 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Legend: | Common (base) | 6 | 4 | 67% | | Common editor/document | 23 | 12 | 52% | | Project manager | 5 | 3 | 60% | -| Board editor (PCB) | 22 | 13 | 59% | +| Board editor (PCB) | 22 | 14 | 64% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **32** | **57%** | +| **Total** | **56** | **33** | **59%** | ### Common (base) @@ -116,7 +116,7 @@ Legend: | `GetVisibleLayers` | Implemented | `KiCadClient::get_visible_layers` | | `SetVisibleLayers` | Not yet | - | | `GetActiveLayer` | Implemented | `KiCadClient::get_active_layer` | -| `SetActiveLayer` | Not yet | - | +| `SetActiveLayer` | Implemented | `KiCadClient::set_active_layer` | | `GetBoardEditorAppearanceSettings` | Implemented | `KiCadClient::get_board_editor_appearance_settings_raw`, `KiCadClient::get_board_editor_appearance_settings` | | `SetBoardEditorAppearanceSettings` | Not yet | - | | `InteractiveMoveItems` | Not yet | - | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index bf8d70e..8f727ac 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -89,6 +89,12 @@ Show active layer: cargo run --bin kicad-ipc-cli -- active-layer ``` +Set active layer: + +```bash +cargo run --bin kicad-ipc-cli -- set-active-layer --layer-id 0 +``` + Show visible layers: ```bash diff --git a/src/client.rs b/src/client.rs index 5cca2da..b46a478 100644 --- a/src/client.rs +++ b/src/client.rs @@ -47,6 +47,7 @@ const CMD_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocuments"; const CMD_GET_NETS: &str = "kiapi.board.commands.GetNets"; const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabledLayers"; const CMD_GET_ACTIVE_LAYER: &str = "kiapi.board.commands.GetActiveLayer"; +const CMD_SET_ACTIVE_LAYER: &str = "kiapi.board.commands.SetActiveLayer"; const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers"; const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin"; const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup"; @@ -581,6 +582,18 @@ impl KiCadClient { Ok(layer_to_model(payload.layer)) } + pub async fn set_active_layer(&self, layer_id: i32) -> Result<(), KiCadError> { + let board = self.current_board_document_proto().await?; + let command = board_commands::SetActiveLayer { + board: Some(board), + layer: layer_id, + }; + + self.send_command(envelope::pack_any(&command, CMD_SET_ACTIVE_LAYER)) + .await?; + Ok(()) + } + pub async fn get_visible_layers(&self) -> Result, KiCadError> { let board = self.current_board_document_proto().await?; let command = board_commands::GetVisibleLayers { board: Some(board) }; diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 7e1a531..233870b 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -48,6 +48,9 @@ enum Command { Nets, EnabledLayers, ActiveLayer, + SetActiveLayer { + layer_id: i32, + }, VisibleLayers, BoardOrigin { kind: BoardOriginKind, @@ -306,6 +309,10 @@ async fn run() -> Result<(), KiCadError> { layer.id, layer.name ); } + Command::SetActiveLayer { layer_id } => { + client.set_active_layer(layer_id).await?; + println!("set_active_layer_id={}", layer_id); + } Command::VisibleLayers => { let layers = client.get_visible_layers().await?; if layers.is_empty() { @@ -784,6 +791,28 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE "nets" => Command::Nets, "enabled-layers" => Command::EnabledLayers, "active-layer" => Command::ActiveLayer, + "set-active-layer" => { + let mut layer_id = None; + let mut i = 1; + while i < args.len() { + if args[i] == "--layer-id" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-active-layer --layer-id".to_string(), + })?; + layer_id = Some(value.parse::().map_err(|err| KiCadError::Config { + reason: format!("invalid set-active-layer --layer-id `{value}`: {err}"), + })?); + i += 2; + continue; + } + i += 1; + } + Command::SetActiveLayer { + layer_id: layer_id.ok_or_else(|| KiCadError::Config { + reason: "set-active-layer requires `--layer-id `".to_string(), + })?, + } + } "visible-layers" => Command::VisibleLayers, "board-origin" => { let mut kind = BoardOriginKind::Grid; @@ -1178,7 +1207,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 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 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 active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type ] Show board origin (`grid` default, 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 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 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 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 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 board-origin [--type ] Show board origin (`grid` default, 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 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" ); } @@ -1789,4 +1818,19 @@ mod tests { other => panic!("unexpected command variant: {other:?}"), } } + + #[test] + fn parse_args_parses_set_active_layer() { + let (_, command) = parse_args_from(vec![ + "set-active-layer".to_string(), + "--layer-id".to_string(), + "31".to_string(), + ]) + .expect("set-active-layer args should parse"); + + match command { + Command::SetActiveLayer { layer_id } => assert_eq!(layer_id, 31), + other => panic!("unexpected command variant: {other:?}"), + } + } } From 438b4999b9e84bcf900519bdd6ce5741d1f2d53e Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 17:06:58 +0800 Subject: [PATCH 05/28] feat(client): add SetVisibleLayers API and CLI command --- README.md | 6 ++-- docs/TEST_CLI.md | 6 ++++ src/client.rs | 13 +++++++++ test-scripts/kicad-ipc-cli.rs | 52 ++++++++++++++++++++++++++++++++++- 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b98d88a..1cc8360 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Legend: | Common (base) | 6 | 4 | 67% | | Common editor/document | 23 | 12 | 52% | | Project manager | 5 | 3 | 60% | -| Board editor (PCB) | 22 | 14 | 64% | +| Board editor (PCB) | 22 | 15 | 68% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **33** | **59%** | +| **Total** | **56** | **34** | **61%** | ### Common (base) @@ -114,7 +114,7 @@ Legend: | `CheckPadstackPresenceOnLayers` | Implemented | `KiCadClient::check_padstack_presence_on_layers_raw`, `KiCadClient::check_padstack_presence_on_layers` | | `InjectDrcError` | Not yet | - | | `GetVisibleLayers` | Implemented | `KiCadClient::get_visible_layers` | -| `SetVisibleLayers` | Not yet | - | +| `SetVisibleLayers` | Implemented | `KiCadClient::set_visible_layers` | | `GetActiveLayer` | Implemented | `KiCadClient::get_active_layer` | | `SetActiveLayer` | Implemented | `KiCadClient::set_active_layer` | | `GetBoardEditorAppearanceSettings` | Implemented | `KiCadClient::get_board_editor_appearance_settings_raw`, `KiCadClient::get_board_editor_appearance_settings` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 8f727ac..5d3c052 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -101,6 +101,12 @@ Show visible layers: cargo run --bin kicad-ipc-cli -- visible-layers ``` +Set visible layers: + +```bash +cargo run --bin kicad-ipc-cli -- set-visible-layers --layer-id 0 --layer-id 31 +``` + Show board origin (grid origin by default): ```bash diff --git a/src/client.rs b/src/client.rs index b46a478..a247035 100644 --- a/src/client.rs +++ b/src/client.rs @@ -49,6 +49,7 @@ const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabled const CMD_GET_ACTIVE_LAYER: &str = "kiapi.board.commands.GetActiveLayer"; const CMD_SET_ACTIVE_LAYER: &str = "kiapi.board.commands.SetActiveLayer"; const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers"; +const CMD_SET_VISIBLE_LAYERS: &str = "kiapi.board.commands.SetVisibleLayers"; const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin"; const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup"; const CMD_GET_GRAPHICS_DEFAULTS: &str = "kiapi.board.commands.GetGraphicsDefaults"; @@ -608,6 +609,18 @@ impl KiCadClient { Ok(payload.layers.into_iter().map(layer_to_model).collect()) } + pub async fn set_visible_layers(&self, layer_ids: Vec) -> Result<(), KiCadError> { + let board = self.current_board_document_proto().await?; + let command = board_commands::SetVisibleLayers { + board: Some(board), + layers: layer_ids, + }; + + self.send_command(envelope::pack_any(&command, CMD_SET_VISIBLE_LAYERS)) + .await?; + Ok(()) + } + pub async fn get_board_origin(&self, kind: BoardOriginKind) -> Result { let board = self.current_board_document_proto().await?; let command = board_commands::GetBoardOrigin { diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 233870b..36db293 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -52,6 +52,9 @@ enum Command { layer_id: i32, }, VisibleLayers, + SetVisibleLayers { + layer_ids: Vec, + }, BoardOrigin { kind: BoardOriginKind, }, @@ -323,6 +326,10 @@ async fn run() -> Result<(), KiCadError> { } } } + Command::SetVisibleLayers { layer_ids } => { + client.set_visible_layers(layer_ids.clone()).await?; + println!("set_visible_layer_count={}", layer_ids.len()); + } Command::BoardOrigin { kind } => { let origin = client.get_board_origin(kind).await?; println!( @@ -814,6 +821,32 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE } } "visible-layers" => Command::VisibleLayers, + "set-visible-layers" => { + let mut layer_ids = Vec::new(); + let mut i = 1; + while i < args.len() { + if args[i] == "--layer-id" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-visible-layers --layer-id".to_string(), + })?; + layer_ids.push(value.parse::().map_err(|err| KiCadError::Config { + reason: format!("invalid set-visible-layers --layer-id `{value}`: {err}"), + })?); + i += 2; + continue; + } + i += 1; + } + + if layer_ids.is_empty() { + return Err(KiCadError::Config { + reason: "set-visible-layers requires one or more `--layer-id ` arguments" + .to_string(), + }); + } + + Command::SetVisibleLayers { layer_ids } + } "board-origin" => { let mut kind = BoardOriginKind::Grid; let mut i = 1; @@ -1207,7 +1240,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 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 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 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 board-origin [--type ] Show board origin (`grid` default, 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 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 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 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 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 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 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" ); } @@ -1833,4 +1866,21 @@ mod tests { other => panic!("unexpected command variant: {other:?}"), } } + + #[test] + fn parse_args_parses_set_visible_layers() { + let (_, command) = parse_args_from(vec![ + "set-visible-layers".to_string(), + "--layer-id".to_string(), + "3".to_string(), + "--layer-id".to_string(), + "47".to_string(), + ]) + .expect("set-visible-layers args should parse"); + + match command { + Command::SetVisibleLayers { layer_ids } => assert_eq!(layer_ids, vec![3, 47]), + other => panic!("unexpected command variant: {other:?}"), + } + } } From 080d5cf5371b8a85ea9164687e10cc7739630699 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 17:14:52 +0800 Subject: [PATCH 06/28] feat(client): add SetBoardOrigin API and CLI command --- README.md | 6 +-- docs/TEST_CLI.md | 6 +++ src/client.rs | 18 ++++++++ test-scripts/kicad-ipc-cli.rs | 86 ++++++++++++++++++++++++++++++++++- 4 files changed, 111 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1cc8360..4934292 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Legend: | Common (base) | 6 | 4 | 67% | | Common editor/document | 23 | 12 | 52% | | Project manager | 5 | 3 | 60% | -| Board editor (PCB) | 22 | 15 | 68% | +| Board editor (PCB) | 22 | 16 | 73% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **34** | **61%** | +| **Total** | **56** | **35** | **63%** | ### Common (base) @@ -104,7 +104,7 @@ Legend: | `SetBoardEnabledLayers` | Not yet | - | | `GetGraphicsDefaults` | Implemented | `KiCadClient::get_graphics_defaults_raw`, `KiCadClient::get_graphics_defaults` | | `GetBoardOrigin` | Implemented | `KiCadClient::get_board_origin` | -| `SetBoardOrigin` | Not yet | - | +| `SetBoardOrigin` | Implemented | `KiCadClient::set_board_origin` | | `GetNets` | Implemented | `KiCadClient::get_nets` | | `GetItemsByNet` | Implemented | `KiCadClient::get_items_by_net_raw`, `KiCadClient::get_items_by_net` | | `GetItemsByNetClass` | Implemented | `KiCadClient::get_items_by_net_class_raw`, `KiCadClient::get_items_by_net_class` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 5d3c052..ecab9e4 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -119,6 +119,12 @@ Show drill origin: cargo run --bin kicad-ipc-cli -- board-origin --type drill ``` +Set board origin: + +```bash +cargo run --bin kicad-ipc-cli -- set-board-origin --type grid --x-nm 1000000 --y-nm 2000000 +``` + Refresh PCB editor: ```bash diff --git a/src/client.rs b/src/client.rs index a247035..0011ba8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -51,6 +51,7 @@ const CMD_SET_ACTIVE_LAYER: &str = "kiapi.board.commands.SetActiveLayer"; const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers"; const CMD_SET_VISIBLE_LAYERS: &str = "kiapi.board.commands.SetVisibleLayers"; const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin"; +const CMD_SET_BOARD_ORIGIN: &str = "kiapi.board.commands.SetBoardOrigin"; const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup"; const CMD_GET_GRAPHICS_DEFAULTS: &str = "kiapi.board.commands.GetGraphicsDefaults"; const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = @@ -639,6 +640,23 @@ impl KiCadClient { }) } + pub async fn set_board_origin( + &self, + kind: BoardOriginKind, + origin: Vector2Nm, + ) -> Result<(), KiCadError> { + let board = self.current_board_document_proto().await?; + let command = board_commands::SetBoardOrigin { + board: Some(board), + r#type: board_origin_kind_to_proto(kind), + origin: Some(vector2_nm_to_proto(origin)), + }; + + self.send_command(envelope::pack_any(&command, CMD_SET_BOARD_ORIGIN)) + .await?; + Ok(()) + } + pub async fn get_selection_summary(&self) -> Result { let document = self.current_board_document_proto().await?; let command = common_commands::GetSelection { diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 36db293..418ea1e 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -58,6 +58,11 @@ enum Command { BoardOrigin { kind: BoardOriginKind, }, + SetBoardOrigin { + kind: BoardOriginKind, + x_nm: i64, + y_nm: i64, + }, RefreshEditor { frame: EditorFrameType, }, @@ -337,6 +342,12 @@ async fn run() -> Result<(), KiCadError> { kind, origin.x_nm, origin.y_nm ); } + Command::SetBoardOrigin { kind, x_nm, y_nm } => { + client + .set_board_origin(kind, Vector2Nm { x_nm, y_nm }) + .await?; + println!("set_origin_kind={} x_nm={} y_nm={}", kind, x_nm, y_nm); + } Command::RefreshEditor { frame } => { client.refresh_editor(frame).await?; println!("refresh_editor=ok frame={}", frame); @@ -864,6 +875,54 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE } Command::BoardOrigin { kind } } + "set-board-origin" => { + let mut kind = BoardOriginKind::Grid; + let mut x_nm = None; + let mut y_nm = None; + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--type" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-board-origin --type".to_string(), + })?; + kind = BoardOriginKind::from_str(value) + .map_err(|err| KiCadError::Config { reason: err })?; + i += 2; + } + "--x-nm" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-board-origin --x-nm".to_string(), + })?; + x_nm = Some(value.parse::().map_err(|err| KiCadError::Config { + reason: format!("invalid set-board-origin --x-nm `{value}`: {err}"), + })?); + i += 2; + } + "--y-nm" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-board-origin --y-nm".to_string(), + })?; + y_nm = Some(value.parse::().map_err(|err| KiCadError::Config { + reason: format!("invalid set-board-origin --y-nm `{value}`: {err}"), + })?); + i += 2; + } + _ => { + i += 1; + } + } + } + Command::SetBoardOrigin { + kind, + x_nm: x_nm.ok_or_else(|| KiCadError::Config { + reason: "set-board-origin requires `--x-nm `".to_string(), + })?, + y_nm: y_nm.ok_or_else(|| KiCadError::Config { + reason: "set-board-origin requires `--y-nm `".to_string(), + })?, + } + } "refresh-editor" => { let mut frame = EditorFrameType::PcbEditor; let mut i = 1; @@ -1240,7 +1299,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 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 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 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 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 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 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 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 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 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" ); } @@ -1793,7 +1852,7 @@ fn hex_char(value: u8) -> char { #[cfg(test)] mod tests { use super::{parse_args_from, Command}; - use kicad_ipc::CommitAction; + use kicad_ipc::{BoardOriginKind, CommitAction}; #[test] fn parse_args_accepts_client_name_for_commit_flow() { @@ -1883,4 +1942,27 @@ mod tests { other => panic!("unexpected command variant: {other:?}"), } } + + #[test] + fn parse_args_parses_set_board_origin() { + let (_, command) = parse_args_from(vec![ + "set-board-origin".to_string(), + "--type".to_string(), + "drill".to_string(), + "--x-nm".to_string(), + "123".to_string(), + "--y-nm".to_string(), + "456".to_string(), + ]) + .expect("set-board-origin args should parse"); + + match command { + Command::SetBoardOrigin { kind, x_nm, y_nm } => { + assert_eq!(kind, BoardOriginKind::Drill); + assert_eq!(x_nm, 123); + assert_eq!(y_nm, 456); + } + other => panic!("unexpected command variant: {other:?}"), + } + } } From e147186fbf3c4270ea8aa8f7763269211a166fcf Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 17:20:00 +0800 Subject: [PATCH 07/28] feat(client): add SetBoardEnabledLayers API and CLI command --- README.md | 6 +-- docs/TEST_CLI.md | 6 +++ src/client.rs | 36 +++++++++++++-- test-scripts/kicad-ipc-cli.rs | 86 ++++++++++++++++++++++++++++++++++- 4 files changed, 126 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4934292..e1c14cb 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Legend: | Common (base) | 6 | 4 | 67% | | Common editor/document | 23 | 12 | 52% | | Project manager | 5 | 3 | 60% | -| Board editor (PCB) | 22 | 16 | 73% | +| Board editor (PCB) | 22 | 17 | 77% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **35** | **63%** | +| **Total** | **56** | **36** | **64%** | ### Common (base) @@ -101,7 +101,7 @@ Legend: | `GetBoardStackup` | Implemented | `KiCadClient::get_board_stackup_raw`, `KiCadClient::get_board_stackup` | | `UpdateBoardStackup` | Not yet | - | | `GetBoardEnabledLayers` | Implemented | `KiCadClient::get_board_enabled_layers` | -| `SetBoardEnabledLayers` | Not yet | - | +| `SetBoardEnabledLayers` | Implemented | `KiCadClient::set_board_enabled_layers` | | `GetGraphicsDefaults` | Implemented | `KiCadClient::get_graphics_defaults_raw`, `KiCadClient::get_graphics_defaults` | | `GetBoardOrigin` | Implemented | `KiCadClient::get_board_origin` | | `SetBoardOrigin` | Implemented | `KiCadClient::set_board_origin` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index ecab9e4..525622d 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -83,6 +83,12 @@ List enabled board layers: cargo run --bin kicad-ipc-cli -- enabled-layers ``` +Set enabled board layers: + +```bash +cargo run --bin kicad-ipc-cli -- set-enabled-layers --copper-layer-count 2 --layer-id 47 --layer-id 52 +``` + Show active layer: ```bash diff --git a/src/client.rs b/src/client.rs index 0011ba8..e5fbf04 100644 --- a/src/client.rs +++ b/src/client.rs @@ -46,6 +46,7 @@ const CMD_REFRESH_EDITOR: &str = "kiapi.common.commands.RefreshEditor"; const CMD_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocuments"; const CMD_GET_NETS: &str = "kiapi.board.commands.GetNets"; const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabledLayers"; +const CMD_SET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.SetBoardEnabledLayers"; const CMD_GET_ACTIVE_LAYER: &str = "kiapi.board.commands.GetActiveLayer"; const CMD_SET_ACTIVE_LAYER: &str = "kiapi.board.commands.SetActiveLayer"; const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers"; @@ -564,10 +565,28 @@ impl KiCadClient { let payload: board_commands::BoardEnabledLayersResponse = envelope::unpack_any(&response, RES_GET_BOARD_ENABLED_LAYERS)?; - Ok(BoardEnabledLayers { - copper_layer_count: payload.copper_layer_count, - layers: payload.layers.into_iter().map(layer_to_model).collect(), - }) + Ok(map_board_enabled_layers_response(payload)) + } + + pub async fn set_board_enabled_layers( + &self, + copper_layer_count: u32, + layer_ids: Vec, + ) -> Result { + let board = self.current_board_document_proto().await?; + let command = board_commands::SetBoardEnabledLayers { + board: Some(board), + copper_layer_count, + layers: layer_ids, + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_SET_BOARD_ENABLED_LAYERS)) + .await?; + + let payload: board_commands::BoardEnabledLayersResponse = + envelope::unpack_any(&response, RES_GET_BOARD_ENABLED_LAYERS)?; + Ok(map_board_enabled_layers_response(payload)) } pub async fn get_active_layer(&self) -> Result { @@ -1618,6 +1637,15 @@ fn layer_to_model(layer_id: i32) -> BoardLayerInfo { BoardLayerInfo { id: layer_id, name } } +fn map_board_enabled_layers_response( + payload: board_commands::BoardEnabledLayersResponse, +) -> BoardEnabledLayers { + BoardEnabledLayers { + copper_layer_count: payload.copper_layer_count, + layers: payload.layers.into_iter().map(layer_to_model).collect(), + } +} + fn board_origin_kind_to_proto(kind: BoardOriginKind) -> i32 { match kind { BoardOriginKind::Grid => board_commands::BoardOriginType::BotGrid as i32, diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 418ea1e..806bb99 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -47,6 +47,10 @@ enum Command { }, Nets, EnabledLayers, + SetEnabledLayers { + copper_layer_count: u32, + layer_ids: Vec, + }, ActiveLayer, SetActiveLayer { layer_id: i32, @@ -310,6 +314,18 @@ async fn run() -> Result<(), KiCadError> { println!("layer_id={} layer_name={}", layer.id, layer.name); } } + Command::SetEnabledLayers { + copper_layer_count, + layer_ids, + } => { + let enabled = client + .set_board_enabled_layers(copper_layer_count, layer_ids) + .await?; + println!("copper_layer_count={}", enabled.copper_layer_count); + for layer in enabled.layers { + println!("layer_id={} layer_name={}", layer.id, layer.name); + } + } Command::ActiveLayer => { let layer = client.get_active_layer().await?; println!( @@ -808,6 +824,49 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE } "nets" => Command::Nets, "enabled-layers" => Command::EnabledLayers, + "set-enabled-layers" => { + let mut copper_layer_count = None; + let mut layer_ids = Vec::new(); + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--copper-layer-count" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-enabled-layers --copper-layer-count" + .to_string(), + })?; + copper_layer_count = + Some(value.parse::().map_err(|err| KiCadError::Config { + reason: format!( + "invalid set-enabled-layers --copper-layer-count `{value}`: {err}" + ), + })?); + i += 2; + } + "--layer-id" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-enabled-layers --layer-id".to_string(), + })?; + layer_ids.push(value.parse::().map_err(|err| KiCadError::Config { + reason: format!( + "invalid set-enabled-layers --layer-id `{value}`: {err}" + ), + })?); + i += 2; + } + _ => { + i += 1; + } + } + } + + Command::SetEnabledLayers { + copper_layer_count: copper_layer_count.ok_or_else(|| KiCadError::Config { + reason: "set-enabled-layers requires `--copper-layer-count `".to_string(), + })?, + layer_ids, + } + } "active-layer" => Command::ActiveLayer, "set-active-layer" => { let mut layer_id = None; @@ -1299,7 +1358,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 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 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 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 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 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 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 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" ); } @@ -1926,6 +1985,31 @@ mod tests { } } + #[test] + fn parse_args_parses_set_enabled_layers() { + let (_, command) = parse_args_from(vec![ + "set-enabled-layers".to_string(), + "--copper-layer-count".to_string(), + "2".to_string(), + "--layer-id".to_string(), + "47".to_string(), + "--layer-id".to_string(), + "52".to_string(), + ]) + .expect("set-enabled-layers args should parse"); + + match command { + Command::SetEnabledLayers { + copper_layer_count, + layer_ids, + } => { + assert_eq!(copper_layer_count, 2); + assert_eq!(layer_ids, vec![47, 52]); + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_visible_layers() { let (_, command) = parse_args_from(vec![ From 909007f74bd8085a38950f757909609f8aaf2050 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 17:31:10 +0800 Subject: [PATCH 08/28] feat(client): add SetBoardEditorAppearanceSettings API and CLI command --- README.md | 6 +- docs/TEST_CLI.md | 6 ++ src/client.rs | 137 +++++++++++++++++++++++-- test-scripts/kicad-ipc-cli.rs | 183 +++++++++++++++++++++++++++++++++- 4 files changed, 319 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e1c14cb..a743132 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Legend: | Common (base) | 6 | 4 | 67% | | Common editor/document | 23 | 12 | 52% | | Project manager | 5 | 3 | 60% | -| Board editor (PCB) | 22 | 17 | 77% | +| Board editor (PCB) | 22 | 18 | 82% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **36** | **64%** | +| **Total** | **56** | **37** | **66%** | ### Common (base) @@ -118,7 +118,7 @@ Legend: | `GetActiveLayer` | Implemented | `KiCadClient::get_active_layer` | | `SetActiveLayer` | Implemented | `KiCadClient::set_active_layer` | | `GetBoardEditorAppearanceSettings` | Implemented | `KiCadClient::get_board_editor_appearance_settings_raw`, `KiCadClient::get_board_editor_appearance_settings` | -| `SetBoardEditorAppearanceSettings` | Not yet | - | +| `SetBoardEditorAppearanceSettings` | Implemented | `KiCadClient::set_board_editor_appearance_settings` | | `InteractiveMoveItems` | Not yet | - | ### Schematic editor diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 525622d..f9d67e2 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -255,6 +255,12 @@ cargo run --bin kicad-ipc-cli -- graphics-defaults cargo run --bin kicad-ipc-cli -- appearance ``` +Set editor appearance: + +```bash +cargo run --bin kicad-ipc-cli -- set-appearance --inactive-layer-display hidden --net-color-display all --board-flip normal --ratsnest-display all-layers +``` + Show typed netclass map: ```bash diff --git a/src/client.rs b/src/client.rs index e5fbf04..f161327 100644 --- a/src/client.rs +++ b/src/client.rs @@ -57,6 +57,8 @@ const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup"; const CMD_GET_GRAPHICS_DEFAULTS: &str = "kiapi.board.commands.GetGraphicsDefaults"; const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = "kiapi.board.commands.GetBoardEditorAppearanceSettings"; +const CMD_SET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = + "kiapi.board.commands.SetBoardEditorAppearanceSettings"; const CMD_GET_ITEMS_BY_NET: &str = "kiapi.board.commands.GetItemsByNet"; const CMD_GET_ITEMS_BY_NET_CLASS: &str = "kiapi.board.commands.GetItemsByNetClass"; const CMD_GET_NETCLASS_FOR_NETS: &str = "kiapi.board.commands.GetNetClassForNets"; @@ -103,6 +105,7 @@ const RES_HIT_TEST_RESPONSE: &str = "kiapi.common.commands.HitTestResponse"; const RES_TITLE_BLOCK_INFO: &str = "kiapi.common.types.TitleBlockInfo"; const RES_SAVED_DOCUMENT_RESPONSE: &str = "kiapi.common.commands.SavedDocumentResponse"; const RES_SAVED_SELECTION_RESPONSE: &str = "kiapi.common.commands.SavedSelectionResponse"; +const RES_PROTOBUF_EMPTY: &str = "google.protobuf.Empty"; const PAD_QUERY_CHUNK_SIZE: usize = 256; @@ -1106,6 +1109,24 @@ impl KiCadClient { Ok(map_board_editor_appearance_settings(response)) } + pub async fn set_board_editor_appearance_settings( + &self, + settings: BoardEditorAppearanceSettings, + ) -> Result { + let command = board_commands::SetBoardEditorAppearanceSettings { + settings: Some(board_editor_appearance_settings_to_proto(settings)), + }; + + let response = self + .send_command(envelope::pack_any( + &command, + CMD_SET_BOARD_EDITOR_APPEARANCE_SETTINGS, + )) + .await?; + let _ = response_payload_as_any(response, RES_PROTOBUF_EMPTY)?; + self.get_board_editor_appearance_settings().await + } + pub async fn get_title_block_info(&self) -> Result { let command = common_commands::GetTitleBlockInfo { document: Some(self.current_board_document_proto().await?), @@ -1930,6 +1951,21 @@ fn map_inactive_layer_display_mode(value: i32) -> InactiveLayerDisplayMode { } } +fn inactive_layer_display_mode_to_proto(value: InactiveLayerDisplayMode) -> i32 { + match value { + InactiveLayerDisplayMode::Normal => { + board_commands::InactiveLayerDisplayMode::IldmNormal as i32 + } + InactiveLayerDisplayMode::Dimmed => { + board_commands::InactiveLayerDisplayMode::IldmDimmed as i32 + } + InactiveLayerDisplayMode::Hidden => { + board_commands::InactiveLayerDisplayMode::IldmHidden as i32 + } + InactiveLayerDisplayMode::Unknown(value) => value, + } +} + fn map_net_color_display_mode(value: i32) -> NetColorDisplayMode { match board_commands::NetColorDisplayMode::try_from(value) { Ok(board_commands::NetColorDisplayMode::NcdmAll) => NetColorDisplayMode::All, @@ -1939,6 +1975,15 @@ fn map_net_color_display_mode(value: i32) -> NetColorDisplayMode { } } +fn net_color_display_mode_to_proto(value: NetColorDisplayMode) -> i32 { + match value { + NetColorDisplayMode::All => board_commands::NetColorDisplayMode::NcdmAll as i32, + NetColorDisplayMode::Ratsnest => board_commands::NetColorDisplayMode::NcdmRatsnest as i32, + NetColorDisplayMode::Off => board_commands::NetColorDisplayMode::NcdmOff as i32, + NetColorDisplayMode::Unknown(value) => value, + } +} + fn map_board_flip_mode(value: i32) -> BoardFlipMode { match board_commands::BoardFlipMode::try_from(value) { Ok(board_commands::BoardFlipMode::BfmNormal) => BoardFlipMode::Normal, @@ -1947,6 +1992,14 @@ fn map_board_flip_mode(value: i32) -> BoardFlipMode { } } +fn board_flip_mode_to_proto(value: BoardFlipMode) -> i32 { + match value { + BoardFlipMode::Normal => board_commands::BoardFlipMode::BfmNormal as i32, + BoardFlipMode::FlippedX => board_commands::BoardFlipMode::BfmFlippedX as i32, + BoardFlipMode::Unknown(value) => value, + } +} + fn map_ratsnest_display_mode(value: i32) -> RatsnestDisplayMode { match board_commands::RatsnestDisplayMode::try_from(value) { Ok(board_commands::RatsnestDisplayMode::RdmAllLayers) => RatsnestDisplayMode::AllLayers, @@ -1957,6 +2010,16 @@ fn map_ratsnest_display_mode(value: i32) -> RatsnestDisplayMode { } } +fn ratsnest_display_mode_to_proto(value: RatsnestDisplayMode) -> i32 { + match value { + RatsnestDisplayMode::AllLayers => board_commands::RatsnestDisplayMode::RdmAllLayers as i32, + RatsnestDisplayMode::VisibleLayers => { + board_commands::RatsnestDisplayMode::RdmVisibleLayers as i32 + } + RatsnestDisplayMode::Unknown(value) => value, + } +} + fn map_board_stackup(stackup: board_proto::BoardStackup) -> BoardStackup { let finish_type_name = stackup .finish @@ -2046,6 +2109,19 @@ fn map_board_editor_appearance_settings( } } +fn board_editor_appearance_settings_to_proto( + settings: BoardEditorAppearanceSettings, +) -> board_commands::BoardEditorAppearanceSettings { + board_commands::BoardEditorAppearanceSettings { + inactive_layer_display: inactive_layer_display_mode_to_proto( + settings.inactive_layer_display, + ), + net_color_display: net_color_display_mode_to_proto(settings.net_color_display), + board_flip: board_flip_mode_to_proto(settings.board_flip), + ratsnest_display: ratsnest_display_mode_to_proto(settings.ratsnest_display), + } +} + fn map_net_class_type(value: i32) -> NetClassType { match common_project::NetClassType::try_from(value) { Ok(common_project::NetClassType::NctExplicit) => NetClassType::Explicit, @@ -2777,12 +2853,13 @@ fn default_client_name() -> String { #[cfg(test)] mod tests { use super::{ - any_to_pretty_debug, commit_action_to_proto, ensure_item_request_ok, layer_to_model, - map_commit_session, map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, - model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, - response_payload_as_any, select_single_board_document, select_single_project_path, - selection_item_detail, summarize_item_details, summarize_selection, - text_horizontal_alignment_to_proto, text_spec_to_proto, PCB_OBJECT_TYPES, + any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto, + ensure_item_request_ok, layer_to_model, map_commit_session, map_hit_test_result, + map_item_bounding_boxes, map_polygon_with_holes, model_document_to_proto, + normalize_socket_uri, pad_netlist_from_footprint_items, response_payload_as_any, + select_single_board_document, select_single_project_path, selection_item_detail, + summarize_item_details, summarize_selection, text_horizontal_alignment_to_proto, + text_spec_to_proto, PCB_OBJECT_TYPES, }; use crate::error::KiCadError; use crate::model::common::{ @@ -2950,6 +3027,35 @@ mod tests { ); } + #[test] + fn board_editor_appearance_settings_to_proto_maps_known_variants() { + let proto = board_editor_appearance_settings_to_proto( + crate::model::board::BoardEditorAppearanceSettings { + inactive_layer_display: crate::model::board::InactiveLayerDisplayMode::Hidden, + net_color_display: crate::model::board::NetColorDisplayMode::Ratsnest, + board_flip: crate::model::board::BoardFlipMode::FlippedX, + ratsnest_display: crate::model::board::RatsnestDisplayMode::VisibleLayers, + }, + ); + + assert_eq!( + proto.inactive_layer_display, + crate::proto::kiapi::board::commands::InactiveLayerDisplayMode::IldmHidden as i32 + ); + assert_eq!( + proto.net_color_display, + crate::proto::kiapi::board::commands::NetColorDisplayMode::NcdmRatsnest as i32 + ); + assert_eq!( + proto.board_flip, + crate::proto::kiapi::board::commands::BoardFlipMode::BfmFlippedX as i32 + ); + assert_eq!( + proto.ratsnest_display, + crate::proto::kiapi::board::commands::RatsnestDisplayMode::RdmVisibleLayers as i32 + ); + } + #[test] fn response_payload_as_any_validates_type_url() { let response = crate::proto::kiapi::common::ApiResponse { @@ -2966,6 +3072,25 @@ mod tests { assert!(matches!(err, KiCadError::UnexpectedPayloadType { .. })); } + #[test] + fn response_payload_as_any_accepts_google_protobuf_empty_type() { + let response = crate::proto::kiapi::common::ApiResponse { + header: None, + status: None, + message: Some(prost_types::Any { + type_url: super::envelope::type_url("google.protobuf.Empty"), + value: Vec::new(), + }), + }; + + let payload = response_payload_as_any(response, "google.protobuf.Empty") + .expect("google.protobuf.Empty payload type should be accepted"); + assert_eq!( + payload.type_url, + super::envelope::type_url("google.protobuf.Empty") + ); + } + #[test] fn summarize_selection_counts_payload_types() { let items = vec![ diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 806bb99..3ae7225 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -6,8 +6,9 @@ use std::str::FromStr; use std::time::Duration; use kicad_ipc::{ - BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, EditorFrameType, - KiCadClient, KiCadError, PadstackPresenceState, PcbObjectTypeCode, TextObjectSpec, + BoardFlipMode, BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, + EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, NetColorDisplayMode, + PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode, TextObjectSpec, TextShapeGeometry, TextSpec, Vector2Nm, }; @@ -117,6 +118,12 @@ enum Command { Stackup, GraphicsDefaults, Appearance, + SetAppearance { + inactive_layer_display: InactiveLayerDisplayMode, + net_color_display: NetColorDisplayMode, + board_flip: BoardFlipMode, + ratsnest_display: RatsnestDisplayMode, + }, NetClass, BoardReadReport { output: PathBuf, @@ -652,6 +659,22 @@ async fn run() -> Result<(), KiCadError> { let appearance = client.get_board_editor_appearance_settings().await?; println!("{appearance:#?}"); } + Command::SetAppearance { + inactive_layer_display, + net_color_display, + board_flip, + ratsnest_display, + } => { + let updated = client + .set_board_editor_appearance_settings(kicad_ipc::BoardEditorAppearanceSettings { + inactive_layer_display, + net_color_display, + board_flip, + ratsnest_display, + }) + .await?; + println!("{updated:#?}"); + } Command::NetClass => { let nets = client.get_nets().await?; let netclasses = client.get_netclass_for_nets(nets).await?; @@ -1301,6 +1324,82 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE "stackup" => Command::Stackup, "graphics-defaults" => Command::GraphicsDefaults, "appearance" => Command::Appearance, + "set-appearance" => { + let mut inactive_layer_display = None; + let mut net_color_display = None; + let mut board_flip = None; + let mut ratsnest_display = None; + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--inactive-layer-display" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-appearance --inactive-layer-display" + .to_string(), + })?; + inactive_layer_display = Some( + parse_inactive_layer_display_mode(value) + .map_err(|err| KiCadError::Config { reason: err })?, + ); + i += 2; + } + "--net-color-display" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-appearance --net-color-display" + .to_string(), + })?; + net_color_display = Some( + parse_net_color_display_mode(value) + .map_err(|err| KiCadError::Config { reason: err })?, + ); + i += 2; + } + "--board-flip" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-appearance --board-flip".to_string(), + })?; + board_flip = Some( + parse_board_flip_mode(value) + .map_err(|err| KiCadError::Config { reason: err })?, + ); + i += 2; + } + "--ratsnest-display" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-appearance --ratsnest-display" + .to_string(), + })?; + ratsnest_display = Some( + parse_ratsnest_display_mode(value) + .map_err(|err| KiCadError::Config { reason: err })?, + ); + i += 2; + } + _ => { + i += 1; + } + } + } + + Command::SetAppearance { + inactive_layer_display: inactive_layer_display.ok_or_else(|| KiCadError::Config { + reason: "set-appearance requires `--inactive-layer-display `".to_string(), + })?, + net_color_display: net_color_display.ok_or_else(|| KiCadError::Config { + reason: "set-appearance requires `--net-color-display `" + .to_string(), + })?, + board_flip: board_flip.ok_or_else(|| KiCadError::Config { + reason: "set-appearance requires `--board-flip `" + .to_string(), + })?, + ratsnest_display: ratsnest_display.ok_or_else(|| KiCadError::Config { + reason: + "set-appearance requires `--ratsnest-display `" + .to_string(), + })?, + } + } "netclass" => Command::NetClass, "proto-coverage-board-read" => Command::ProtoCoverageBoardRead, "board-read-report" => { @@ -1347,6 +1446,48 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE Ok((config, command)) } +fn parse_inactive_layer_display_mode(value: &str) -> Result { + match value { + "normal" => Ok(InactiveLayerDisplayMode::Normal), + "dimmed" => Ok(InactiveLayerDisplayMode::Dimmed), + "hidden" => Ok(InactiveLayerDisplayMode::Hidden), + _ => Err(format!( + "unknown inactive layer display `{value}`; expected normal, dimmed, or hidden" + )), + } +} + +fn parse_net_color_display_mode(value: &str) -> Result { + match value { + "all" => Ok(NetColorDisplayMode::All), + "ratsnest" => Ok(NetColorDisplayMode::Ratsnest), + "off" => Ok(NetColorDisplayMode::Off), + _ => Err(format!( + "unknown net color display `{value}`; expected all, ratsnest, or off" + )), + } +} + +fn parse_board_flip_mode(value: &str) -> Result { + match value { + "normal" => Ok(BoardFlipMode::Normal), + "flipped-x" => Ok(BoardFlipMode::FlippedX), + _ => Err(format!( + "unknown board flip mode `{value}`; expected normal or flipped-x" + )), + } +} + +fn parse_ratsnest_display_mode(value: &str) -> Result { + match value { + "all-layers" => Ok(RatsnestDisplayMode::AllLayers), + "visible-layers" => Ok(RatsnestDisplayMode::VisibleLayers), + _ => Err(format!( + "unknown ratsnest display `{value}`; expected all-layers or visible-layers" + )), + } +} + fn default_config() -> CliConfig { CliConfig { socket: None, @@ -1358,7 +1499,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 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 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 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 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 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 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" ); } @@ -1911,7 +2052,10 @@ fn hex_char(value: u8) -> char { #[cfg(test)] mod tests { use super::{parse_args_from, Command}; - use kicad_ipc::{BoardOriginKind, CommitAction}; + use kicad_ipc::{ + BoardFlipMode, BoardOriginKind, CommitAction, InactiveLayerDisplayMode, + NetColorDisplayMode, RatsnestDisplayMode, + }; #[test] fn parse_args_accepts_client_name_for_commit_flow() { @@ -2049,4 +2193,35 @@ mod tests { other => panic!("unexpected command variant: {other:?}"), } } + + #[test] + fn parse_args_parses_set_appearance() { + let (_, command) = parse_args_from(vec![ + "set-appearance".to_string(), + "--inactive-layer-display".to_string(), + "hidden".to_string(), + "--net-color-display".to_string(), + "off".to_string(), + "--board-flip".to_string(), + "flipped-x".to_string(), + "--ratsnest-display".to_string(), + "visible-layers".to_string(), + ]) + .expect("set-appearance args should parse"); + + match command { + Command::SetAppearance { + inactive_layer_display, + net_color_display, + board_flip, + ratsnest_display, + } => { + assert_eq!(inactive_layer_display, InactiveLayerDisplayMode::Hidden); + assert_eq!(net_color_display, NetColorDisplayMode::Off); + assert_eq!(board_flip, BoardFlipMode::FlippedX); + assert_eq!(ratsnest_display, RatsnestDisplayMode::VisibleLayers); + } + other => panic!("unexpected command variant: {other:?}"), + } + } } From a896a1c38e1996a87fb10c9db8746dc7c1db942e Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 17:38:07 +0800 Subject: [PATCH 09/28] feat(client): add InjectDrcError API and CLI command --- README.md | 6 +- docs/TEST_CLI.md | 6 ++ src/client.rs | 81 +++++++++++++++-- src/lib.rs | 2 +- src/model/board.rs | 68 ++++++++++++++- test-scripts/kicad-ipc-cli.rs | 158 ++++++++++++++++++++++++++++++++-- 6 files changed, 304 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a743132..f4c47f7 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Legend: | Common (base) | 6 | 4 | 67% | | Common editor/document | 23 | 12 | 52% | | Project manager | 5 | 3 | 60% | -| Board editor (PCB) | 22 | 18 | 82% | +| Board editor (PCB) | 22 | 19 | 86% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **37** | **66%** | +| **Total** | **56** | **38** | **68%** | ### Common (base) @@ -112,7 +112,7 @@ Legend: | `RefillZones` | Not yet | - | | `GetPadShapeAsPolygon` | Implemented | `KiCadClient::get_pad_shape_as_polygon_raw`, `KiCadClient::get_pad_shape_as_polygon` | | `CheckPadstackPresenceOnLayers` | Implemented | `KiCadClient::check_padstack_presence_on_layers_raw`, `KiCadClient::check_padstack_presence_on_layers` | -| `InjectDrcError` | Not yet | - | +| `InjectDrcError` | Implemented | `KiCadClient::inject_drc_error_raw`, `KiCadClient::inject_drc_error` | | `GetVisibleLayers` | Implemented | `KiCadClient::get_visible_layers` | | `SetVisibleLayers` | Implemented | `KiCadClient::set_visible_layers` | | `GetActiveLayer` | Implemented | `KiCadClient::get_active_layer` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index f9d67e2..a3bbb3b 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -261,6 +261,12 @@ Set editor appearance: cargo run --bin kicad-ipc-cli -- set-appearance --inactive-layer-display hidden --net-color-display all --board-flip normal --ratsnest-display all-layers ``` +Inject DRC marker: + +```bash +cargo run --bin kicad-ipc-cli -- inject-drc-error --severity error --message "API marker test" --x-nm 1000000 --y-nm 1000000 +``` + Show typed netclass map: ```bash diff --git a/src/client.rs b/src/client.rs index f161327..639a6dd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -9,7 +9,7 @@ use crate::model::board::{ ArcStartMidEndNm, BoardEditorAppearanceSettings, BoardEnabledLayers, BoardFlipMode, BoardLayerClass, BoardLayerGraphicsDefault, BoardLayerInfo, BoardNet, BoardOriginKind, BoardStackup, BoardStackupDielectricProperties, BoardStackupLayer, BoardStackupLayerType, - ColorRgba, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings, + ColorRgba, DrcSeverity, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings, NetClassForNetEntry, NetClassInfo, NetClassType, NetColorDisplayMode, PadNetEntry, PadShapeAsPolygonEntry, PadstackPresenceEntry, PadstackPresenceState, PcbArc, PcbBoardGraphicShape, PcbBoardText, PcbBoardTextBox, PcbDimension, PcbField, PcbFootprint, @@ -65,6 +65,7 @@ const CMD_GET_NETCLASS_FOR_NETS: &str = "kiapi.board.commands.GetNetClassForNets const CMD_GET_PAD_SHAPE_AS_POLYGON: &str = "kiapi.board.commands.GetPadShapeAsPolygon"; const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str = "kiapi.board.commands.CheckPadstackPresenceOnLayers"; +const CMD_INJECT_DRC_ERROR: &str = "kiapi.board.commands.InjectDrcError"; const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection"; const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; @@ -95,6 +96,7 @@ const RES_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = const RES_NETCLASS_FOR_NETS_RESPONSE: &str = "kiapi.board.commands.NetClassForNetsResponse"; const RES_PAD_SHAPE_AS_POLYGON_RESPONSE: &str = "kiapi.board.commands.PadShapeAsPolygonResponse"; const RES_PADSTACK_PRESENCE_RESPONSE: &str = "kiapi.board.commands.PadstackPresenceResponse"; +const RES_INJECT_DRC_ERROR_RESPONSE: &str = "kiapi.board.commands.InjectDrcErrorResponse"; const RES_VECTOR2: &str = "kiapi.common.types.Vector2"; const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse"; const RES_BEGIN_COMMIT_RESPONSE: &str = "kiapi.common.commands.BeginCommitResponse"; @@ -1047,6 +1049,46 @@ impl KiCadClient { Ok(entries) } + pub async fn inject_drc_error_raw( + &self, + severity: DrcSeverity, + message: impl Into, + position: Option, + item_ids: Vec, + ) -> Result { + let board = self.current_board_document_proto().await?; + let command = board_commands::InjectDrcError { + board: Some(board), + severity: drc_severity_to_proto(severity), + message: message.into(), + position: position.map(vector2_nm_to_proto), + items: item_ids + .into_iter() + .map(|value| common_types::Kiid { value }) + .collect(), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_INJECT_DRC_ERROR)) + .await?; + response_payload_as_any(response, RES_INJECT_DRC_ERROR_RESPONSE) + } + + pub async fn inject_drc_error( + &self, + severity: DrcSeverity, + message: impl Into, + position: Option, + item_ids: Vec, + ) -> Result, KiCadError> { + let payload = self + .inject_drc_error_raw(severity, message, position, item_ids) + .await?; + let response: board_commands::InjectDrcErrorResponse = + decode_any(&payload, RES_INJECT_DRC_ERROR_RESPONSE)?; + Ok(response.marker.map(|marker| marker.value)) + } + pub async fn get_board_stackup_raw(&self) -> Result { let command = board_commands::GetBoardStackup { board: Some(self.current_board_document_proto().await?), @@ -1674,6 +1716,19 @@ fn board_origin_kind_to_proto(kind: BoardOriginKind) -> i32 { } } +fn drc_severity_to_proto(value: DrcSeverity) -> i32 { + match value { + DrcSeverity::Warning => board_commands::DrcSeverity::DrsWarning as i32, + DrcSeverity::Error => board_commands::DrcSeverity::DrsError as i32, + DrcSeverity::Exclusion => board_commands::DrcSeverity::DrsExclusion as i32, + DrcSeverity::Ignore => board_commands::DrcSeverity::DrsIgnore as i32, + DrcSeverity::Info => board_commands::DrcSeverity::DrsInfo as i32, + DrcSeverity::Action => board_commands::DrcSeverity::DrsAction as i32, + DrcSeverity::Debug => board_commands::DrcSeverity::DrsDebug as i32, + DrcSeverity::Undefined => board_commands::DrcSeverity::DrsUndefined as i32, + } +} + fn commit_action_to_proto(action: CommitAction) -> i32 { match action { CommitAction::Commit => common_commands::CommitAction::CmaCommit as i32, @@ -2854,12 +2909,12 @@ fn default_client_name() -> String { mod tests { use super::{ any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto, - ensure_item_request_ok, layer_to_model, map_commit_session, map_hit_test_result, - map_item_bounding_boxes, map_polygon_with_holes, model_document_to_proto, - normalize_socket_uri, pad_netlist_from_footprint_items, response_payload_as_any, - select_single_board_document, select_single_project_path, selection_item_detail, - summarize_item_details, summarize_selection, text_horizontal_alignment_to_proto, - text_spec_to_proto, PCB_OBJECT_TYPES, + drc_severity_to_proto, ensure_item_request_ok, layer_to_model, map_commit_session, + map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, + model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, + response_payload_as_any, select_single_board_document, select_single_project_path, + selection_item_detail, summarize_item_details, summarize_selection, + text_horizontal_alignment_to_proto, text_spec_to_proto, PCB_OBJECT_TYPES, }; use crate::error::KiCadError; use crate::model::common::{ @@ -3027,6 +3082,18 @@ mod tests { ); } + #[test] + fn drc_severity_to_proto_maps_known_variants() { + assert_eq!( + drc_severity_to_proto(crate::model::board::DrcSeverity::Warning), + crate::proto::kiapi::board::commands::DrcSeverity::DrsWarning as i32 + ); + assert_eq!( + drc_severity_to_proto(crate::model::board::DrcSeverity::Error), + crate::proto::kiapi::board::commands::DrcSeverity::DrsError as i32 + ); + } + #[test] fn board_editor_appearance_settings_to_proto_maps_known_variants() { let proto = board_editor_appearance_settings_to_proto( diff --git a/src/lib.rs b/src/lib.rs index 23bf66e..b922904 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ pub use crate::model::board::{ ArcStartMidEndNm, BoardEditorAppearanceSettings, BoardEnabledLayers, BoardFlipMode, BoardLayerClass, BoardLayerGraphicsDefault, BoardLayerInfo, BoardNet, BoardOriginKind, BoardStackup, BoardStackupDielectricProperties, BoardStackupLayer, BoardStackupLayerType, - ColorRgba, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings, + ColorRgba, DrcSeverity, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings, NetClassForNetEntry, NetClassInfo, NetClassType, NetColorDisplayMode, PadNetEntry, PadShapeAsPolygonEntry, PadstackPresenceEntry, PadstackPresenceState, PcbArc, PcbBoardGraphicShape, PcbBoardText, PcbBoardTextBox, PcbDimension, PcbField, PcbFootprint, diff --git a/src/model/board.rs b/src/model/board.rs index ca3f97d..c289065 100644 --- a/src/model/board.rs +++ b/src/model/board.rs @@ -224,6 +224,54 @@ pub enum RatsnestDisplayMode { Unknown(i32), } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DrcSeverity { + Warning, + Error, + Exclusion, + Ignore, + Info, + Action, + Debug, + Undefined, +} + +impl std::fmt::Display for DrcSeverity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let value = match self { + Self::Warning => "warning", + Self::Error => "error", + Self::Exclusion => "exclusion", + Self::Ignore => "ignore", + Self::Info => "info", + Self::Action => "action", + Self::Debug => "debug", + Self::Undefined => "undefined", + }; + write!(f, "{value}") + } +} + +impl FromStr for DrcSeverity { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "warning" => Ok(Self::Warning), + "error" => Ok(Self::Error), + "exclusion" => Ok(Self::Exclusion), + "ignore" => Ok(Self::Ignore), + "info" => Ok(Self::Info), + "action" => Ok(Self::Action), + "debug" => Ok(Self::Debug), + "undefined" => Ok(Self::Undefined), + _ => Err(format!( + "unknown drc severity `{value}`; expected warning, error, exclusion, ignore, info, action, debug, or undefined" + )), + } + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct BoardEditorAppearanceSettings { pub inactive_layer_display: InactiveLayerDisplayMode, @@ -424,7 +472,7 @@ pub enum PcbItem { mod tests { use std::str::FromStr; - use super::BoardOriginKind; + use super::{BoardOriginKind, DrcSeverity}; #[test] fn board_origin_kind_parses_known_values() { @@ -443,4 +491,22 @@ mod tests { let result = BoardOriginKind::from_str("other"); assert!(result.is_err()); } + + #[test] + fn drc_severity_parses_known_values() { + assert_eq!( + DrcSeverity::from_str("warning").expect("warning should parse"), + DrcSeverity::Warning + ); + assert_eq!( + DrcSeverity::from_str("error").expect("error should parse"), + DrcSeverity::Error + ); + } + + #[test] + fn drc_severity_rejects_unknown_values() { + let result = DrcSeverity::from_str("fatal"); + assert!(result.is_err()); + } } diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 3ae7225..bf97cb1 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -7,9 +7,9 @@ use std::time::Duration; use kicad_ipc::{ BoardFlipMode, BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, - EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, NetColorDisplayMode, - PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode, TextObjectSpec, - TextShapeGeometry, TextSpec, Vector2Nm, + DrcSeverity, EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, + NetColorDisplayMode, PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode, + TextObjectSpec, TextShapeGeometry, TextSpec, Vector2Nm, }; const REPORT_MAX_PAD_NET_ROWS: usize = 2_000; @@ -68,6 +68,13 @@ enum Command { x_nm: i64, y_nm: i64, }, + InjectDrcError { + severity: DrcSeverity, + message: String, + x_nm: Option, + y_nm: Option, + item_ids: Vec, + }, RefreshEditor { frame: EditorFrameType, }, @@ -371,6 +378,25 @@ async fn run() -> Result<(), KiCadError> { .await?; println!("set_origin_kind={} x_nm={} y_nm={}", kind, x_nm, y_nm); } + Command::InjectDrcError { + severity, + message, + x_nm, + y_nm, + item_ids, + } => { + let position = match (x_nm, y_nm) { + (Some(x_nm), Some(y_nm)) => Some(Vector2Nm { x_nm, y_nm }), + _ => None, + }; + let marker = client + .inject_drc_error(severity, message, position, item_ids) + .await?; + println!( + "drc_marker_id={}", + marker.unwrap_or_else(|| "-".to_string()) + ); + } Command::RefreshEditor { frame } => { client.refresh_editor(frame).await?; println!("refresh_editor=ok frame={}", frame); @@ -1005,6 +1031,79 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE })?, } } + "inject-drc-error" => { + let mut severity = DrcSeverity::Error; + let mut message = None; + let mut x_nm = None; + let mut y_nm = None; + let mut item_ids = Vec::new(); + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--severity" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --severity".to_string(), + })?; + severity = parse_drc_severity(value) + .map_err(|err| KiCadError::Config { reason: err })?; + i += 2; + } + "--message" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --message".to_string(), + })?; + message = Some(value.clone()); + i += 2; + } + "--x-nm" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --x-nm".to_string(), + })?; + x_nm = Some(value.parse::().map_err(|err| KiCadError::Config { + reason: format!("invalid inject-drc-error --x-nm `{value}`: {err}"), + })?); + i += 2; + } + "--y-nm" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --y-nm".to_string(), + })?; + y_nm = Some(value.parse::().map_err(|err| KiCadError::Config { + reason: format!("invalid inject-drc-error --y-nm `{value}`: {err}"), + })?); + i += 2; + } + "--item-id" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for inject-drc-error --item-id".to_string(), + })?; + item_ids.push(value.clone()); + i += 2; + } + _ => { + i += 1; + } + } + } + + if (x_nm.is_some() && y_nm.is_none()) || (x_nm.is_none() && y_nm.is_some()) { + return Err(KiCadError::Config { + reason: + "inject-drc-error requires both --x-nm and --y-nm when providing a position" + .to_string(), + }); + } + + Command::InjectDrcError { + severity, + message: message.ok_or_else(|| KiCadError::Config { + reason: "inject-drc-error requires `--message `".to_string(), + })?, + x_nm, + y_nm, + item_ids, + } + } "refresh-editor" => { let mut frame = EditorFrameType::PcbEditor; let mut i = 1; @@ -1488,6 +1587,22 @@ fn parse_ratsnest_display_mode(value: &str) -> Result Result { + match value { + "warning" => Ok(DrcSeverity::Warning), + "error" => Ok(DrcSeverity::Error), + "exclusion" => Ok(DrcSeverity::Exclusion), + "ignore" => Ok(DrcSeverity::Ignore), + "info" => Ok(DrcSeverity::Info), + "action" => Ok(DrcSeverity::Action), + "debug" => Ok(DrcSeverity::Debug), + "undefined" => Ok(DrcSeverity::Undefined), + _ => Err(format!( + "unknown drc severity `{value}`; expected warning, error, exclusion, ignore, info, action, debug, or undefined" + )), + } +} + fn default_config() -> CliConfig { CliConfig { socket: None, @@ -1499,7 +1614,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 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 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 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 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 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 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" ); } @@ -2053,7 +2168,7 @@ fn hex_char(value: u8) -> char { mod tests { use super::{parse_args_from, Command}; use kicad_ipc::{ - BoardFlipMode, BoardOriginKind, CommitAction, InactiveLayerDisplayMode, + BoardFlipMode, BoardOriginKind, CommitAction, DrcSeverity, InactiveLayerDisplayMode, NetColorDisplayMode, RatsnestDisplayMode, }; @@ -2224,4 +2339,37 @@ mod tests { other => panic!("unexpected command variant: {other:?}"), } } + + #[test] + fn parse_args_parses_inject_drc_error() { + let (_, command) = parse_args_from(vec![ + "inject-drc-error".to_string(), + "--severity".to_string(), + "warning".to_string(), + "--message".to_string(), + "marker".to_string(), + "--x-nm".to_string(), + "100".to_string(), + "--y-nm".to_string(), + "200".to_string(), + ]) + .expect("inject-drc-error args should parse"); + + match command { + Command::InjectDrcError { + severity, + message, + x_nm, + y_nm, + item_ids, + } => { + assert_eq!(severity, DrcSeverity::Warning); + assert_eq!(message, "marker"); + assert_eq!(x_nm, Some(100)); + assert_eq!(y_nm, Some(200)); + assert!(item_ids.is_empty()); + } + other => panic!("unexpected command variant: {other:?}"), + } + } } From feb29250d020d26e2d997d46bb530bb577d12419 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:00:37 +0800 Subject: [PATCH 10/28] feat(client): add RefillZones API and CLI command --- README.md | 6 ++--- docs/TEST_CLI.md | 6 +++++ src/client.rs | 18 ++++++++++++++ test-scripts/kicad-ipc-cli.rs | 46 +++++++++++++++++++++++++++++++++-- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f4c47f7..6bad699 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Legend: | Common (base) | 6 | 4 | 67% | | Common editor/document | 23 | 12 | 52% | | Project manager | 5 | 3 | 60% | -| Board editor (PCB) | 22 | 19 | 86% | +| Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **38** | **68%** | +| **Total** | **56** | **39** | **70%** | ### Common (base) @@ -109,7 +109,7 @@ Legend: | `GetItemsByNet` | Implemented | `KiCadClient::get_items_by_net_raw`, `KiCadClient::get_items_by_net` | | `GetItemsByNetClass` | Implemented | `KiCadClient::get_items_by_net_class_raw`, `KiCadClient::get_items_by_net_class` | | `GetNetClassForNets` | Implemented | `KiCadClient::get_netclass_for_nets_raw`, `KiCadClient::get_netclass_for_nets` | -| `RefillZones` | Not yet | - | +| `RefillZones` | Implemented | `KiCadClient::refill_zones` | | `GetPadShapeAsPolygon` | Implemented | `KiCadClient::get_pad_shape_as_polygon_raw`, `KiCadClient::get_pad_shape_as_polygon` | | `CheckPadstackPresenceOnLayers` | Implemented | `KiCadClient::check_padstack_presence_on_layers_raw`, `KiCadClient::check_padstack_presence_on_layers` | | `InjectDrcError` | Implemented | `KiCadClient::inject_drc_error_raw`, `KiCadClient::inject_drc_error` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index a3bbb3b..de129cc 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -267,6 +267,12 @@ Inject DRC marker: cargo run --bin kicad-ipc-cli -- inject-drc-error --severity error --message "API marker test" --x-nm 1000000 --y-nm 1000000 ``` +Refill all zones: + +```bash +cargo run --bin kicad-ipc-cli -- refill-zones +``` + Show typed netclass map: ```bash diff --git a/src/client.rs b/src/client.rs index 639a6dd..b725508 100644 --- a/src/client.rs +++ b/src/client.rs @@ -62,6 +62,7 @@ const CMD_SET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = const CMD_GET_ITEMS_BY_NET: &str = "kiapi.board.commands.GetItemsByNet"; const CMD_GET_ITEMS_BY_NET_CLASS: &str = "kiapi.board.commands.GetItemsByNetClass"; const CMD_GET_NETCLASS_FOR_NETS: &str = "kiapi.board.commands.GetNetClassForNets"; +const CMD_REFILL_ZONES: &str = "kiapi.board.commands.RefillZones"; const CMD_GET_PAD_SHAPE_AS_POLYGON: &str = "kiapi.board.commands.GetPadShapeAsPolygon"; const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str = "kiapi.board.commands.CheckPadstackPresenceOnLayers"; @@ -903,6 +904,23 @@ impl KiCadClient { Ok(map_netclass_for_nets_response(response)) } + pub async fn refill_zones(&self, zone_ids: Vec) -> Result<(), KiCadError> { + let board = self.current_board_document_proto().await?; + let command = board_commands::RefillZones { + board: Some(board), + zones: zone_ids + .into_iter() + .map(|value| common_types::Kiid { value }) + .collect(), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_REFILL_ZONES)) + .await?; + let _ = response_payload_as_any(response, RES_PROTOBUF_EMPTY)?; + Ok(()) + } + pub async fn get_pad_shape_as_polygon_raw( &self, pad_ids: Vec, diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index bf97cb1..5be4495 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -131,6 +131,9 @@ enum Command { board_flip: BoardFlipMode, ratsnest_display: RatsnestDisplayMode, }, + RefillZones { + zone_ids: Vec, + }, NetClass, BoardReadReport { output: PathBuf, @@ -701,6 +704,10 @@ async fn run() -> Result<(), KiCadError> { .await?; println!("{updated:#?}"); } + Command::RefillZones { zone_ids } => { + client.refill_zones(zone_ids).await?; + println!("refill_zones_dispatched=ok"); + } Command::NetClass => { let nets = client.get_nets().await?; let netclasses = client.get_netclass_for_nets(nets).await?; @@ -1496,9 +1503,25 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE reason: "set-appearance requires `--ratsnest-display `" .to_string(), - })?, + })?, } } + "refill-zones" => { + let mut zone_ids = Vec::new(); + let mut i = 1; + while i < args.len() { + if args[i] == "--zone-id" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for refill-zones --zone-id".to_string(), + })?; + zone_ids.push(value.clone()); + i += 2; + continue; + } + i += 1; + } + Command::RefillZones { zone_ids } + } "netclass" => Command::NetClass, "proto-coverage-board-read" => Command::ProtoCoverageBoardRead, "board-read-report" => { @@ -1614,7 +1637,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 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 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 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 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 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" ); } @@ -2372,4 +2395,23 @@ mod tests { other => panic!("unexpected command variant: {other:?}"), } } + + #[test] + fn parse_args_parses_refill_zones() { + let (_, command) = parse_args_from(vec![ + "refill-zones".to_string(), + "--zone-id".to_string(), + "zone-1".to_string(), + "--zone-id".to_string(), + "zone-2".to_string(), + ]) + .expect("refill-zones args should parse"); + + match command { + Command::RefillZones { zone_ids } => { + assert_eq!(zone_ids, vec!["zone-1".to_string(), "zone-2".to_string()]); + } + other => panic!("unexpected command variant: {other:?}"), + } + } } From 1a124c145b8e6bbf53b3b5a40553b06ddfe05c1c Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:01:13 +0800 Subject: [PATCH 11/28] docs(readme): note tested KiCad version and unhandled commands --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 6bad699..9f4822f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,17 @@ Early scaffold phase. Core architecture + step-by-step implementation plan: - CLI runbook: `/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rust/docs/TEST_CLI.md` +## Runtime Compatibility Notes (Current Test Rig) + +- Last verified: 2026-02-20 +- KiCad version (`kicad-ipc-cli version`): `10.0.0 (10.0.0-rc1)` + +Commands wrapped in this crate but currently unhandled/unsupported by this KiCad build: + +| Command | Runtime status | Notes | +| --- | --- | --- | +| `RefreshEditor` | `AS_UNHANDLED` | KiCad responds `no handler available for request of type kiapi.common.commands.RefreshEditor`. | + ## KiCad v10 RC1.1 API Completion Matrix Source of truth for this matrix: From 2867c4a874f92bcf95016b728ce0e1f96c11568b Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:05:05 +0800 Subject: [PATCH 12/28] feat(client): add ClearSelection API and CLI command --- README.md | 6 +++--- docs/TEST_CLI.md | 6 ++++++ src/client.rs | 28 ++++++++++++++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 15 ++++++++++++++- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9f4822f..531dc05 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 4 | 67% | -| Common editor/document | 23 | 12 | 52% | +| Common editor/document | 23 | 13 | 57% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **39** | **70%** | +| **Total** | **56** | **40** | **71%** | ### Common (base) @@ -88,7 +88,7 @@ Legend: | `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` | | `AddToSelection` | Not yet | - | | `RemoveFromSelection` | Not yet | - | -| `ClearSelection` | Not yet | - | +| `ClearSelection` | Implemented | `KiCadClient::clear_selection_raw`, `KiCadClient::clear_selection` | | `HitTest` | Implemented | `KiCadClient::hit_test_item` | | `GetTitleBlockInfo` | Implemented | `KiCadClient::get_title_block_info` | | `SaveDocumentToString` | Implemented | `KiCadClient::get_board_as_string` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index de129cc..bc6bf85 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -169,6 +169,12 @@ Show raw protobuf payload bytes for selected items: cargo run --bin kicad-ipc-cli -- selection-raw ``` +Clear current selection: + +```bash +cargo run --bin kicad-ipc-cli -- clear-selection +``` + Show pad-level netlist entries (footprint/pad/net): ```bash diff --git a/src/client.rs b/src/client.rs index b725508..e90e6e8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -68,6 +68,7 @@ const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str = "kiapi.board.commands.CheckPadstackPresenceOnLayers"; const CMD_INJECT_DRC_ERROR: &str = "kiapi.board.commands.InjectDrcError"; const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection"; +const CMD_CLEAR_SELECTION: &str = "kiapi.common.commands.ClearSelection"; const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems"; @@ -729,6 +730,33 @@ impl KiCadClient { decode_pcb_items(items) } + pub async fn clear_selection_raw(&self) -> Result, KiCadError> { + let command = common_commands::ClearSelection { + header: Some(self.current_board_item_header().await?), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_CLEAR_SELECTION)) + .await?; + + match envelope::unpack_any::( + &response, + RES_SELECTION_RESPONSE, + ) { + Ok(payload) => Ok(payload.items), + Err(KiCadError::UnexpectedPayloadType { + expected_type_url: _, + actual_type_url, + }) if actual_type_url == envelope::type_url(RES_PROTOBUF_EMPTY) => Ok(Vec::new()), + Err(err) => Err(err), + } + } + + pub async fn clear_selection(&self) -> Result { + let items = self.clear_selection_raw().await?; + Ok(summarize_selection(items)) + } + pub async fn get_pad_netlist(&self) -> Result, KiCadError> { let footprint_items = self .get_items_raw(vec![common_types::KiCadObjectType::KotPcbFootprint as i32]) diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 5be4495..4f7741f 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -84,6 +84,7 @@ enum Command { action: CommitAction, message: String, }, + ClearSelection, SelectionSummary, SelectionDetails, SelectionRaw, @@ -418,6 +419,10 @@ async fn run() -> Result<(), KiCadError> { .await?; println!("end_commit=ok action={}", action); } + Command::ClearSelection => { + let summary = client.clear_selection().await?; + println!("selection_total={}", summary.total_items); + } Command::SelectionSummary => { let summary = client.get_selection_summary().await?; println!("selection_total={}", summary.total_items); @@ -1172,6 +1177,7 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE message, } } + "clear-selection" => Command::ClearSelection, "selection-summary" => Command::SelectionSummary, "selection-details" => Command::SelectionDetails, "selection-raw" => Command::SelectionRaw, @@ -1637,7 +1643,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 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 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 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 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" ); } @@ -2252,6 +2258,13 @@ mod tests { } } + #[test] + fn parse_args_parses_clear_selection() { + let (_, command) = parse_args_from(vec!["clear-selection".to_string()]) + .expect("clear-selection should parse"); + assert!(matches!(command, Command::ClearSelection)); + } + #[test] fn parse_args_parses_set_active_layer() { let (_, command) = parse_args_from(vec![ From 9dac48ed87c8fffab752c450a5f9417784b0c292 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:08:45 +0800 Subject: [PATCH 13/28] feat(client): add AddToSelection API and CLI command --- README.md | 6 +++--- docs/TEST_CLI.md | 6 ++++++ src/client.rs | 38 +++++++++++++++++++++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 35 +++++++++++++++++++++++++++++++- 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 531dc05..b41cc65 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 4 | 67% | -| Common editor/document | 23 | 13 | 57% | +| Common editor/document | 23 | 14 | 61% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **40** | **71%** | +| **Total** | **56** | **41** | **73%** | ### Common (base) @@ -86,7 +86,7 @@ Legend: | `DeleteItems` | Not yet | - | | `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` | | `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` | -| `AddToSelection` | Not yet | - | +| `AddToSelection` | Implemented | `KiCadClient::add_to_selection_raw`, `KiCadClient::add_to_selection` | | `RemoveFromSelection` | Not yet | - | | `ClearSelection` | Implemented | `KiCadClient::clear_selection_raw`, `KiCadClient::clear_selection` | | `HitTest` | Implemented | `KiCadClient::hit_test_item` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index bc6bf85..2d469e2 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -169,6 +169,12 @@ Show raw protobuf payload bytes for selected items: cargo run --bin kicad-ipc-cli -- selection-raw ``` +Add items to current selection: + +```bash +cargo run --bin kicad-ipc-cli -- add-to-selection --id --id +``` + Clear current selection: ```bash diff --git a/src/client.rs b/src/client.rs index e90e6e8..de9c263 100644 --- a/src/client.rs +++ b/src/client.rs @@ -68,6 +68,7 @@ const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str = "kiapi.board.commands.CheckPadstackPresenceOnLayers"; const CMD_INJECT_DRC_ERROR: &str = "kiapi.board.commands.InjectDrcError"; const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection"; +const CMD_ADD_TO_SELECTION: &str = "kiapi.common.commands.AddToSelection"; const CMD_CLEAR_SELECTION: &str = "kiapi.common.commands.ClearSelection"; const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; @@ -730,6 +731,43 @@ impl KiCadClient { decode_pcb_items(items) } + pub async fn add_to_selection_raw( + &self, + item_ids: Vec, + ) -> Result, KiCadError> { + let command = common_commands::AddToSelection { + header: Some(self.current_board_item_header().await?), + items: item_ids + .into_iter() + .map(|value| common_types::Kiid { value }) + .collect(), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_ADD_TO_SELECTION)) + .await?; + + match envelope::unpack_any::( + &response, + RES_SELECTION_RESPONSE, + ) { + Ok(payload) => Ok(payload.items), + Err(KiCadError::UnexpectedPayloadType { + expected_type_url: _, + actual_type_url, + }) if actual_type_url == envelope::type_url(RES_PROTOBUF_EMPTY) => Ok(Vec::new()), + Err(err) => Err(err), + } + } + + pub async fn add_to_selection( + &self, + item_ids: Vec, + ) -> Result { + let items = self.add_to_selection_raw(item_ids).await?; + Ok(summarize_selection(items)) + } + pub async fn clear_selection_raw(&self) -> Result, KiCadError> { let command = common_commands::ClearSelection { header: Some(self.current_board_item_header().await?), diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 4f7741f..fe6cf0a 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -84,6 +84,9 @@ enum Command { action: CommitAction, message: String, }, + AddToSelection { + item_ids: Vec, + }, ClearSelection, SelectionSummary, SelectionDetails, @@ -419,6 +422,13 @@ async fn run() -> Result<(), KiCadError> { .await?; println!("end_commit=ok action={}", action); } + Command::AddToSelection { item_ids } => { + let summary = client.add_to_selection(item_ids).await?; + println!("selection_total={}", summary.total_items); + for entry in summary.type_url_counts { + println!("type_url={} count={}", entry.type_url, entry.count); + } + } Command::ClearSelection => { let summary = client.clear_selection().await?; println!("selection_total={}", summary.total_items); @@ -1177,6 +1187,10 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE message, } } + "add-to-selection" => { + let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; + Command::AddToSelection { item_ids } + } "clear-selection" => Command::ClearSelection, "selection-summary" => Command::SelectionSummary, "selection-details" => Command::SelectionDetails, @@ -1643,7 +1657,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 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 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 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 add-to-selection --id ...\n Add items to 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" ); } @@ -2265,6 +2279,25 @@ mod tests { assert!(matches!(command, Command::ClearSelection)); } + #[test] + fn parse_args_parses_add_to_selection() { + let (_, command) = parse_args_from(vec![ + "add-to-selection".to_string(), + "--id".to_string(), + "zone-1".to_string(), + "--id".to_string(), + "zone-2".to_string(), + ]) + .expect("add-to-selection args should parse"); + + match command { + Command::AddToSelection { item_ids } => { + assert_eq!(item_ids, vec!["zone-1".to_string(), "zone-2".to_string()]); + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_active_layer() { let (_, command) = parse_args_from(vec![ From 3305de0a8e8d588494bed9fe27e0c1c298547307 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:11:11 +0800 Subject: [PATCH 14/28] feat(client): add RemoveFromSelection API and CLI command --- README.md | 6 +++--- docs/TEST_CLI.md | 6 ++++++ src/client.rs | 38 +++++++++++++++++++++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 35 +++++++++++++++++++++++++++++++- 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b41cc65..66e48bd 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 4 | 67% | -| Common editor/document | 23 | 14 | 61% | +| Common editor/document | 23 | 15 | 65% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **41** | **73%** | +| **Total** | **56** | **42** | **75%** | ### Common (base) @@ -87,7 +87,7 @@ Legend: | `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` | | `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` | | `AddToSelection` | Implemented | `KiCadClient::add_to_selection_raw`, `KiCadClient::add_to_selection` | -| `RemoveFromSelection` | Not yet | - | +| `RemoveFromSelection` | Implemented | `KiCadClient::remove_from_selection_raw`, `KiCadClient::remove_from_selection` | | `ClearSelection` | Implemented | `KiCadClient::clear_selection_raw`, `KiCadClient::clear_selection` | | `HitTest` | Implemented | `KiCadClient::hit_test_item` | | `GetTitleBlockInfo` | Implemented | `KiCadClient::get_title_block_info` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 2d469e2..5521272 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -175,6 +175,12 @@ Add items to current selection: cargo run --bin kicad-ipc-cli -- add-to-selection --id --id ``` +Remove items from current selection: + +```bash +cargo run --bin kicad-ipc-cli -- remove-from-selection --id --id +``` + Clear current selection: ```bash diff --git a/src/client.rs b/src/client.rs index de9c263..a053950 100644 --- a/src/client.rs +++ b/src/client.rs @@ -69,6 +69,7 @@ const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str = const CMD_INJECT_DRC_ERROR: &str = "kiapi.board.commands.InjectDrcError"; const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection"; const CMD_ADD_TO_SELECTION: &str = "kiapi.common.commands.AddToSelection"; +const CMD_REMOVE_FROM_SELECTION: &str = "kiapi.common.commands.RemoveFromSelection"; const CMD_CLEAR_SELECTION: &str = "kiapi.common.commands.ClearSelection"; const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; @@ -795,6 +796,43 @@ impl KiCadClient { Ok(summarize_selection(items)) } + pub async fn remove_from_selection_raw( + &self, + item_ids: Vec, + ) -> Result, KiCadError> { + let command = common_commands::RemoveFromSelection { + header: Some(self.current_board_item_header().await?), + items: item_ids + .into_iter() + .map(|value| common_types::Kiid { value }) + .collect(), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_REMOVE_FROM_SELECTION)) + .await?; + + match envelope::unpack_any::( + &response, + RES_SELECTION_RESPONSE, + ) { + Ok(payload) => Ok(payload.items), + Err(KiCadError::UnexpectedPayloadType { + expected_type_url: _, + actual_type_url, + }) if actual_type_url == envelope::type_url(RES_PROTOBUF_EMPTY) => Ok(Vec::new()), + Err(err) => Err(err), + } + } + + pub async fn remove_from_selection( + &self, + item_ids: Vec, + ) -> Result { + let items = self.remove_from_selection_raw(item_ids).await?; + Ok(summarize_selection(items)) + } + pub async fn get_pad_netlist(&self) -> Result, KiCadError> { let footprint_items = self .get_items_raw(vec![common_types::KiCadObjectType::KotPcbFootprint as i32]) diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index fe6cf0a..6f45aa2 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -87,6 +87,9 @@ enum Command { AddToSelection { item_ids: Vec, }, + RemoveFromSelection { + item_ids: Vec, + }, ClearSelection, SelectionSummary, SelectionDetails, @@ -429,6 +432,13 @@ async fn run() -> Result<(), KiCadError> { println!("type_url={} count={}", entry.type_url, entry.count); } } + Command::RemoveFromSelection { item_ids } => { + let summary = client.remove_from_selection(item_ids).await?; + println!("selection_total={}", summary.total_items); + for entry in summary.type_url_counts { + println!("type_url={} count={}", entry.type_url, entry.count); + } + } Command::ClearSelection => { let summary = client.clear_selection().await?; println!("selection_total={}", summary.total_items); @@ -1191,6 +1201,10 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; Command::AddToSelection { item_ids } } + "remove-from-selection" => { + let item_ids = parse_item_ids(&args[1..], "remove-from-selection")?; + Command::RemoveFromSelection { item_ids } + } "clear-selection" => Command::ClearSelection, "selection-summary" => Command::SelectionSummary, "selection-details" => Command::SelectionDetails, @@ -1657,7 +1671,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 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 add-to-selection --id ...\n Add items to 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 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 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" ); } @@ -2298,6 +2312,25 @@ mod tests { } } + #[test] + fn parse_args_parses_remove_from_selection() { + let (_, command) = parse_args_from(vec![ + "remove-from-selection".to_string(), + "--id".to_string(), + "zone-1".to_string(), + "--id".to_string(), + "zone-2".to_string(), + ]) + .expect("remove-from-selection args should parse"); + + match command { + Command::RemoveFromSelection { item_ids } => { + assert_eq!(item_ids, vec!["zone-1".to_string(), "zone-2".to_string()]); + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_active_layer() { let (_, command) = parse_args_from(vec![ From e7390f654923cb78b327713c13ad6ddf78d032f6 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:18:04 +0800 Subject: [PATCH 15/28] feat(base): add GetKiCadBinaryPath API and CLI command --- README.md | 10 ++++++--- docs/TEST_CLI.md | 6 ++++++ src/client.rs | 24 +++++++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 40 ++++++++++++++++++++++++++++++++++- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 66e48bd..359f64d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,10 @@ Commands wrapped in this crate but currently unhandled/unsupported by this KiCad | --- | --- | --- | | `RefreshEditor` | `AS_UNHANDLED` | KiCad responds `no handler available for request of type kiapi.common.commands.RefreshEditor`. | +Deferred manual/runtime verification (implemented after 2026-02-20 while user unavailable): + +- `GetKiCadBinaryPath` + ## KiCad v10 RC1.1 API Completion Matrix Source of truth for this matrix: @@ -49,12 +53,12 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | -| Common (base) | 6 | 4 | 67% | +| Common (base) | 6 | 5 | 83% | | Common editor/document | 23 | 15 | 65% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **42** | **75%** | +| **Total** | **56** | **43** | **77%** | ### Common (base) @@ -62,7 +66,7 @@ Legend: | --- | --- | --- | | `Ping` | Implemented | `KiCadClient::ping` | | `GetVersion` | Implemented | `KiCadClient::get_version` | -| `GetKiCadBinaryPath` | Not yet | - | +| `GetKiCadBinaryPath` | Implemented | `KiCadClient::get_kicad_binary_path_raw`, `KiCadClient::get_kicad_binary_path` | | `GetTextExtents` | Implemented | `KiCadClient::get_text_extents_raw`, `KiCadClient::get_text_extents` | | `GetTextAsShapes` | Implemented | `KiCadClient::get_text_as_shapes_raw`, `KiCadClient::get_text_as_shapes` | | `GetPluginSettingsPath` | Not yet | - | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 5521272..de209b1 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -29,6 +29,12 @@ Version: cargo run --bin kicad-ipc-cli -- version ``` +Resolve KiCad binary path (default `kicad-cli`): + +```bash +cargo run --bin kicad-ipc-cli -- kicad-binary-path --binary-name kicad-cli +``` + List open PCB docs: ```bash diff --git a/src/client.rs b/src/client.rs index a053950..b0fbbf1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -37,6 +37,7 @@ const KICAD_API_TOKEN_ENV: &str = "KICAD_API_TOKEN"; const CMD_PING: &str = "kiapi.common.commands.Ping"; const CMD_GET_VERSION: &str = "kiapi.common.commands.GetVersion"; +const CMD_GET_KICAD_BINARY_PATH: &str = "kiapi.common.commands.GetKiCadBinaryPath"; const CMD_GET_NET_CLASSES: &str = "kiapi.common.commands.GetNetClasses"; const CMD_GET_TEXT_VARIABLES: &str = "kiapi.common.commands.GetTextVariables"; const CMD_EXPAND_TEXT_VARIABLES: &str = "kiapi.common.commands.ExpandTextVariables"; @@ -82,6 +83,7 @@ const CMD_SAVE_DOCUMENT_TO_STRING: &str = "kiapi.common.commands.SaveDocumentToS const CMD_SAVE_SELECTION_TO_STRING: &str = "kiapi.common.commands.SaveSelectionToString"; const RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse"; +const RES_PATH_RESPONSE: &str = "kiapi.common.commands.PathResponse"; const RES_NET_CLASSES_RESPONSE: &str = "kiapi.common.commands.NetClassesResponse"; const RES_TEXT_VARIABLES: &str = "kiapi.common.project.TextVariables"; const RES_EXPAND_TEXT_VARIABLES_RESPONSE: &str = @@ -337,6 +339,28 @@ impl KiCadClient { }) } + pub async fn get_kicad_binary_path_raw( + &self, + binary_name: impl Into, + ) -> Result { + let command = common_commands::GetKiCadBinaryPath { + binary_name: binary_name.into(), + }; + let response = self + .send_command(envelope::pack_any(&command, CMD_GET_KICAD_BINARY_PATH)) + .await?; + response_payload_as_any(response, RES_PATH_RESPONSE) + } + + pub async fn get_kicad_binary_path( + &self, + binary_name: impl Into, + ) -> Result { + let payload = self.get_kicad_binary_path_raw(binary_name).await?; + let response: common_commands::PathResponse = decode_any(&payload, RES_PATH_RESPONSE)?; + Ok(response.path) + } + pub async fn get_open_documents( &self, document_type: DocumentType, diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 6f45aa2..b443a75 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -30,6 +30,9 @@ struct CliConfig { enum Command { Ping, Version, + KiCadBinaryPath { + binary_name: String, + }, OpenDocs { document_type: DocumentType, }, @@ -209,6 +212,10 @@ async fn run() -> Result<(), KiCadError> { version.major, version.minor, version.patch, version.full_version ); } + Command::KiCadBinaryPath { binary_name } => { + let path = client.get_kicad_binary_path(binary_name).await?; + println!("kicad_binary_path={path}"); + } Command::OpenDocs { document_type } => { let docs = client.get_open_documents(document_type).await?; if docs.is_empty() { @@ -821,6 +828,22 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE "help" | "--help" | "-h" => Command::Help, "ping" => Command::Ping, "version" => Command::Version, + "kicad-binary-path" => { + let mut binary_name = "kicad-cli".to_string(); + let mut i = 1; + while i < args.len() { + if args[i] == "--binary-name" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for kicad-binary-path --binary-name".to_string(), + })?; + binary_name = value.clone(); + i += 2; + continue; + } + i += 1; + } + Command::KiCadBinaryPath { binary_name } + } "project-path" => Command::ProjectPath, "board-open" => Command::BoardOpen, "net-classes" => Command::NetClasses, @@ -1671,7 +1694,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 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 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 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 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" ); } @@ -2346,6 +2369,21 @@ mod tests { } } + #[test] + fn parse_args_parses_kicad_binary_path() { + let (_, command) = parse_args_from(vec![ + "kicad-binary-path".to_string(), + "--binary-name".to_string(), + "kicad-cli".to_string(), + ]) + .expect("kicad-binary-path args should parse"); + + match command { + Command::KiCadBinaryPath { binary_name } => assert_eq!(binary_name, "kicad-cli"), + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![ From 7bd0313b300e45cb33601bbb8d5a727fbedea7ba Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:19:50 +0800 Subject: [PATCH 16/28] feat(base): add GetPluginSettingsPath API and CLI command --- README.md | 7 +++--- docs/TEST_CLI.md | 6 +++++ src/client.rs | 24 ++++++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 42 ++++++++++++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 359f64d..b3893fd 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Commands wrapped in this crate but currently unhandled/unsupported by this KiCad Deferred manual/runtime verification (implemented after 2026-02-20 while user unavailable): - `GetKiCadBinaryPath` +- `GetPluginSettingsPath` ## KiCad v10 RC1.1 API Completion Matrix @@ -53,12 +54,12 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | -| Common (base) | 6 | 5 | 83% | +| Common (base) | 6 | 6 | 100% | | Common editor/document | 23 | 15 | 65% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **43** | **77%** | +| **Total** | **56** | **44** | **79%** | ### Common (base) @@ -69,7 +70,7 @@ Legend: | `GetKiCadBinaryPath` | Implemented | `KiCadClient::get_kicad_binary_path_raw`, `KiCadClient::get_kicad_binary_path` | | `GetTextExtents` | Implemented | `KiCadClient::get_text_extents_raw`, `KiCadClient::get_text_extents` | | `GetTextAsShapes` | Implemented | `KiCadClient::get_text_as_shapes_raw`, `KiCadClient::get_text_as_shapes` | -| `GetPluginSettingsPath` | Not yet | - | +| `GetPluginSettingsPath` | Implemented | `KiCadClient::get_plugin_settings_path_raw`, `KiCadClient::get_plugin_settings_path` | ### Common editor/document diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index de209b1..01105b2 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -35,6 +35,12 @@ Resolve KiCad binary path (default `kicad-cli`): cargo run --bin kicad-ipc-cli -- kicad-binary-path --binary-name kicad-cli ``` +Resolve plugin settings path (default identifier `kicad-ipc-rust`): + +```bash +cargo run --bin kicad-ipc-cli -- plugin-settings-path --identifier kicad-ipc-rust +``` + List open PCB docs: ```bash diff --git a/src/client.rs b/src/client.rs index b0fbbf1..2bbafd1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -38,6 +38,7 @@ const KICAD_API_TOKEN_ENV: &str = "KICAD_API_TOKEN"; const CMD_PING: &str = "kiapi.common.commands.Ping"; const CMD_GET_VERSION: &str = "kiapi.common.commands.GetVersion"; const CMD_GET_KICAD_BINARY_PATH: &str = "kiapi.common.commands.GetKiCadBinaryPath"; +const CMD_GET_PLUGIN_SETTINGS_PATH: &str = "kiapi.common.commands.GetPluginSettingsPath"; const CMD_GET_NET_CLASSES: &str = "kiapi.common.commands.GetNetClasses"; const CMD_GET_TEXT_VARIABLES: &str = "kiapi.common.commands.GetTextVariables"; const CMD_EXPAND_TEXT_VARIABLES: &str = "kiapi.common.commands.ExpandTextVariables"; @@ -84,6 +85,7 @@ const CMD_SAVE_SELECTION_TO_STRING: &str = "kiapi.common.commands.SaveSelectionT const RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse"; const RES_PATH_RESPONSE: &str = "kiapi.common.commands.PathResponse"; +const RES_STRING_RESPONSE: &str = "kiapi.common.commands.StringResponse"; const RES_NET_CLASSES_RESPONSE: &str = "kiapi.common.commands.NetClassesResponse"; const RES_TEXT_VARIABLES: &str = "kiapi.common.project.TextVariables"; const RES_EXPAND_TEXT_VARIABLES_RESPONSE: &str = @@ -361,6 +363,28 @@ impl KiCadClient { Ok(response.path) } + pub async fn get_plugin_settings_path_raw( + &self, + identifier: impl Into, + ) -> Result { + let command = common_commands::GetPluginSettingsPath { + identifier: identifier.into(), + }; + let response = self + .send_command(envelope::pack_any(&command, CMD_GET_PLUGIN_SETTINGS_PATH)) + .await?; + response_payload_as_any(response, RES_STRING_RESPONSE) + } + + pub async fn get_plugin_settings_path( + &self, + identifier: impl Into, + ) -> Result { + let payload = self.get_plugin_settings_path_raw(identifier).await?; + let response: common_commands::StringResponse = decode_any(&payload, RES_STRING_RESPONSE)?; + Ok(response.response) + } + pub async fn get_open_documents( &self, document_type: DocumentType, diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index b443a75..4b27aed 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -33,6 +33,9 @@ enum Command { KiCadBinaryPath { binary_name: String, }, + PluginSettingsPath { + identifier: String, + }, OpenDocs { document_type: DocumentType, }, @@ -216,6 +219,10 @@ async fn run() -> Result<(), KiCadError> { let path = client.get_kicad_binary_path(binary_name).await?; println!("kicad_binary_path={path}"); } + Command::PluginSettingsPath { identifier } => { + let path = client.get_plugin_settings_path(identifier).await?; + println!("plugin_settings_path={path}"); + } Command::OpenDocs { document_type } => { let docs = client.get_open_documents(document_type).await?; if docs.is_empty() { @@ -844,6 +851,22 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE } Command::KiCadBinaryPath { binary_name } } + "plugin-settings-path" => { + let mut identifier = "kicad-ipc-rust".to_string(); + let mut i = 1; + while i < args.len() { + if args[i] == "--identifier" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for plugin-settings-path --identifier".to_string(), + })?; + identifier = value.clone(); + i += 2; + continue; + } + i += 1; + } + Command::PluginSettingsPath { identifier } + } "project-path" => Command::ProjectPath, "board-open" => Command::BoardOpen, "net-classes" => Command::NetClasses, @@ -1694,7 +1717,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 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 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 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" ); } @@ -2384,6 +2407,23 @@ mod tests { } } + #[test] + fn parse_args_parses_plugin_settings_path() { + let (_, command) = parse_args_from(vec![ + "plugin-settings-path".to_string(), + "--identifier".to_string(), + "com.example.test".to_string(), + ]) + .expect("plugin-settings-path args should parse"); + + match command { + Command::PluginSettingsPath { identifier } => { + assert_eq!(identifier, "com.example.test") + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![ From 1a7c125316281d1e96a7c9ba8209b97d3c253ad9 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:21:38 +0800 Subject: [PATCH 17/28] feat(common): add SaveDocument API and CLI command --- README.md | 7 ++++--- docs/TEST_CLI.md | 6 ++++++ src/client.rs | 17 +++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 15 ++++++++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b3893fd..2489be7 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `GetKiCadBinaryPath` - `GetPluginSettingsPath` +- `SaveDocument` ## KiCad v10 RC1.1 API Completion Matrix @@ -55,11 +56,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | -| Common editor/document | 23 | 15 | 65% | +| Common editor/document | 23 | 16 | 70% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **44** | **79%** | +| **Total** | **56** | **45** | **80%** | ### Common (base) @@ -78,7 +79,7 @@ Legend: | --- | --- | --- | | `RefreshEditor` | Implemented | `KiCadClient::refresh_editor` | | `GetOpenDocuments` | Implemented | `KiCadClient::get_open_documents`, `KiCadClient::get_current_project_path`, `KiCadClient::has_open_board` | -| `SaveDocument` | Not yet | - | +| `SaveDocument` | Implemented | `KiCadClient::save_document_raw`, `KiCadClient::save_document` | | `SaveCopyOfDocument` | Not yet | - | | `RevertDocument` | Not yet | - | | `RunAction` | Not yet | - | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 01105b2..8bb3300 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -163,6 +163,12 @@ End a staged commit: cargo run --bin kicad-ipc-cli -- --client-name write-test end-commit --id --action drop --message "cli test cleanup" ``` +Save current board document: + +```bash +cargo run --bin kicad-ipc-cli -- save-doc +``` + Show summary of current PCB selection by item type: ```bash diff --git a/src/client.rs b/src/client.rs index 2bbafd1..052a101 100644 --- a/src/client.rs +++ b/src/client.rs @@ -80,6 +80,7 @@ const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById"; 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_DOCUMENT_TO_STRING: &str = "kiapi.common.commands.SaveDocumentToString"; const CMD_SAVE_SELECTION_TO_STRING: &str = "kiapi.common.commands.SaveSelectionToString"; @@ -1374,6 +1375,22 @@ impl KiCadClient { }) } + pub async fn save_document_raw(&self) -> Result { + let command = common_commands::SaveDocument { + document: Some(self.current_board_document_proto().await?), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_SAVE_DOCUMENT)) + .await?; + response_payload_as_any(response, RES_PROTOBUF_EMPTY) + } + + pub async fn save_document(&self) -> Result<(), KiCadError> { + let _ = self.save_document_raw().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 4b27aed..ed00a76 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -90,6 +90,7 @@ enum Command { action: CommitAction, message: String, }, + SaveDoc, AddToSelection { item_ids: Vec, }, @@ -439,6 +440,10 @@ async fn run() -> Result<(), KiCadError> { .await?; println!("end_commit=ok action={}", action); } + Command::SaveDoc => { + client.save_document().await?; + println!("save_document=ok"); + } Command::AddToSelection { item_ids } => { let summary = client.add_to_selection(item_ids).await?; println!("selection_total={}", summary.total_items); @@ -1243,6 +1248,7 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE message, } } + "save-doc" => Command::SaveDoc, "add-to-selection" => { let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; Command::AddToSelection { item_ids } @@ -1717,7 +1723,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 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 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" ); } @@ -2424,6 +2430,13 @@ mod tests { } } + #[test] + fn parse_args_parses_save_doc() { + let (_, command) = + parse_args_from(vec!["save-doc".to_string()]).expect("save-doc should parse"); + assert!(matches!(command, Command::SaveDoc)); + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![ From 14856ec9d6019ea1cbd13522cda6fff925207d01 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:23:41 +0800 Subject: [PATCH 18/28] 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![ From e03bc9ab8937edf7fb87f1330b56e1038e59b681 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:25:23 +0800 Subject: [PATCH 19/28] feat(common): add RevertDocument API and CLI command --- README.md | 7 ++++--- docs/TEST_CLI.md | 6 ++++++ src/client.rs | 17 +++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 15 ++++++++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d6cce97..c220855 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `GetPluginSettingsPath` - `SaveDocument` - `SaveCopyOfDocument` +- `RevertDocument` ## KiCad v10 RC1.1 API Completion Matrix @@ -57,11 +58,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | -| Common editor/document | 23 | 17 | 74% | +| Common editor/document | 23 | 18 | 78% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **46** | **82%** | +| **Total** | **56** | **47** | **84%** | ### Common (base) @@ -82,7 +83,7 @@ Legend: | `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` | Implemented | `KiCadClient::save_copy_of_document_raw`, `KiCadClient::save_copy_of_document` | -| `RevertDocument` | Not yet | - | +| `RevertDocument` | Implemented | `KiCadClient::revert_document_raw`, `KiCadClient::revert_document` | | `RunAction` | Not yet | - | | `BeginCommit` | Implemented | `KiCadClient::begin_commit_raw`, `KiCadClient::begin_commit` | | `EndCommit` | Implemented | `KiCadClient::end_commit_raw`, `KiCadClient::end_commit` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 038d458..fc8c547 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -175,6 +175,12 @@ Save a copy of current board document: cargo run --bin kicad-ipc-cli -- save-copy --path /tmp/example.kicad_pcb --overwrite --include-project ``` +Revert current board document from disk: + +```bash +cargo run --bin kicad-ipc-cli -- revert-doc +``` + Show summary of current PCB selection by item type: ```bash diff --git a/src/client.rs b/src/client.rs index 4541bb4..ac2dd9e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -82,6 +82,7 @@ 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_REVERT_DOCUMENT: &str = "kiapi.common.commands.RevertDocument"; const CMD_SAVE_DOCUMENT_TO_STRING: &str = "kiapi.common.commands.SaveDocumentToString"; const CMD_SAVE_SELECTION_TO_STRING: &str = "kiapi.common.commands.SaveSelectionToString"; @@ -1425,6 +1426,22 @@ impl KiCadClient { Ok(()) } + pub async fn revert_document_raw(&self) -> Result { + let command = common_commands::RevertDocument { + document: Some(self.current_board_document_proto().await?), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_REVERT_DOCUMENT)) + .await?; + response_payload_as_any(response, RES_PROTOBUF_EMPTY) + } + + pub async fn revert_document(&self) -> Result<(), KiCadError> { + let _ = self.revert_document_raw().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 61671fa..9ae6526 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -96,6 +96,7 @@ enum Command { overwrite: bool, include_project: bool, }, + RevertDoc, AddToSelection { item_ids: Vec, }, @@ -462,6 +463,10 @@ async fn run() -> Result<(), KiCadError> { overwrite, include_project ); } + Command::RevertDoc => { + client.revert_document().await?; + println!("revert_document=ok"); + } Command::AddToSelection { item_ids } => { let summary = client.add_to_selection(item_ids).await?; println!("selection_total={}", summary.total_items); @@ -1301,6 +1306,7 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE include_project, } } + "revert-doc" => Command::RevertDoc, "add-to-selection" => { let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; Command::AddToSelection { item_ids } @@ -1775,7 +1781,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 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" + "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 revert-doc Revert current board document from disk\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" ); } @@ -2514,6 +2520,13 @@ mod tests { } } + #[test] + fn parse_args_parses_revert_doc() { + let (_, command) = + parse_args_from(vec!["revert-doc".to_string()]).expect("revert-doc should parse"); + assert!(matches!(command, Command::RevertDoc)); + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![ From a29a573c6d41b63b05f54847e504604d2c029441 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:30:18 +0800 Subject: [PATCH 20/28] feat(common): add RunAction API and CLI command --- README.md | 7 ++-- docs/TEST_CLI.md | 6 +++ src/client.rs | 78 +++++++++++++++++++++++++++++++---- src/lib.rs | 2 +- src/model/common.rs | 8 ++++ test-scripts/kicad-ipc-cli.rs | 46 ++++++++++++++++++++- 6 files changed, 135 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c220855..40bdd82 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `SaveDocument` - `SaveCopyOfDocument` - `RevertDocument` +- `RunAction` ## KiCad v10 RC1.1 API Completion Matrix @@ -58,11 +59,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | -| Common editor/document | 23 | 18 | 78% | +| Common editor/document | 23 | 19 | 83% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **47** | **84%** | +| **Total** | **56** | **48** | **86%** | ### Common (base) @@ -84,7 +85,7 @@ Legend: | `SaveDocument` | Implemented | `KiCadClient::save_document_raw`, `KiCadClient::save_document` | | `SaveCopyOfDocument` | Implemented | `KiCadClient::save_copy_of_document_raw`, `KiCadClient::save_copy_of_document` | | `RevertDocument` | Implemented | `KiCadClient::revert_document_raw`, `KiCadClient::revert_document` | -| `RunAction` | Not yet | - | +| `RunAction` | Implemented | `KiCadClient::run_action_raw`, `KiCadClient::run_action` | | `BeginCommit` | Implemented | `KiCadClient::begin_commit_raw`, `KiCadClient::begin_commit` | | `EndCommit` | Implemented | `KiCadClient::end_commit_raw`, `KiCadClient::end_commit` | | `CreateItems` | Not yet | - | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index fc8c547..bab07a3 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -181,6 +181,12 @@ Revert current board document from disk: cargo run --bin kicad-ipc-cli -- revert-doc ``` +Run a raw KiCad tool action: + +```bash +cargo run --bin kicad-ipc-cli -- run-action --action pcbnew.InteractiveSelection.ClearSelection +``` + Show summary of current PCB selection by item type: ```bash diff --git a/src/client.rs b/src/client.rs index ac2dd9e..d27ebd7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,9 +19,9 @@ use crate::model::board::{ }; use crate::model::common::{ CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, - ItemHitTestResult, PcbObjectTypeCode, ProjectInfo, SelectionItemDetail, SelectionSummary, - SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, - TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, + ItemHitTestResult, PcbObjectTypeCode, ProjectInfo, RunActionStatus, SelectionItemDetail, + SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, + TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, VersionInfo, }; use crate::proto::kiapi::board as board_proto; @@ -46,6 +46,7 @@ const CMD_GET_TEXT_EXTENTS: &str = "kiapi.common.commands.GetTextExtents"; const CMD_GET_TEXT_AS_SHAPES: &str = "kiapi.common.commands.GetTextAsShapes"; const CMD_REFRESH_EDITOR: &str = "kiapi.common.commands.RefreshEditor"; const CMD_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocuments"; +const CMD_RUN_ACTION: &str = "kiapi.common.commands.RunAction"; const CMD_GET_NETS: &str = "kiapi.board.commands.GetNets"; const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabledLayers"; const CMD_SET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.SetBoardEnabledLayers"; @@ -96,6 +97,7 @@ const RES_EXPAND_TEXT_VARIABLES_RESPONSE: &str = const RES_BOX2: &str = "kiapi.common.types.Box2"; const RES_GET_TEXT_AS_SHAPES_RESPONSE: &str = "kiapi.common.commands.GetTextAsShapesResponse"; const RES_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocumentsResponse"; +const RES_RUN_ACTION_RESPONSE: &str = "kiapi.common.commands.RunActionResponse"; const RES_GET_NETS: &str = "kiapi.board.commands.NetsResponse"; const RES_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.BoardEnabledLayersResponse"; const RES_BOARD_LAYER_RESPONSE: &str = "kiapi.board.commands.BoardLayerResponse"; @@ -325,6 +327,29 @@ impl KiCadClient { Ok(()) } + pub async fn run_action_raw( + &self, + action: impl Into, + ) -> Result { + let command = common_commands::RunAction { + action: action.into(), + }; + let response = self + .send_command(envelope::pack_any(&command, CMD_RUN_ACTION)) + .await?; + response_payload_as_any(response, RES_RUN_ACTION_RESPONSE) + } + + pub async fn run_action( + &self, + action: impl Into, + ) -> Result { + let payload = self.run_action_raw(action).await?; + let response: common_commands::RunActionResponse = + decode_any(&payload, RES_RUN_ACTION_RESPONSE)?; + Ok(map_run_action_status(response.status)) + } + pub async fn get_version(&self) -> Result { let command = envelope::pack_any(&common_commands::GetVersion {}, CMD_GET_VERSION); let response = self.send_command(command).await?; @@ -2074,6 +2099,18 @@ fn map_hit_test_result(value: i32) -> ItemHitTestResult { } } +fn map_run_action_status(value: i32) -> RunActionStatus { + let status = common_commands::RunActionStatus::try_from(value) + .unwrap_or(common_commands::RunActionStatus::RasUnknown); + + match status { + common_commands::RunActionStatus::RasOk => RunActionStatus::Ok, + common_commands::RunActionStatus::RasInvalid => RunActionStatus::Invalid, + common_commands::RunActionStatus::RasFrameNotOpen => RunActionStatus::FrameNotOpen, + common_commands::RunActionStatus::RasUnknown => RunActionStatus::Unknown(value), + } +} + fn map_polygon_with_holes( polygon: common_types::PolygonWithHoles, ) -> Result { @@ -3149,10 +3186,11 @@ mod tests { any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto, drc_severity_to_proto, ensure_item_request_ok, layer_to_model, map_commit_session, map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, - model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, - response_payload_as_any, select_single_board_document, select_single_project_path, - selection_item_detail, summarize_item_details, summarize_selection, - text_horizontal_alignment_to_proto, text_spec_to_proto, PCB_OBJECT_TYPES, + map_run_action_status, model_document_to_proto, normalize_socket_uri, + pad_netlist_from_footprint_items, response_payload_as_any, select_single_board_document, + select_single_project_path, selection_item_detail, summarize_item_details, + summarize_selection, text_horizontal_alignment_to_proto, text_spec_to_proto, + PCB_OBJECT_TYPES, }; use crate::error::KiCadError; use crate::model::common::{ @@ -3607,6 +3645,32 @@ mod tests { ); } + #[test] + fn map_run_action_status_covers_known_variants() { + assert_eq!( + map_run_action_status( + crate::proto::kiapi::common::commands::RunActionStatus::RasOk as i32 + ), + crate::model::common::RunActionStatus::Ok + ); + assert_eq!( + map_run_action_status( + crate::proto::kiapi::common::commands::RunActionStatus::RasInvalid as i32 + ), + crate::model::common::RunActionStatus::Invalid + ); + assert_eq!( + map_run_action_status( + crate::proto::kiapi::common::commands::RunActionStatus::RasFrameNotOpen as i32 + ), + crate::model::common::RunActionStatus::FrameNotOpen + ); + assert_eq!( + map_run_action_status(1234), + crate::model::common::RunActionStatus::Unknown(1234) + ); + } + #[test] fn text_horizontal_alignment_to_proto_covers_known_variants() { assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index b922904..de9a66d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ pub use crate::model::board::{ }; pub use crate::model::common::{ CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, - ItemHitTestResult, PcbObjectTypeCode, SelectionItemDetail, SelectionSummary, + ItemHitTestResult, PcbObjectTypeCode, RunActionStatus, SelectionItemDetail, SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, VersionInfo, diff --git a/src/model/common.rs b/src/model/common.rs index d8d4e09..1c12352 100644 --- a/src/model/common.rs +++ b/src/model/common.rs @@ -183,6 +183,14 @@ pub enum CommitAction { Drop, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RunActionStatus { + Ok, + Invalid, + FrameNotOpen, + Unknown(i32), +} + impl std::fmt::Display for CommitAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 9ae6526..779d3aa 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -97,6 +97,9 @@ enum Command { include_project: bool, }, RevertDoc, + RunAction { + action: String, + }, AddToSelection { item_ids: Vec, }, @@ -467,6 +470,10 @@ async fn run() -> Result<(), KiCadError> { client.revert_document().await?; println!("revert_document=ok"); } + Command::RunAction { action } => { + let status = client.run_action(action).await?; + println!("run_action_status={status:?}"); + } Command::AddToSelection { item_ids } => { let summary = client.add_to_selection(item_ids).await?; println!("selection_total={}", summary.total_items); @@ -1307,6 +1314,26 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE } } "revert-doc" => Command::RevertDoc, + "run-action" => { + let mut action = None; + let mut i = 1; + while i < args.len() { + if args[i] == "--action" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for run-action --action".to_string(), + })?; + action = Some(value.clone()); + i += 2; + continue; + } + i += 1; + } + Command::RunAction { + action: action.ok_or_else(|| KiCadError::Config { + reason: "run-action requires `--action `".to_string(), + })?, + } + } "add-to-selection" => { let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; Command::AddToSelection { item_ids } @@ -1781,7 +1808,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 save-copy --path [--overwrite] [--include-project]\n Save current board document to a new location\n revert-doc Revert current board document from disk\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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\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" ); } @@ -2527,6 +2554,23 @@ mod tests { assert!(matches!(command, Command::RevertDoc)); } + #[test] + fn parse_args_parses_run_action() { + let (_, command) = parse_args_from(vec![ + "run-action".to_string(), + "--action".to_string(), + "pcbnew.InteractiveSelection.ClearSelection".to_string(), + ]) + .expect("run-action args should parse"); + + match command { + Command::RunAction { action } => { + assert_eq!(action, "pcbnew.InteractiveSelection.ClearSelection") + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![ From 01ed710ae29be808a849354e04e1bbbdaa591035 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:34:23 +0800 Subject: [PATCH 21/28] feat(common): add CreateItems API and CLI command --- README.md | 7 +- docs/TEST_CLI.md | 6 ++ src/client.rs | 84 +++++++++++++++++++++++- test-scripts/kicad-ipc-cli.rs | 120 +++++++++++++++++++++++++++++++++- 4 files changed, 211 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 40bdd82..721d1fd 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `SaveCopyOfDocument` - `RevertDocument` - `RunAction` +- `CreateItems` ## KiCad v10 RC1.1 API Completion Matrix @@ -59,11 +60,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | -| Common editor/document | 23 | 19 | 83% | +| Common editor/document | 23 | 20 | 87% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **48** | **86%** | +| **Total** | **56** | **49** | **88%** | ### Common (base) @@ -88,7 +89,7 @@ Legend: | `RunAction` | Implemented | `KiCadClient::run_action_raw`, `KiCadClient::run_action` | | `BeginCommit` | Implemented | `KiCadClient::begin_commit_raw`, `KiCadClient::begin_commit` | | `EndCommit` | Implemented | `KiCadClient::end_commit_raw`, `KiCadClient::end_commit` | -| `CreateItems` | Not yet | - | +| `CreateItems` | Implemented | `KiCadClient::create_items_raw`, `KiCadClient::create_items` | | `GetItems` | Implemented | `KiCadClient::get_items_raw_by_type_codes`, `KiCadClient::get_items_by_type_codes`, `KiCadClient::get_items_details_by_type_codes`, `KiCadClient::get_all_pcb_items_raw`, `KiCadClient::get_all_pcb_items`, `KiCadClient::get_all_pcb_items_details`, `KiCadClient::get_pad_netlist` | | `GetItemsById` | Implemented | `KiCadClient::get_items_by_id_raw`, `KiCadClient::get_items_by_id`, `KiCadClient::get_items_by_id_details` | | `UpdateItems` | Not yet | - | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index bab07a3..1d53d60 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -187,6 +187,12 @@ Run a raw KiCad tool action: cargo run --bin kicad-ipc-cli -- run-action --action pcbnew.InteractiveSelection.ClearSelection ``` +Create raw Any item payload(s): + +```bash +cargo run --bin kicad-ipc-cli -- create-items --item type.googleapis.com/kiapi.board.types.Text= +``` + Show summary of current PCB selection by item type: ```bash diff --git a/src/client.rs b/src/client.rs index d27ebd7..ba03b73 100644 --- a/src/client.rs +++ b/src/client.rs @@ -76,6 +76,7 @@ const CMD_REMOVE_FROM_SELECTION: &str = "kiapi.common.commands.RemoveFromSelecti const CMD_CLEAR_SELECTION: &str = "kiapi.common.commands.ClearSelection"; const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; +const CMD_CREATE_ITEMS: &str = "kiapi.common.commands.CreateItems"; const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems"; const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById"; const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox"; @@ -114,6 +115,7 @@ const RES_VECTOR2: &str = "kiapi.common.types.Vector2"; const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse"; const RES_BEGIN_COMMIT_RESPONSE: &str = "kiapi.common.commands.BeginCommitResponse"; const RES_END_COMMIT_RESPONSE: &str = "kiapi.common.commands.EndCommitResponse"; +const RES_CREATE_ITEMS_RESPONSE: &str = "kiapi.common.commands.CreateItemsResponse"; const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse"; const RES_GET_BOUNDING_BOX_RESPONSE: &str = "kiapi.common.commands.GetBoundingBoxResponse"; const RES_HIT_TEST_RESPONSE: &str = "kiapi.common.commands.HitTestResponse"; @@ -616,6 +618,45 @@ impl KiCadClient { Ok(()) } + pub async fn create_items_raw( + &self, + items: Vec, + container_id: Option, + ) -> Result { + let command = common_commands::CreateItems { + header: Some(self.current_board_item_header().await?), + items, + container: container_id.map(|value| common_types::Kiid { value }), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_CREATE_ITEMS)) + .await?; + response_payload_as_any(response, RES_CREATE_ITEMS_RESPONSE) + } + + pub async fn create_items( + &self, + items: Vec, + container_id: Option, + ) -> Result, KiCadError> { + let payload = self.create_items_raw(items, container_id).await?; + let response: common_commands::CreateItemsResponse = + decode_any(&payload, RES_CREATE_ITEMS_RESPONSE)?; + ensure_item_request_ok(response.status)?; + + response + .created_items + .into_iter() + .map(|row| { + ensure_item_status_ok(row.status)?; + row.item.ok_or_else(|| KiCadError::InvalidResponse { + reason: "CreateItemsResponse missing created item payload".to_string(), + }) + }) + .collect() + } + pub async fn get_nets(&self) -> Result, KiCadError> { let board = self.current_board_document_proto().await?; let command = board_commands::GetNets { @@ -2063,6 +2104,24 @@ fn ensure_item_request_ok(status: i32) -> Result<(), KiCadError> { Ok(()) } +fn ensure_item_status_ok(status: Option) -> Result<(), KiCadError> { + let status = status.unwrap_or_default(); + let code = common_commands::ItemStatusCode::try_from(status.code) + .unwrap_or(common_commands::ItemStatusCode::IscUnknown); + + if code != common_commands::ItemStatusCode::IscOk { + let detail = if status.error_message.is_empty() { + code.as_str_name().to_string() + } else { + format!("{}: {}", code.as_str_name(), status.error_message) + }; + + return Err(KiCadError::ItemStatus { code: detail }); + } + + Ok(()) +} + fn map_item_bounding_boxes( item_ids: Vec, boxes: Vec, @@ -3184,8 +3243,8 @@ fn default_client_name() -> String { mod tests { use super::{ any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto, - drc_severity_to_proto, ensure_item_request_ok, layer_to_model, map_commit_session, - map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, + drc_severity_to_proto, ensure_item_request_ok, ensure_item_status_ok, layer_to_model, + map_commit_session, map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, map_run_action_status, model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, response_payload_as_any, select_single_board_document, select_single_project_path, selection_item_detail, summarize_item_details, @@ -3595,6 +3654,27 @@ mod tests { .is_err()); } + #[test] + fn ensure_item_status_ok_accepts_ok_and_rejects_non_ok() { + assert!( + ensure_item_status_ok(Some(crate::proto::kiapi::common::commands::ItemStatus { + code: crate::proto::kiapi::common::commands::ItemStatusCode::IscOk as i32, + error_message: String::new(), + })) + .is_ok() + ); + + let err = ensure_item_status_ok(Some(crate::proto::kiapi::common::commands::ItemStatus { + code: crate::proto::kiapi::common::commands::ItemStatusCode::IscInvalidType as i32, + error_message: "bad item type".to_string(), + })) + .expect_err("non-OK item status should fail"); + match err { + KiCadError::ItemStatus { code } => assert!(code.contains("ISC_INVALID_TYPE")), + _ => panic!("expected item status error"), + } + } + #[test] fn summarize_item_details_reports_unknown_payload_as_unparsed() { let items = vec![prost_types::Any { diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 779d3aa..05bd803 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -100,6 +100,10 @@ enum Command { RunAction { action: String, }, + CreateItems { + items: Vec, + container_id: Option, + }, AddToSelection { item_ids: Vec, }, @@ -474,6 +478,20 @@ async fn run() -> Result<(), KiCadError> { let status = client.run_action(action).await?; println!("run_action_status={status:?}"); } + Command::CreateItems { + items, + container_id, + } => { + let created = client.create_items(items, container_id).await?; + println!("created_item_count={}", created.len()); + for (index, item) in created.iter().enumerate() { + println!( + "[{index}] type_url={} raw_len={}", + item.type_url, + item.value.len() + ); + } + } Command::AddToSelection { item_ids } => { let summary = client.add_to_selection(item_ids).await?; println!("selection_total={}", summary.total_items); @@ -1334,6 +1352,51 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE })?, } } + "create-items" => { + let mut items = Vec::new(); + let mut container_id = None; + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--item" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for create-items --item".to_string(), + })?; + let (type_url, hex) = + value.split_once('=').ok_or_else(|| KiCadError::Config { + reason: "create-items --item requires `=`" + .to_string(), + })?; + items.push(prost_types::Any { + type_url: type_url.to_string(), + value: hex_to_bytes(hex) + .map_err(|reason| KiCadError::Config { reason })?, + }); + i += 2; + } + "--container-id" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for create-items --container-id".to_string(), + })?; + container_id = Some(value.clone()); + i += 2; + } + _ => i += 1, + } + } + + if items.is_empty() { + return Err(KiCadError::Config { + reason: "create-items requires one or more `--item =` values" + .to_string(), + }); + } + + Command::CreateItems { + items, + container_id, + } + } "add-to-selection" => { let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; Command::AddToSelection { item_ids } @@ -1808,7 +1871,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 save-copy --path [--overwrite] [--include-project]\n Save current board document to a new location\n revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in 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" ); } @@ -2358,6 +2421,33 @@ fn hex_char(value: u8) -> char { } } +fn hex_to_bytes(hex: &str) -> Result, String> { + if !hex.len().is_multiple_of(2) { + return Err("hex payload must have an even number of characters".to_string()); + } + + let mut bytes = Vec::with_capacity(hex.len() / 2); + let chars: Vec = hex.chars().collect(); + let mut i = 0; + while i < chars.len() { + let high = hex_nibble(chars[i])?; + let low = hex_nibble(chars[i + 1])?; + bytes.push((high << 4) | low); + i += 2; + } + + Ok(bytes) +} + +fn hex_nibble(c: char) -> Result { + match c { + '0'..='9' => Ok((c as u8) - b'0'), + 'a'..='f' => Ok((c as u8) - b'a' + 10), + 'A'..='F' => Ok((c as u8) - b'A' + 10), + _ => Err(format!("invalid hex character `{c}`")), + } +} + #[cfg(test)] mod tests { use super::{parse_args_from, Command}; @@ -2571,6 +2661,34 @@ mod tests { } } + #[test] + fn parse_args_parses_create_items() { + let (_, command) = parse_args_from(vec![ + "create-items".to_string(), + "--item".to_string(), + "type.googleapis.com/kiapi.board.types.Text=0a00".to_string(), + "--container-id".to_string(), + "container-1".to_string(), + ]) + .expect("create-items args should parse"); + + match command { + Command::CreateItems { + items, + container_id, + } => { + assert_eq!(items.len(), 1); + assert_eq!( + items[0].type_url, + "type.googleapis.com/kiapi.board.types.Text" + ); + assert_eq!(items[0].value, vec![0x0a, 0x00]); + assert_eq!(container_id.as_deref(), Some("container-1")); + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![ From b26a04e392eb4d7f81992b547a8789a9ede8061e Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:36:18 +0800 Subject: [PATCH 22/28] feat(common): add UpdateItems API and CLI command --- README.md | 7 ++-- docs/TEST_CLI.md | 6 +++ src/client.rs | 38 +++++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 69 ++++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 721d1fd..ccee33d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `RevertDocument` - `RunAction` - `CreateItems` +- `UpdateItems` ## KiCad v10 RC1.1 API Completion Matrix @@ -60,11 +61,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | -| Common editor/document | 23 | 20 | 87% | +| Common editor/document | 23 | 21 | 91% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **49** | **88%** | +| **Total** | **56** | **50** | **89%** | ### Common (base) @@ -92,7 +93,7 @@ Legend: | `CreateItems` | Implemented | `KiCadClient::create_items_raw`, `KiCadClient::create_items` | | `GetItems` | Implemented | `KiCadClient::get_items_raw_by_type_codes`, `KiCadClient::get_items_by_type_codes`, `KiCadClient::get_items_details_by_type_codes`, `KiCadClient::get_all_pcb_items_raw`, `KiCadClient::get_all_pcb_items`, `KiCadClient::get_all_pcb_items_details`, `KiCadClient::get_pad_netlist` | | `GetItemsById` | Implemented | `KiCadClient::get_items_by_id_raw`, `KiCadClient::get_items_by_id`, `KiCadClient::get_items_by_id_details` | -| `UpdateItems` | Not yet | - | +| `UpdateItems` | Implemented | `KiCadClient::update_items_raw`, `KiCadClient::update_items` | | `DeleteItems` | Not yet | - | | `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` | | `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 1d53d60..5f4b2f8 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -193,6 +193,12 @@ Create raw Any item payload(s): cargo run --bin kicad-ipc-cli -- create-items --item type.googleapis.com/kiapi.board.types.Text= ``` +Update raw Any item payload(s): + +```bash +cargo run --bin kicad-ipc-cli -- update-items --item type.googleapis.com/kiapi.board.types.Text= +``` + Show summary of current PCB selection by item type: ```bash diff --git a/src/client.rs b/src/client.rs index ba03b73..d52b3a7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -77,6 +77,7 @@ const CMD_CLEAR_SELECTION: &str = "kiapi.common.commands.ClearSelection"; const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; const CMD_CREATE_ITEMS: &str = "kiapi.common.commands.CreateItems"; +const CMD_UPDATE_ITEMS: &str = "kiapi.common.commands.UpdateItems"; const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems"; const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById"; const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox"; @@ -116,6 +117,7 @@ const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse"; const RES_BEGIN_COMMIT_RESPONSE: &str = "kiapi.common.commands.BeginCommitResponse"; const RES_END_COMMIT_RESPONSE: &str = "kiapi.common.commands.EndCommitResponse"; const RES_CREATE_ITEMS_RESPONSE: &str = "kiapi.common.commands.CreateItemsResponse"; +const RES_UPDATE_ITEMS_RESPONSE: &str = "kiapi.common.commands.UpdateItemsResponse"; const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse"; const RES_GET_BOUNDING_BOX_RESPONSE: &str = "kiapi.common.commands.GetBoundingBoxResponse"; const RES_HIT_TEST_RESPONSE: &str = "kiapi.common.commands.HitTestResponse"; @@ -657,6 +659,42 @@ impl KiCadClient { .collect() } + pub async fn update_items_raw( + &self, + items: Vec, + ) -> Result { + let command = common_commands::UpdateItems { + header: Some(self.current_board_item_header().await?), + items, + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_UPDATE_ITEMS)) + .await?; + response_payload_as_any(response, RES_UPDATE_ITEMS_RESPONSE) + } + + pub async fn update_items( + &self, + items: Vec, + ) -> Result, KiCadError> { + let payload = self.update_items_raw(items).await?; + let response: common_commands::UpdateItemsResponse = + decode_any(&payload, RES_UPDATE_ITEMS_RESPONSE)?; + ensure_item_request_ok(response.status)?; + + response + .updated_items + .into_iter() + .map(|row| { + ensure_item_status_ok(row.status)?; + row.item.ok_or_else(|| KiCadError::InvalidResponse { + reason: "UpdateItemsResponse missing updated item payload".to_string(), + }) + }) + .collect() + } + pub async fn get_nets(&self) -> Result, KiCadError> { let board = self.current_board_document_proto().await?; let command = board_commands::GetNets { diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 05bd803..208020e 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -104,6 +104,9 @@ enum Command { items: Vec, container_id: Option, }, + UpdateItems { + items: Vec, + }, AddToSelection { item_ids: Vec, }, @@ -492,6 +495,17 @@ async fn run() -> Result<(), KiCadError> { ); } } + Command::UpdateItems { items } => { + let updated = client.update_items(items).await?; + println!("updated_item_count={}", updated.len()); + for (index, item) in updated.iter().enumerate() { + println!( + "[{index}] type_url={} raw_len={}", + item.type_url, + item.value.len() + ); + } + } Command::AddToSelection { item_ids } => { let summary = client.add_to_selection(item_ids).await?; println!("selection_total={}", summary.total_items); @@ -1397,6 +1411,37 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE container_id, } } + "update-items" => { + let mut items = Vec::new(); + let mut i = 1; + while i < args.len() { + if args[i] == "--item" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for update-items --item".to_string(), + })?; + let (type_url, hex) = + value.split_once('=').ok_or_else(|| KiCadError::Config { + reason: "update-items --item requires `=`".to_string(), + })?; + items.push(prost_types::Any { + type_url: type_url.to_string(), + value: hex_to_bytes(hex).map_err(|reason| KiCadError::Config { reason })?, + }); + i += 2; + continue; + } + i += 1; + } + + if items.is_empty() { + return Err(KiCadError::Config { + reason: "update-items requires one or more `--item =` values" + .to_string(), + }); + } + + Command::UpdateItems { items } + } "add-to-selection" => { let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; Command::AddToSelection { item_ids } @@ -1871,7 +1916,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 save-copy --path [--overwrite] [--include-project]\n Save current board document to a new location\n revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in 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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in 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" ); } @@ -2689,6 +2734,28 @@ mod tests { } } + #[test] + fn parse_args_parses_update_items() { + let (_, command) = parse_args_from(vec![ + "update-items".to_string(), + "--item".to_string(), + "type.googleapis.com/kiapi.board.types.Text=0a00".to_string(), + ]) + .expect("update-items args should parse"); + + match command { + Command::UpdateItems { items } => { + assert_eq!(items.len(), 1); + assert_eq!( + items[0].type_url, + "type.googleapis.com/kiapi.board.types.Text" + ); + assert_eq!(items[0].value, vec![0x0a, 0x00]); + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![ From a2a3dbc7711726a5d6266e432a4ff493e5c622bb Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:38:48 +0800 Subject: [PATCH 23/28] feat(common): add DeleteItems API and CLI command --- README.md | 7 +-- docs/TEST_CLI.md | 6 +++ src/client.rs | 84 ++++++++++++++++++++++++++++++++--- test-scripts/kicad-ipc-cli.rs | 35 ++++++++++++++- 4 files changed, 121 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ccee33d..91ff478 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `RunAction` - `CreateItems` - `UpdateItems` +- `DeleteItems` ## KiCad v10 RC1.1 API Completion Matrix @@ -61,11 +62,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | -| Common editor/document | 23 | 21 | 91% | +| Common editor/document | 23 | 22 | 96% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **50** | **89%** | +| **Total** | **56** | **51** | **91%** | ### Common (base) @@ -94,7 +95,7 @@ Legend: | `GetItems` | Implemented | `KiCadClient::get_items_raw_by_type_codes`, `KiCadClient::get_items_by_type_codes`, `KiCadClient::get_items_details_by_type_codes`, `KiCadClient::get_all_pcb_items_raw`, `KiCadClient::get_all_pcb_items`, `KiCadClient::get_all_pcb_items_details`, `KiCadClient::get_pad_netlist` | | `GetItemsById` | Implemented | `KiCadClient::get_items_by_id_raw`, `KiCadClient::get_items_by_id`, `KiCadClient::get_items_by_id_details` | | `UpdateItems` | Implemented | `KiCadClient::update_items_raw`, `KiCadClient::update_items` | -| `DeleteItems` | Not yet | - | +| `DeleteItems` | Implemented | `KiCadClient::delete_items_raw`, `KiCadClient::delete_items` | | `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` | | `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` | | `AddToSelection` | Implemented | `KiCadClient::add_to_selection_raw`, `KiCadClient::add_to_selection` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 5f4b2f8..d4d4a17 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -199,6 +199,12 @@ Update raw Any item payload(s): cargo run --bin kicad-ipc-cli -- update-items --item type.googleapis.com/kiapi.board.types.Text= ``` +Delete items by ID: + +```bash +cargo run --bin kicad-ipc-cli -- delete-items --id --id +``` + Show summary of current PCB selection by item type: ```bash diff --git a/src/client.rs b/src/client.rs index d52b3a7..22182b9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -78,6 +78,7 @@ const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; const CMD_CREATE_ITEMS: &str = "kiapi.common.commands.CreateItems"; const CMD_UPDATE_ITEMS: &str = "kiapi.common.commands.UpdateItems"; +const CMD_DELETE_ITEMS: &str = "kiapi.common.commands.DeleteItems"; const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems"; const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById"; const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox"; @@ -118,6 +119,7 @@ const RES_BEGIN_COMMIT_RESPONSE: &str = "kiapi.common.commands.BeginCommitRespon const RES_END_COMMIT_RESPONSE: &str = "kiapi.common.commands.EndCommitResponse"; const RES_CREATE_ITEMS_RESPONSE: &str = "kiapi.common.commands.CreateItemsResponse"; const RES_UPDATE_ITEMS_RESPONSE: &str = "kiapi.common.commands.UpdateItemsResponse"; +const RES_DELETE_ITEMS_RESPONSE: &str = "kiapi.common.commands.DeleteItemsResponse"; const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse"; const RES_GET_BOUNDING_BOX_RESPONSE: &str = "kiapi.common.commands.GetBoundingBoxResponse"; const RES_HIT_TEST_RESPONSE: &str = "kiapi.common.commands.HitTestResponse"; @@ -695,6 +697,44 @@ impl KiCadClient { .collect() } + pub async fn delete_items_raw( + &self, + item_ids: Vec, + ) -> Result { + let command = common_commands::DeleteItems { + header: Some(self.current_board_item_header().await?), + item_ids: item_ids + .into_iter() + .map(|value| common_types::Kiid { value }) + .collect(), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_DELETE_ITEMS)) + .await?; + response_payload_as_any(response, RES_DELETE_ITEMS_RESPONSE) + } + + pub async fn delete_items(&self, item_ids: Vec) -> Result, KiCadError> { + let payload = self.delete_items_raw(item_ids).await?; + let response: common_commands::DeleteItemsResponse = + decode_any(&payload, RES_DELETE_ITEMS_RESPONSE)?; + ensure_item_request_ok(response.status)?; + + response + .deleted_items + .into_iter() + .map(|row| { + ensure_item_deletion_status_ok(row.status)?; + row.id + .map(|id| id.value) + .ok_or_else(|| KiCadError::InvalidResponse { + reason: "DeleteItemsResponse missing deleted item id".to_string(), + }) + }) + .collect() + } + pub async fn get_nets(&self) -> Result, KiCadError> { let board = self.current_board_document_proto().await?; let command = board_commands::GetNets { @@ -2160,6 +2200,19 @@ fn ensure_item_status_ok(status: Option) -> Result< Ok(()) } +fn ensure_item_deletion_status_ok(status: i32) -> Result<(), KiCadError> { + let code = common_commands::ItemDeletionStatus::try_from(status) + .unwrap_or(common_commands::ItemDeletionStatus::IdsUnknown); + + if code != common_commands::ItemDeletionStatus::IdsOk { + return Err(KiCadError::ItemStatus { + code: code.as_str_name().to_string(), + }); + } + + Ok(()) +} + fn map_item_bounding_boxes( item_ids: Vec, boxes: Vec, @@ -3281,13 +3334,13 @@ fn default_client_name() -> String { mod tests { use super::{ any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto, - drc_severity_to_proto, ensure_item_request_ok, ensure_item_status_ok, layer_to_model, - map_commit_session, map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes, - map_run_action_status, model_document_to_proto, normalize_socket_uri, - pad_netlist_from_footprint_items, response_payload_as_any, select_single_board_document, - select_single_project_path, selection_item_detail, summarize_item_details, - summarize_selection, text_horizontal_alignment_to_proto, text_spec_to_proto, - PCB_OBJECT_TYPES, + drc_severity_to_proto, ensure_item_deletion_status_ok, ensure_item_request_ok, + ensure_item_status_ok, layer_to_model, map_commit_session, map_hit_test_result, + map_item_bounding_boxes, map_polygon_with_holes, map_run_action_status, + model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, + response_payload_as_any, select_single_board_document, select_single_project_path, + selection_item_detail, summarize_item_details, summarize_selection, + text_horizontal_alignment_to_proto, text_spec_to_proto, PCB_OBJECT_TYPES, }; use crate::error::KiCadError; use crate::model::common::{ @@ -3713,6 +3766,23 @@ mod tests { } } + #[test] + fn ensure_item_deletion_status_ok_accepts_ok_and_rejects_non_ok() { + assert!(ensure_item_deletion_status_ok( + crate::proto::kiapi::common::commands::ItemDeletionStatus::IdsOk as i32 + ) + .is_ok()); + + let err = ensure_item_deletion_status_ok( + crate::proto::kiapi::common::commands::ItemDeletionStatus::IdsNonexistent as i32, + ) + .expect_err("non-OK item deletion status should fail"); + match err { + KiCadError::ItemStatus { code } => assert_eq!(code, "IDS_NONEXISTENT"), + _ => panic!("expected item status error"), + } + } + #[test] fn summarize_item_details_reports_unknown_payload_as_unparsed() { let items = vec![prost_types::Any { diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 208020e..7037f42 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -107,6 +107,9 @@ enum Command { UpdateItems { items: Vec, }, + DeleteItems { + item_ids: Vec, + }, AddToSelection { item_ids: Vec, }, @@ -506,6 +509,13 @@ async fn run() -> Result<(), KiCadError> { ); } } + Command::DeleteItems { item_ids } => { + let deleted = client.delete_items(item_ids).await?; + println!("deleted_item_count={}", deleted.len()); + for (index, item_id) in deleted.iter().enumerate() { + println!("[{index}] id={item_id}"); + } + } Command::AddToSelection { item_ids } => { let summary = client.add_to_selection(item_ids).await?; println!("selection_total={}", summary.total_items); @@ -1442,6 +1452,10 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE Command::UpdateItems { items } } + "delete-items" => { + let item_ids = parse_item_ids(&args[1..], "delete-items")?; + Command::DeleteItems { item_ids } + } "add-to-selection" => { let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; Command::AddToSelection { item_ids } @@ -1916,7 +1930,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 save-copy --path [--overwrite] [--include-project]\n Save current board document to a new location\n revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in 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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from 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" ); } @@ -2756,6 +2770,25 @@ mod tests { } } + #[test] + fn parse_args_parses_delete_items() { + let (_, command) = parse_args_from(vec![ + "delete-items".to_string(), + "--id".to_string(), + "item-1".to_string(), + "--id".to_string(), + "item-2".to_string(), + ]) + .expect("delete-items args should parse"); + + match command { + Command::DeleteItems { item_ids } => { + assert_eq!(item_ids, vec!["item-1".to_string(), "item-2".to_string()]); + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![ From 1c57eef959410d87a4e3568e92ee64ef69f631eb Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:40:42 +0800 Subject: [PATCH 24/28] feat(common): add ParseAndCreateItemsFromString API and CLI command --- README.md | 7 +++-- docs/TEST_CLI.md | 6 ++++ src/client.rs | 43 ++++++++++++++++++++++++++++ test-scripts/kicad-ipc-cli.rs | 54 ++++++++++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 91ff478..4cae709 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `CreateItems` - `UpdateItems` - `DeleteItems` +- `ParseAndCreateItemsFromString` ## KiCad v10 RC1.1 API Completion Matrix @@ -62,11 +63,11 @@ Legend: | Section | Proto Commands | Implemented | Coverage | | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | -| Common editor/document | 23 | 22 | 96% | +| Common editor/document | 23 | 23 | 100% | | Project manager | 5 | 3 | 60% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **51** | **91%** | +| **Total** | **56** | **52** | **93%** | ### Common (base) @@ -105,7 +106,7 @@ Legend: | `GetTitleBlockInfo` | Implemented | `KiCadClient::get_title_block_info` | | `SaveDocumentToString` | Implemented | `KiCadClient::get_board_as_string` | | `SaveSelectionToString` | Implemented | `KiCadClient::get_selection_as_string` | -| `ParseAndCreateItemsFromString` | Not yet | - | +| `ParseAndCreateItemsFromString` | Implemented | `KiCadClient::parse_and_create_items_from_string_raw`, `KiCadClient::parse_and_create_items_from_string` | ### Project manager diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index d4d4a17..4a9931a 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -205,6 +205,12 @@ Delete items by ID: cargo run --bin kicad-ipc-cli -- delete-items --id --id ``` +Parse and create items from s-expression: + +```bash +cargo run --bin kicad-ipc-cli -- parse-create-items --contents "(kicad_pcb (version 20240108))" +``` + Show summary of current PCB selection by item type: ```bash diff --git a/src/client.rs b/src/client.rs index 22182b9..1763d7d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -79,6 +79,8 @@ const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; const CMD_CREATE_ITEMS: &str = "kiapi.common.commands.CreateItems"; const CMD_UPDATE_ITEMS: &str = "kiapi.common.commands.UpdateItems"; const CMD_DELETE_ITEMS: &str = "kiapi.common.commands.DeleteItems"; +const CMD_PARSE_AND_CREATE_ITEMS_FROM_STRING: &str = + "kiapi.common.commands.ParseAndCreateItemsFromString"; const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems"; const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById"; const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox"; @@ -735,6 +737,47 @@ impl KiCadClient { .collect() } + pub async fn parse_and_create_items_from_string_raw( + &self, + contents: impl Into, + ) -> Result { + let command = common_commands::ParseAndCreateItemsFromString { + document: Some(self.current_board_document_proto().await?), + contents: contents.into(), + }; + + let response = self + .send_command(envelope::pack_any( + &command, + CMD_PARSE_AND_CREATE_ITEMS_FROM_STRING, + )) + .await?; + response_payload_as_any(response, RES_CREATE_ITEMS_RESPONSE) + } + + pub async fn parse_and_create_items_from_string( + &self, + contents: impl Into, + ) -> Result, KiCadError> { + let payload = self + .parse_and_create_items_from_string_raw(contents) + .await?; + let response: common_commands::CreateItemsResponse = + decode_any(&payload, RES_CREATE_ITEMS_RESPONSE)?; + ensure_item_request_ok(response.status)?; + + response + .created_items + .into_iter() + .map(|row| { + ensure_item_status_ok(row.status)?; + row.item.ok_or_else(|| KiCadError::InvalidResponse { + reason: "CreateItemsResponse missing created item payload".to_string(), + }) + }) + .collect() + } + pub async fn get_nets(&self) -> Result, KiCadError> { let board = self.current_board_document_proto().await?; let command = board_commands::GetNets { diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 7037f42..fe5edb0 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -110,6 +110,9 @@ enum Command { DeleteItems { item_ids: Vec, }, + ParseCreateItemsFromString { + contents: String, + }, AddToSelection { item_ids: Vec, }, @@ -516,6 +519,17 @@ async fn run() -> Result<(), KiCadError> { println!("[{index}] id={item_id}"); } } + Command::ParseCreateItemsFromString { contents } => { + let created = client.parse_and_create_items_from_string(contents).await?; + println!("created_item_count={}", created.len()); + for (index, item) in created.iter().enumerate() { + println!( + "[{index}] type_url={} raw_len={}", + item.type_url, + item.value.len() + ); + } + } Command::AddToSelection { item_ids } => { let summary = client.add_to_selection(item_ids).await?; println!("selection_total={}", summary.total_items); @@ -1456,6 +1470,27 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE let item_ids = parse_item_ids(&args[1..], "delete-items")?; Command::DeleteItems { item_ids } } + "parse-create-items" => { + let mut contents = None; + let mut i = 1; + while i < args.len() { + if args[i] == "--contents" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for parse-create-items --contents".to_string(), + })?; + contents = Some(value.clone()); + i += 2; + continue; + } + i += 1; + } + + Command::ParseCreateItemsFromString { + contents: contents.ok_or_else(|| KiCadError::Config { + reason: "parse-create-items requires `--contents `".to_string(), + })?, + } + } "add-to-selection" => { let item_ids = parse_item_ids(&args[1..], "add-to-selection")?; Command::AddToSelection { item_ids } @@ -1930,7 +1965,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 save-copy --path [--overwrite] [--include-project]\n Save current board document to a new location\n revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from 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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\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" ); } @@ -2789,6 +2824,23 @@ mod tests { } } + #[test] + fn parse_args_parses_parse_create_items() { + let (_, command) = parse_args_from(vec![ + "parse-create-items".to_string(), + "--contents".to_string(), + "(kicad_pcb (version 20240108))".to_string(), + ]) + .expect("parse-create-items args should parse"); + + match command { + Command::ParseCreateItemsFromString { contents } => { + assert_eq!(contents, "(kicad_pcb (version 20240108))"); + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_set_enabled_layers() { let (_, command) = parse_args_from(vec![ From fff70f61c13caea2f4398b39d69fabe7db9df0bc Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:45:41 +0800 Subject: [PATCH 25/28] feat(project): add SetNetClasses API and CLI command --- README.md | 7 +- docs/TEST_CLI.md | 6 ++ src/client.rs | 122 +++++++++++++++++++++++++++++++--- src/lib.rs | 6 +- src/model/common.rs | 42 +++++++++++- test-scripts/kicad-ipc-cli.rs | 50 +++++++++++++- 6 files changed, 215 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4cae709..e684c5b 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `UpdateItems` - `DeleteItems` - `ParseAndCreateItemsFromString` +- `SetNetClasses` ## KiCad v10 RC1.1 API Completion Matrix @@ -64,10 +65,10 @@ Legend: | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | | Common editor/document | 23 | 23 | 100% | -| Project manager | 5 | 3 | 60% | +| Project manager | 5 | 4 | 80% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **52** | **93%** | +| **Total** | **56** | **53** | **95%** | ### Common (base) @@ -113,7 +114,7 @@ Legend: | KiCad Command | Status | Rust API | | --- | --- | --- | | `GetNetClasses` | Implemented | `KiCadClient::get_net_classes_raw`, `KiCadClient::get_net_classes` | -| `SetNetClasses` | Not yet | - | +| `SetNetClasses` | Implemented | `KiCadClient::set_net_classes_raw`, `KiCadClient::set_net_classes` | | `ExpandTextVariables` | Implemented | `KiCadClient::expand_text_variables_raw`, `KiCadClient::expand_text_variables` | | `GetTextVariables` | Implemented | `KiCadClient::get_text_variables_raw`, `KiCadClient::get_text_variables` | | `SetTextVariables` | Not yet | - | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 4a9931a..5370e61 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -65,6 +65,12 @@ List project net classes: cargo run --bin kicad-ipc-cli -- net-classes ``` +Write current net classes back with selected merge mode: + +```bash +cargo run --bin kicad-ipc-cli -- set-net-classes --merge-mode merge +``` + List text variables for current board document: ```bash diff --git a/src/client.rs b/src/client.rs index 1763d7d..5f63cd4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,10 +19,10 @@ use crate::model::board::{ }; use crate::model::common::{ CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, - ItemHitTestResult, PcbObjectTypeCode, ProjectInfo, RunActionStatus, SelectionItemDetail, - SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, - TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, - TextVerticalAlignment, TitleBlockInfo, VersionInfo, + ItemHitTestResult, MapMergeMode, PcbObjectTypeCode, ProjectInfo, RunActionStatus, + SelectionItemDetail, SelectionSummary, SelectionTypeCount, TextAsShapesEntry, + TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, TextObjectSpec, + TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, VersionInfo, }; use crate::proto::kiapi::board as board_proto; use crate::proto::kiapi::board::commands as board_commands; @@ -40,6 +40,7 @@ const CMD_GET_VERSION: &str = "kiapi.common.commands.GetVersion"; const CMD_GET_KICAD_BINARY_PATH: &str = "kiapi.common.commands.GetKiCadBinaryPath"; const CMD_GET_PLUGIN_SETTINGS_PATH: &str = "kiapi.common.commands.GetPluginSettingsPath"; const CMD_GET_NET_CLASSES: &str = "kiapi.common.commands.GetNetClasses"; +const CMD_SET_NET_CLASSES: &str = "kiapi.common.commands.SetNetClasses"; const CMD_GET_TEXT_VARIABLES: &str = "kiapi.common.commands.GetTextVariables"; const CMD_EXPAND_TEXT_VARIABLES: &str = "kiapi.common.commands.ExpandTextVariables"; const CMD_GET_TEXT_EXTENTS: &str = "kiapi.common.commands.GetTextExtents"; @@ -465,6 +466,33 @@ impl KiCadClient { Ok(classes) } + pub async fn set_net_classes_raw( + &self, + net_classes: Vec, + merge_mode: MapMergeMode, + ) -> Result { + let command = common_commands::SetNetClasses { + net_classes: net_classes + .into_iter() + .map(net_class_info_to_proto) + .collect(), + merge_mode: map_merge_mode_to_proto(merge_mode), + }; + let response = self + .send_command(envelope::pack_any(&command, CMD_SET_NET_CLASSES)) + .await?; + response_payload_as_any(response, RES_PROTOBUF_EMPTY) + } + + pub async fn set_net_classes( + &self, + net_classes: Vec, + merge_mode: MapMergeMode, + ) -> Result, KiCadError> { + let _ = self.set_net_classes_raw(net_classes, merge_mode).await?; + self.get_net_classes().await + } + pub async fn get_text_variables_raw(&self) -> Result { let command = common_commands::GetTextVariables { document: Some(self.current_board_document_proto().await?), @@ -2161,6 +2189,13 @@ fn commit_action_to_proto(action: CommitAction) -> i32 { } } +fn map_merge_mode_to_proto(value: MapMergeMode) -> i32 { + match value { + MapMergeMode::Merge => common_types::MapMergeMode::MmmMerge as i32, + MapMergeMode::Replace => common_types::MapMergeMode::MmmReplace as i32, + } +} + fn summarize_selection(items: Vec) -> SelectionSummary { let mut counts = BTreeMap::::new(); @@ -2645,6 +2680,62 @@ fn board_editor_appearance_settings_to_proto( } } +fn net_class_type_to_proto(value: NetClassType) -> i32 { + match value { + NetClassType::Explicit => common_project::NetClassType::NctExplicit as i32, + NetClassType::Implicit => common_project::NetClassType::NctImplicit as i32, + NetClassType::Unknown(raw) => raw, + } +} + +fn net_class_info_to_proto(value: NetClassInfo) -> common_project::NetClass { + let board = value + .board + .map(|board| common_project::NetClassBoardSettings { + clearance: board + .clearance_nm + .map(|value_nm| common_types::Distance { value_nm }), + track_width: board + .track_width_nm + .map(|value_nm| common_types::Distance { value_nm }), + diff_pair_track_width: board + .diff_pair_track_width_nm + .map(|value_nm| common_types::Distance { value_nm }), + diff_pair_gap: board + .diff_pair_gap_nm + .map(|value_nm| common_types::Distance { value_nm }), + diff_pair_via_gap: board + .diff_pair_via_gap_nm + .map(|value_nm| common_types::Distance { value_nm }), + via_stack: if board.has_via_stack { + Some(board_types::PadStack::default()) + } else { + None + }, + microvia_stack: if board.has_microvia_stack { + Some(board_types::PadStack::default()) + } else { + None + }, + color: board.color.map(|color| common_types::Color { + r: color.r, + g: color.g, + b: color.b, + a: color.a, + }), + tuning_profile: board.tuning_profile, + }); + + common_project::NetClass { + name: value.name, + priority: value.priority, + board, + schematic: None, + r#type: net_class_type_to_proto(value.class_type), + constituents: value.constituents, + } +} + fn map_net_class_type(value: i32) -> NetClassType { match common_project::NetClassType::try_from(value) { Ok(common_project::NetClassType::NctExplicit) => NetClassType::Explicit, @@ -3379,11 +3470,12 @@ mod tests { any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto, drc_severity_to_proto, ensure_item_deletion_status_ok, ensure_item_request_ok, ensure_item_status_ok, layer_to_model, map_commit_session, map_hit_test_result, - map_item_bounding_boxes, map_polygon_with_holes, map_run_action_status, - model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, - response_payload_as_any, select_single_board_document, select_single_project_path, - selection_item_detail, summarize_item_details, summarize_selection, - text_horizontal_alignment_to_proto, text_spec_to_proto, PCB_OBJECT_TYPES, + map_item_bounding_boxes, map_merge_mode_to_proto, map_polygon_with_holes, + map_run_action_status, model_document_to_proto, normalize_socket_uri, + pad_netlist_from_footprint_items, response_payload_as_any, select_single_board_document, + select_single_project_path, selection_item_detail, summarize_item_details, + summarize_selection, text_horizontal_alignment_to_proto, text_spec_to_proto, + PCB_OBJECT_TYPES, }; use crate::error::KiCadError; use crate::model::common::{ @@ -3551,6 +3643,18 @@ mod tests { ); } + #[test] + fn map_merge_mode_to_proto_maps_known_variants() { + assert_eq!( + map_merge_mode_to_proto(crate::model::common::MapMergeMode::Merge), + crate::proto::kiapi::common::types::MapMergeMode::MmmMerge as i32 + ); + assert_eq!( + map_merge_mode_to_proto(crate::model::common::MapMergeMode::Replace), + crate::proto::kiapi::common::types::MapMergeMode::MmmReplace as i32 + ); + } + #[test] fn drc_severity_to_proto_maps_known_variants() { assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index de9a66d..5f4b03a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,8 +34,8 @@ pub use crate::model::board::{ }; pub use crate::model::common::{ CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, - ItemHitTestResult, PcbObjectTypeCode, RunActionStatus, SelectionItemDetail, SelectionSummary, - SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, - TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, + ItemHitTestResult, MapMergeMode, PcbObjectTypeCode, RunActionStatus, SelectionItemDetail, + SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, + TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, VersionInfo, }; diff --git a/src/model/common.rs b/src/model/common.rs index 1c12352..745b0e3 100644 --- a/src/model/common.rs +++ b/src/model/common.rs @@ -191,6 +191,35 @@ pub enum RunActionStatus { Unknown(i32), } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MapMergeMode { + Merge, + Replace, +} + +impl std::fmt::Display for MapMergeMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Merge => write!(f, "merge"), + Self::Replace => write!(f, "replace"), + } + } +} + +impl FromStr for MapMergeMode { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "merge" => Ok(Self::Merge), + "replace" => Ok(Self::Replace), + _ => Err(format!( + "unknown merge mode `{value}`; expected `merge` or `replace`" + )), + } + } +} + impl std::fmt::Display for CommitAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -403,7 +432,7 @@ impl std::fmt::Display for ItemHitTestResult { #[cfg(test)] mod tests { - use super::{CommitAction, EditorFrameType}; + use super::{CommitAction, EditorFrameType, MapMergeMode}; use std::str::FromStr; #[test] @@ -433,4 +462,15 @@ mod tests { fn editor_frame_type_rejects_unknown_values() { assert!(EditorFrameType::from_str("layout").is_err()); } + + #[test] + fn map_merge_mode_parses_known_values() { + assert_eq!(MapMergeMode::from_str("merge"), Ok(MapMergeMode::Merge)); + assert_eq!(MapMergeMode::from_str("replace"), Ok(MapMergeMode::Replace)); + } + + #[test] + fn map_merge_mode_rejects_unknown_values() { + assert!(MapMergeMode::from_str("upsert").is_err()); + } } diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index fe5edb0..bfabeec 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -7,7 +7,7 @@ use std::time::Duration; use kicad_ipc::{ BoardFlipMode, BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, - DrcSeverity, EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, + DrcSeverity, EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, MapMergeMode, NetColorDisplayMode, PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode, TextObjectSpec, TextShapeGeometry, TextSpec, Vector2Nm, }; @@ -42,6 +42,9 @@ enum Command { ProjectPath, BoardOpen, NetClasses, + SetNetClasses { + merge_mode: MapMergeMode, + }, TextVariables, ExpandTextVariables { text: Vec, @@ -296,6 +299,15 @@ async fn run() -> Result<(), KiCadError> { ); } } + Command::SetNetClasses { merge_mode } => { + let classes = client.get_net_classes().await?; + let updated = client.set_net_classes(classes, merge_mode).await?; + println!( + "net_class_count={} merge_mode={}", + updated.len(), + merge_mode + ); + } Command::TextVariables => { let variables = client.get_text_variables().await?; println!("text_variable_count={}", variables.len()); @@ -961,6 +973,23 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE "project-path" => Command::ProjectPath, "board-open" => Command::BoardOpen, "net-classes" => Command::NetClasses, + "set-net-classes" => { + let mut merge_mode = MapMergeMode::Merge; + let mut i = 1; + while i < args.len() { + if args[i] == "--merge-mode" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-net-classes --merge-mode".to_string(), + })?; + merge_mode = MapMergeMode::from_str(value) + .map_err(|reason| KiCadError::Config { reason })?; + i += 2; + continue; + } + i += 1; + } + Command::SetNetClasses { merge_mode } + } "text-variables" => Command::TextVariables, "expand-text-variables" => { let mut text = Vec::new(); @@ -1965,7 +1994,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 save-copy --path [--overwrite] [--include-project]\n Save current board document to a new location\n revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\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 set-net-classes [--merge-mode ]\n Write current netclass set back with selected merge mode\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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\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" ); } @@ -2699,6 +2728,23 @@ mod tests { } } + #[test] + fn parse_args_parses_set_net_classes() { + let (_, command) = parse_args_from(vec![ + "set-net-classes".to_string(), + "--merge-mode".to_string(), + "replace".to_string(), + ]) + .expect("set-net-classes args should parse"); + + match command { + Command::SetNetClasses { merge_mode } => { + assert_eq!(merge_mode, kicad_ipc::MapMergeMode::Replace) + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_save_doc() { let (_, command) = From 0e8217fd8fb8a0c84c1a220cab645142dbc9bc51 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:48:38 +0800 Subject: [PATCH 26/28] feat(project): add SetTextVariables API and CLI command --- README.md | 7 ++-- docs/TEST_CLI.md | 6 +++ src/client.rs | 28 +++++++++++++ test-scripts/kicad-ipc-cli.rs | 77 ++++++++++++++++++++++++++++++++++- 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e684c5b..3426aa9 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `DeleteItems` - `ParseAndCreateItemsFromString` - `SetNetClasses` +- `SetTextVariables` ## KiCad v10 RC1.1 API Completion Matrix @@ -65,10 +66,10 @@ Legend: | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | | Common editor/document | 23 | 23 | 100% | -| Project manager | 5 | 4 | 80% | +| Project manager | 5 | 5 | 100% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **53** | **95%** | +| **Total** | **56** | **54** | **96%** | ### Common (base) @@ -117,7 +118,7 @@ Legend: | `SetNetClasses` | Implemented | `KiCadClient::set_net_classes_raw`, `KiCadClient::set_net_classes` | | `ExpandTextVariables` | Implemented | `KiCadClient::expand_text_variables_raw`, `KiCadClient::expand_text_variables` | | `GetTextVariables` | Implemented | `KiCadClient::get_text_variables_raw`, `KiCadClient::get_text_variables` | -| `SetTextVariables` | Not yet | - | +| `SetTextVariables` | Implemented | `KiCadClient::set_text_variables_raw`, `KiCadClient::set_text_variables` | ### Board editor (PCB) diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 5370e61..f4a013c 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -77,6 +77,12 @@ List text variables for current board document: cargo run --bin kicad-ipc-cli -- text-variables ``` +Set text variables: + +```bash +cargo run --bin kicad-ipc-cli -- set-text-variables --merge-mode merge --var REV=A +``` + Expand text variables in one or more input strings: ```bash diff --git a/src/client.rs b/src/client.rs index 5f63cd4..9cc6609 100644 --- a/src/client.rs +++ b/src/client.rs @@ -42,6 +42,7 @@ const CMD_GET_PLUGIN_SETTINGS_PATH: &str = "kiapi.common.commands.GetPluginSetti const CMD_GET_NET_CLASSES: &str = "kiapi.common.commands.GetNetClasses"; const CMD_SET_NET_CLASSES: &str = "kiapi.common.commands.SetNetClasses"; const CMD_GET_TEXT_VARIABLES: &str = "kiapi.common.commands.GetTextVariables"; +const CMD_SET_TEXT_VARIABLES: &str = "kiapi.common.commands.SetTextVariables"; const CMD_EXPAND_TEXT_VARIABLES: &str = "kiapi.common.commands.ExpandTextVariables"; const CMD_GET_TEXT_EXTENTS: &str = "kiapi.common.commands.GetTextExtents"; const CMD_GET_TEXT_AS_SHAPES: &str = "kiapi.common.commands.GetTextAsShapes"; @@ -509,6 +510,33 @@ impl KiCadClient { Ok(response.variables.into_iter().collect()) } + pub async fn set_text_variables_raw( + &self, + variables: BTreeMap, + merge_mode: MapMergeMode, + ) -> Result { + let command = common_commands::SetTextVariables { + document: Some(self.current_board_document_proto().await?), + variables: Some(common_project::TextVariables { + variables: variables.into_iter().collect(), + }), + merge_mode: map_merge_mode_to_proto(merge_mode), + }; + let response = self + .send_command(envelope::pack_any(&command, CMD_SET_TEXT_VARIABLES)) + .await?; + response_payload_as_any(response, RES_PROTOBUF_EMPTY) + } + + pub async fn set_text_variables( + &self, + variables: BTreeMap, + merge_mode: MapMergeMode, + ) -> Result, KiCadError> { + let _ = self.set_text_variables_raw(variables, merge_mode).await?; + self.get_text_variables().await + } + pub async fn expand_text_variables_raw( &self, text: Vec, diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index bfabeec..f9fd45d 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -46,6 +46,10 @@ enum Command { merge_mode: MapMergeMode, }, TextVariables, + SetTextVariables { + merge_mode: MapMergeMode, + variables: BTreeMap, + }, ExpandTextVariables { text: Vec, }, @@ -315,6 +319,20 @@ async fn run() -> Result<(), KiCadError> { println!("name={} value={}", name, value); } } + Command::SetTextVariables { + merge_mode, + variables, + } => { + let updated = client.set_text_variables(variables, merge_mode).await?; + println!( + "text_variable_count={} merge_mode={}", + updated.len(), + merge_mode + ); + for (name, value) in updated { + println!("name={} value={}", name, value); + } + } Command::ExpandTextVariables { text } => { let expanded = client.expand_text_variables(text.clone()).await?; println!("expanded_count={}", expanded.len()); @@ -991,6 +1009,40 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE Command::SetNetClasses { merge_mode } } "text-variables" => Command::TextVariables, + "set-text-variables" => { + let mut merge_mode = MapMergeMode::Merge; + let mut variables = BTreeMap::new(); + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--merge-mode" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-text-variables --merge-mode".to_string(), + })?; + merge_mode = MapMergeMode::from_str(value) + .map_err(|reason| KiCadError::Config { reason })?; + i += 2; + } + "--var" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-text-variables --var".to_string(), + })?; + let (name, text) = + value.split_once('=').ok_or_else(|| KiCadError::Config { + reason: "set-text-variables --var requires `=`" + .to_string(), + })?; + variables.insert(name.to_string(), text.to_string()); + i += 2; + } + _ => i += 1, + } + } + Command::SetTextVariables { + merge_mode, + variables, + } + } "expand-text-variables" => { let mut text = Vec::new(); let mut i = 1; @@ -1994,7 +2046,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 set-net-classes [--merge-mode ]\n Write current netclass set back with selected merge mode\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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\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 set-net-classes [--merge-mode ]\n Write current netclass set back with selected merge mode\n text-variables List text variables for current board document\n set-text-variables [--merge-mode ] [--var ...]\n Set 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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\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" ); } @@ -2745,6 +2797,29 @@ mod tests { } } + #[test] + fn parse_args_parses_set_text_variables() { + let (_, command) = parse_args_from(vec![ + "set-text-variables".to_string(), + "--merge-mode".to_string(), + "replace".to_string(), + "--var".to_string(), + "REV=A".to_string(), + ]) + .expect("set-text-variables args should parse"); + + match command { + Command::SetTextVariables { + merge_mode, + variables, + } => { + assert_eq!(merge_mode, kicad_ipc::MapMergeMode::Replace); + assert_eq!(variables.get("REV").map(|value| value.as_str()), Some("A")); + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_save_doc() { let (_, command) = From deb03b9c486b8c073c33ec334ba7a79a8a177832 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:56:32 +0800 Subject: [PATCH 27/28] feat(board): add UpdateBoardStackup API and CLI command --- README.md | 7 +- docs/TEST_CLI.md | 1 + src/client.rs | 250 ++++++++++++++++++++++++++++++++-- src/model/board.rs | 1 + test-scripts/kicad-ipc-cli.rs | 16 ++- 5 files changed, 262 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3426aa9..3ad8fda 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `ParseAndCreateItemsFromString` - `SetNetClasses` - `SetTextVariables` +- `UpdateBoardStackup` ## KiCad v10 RC1.1 API Completion Matrix @@ -67,9 +68,9 @@ Legend: | Common (base) | 6 | 6 | 100% | | Common editor/document | 23 | 23 | 100% | | Project manager | 5 | 5 | 100% | -| Board editor (PCB) | 22 | 20 | 91% | +| Board editor (PCB) | 22 | 21 | 95% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **54** | **96%** | +| **Total** | **56** | **55** | **98%** | ### Common (base) @@ -125,7 +126,7 @@ Legend: | KiCad Command | Status | Rust API | | --- | --- | --- | | `GetBoardStackup` | Implemented | `KiCadClient::get_board_stackup_raw`, `KiCadClient::get_board_stackup` | -| `UpdateBoardStackup` | Not yet | - | +| `UpdateBoardStackup` | Implemented | `KiCadClient::update_board_stackup_raw`, `KiCadClient::update_board_stackup` | | `GetBoardEnabledLayers` | Implemented | `KiCadClient::get_board_enabled_layers` | | `SetBoardEnabledLayers` | Implemented | `KiCadClient::set_board_enabled_layers` | | `GetGraphicsDefaults` | Implemented | `KiCadClient::get_graphics_defaults_raw`, `KiCadClient::get_graphics_defaults` | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index f4a013c..b9b7943 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -341,6 +341,7 @@ Show typed stackup/graphics/appearance: ```bash cargo run --bin kicad-ipc-cli -- stackup +cargo run --bin kicad-ipc-cli -- update-stackup cargo run --bin kicad-ipc-cli -- graphics-defaults cargo run --bin kicad-ipc-cli -- appearance ``` diff --git a/src/client.rs b/src/client.rs index 9cc6609..a4e2480 100644 --- a/src/client.rs +++ b/src/client.rs @@ -59,6 +59,7 @@ const CMD_SET_VISIBLE_LAYERS: &str = "kiapi.board.commands.SetVisibleLayers"; const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin"; const CMD_SET_BOARD_ORIGIN: &str = "kiapi.board.commands.SetBoardOrigin"; const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup"; +const CMD_UPDATE_BOARD_STACKUP: &str = "kiapi.board.commands.UpdateBoardStackup"; const CMD_GET_GRAPHICS_DEFAULTS: &str = "kiapi.board.commands.GetGraphicsDefaults"; const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = "kiapi.board.commands.GetBoardEditorAppearanceSettings"; @@ -1524,6 +1525,32 @@ impl KiCadClient { Ok(map_board_stackup(response.stackup.unwrap_or_default())) } + pub async fn update_board_stackup_raw( + &self, + stackup: BoardStackup, + ) -> Result { + let command = board_commands::UpdateBoardStackup { + board: Some(self.current_board_document_proto().await?), + stackup: Some(board_stackup_to_proto(stackup)), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_UPDATE_BOARD_STACKUP)) + .await?; + + response_payload_as_any(response, RES_BOARD_STACKUP_RESPONSE) + } + + pub async fn update_board_stackup( + &self, + stackup: BoardStackup, + ) -> Result { + let payload = self.update_board_stackup_raw(stackup).await?; + let response: board_commands::BoardStackupResponse = + decode_any(&payload, RES_BOARD_STACKUP_RESPONSE)?; + Ok(map_board_stackup(response.stackup.unwrap_or_default())) + } + pub async fn get_graphics_defaults_raw(&self) -> Result { let command = board_commands::GetGraphicsDefaults { board: Some(self.current_board_document_proto().await?), @@ -2510,6 +2537,28 @@ fn map_board_stackup_layer_type(value: i32) -> BoardStackupLayerType { } } +fn board_stackup_layer_type_to_proto(value: BoardStackupLayerType) -> i32 { + match value { + BoardStackupLayerType::Copper => board_proto::BoardStackupLayerType::BsltCopper as i32, + BoardStackupLayerType::Dielectric => { + board_proto::BoardStackupLayerType::BsltDielectric as i32 + } + BoardStackupLayerType::Silkscreen => { + board_proto::BoardStackupLayerType::BsltSilkscreen as i32 + } + BoardStackupLayerType::SolderMask => { + board_proto::BoardStackupLayerType::BsltSoldermask as i32 + } + BoardStackupLayerType::SolderPaste => { + board_proto::BoardStackupLayerType::BsltSolderpaste as i32 + } + BoardStackupLayerType::Undefined => { + board_proto::BoardStackupLayerType::BsltUndefined as i32 + } + BoardStackupLayerType::Unknown(value) => value, + } +} + fn map_board_layer_class(value: i32) -> BoardLayerClass { match board_proto::BoardLayerClass::try_from(value) { Ok(board_proto::BoardLayerClass::BlcSilkscreen) => BoardLayerClass::Silkscreen, @@ -2616,6 +2665,7 @@ fn map_board_stackup(stackup: board_proto::BoardStackup) -> BoardStackup { .map(|impedance| impedance.is_controlled) .unwrap_or(false); let edge = stackup.edge.unwrap_or_default(); + let edge_has_connector = edge.connector.is_some(); let edge_has_castellated_pads = edge .castellation .map(|value| value.has_castellated_pads) @@ -2654,12 +2704,75 @@ fn map_board_stackup(stackup: board_proto::BoardStackup) -> BoardStackup { BoardStackup { finish_type_name, impedance_controlled, + edge_has_connector, edge_has_castellated_pads, edge_has_edge_plating, layers, } } +fn board_stackup_to_proto(stackup: BoardStackup) -> board_proto::BoardStackup { + board_proto::BoardStackup { + finish: (!stackup.finish_type_name.is_empty()).then_some(board_proto::BoardFinish { + type_name: stackup.finish_type_name, + }), + impedance: Some(board_proto::BoardImpedanceControl { + is_controlled: stackup.impedance_controlled, + }), + edge: Some(board_proto::BoardEdgeSettings { + connector: stackup + .edge_has_connector + .then_some(board_proto::BoardEdgeConnector {}), + castellation: Some(board_proto::Castellation { + has_castellated_pads: stackup.edge_has_castellated_pads, + }), + plating: Some(board_proto::EdgePlating { + has_edge_plating: stackup.edge_has_edge_plating, + }), + }), + layers: stackup + .layers + .into_iter() + .map(board_stackup_layer_to_proto) + .collect(), + } +} + +fn board_stackup_layer_to_proto(layer: BoardStackupLayer) -> board_proto::BoardStackupLayer { + board_proto::BoardStackupLayer { + thickness: layer + .thickness_nm + .map(|value_nm| common_types::Distance { value_nm }), + layer: layer.layer.id, + enabled: layer.enabled, + r#type: board_stackup_layer_type_to_proto(layer.layer_type), + dielectric: (!layer.dielectric_layers.is_empty()).then(|| { + board_proto::BoardStackupDielectricLayer { + layer: layer + .dielectric_layers + .into_iter() + .map(|dielectric| board_proto::BoardStackupDielectricProperties { + epsilon_r: dielectric.epsilon_r, + loss_tangent: dielectric.loss_tangent, + material_name: dielectric.material_name, + thickness: dielectric + .thickness_nm + .map(|value_nm| common_types::Distance { value_nm }), + }) + .collect(), + } + }), + color: layer.color.map(|color| common_types::Color { + r: color.r, + g: color.g, + b: color.b, + a: color.a, + }), + material_name: layer.material_name, + user_name: layer.user_name, + } +} + fn map_graphics_defaults(defaults: board_proto::GraphicsDefaults) -> GraphicsDefaults { GraphicsDefaults { layers: defaults @@ -3495,17 +3608,20 @@ fn default_client_name() -> String { #[cfg(test)] mod tests { use super::{ - any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto, - drc_severity_to_proto, ensure_item_deletion_status_ok, ensure_item_request_ok, - ensure_item_status_ok, layer_to_model, map_commit_session, map_hit_test_result, - map_item_bounding_boxes, map_merge_mode_to_proto, map_polygon_with_holes, - map_run_action_status, model_document_to_proto, normalize_socket_uri, - pad_netlist_from_footprint_items, response_payload_as_any, select_single_board_document, - select_single_project_path, selection_item_detail, summarize_item_details, - summarize_selection, text_horizontal_alignment_to_proto, text_spec_to_proto, - PCB_OBJECT_TYPES, + any_to_pretty_debug, board_editor_appearance_settings_to_proto, board_stackup_to_proto, + commit_action_to_proto, drc_severity_to_proto, ensure_item_deletion_status_ok, + ensure_item_request_ok, ensure_item_status_ok, layer_to_model, map_board_stackup, + map_commit_session, map_hit_test_result, map_item_bounding_boxes, map_merge_mode_to_proto, + map_polygon_with_holes, map_run_action_status, model_document_to_proto, + normalize_socket_uri, pad_netlist_from_footprint_items, response_payload_as_any, + select_single_board_document, select_single_project_path, selection_item_detail, + summarize_item_details, summarize_selection, text_horizontal_alignment_to_proto, + text_spec_to_proto, PCB_OBJECT_TYPES, }; use crate::error::KiCadError; + use crate::model::board::{ + BoardLayerInfo, BoardStackup, BoardStackupLayer, BoardStackupLayerType, + }; use crate::model::common::{ CommitAction, DocumentSpecifier, DocumentType, ProjectInfo, TextAttributesSpec, TextHorizontalAlignment, TextSpec, @@ -3724,6 +3840,122 @@ mod tests { ); } + #[test] + fn map_board_stackup_defaults_missing_optional_messages() { + let mapped = map_board_stackup(crate::proto::kiapi::board::BoardStackup::default()); + assert_eq!(mapped.finish_type_name, ""); + assert!(!mapped.impedance_controlled); + assert!(!mapped.edge_has_connector); + assert!(!mapped.edge_has_castellated_pads); + assert!(!mapped.edge_has_edge_plating); + assert!(mapped.layers.is_empty()); + } + + #[test] + fn map_board_stackup_maps_unknown_layer_type_enum() { + let mapped = map_board_stackup(crate::proto::kiapi::board::BoardStackup { + finish: None, + impedance: None, + edge: None, + layers: vec![crate::proto::kiapi::board::BoardStackupLayer { + thickness: None, + layer: crate::proto::kiapi::board::types::BoardLayer::BlFCu as i32, + enabled: true, + r#type: 777, + dielectric: None, + color: None, + material_name: String::new(), + user_name: String::new(), + }], + }); + assert!(matches!( + mapped.layers.first().map(|layer| layer.layer_type), + Some(BoardStackupLayerType::Unknown(777)) + )); + } + + #[test] + fn board_stackup_to_proto_maps_unknown_layer_type_and_missing_nested_messages() { + let proto = board_stackup_to_proto(BoardStackup { + finish_type_name: String::new(), + impedance_controlled: false, + edge_has_connector: false, + edge_has_castellated_pads: false, + edge_has_edge_plating: false, + layers: vec![BoardStackupLayer { + layer: BoardLayerInfo { + id: crate::proto::kiapi::board::types::BoardLayer::BlFCu as i32, + name: "BL_F_Cu".to_string(), + }, + user_name: "F.Cu".to_string(), + material_name: "Copper".to_string(), + enabled: true, + thickness_nm: None, + layer_type: BoardStackupLayerType::Unknown(321), + color: None, + dielectric_layers: Vec::new(), + }], + }); + + assert!(proto.finish.is_none()); + assert_eq!( + proto + .impedance + .expect("impedance should always be present") + .is_controlled, + false + ); + let edge = proto.edge.expect("edge should always be present"); + assert!(edge.connector.is_none()); + assert_eq!( + edge.castellation + .expect("castellation should be present") + .has_castellated_pads, + false + ); + assert_eq!( + edge.plating + .expect("plating should be present") + .has_edge_plating, + false + ); + let layer = proto.layers.first().expect("one layer should be present"); + assert!(layer.thickness.is_none()); + assert_eq!(layer.r#type, 321); + assert!(layer.dielectric.is_none()); + assert!(layer.color.is_none()); + } + + #[test] + fn board_stackup_to_proto_preserves_edge_connector_presence() { + let proto = board_stackup_to_proto(BoardStackup { + finish_type_name: "ENIG".to_string(), + impedance_controlled: true, + edge_has_connector: true, + edge_has_castellated_pads: true, + edge_has_edge_plating: true, + layers: Vec::new(), + }); + assert_eq!( + proto.finish.expect("finish should be present").type_name, + "ENIG" + ); + let edge = proto.edge.expect("edge should be present"); + assert!(edge.connector.is_some()); + assert_eq!( + edge.castellation + .expect("castellation should be present") + .has_castellated_pads, + true + ); + assert_eq!( + edge.plating + .expect("plating should be present") + .has_edge_plating, + true + ); + } + #[test] fn response_payload_as_any_validates_type_url() { let response = crate::proto::kiapi::common::ApiResponse { diff --git a/src/model/board.rs b/src/model/board.rs index c289065..86b5d36 100644 --- a/src/model/board.rs +++ b/src/model/board.rs @@ -164,6 +164,7 @@ pub struct BoardStackupLayer { pub struct BoardStackup { pub finish_type_name: String, pub impedance_controlled: bool, + pub edge_has_connector: bool, pub edge_has_castellated_pads: bool, pub edge_has_edge_plating: bool, pub layers: Vec, diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index f9fd45d..2821748 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -166,6 +166,7 @@ enum Command { BoardAsString, SelectionAsString, Stackup, + UpdateStackup, GraphicsDefaults, Appearance, SetAppearance { @@ -840,6 +841,11 @@ async fn run() -> Result<(), KiCadError> { let stackup = client.get_board_stackup().await?; println!("{stackup:#?}"); } + Command::UpdateStackup => { + let stackup = client.get_board_stackup().await?; + let updated = client.update_board_stackup(stackup).await?; + println!("{updated:#?}"); + } Command::GraphicsDefaults => { let defaults = client.get_graphics_defaults().await?; println!("{defaults:#?}"); @@ -1837,6 +1843,7 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE "board-as-string" => Command::BoardAsString, "selection-as-string" => Command::SelectionAsString, "stackup" => Command::Stackup, + "update-stackup" => Command::UpdateStackup, "graphics-defaults" => Command::GraphicsDefaults, "appearance" => Command::Appearance, "set-appearance" => { @@ -2046,7 +2053,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 set-net-classes [--merge-mode ]\n Write current netclass set back with selected merge mode\n text-variables List text variables for current board document\n set-text-variables [--merge-mode ] [--var ...]\n Set 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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\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 set-net-classes [--merge-mode ]\n Write current netclass set back with selected merge mode\n text-variables List text variables for current board document\n set-text-variables [--merge-mode ] [--var ...]\n Set 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 update-stackup Round-trip current stackup through UpdateBoardStackup\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 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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\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" ); } @@ -3109,4 +3116,11 @@ mod tests { other => panic!("unexpected command variant: {other:?}"), } } + + #[test] + fn parse_args_parses_update_stackup() { + let (_, command) = parse_args_from(vec!["update-stackup".to_string()]) + .expect("update-stackup should parse"); + assert!(matches!(command, Command::UpdateStackup)); + } } From cdf37bb7b62cb2a796f3679f58d4cd70f04fa606 Mon Sep 17 00:00:00 2001 From: Milind Sharma Date: Fri, 20 Feb 2026 18:59:02 +0800 Subject: [PATCH 28/28] feat(board): add InteractiveMoveItems API and CLI command --- README.md | 7 +- docs/TEST_CLI.md | 6 ++ src/client.rs | 30 +++++++ test-scripts/kicad-ipc-cli.rs | 153 +++++++++++++++++++++++++++++++++- 4 files changed, 192 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3ad8fda..8d4b643 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `SetNetClasses` - `SetTextVariables` - `UpdateBoardStackup` +- `InteractiveMoveItems` ## KiCad v10 RC1.1 API Completion Matrix @@ -68,9 +69,9 @@ Legend: | Common (base) | 6 | 6 | 100% | | Common editor/document | 23 | 23 | 100% | | Project manager | 5 | 5 | 100% | -| Board editor (PCB) | 22 | 21 | 95% | +| Board editor (PCB) | 22 | 22 | 100% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **55** | **98%** | +| **Total** | **56** | **56** | **100%** | ### Common (base) @@ -146,7 +147,7 @@ Legend: | `SetActiveLayer` | Implemented | `KiCadClient::set_active_layer` | | `GetBoardEditorAppearanceSettings` | Implemented | `KiCadClient::get_board_editor_appearance_settings_raw`, `KiCadClient::get_board_editor_appearance_settings` | | `SetBoardEditorAppearanceSettings` | Implemented | `KiCadClient::set_board_editor_appearance_settings` | -| `InteractiveMoveItems` | Not yet | - | +| `InteractiveMoveItems` | Implemented | `KiCadClient::interactive_move_items_raw`, `KiCadClient::interactive_move_items` | ### Schematic editor diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index b9b7943..ab0fe02 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -364,6 +364,12 @@ Refill all zones: cargo run --bin kicad-ipc-cli -- refill-zones ``` +Start interactive move tool for one or more item IDs: + +```bash +cargo run --bin kicad-ipc-cli -- interactive-move --id --id +``` + Show typed netclass map: ```bash diff --git a/src/client.rs b/src/client.rs index a4e2480..10d7a42 100644 --- a/src/client.rs +++ b/src/client.rs @@ -65,6 +65,7 @@ const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = "kiapi.board.commands.GetBoardEditorAppearanceSettings"; const CMD_SET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = "kiapi.board.commands.SetBoardEditorAppearanceSettings"; +const CMD_INTERACTIVE_MOVE_ITEMS: &str = "kiapi.board.commands.InteractiveMoveItems"; const CMD_GET_ITEMS_BY_NET: &str = "kiapi.board.commands.GetItemsByNet"; const CMD_GET_ITEMS_BY_NET_CLASS: &str = "kiapi.board.commands.GetItemsByNetClass"; const CMD_GET_NETCLASS_FOR_NETS: &str = "kiapi.board.commands.GetNetClassForNets"; @@ -1612,6 +1613,35 @@ impl KiCadClient { self.get_board_editor_appearance_settings().await } + pub async fn interactive_move_items_raw( + &self, + item_ids: Vec, + ) -> Result { + if item_ids.is_empty() { + return Err(KiCadError::Config { + reason: "interactive_move_items_raw requires at least one item id".to_string(), + }); + } + + let command = board_commands::InteractiveMoveItems { + board: Some(self.current_board_document_proto().await?), + items: item_ids + .into_iter() + .map(|value| common_types::Kiid { value }) + .collect(), + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_INTERACTIVE_MOVE_ITEMS)) + .await?; + response_payload_as_any(response, RES_PROTOBUF_EMPTY) + } + + pub async fn interactive_move_items(&self, item_ids: Vec) -> Result<(), KiCadError> { + let _ = self.interactive_move_items_raw(item_ids).await?; + Ok(()) + } + pub async fn get_title_block_info(&self) -> Result { let command = common_commands::GetTitleBlockInfo { 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 2821748..942b6cd 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -178,6 +178,9 @@ enum Command { RefillZones { zone_ids: Vec, }, + InteractiveMoveItems { + item_ids: Vec, + }, NetClass, BoardReadReport { output: PathBuf, @@ -874,6 +877,10 @@ async fn run() -> Result<(), KiCadError> { client.refill_zones(zone_ids).await?; println!("refill_zones_dispatched=ok"); } + Command::InteractiveMoveItems { item_ids } => { + client.interactive_move_items(item_ids.clone()).await?; + println!("interactive_move_item_count={}", item_ids.len()); + } Command::NetClass => { let nets = client.get_nets().await?; let netclasses = client.get_netclass_for_nets(nets).await?; @@ -1938,6 +1945,28 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE } Command::RefillZones { zone_ids } } + "interactive-move" => { + let mut item_ids = Vec::new(); + let mut i = 1; + while i < args.len() { + if args[i] == "--id" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for interactive-move --id".to_string(), + })?; + item_ids.push(value.clone()); + i += 2; + continue; + } + i += 1; + } + if item_ids.is_empty() { + return Err(KiCadError::Config { + reason: "interactive-move requires one or more `--id ` arguments" + .to_string(), + }); + } + Command::InteractiveMoveItems { item_ids } + } "netclass" => Command::NetClass, "proto-coverage-board-read" => Command::ProtoCoverageBoardRead, "board-read-report" => { @@ -2053,7 +2082,110 @@ 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 set-net-classes [--merge-mode ]\n Write current netclass set back with selected merge mode\n text-variables List text variables for current board document\n set-text-variables [--merge-mode ] [--var ...]\n Set 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 update-stackup Round-trip current stackup through UpdateBoardStackup\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 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 revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\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" + r#"kicad-ipc-cli + +USAGE: + cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--client-name NAME] [--timeout-ms N] [command options] + +COMMANDS: + ping Check IPC connectivity + version Fetch KiCad version + kicad-binary-path [--binary-name ] + Resolve absolute path for a KiCad binary (default: kicad-cli) + plugin-settings-path [--identifier ] + Resolve writeable plugin settings directory (default: kicad-ipc-rust) + open-docs [--type ] List open docs (default type: pcb) + project-path Get current project path from open PCB docs + board-open Exit non-zero if no PCB doc is open + net-classes List project netclass definitions + set-net-classes [--merge-mode ] + Write current netclass set back with selected merge mode + text-variables List text variables for current board document + set-text-variables [--merge-mode ] [--var ...] + Set text variables for current board document + expand-text-variables Expand variables in provided text values + Options: --text (repeatable) + text-extents Measure text bounding box + Options: --text + text-as-shapes Convert text to rendered shapes + Options: --text (repeatable) + nets List board nets (requires one open PCB) + netlist-pads Emit pad-level netlist data (with footprint context) + items-by-id --id ... Show parsed details for specific item IDs + item-bbox --id ... Show bounding boxes for item IDs + hit-test --id --x-nm --y-nm [--tolerance-nm ] + Hit-test one item at a point + types-pcb List PCB KiCad object type IDs from proto enum + items-raw --type-id ... Dump raw Any payloads for requested item type IDs + items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types + pad-shape-polygon --pad-id ... --layer-id [--debug] + Dump pad polygons on a target layer + padstack-presence --item-id ... --layer-id ... [--debug] + Check padstack shape presence matrix across layers + title-block Show title block fields + board-as-string Dump board as KiCad s-expression text + selection-as-string Dump current selection as KiCad s-expression text + stackup Show typed board stackup + update-stackup Round-trip current stackup through UpdateBoardStackup + graphics-defaults Show typed graphics defaults + appearance Show typed editor appearance settings + set-appearance --inactive-layer-display + --net-color-display + --board-flip + --ratsnest-display + Set editor appearance settings + inject-drc-error --severity --message [--x-nm --y-nm ] [--item-id ...] + Inject a DRC marker (severity: warning|error|exclusion|ignore|info|action|debug|undefined) + refill-zones [--zone-id ...] + Refill all zones or a provided subset + interactive-move --id ... + Start interactive move tool for item IDs + netclass Show typed netclass map for current board nets + proto-coverage-board-read Print board-read command coverage vs proto + board-read-report [--out P] Write markdown board reconstruction report + enabled-layers List enabled board layers + set-enabled-layers --copper-layer-count [--layer-id ...] + Set enabled board layer set + active-layer Show active board layer + set-active-layer --layer-id + Set active board layer + visible-layers Show currently visible board layers + set-visible-layers --layer-id ... + Set visible board layers + board-origin [--type ] Show board origin (`grid` default, or `drill`) + set-board-origin --type --x-nm --y-nm + Set board origin (`grid` or `drill`) + refresh-editor [--frame ] Refresh a specific editor frame (default: pcb) + begin-commit Start staged commit and print commit ID + end-commit --id [--action ] [--message ] + End staged commit with commit/drop action + save-doc Save current board document + save-copy --path [--overwrite] [--include-project] + Save current board document to a new location + revert-doc Revert current board document from disk + run-action --action Run a raw KiCad tool action + create-items --item = ... [--container-id ] + Create raw Any payload items in current board document + update-items --item = ... + Update raw Any payload items in current board document + delete-items --id ... + Delete item IDs from current board document + parse-create-items --contents + Parse s-expression and create resulting items + add-to-selection --id ... + Add items to current selection + remove-from-selection --id ... + Remove items from current selection + clear-selection Clear current item selection + selection-summary Show current selection item type counts + selection-details Show parsed details for selected items + selection-raw Show raw Any payload bytes for selected items + smoke ping + version + board-open summary + help Show help + +TYPES: + schematic | symbol | pcb | footprint | drawing-sheet | project +"# ); } @@ -3123,4 +3255,23 @@ mod tests { .expect("update-stackup should parse"); assert!(matches!(command, Command::UpdateStackup)); } + + #[test] + fn parse_args_parses_interactive_move_items() { + let (_, command) = parse_args_from(vec![ + "interactive-move".to_string(), + "--id".to_string(), + "item-1".to_string(), + "--id".to_string(), + "item-2".to_string(), + ]) + .expect("interactive-move args should parse"); + + match command { + Command::InteractiveMoveItems { item_ids } => { + assert_eq!(item_ids, vec!["item-1".to_string(), "item-2".to_string()]); + } + other => panic!("unexpected command variant: {other:?}"), + } + } }