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![