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" ); }