fix(api): strengthen crate-level and high-impact API docs (#13)

* docs(api): strengthen crate and high-impact rustdoc

* fix(readme): update stale crate version references
This commit is contained in:
Milind Sharma 2026-02-28 12:18:46 +08:00 committed by GitHub
parent 2aada9a247
commit 22f168017a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 257 additions and 5 deletions

View File

@ -8,7 +8,7 @@ Maintainer workflow: see `CONTRIBUTIONS.md`.
## Status ## Status
Alpha. `v0.1.1` released. Alpha. `v0.3.0` released.
- Async API (default): implemented and usable. - Async API (default): implemented and usable.
- Sync/blocking wrapper API (`feature = "blocking"`): implemented with full async parity. - Sync/blocking wrapper API (`feature = "blocking"`): implemented with full async parity.
@ -23,7 +23,7 @@ Alpha. `v0.1.1` released.
```toml ```toml
[dependencies] [dependencies]
kicad-ipc-rs = "0.1.1" kicad-ipc-rs = "0.3.0"
tokio = { version = "1", features = ["macros", "rt"] } tokio = { version = "1", features = ["macros", "rt"] }
``` ```
@ -48,7 +48,7 @@ Enable the `blocking` feature and use `KiCadClientBlocking` for synchronous call
```toml ```toml
[dependencies] [dependencies]
kicad-ipc-rs = { version = "0.1.1", features = ["blocking"] } kicad-ipc-rs = { version = "0.3.0", features = ["blocking"] }
``` ```
```rust ```rust
@ -226,3 +226,9 @@ Legend:
- Expand runtime + integration testing coverage. - Expand runtime + integration testing coverage.
- Set up CI to run checks/tests on commits and PRs. - Set up CI to run checks/tests on commits and PRs.
- Continue API hardening/docs/examples for stable `1.0` path. - Continue API hardening/docs/examples for stable `1.0` path.
## Future Work: Public Surface + Docs
- This crate is still in alpha, and some lower-level modules currently remain public for advanced/debugging workflows.
- `#![warn(missing_docs)]` is enabled; high-impact user APIs are documented first, and remaining warnings are being burned down incrementally.
- As usage data accumulates, internal surfaces (`commands`, `envelope`, transport/proto-adjacent helpers) may be narrowed or made `pub(crate)` where possible without breaking user workflows.

View File

@ -212,6 +212,10 @@ const PCB_OBJECT_TYPES: [PcbObjectTypeCode; 18] = [
]; ];
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
/// Async IPC client for communicating with a running KiCad instance.
///
/// Create with [`KiCadClient::connect`] for defaults or [`KiCadClient::builder`]
/// to override socket path, timeout, token, or client name.
pub struct KiCadClient { pub struct KiCadClient {
inner: Arc<ClientInner>, inner: Arc<ClientInner>,
} }
@ -234,11 +238,19 @@ struct ClientConfig {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
/// Builder for [`KiCadClient`].
///
/// Defaults:
/// - timeout: `3s`
/// - socket path: `KICAD_API_SOCKET` env var, then platform default
/// - token: `KICAD_API_TOKEN` env var, then empty
/// - client name: autogenerated
pub struct ClientBuilder { pub struct ClientBuilder {
config: ClientConfig, config: ClientConfig,
} }
impl ClientBuilder { impl ClientBuilder {
/// Creates a builder with sensible defaults for local KiCad IPC usage.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
config: ClientConfig { config: ClientConfig {
@ -250,26 +262,39 @@ impl ClientBuilder {
} }
} }
/// Sets per-request timeout used by the IPC transport.
pub fn timeout(mut self, timeout: Duration) -> Self { pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout; self.config.timeout = timeout;
self self
} }
/// Sets explicit KiCad IPC socket URI/path.
///
/// If unset, the builder resolves from environment/defaults.
pub fn socket_path(mut self, socket_path: impl Into<String>) -> Self { pub fn socket_path(mut self, socket_path: impl Into<String>) -> Self {
self.config.socket_uri = Some(socket_path.into()); self.config.socket_uri = Some(socket_path.into());
self self
} }
/// Sets the IPC authentication token.
///
/// If unset, the builder uses `KICAD_API_TOKEN` when present.
pub fn token(mut self, token: impl Into<String>) -> Self { pub fn token(mut self, token: impl Into<String>) -> Self {
self.config.token = Some(token.into()); self.config.token = Some(token.into());
self self
} }
/// Sets the client name reported to KiCad.
pub fn client_name(mut self, client_name: impl Into<String>) -> Self { pub fn client_name(mut self, client_name: impl Into<String>) -> Self {
self.config.client_name = Some(client_name.into()); self.config.client_name = Some(client_name.into());
self self
} }
/// Connects to KiCad IPC with the configured options.
///
/// # Errors
/// Returns [`KiCadError`] when socket discovery, connection, or transport
/// initialization fails.
pub async fn connect(self) -> Result<KiCadClient, KiCadError> { pub async fn connect(self) -> Result<KiCadClient, KiCadError> {
let socket_uri = resolve_socket_uri(self.config.socket_uri.as_deref()); let socket_uri = resolve_socket_uri(self.config.socket_uri.as_deref());
if is_missing_ipc_socket(&socket_uri) { if is_missing_ipc_socket(&socket_uri) {
@ -306,28 +331,34 @@ impl Default for ClientBuilder {
} }
impl KiCadClient { impl KiCadClient {
/// Returns a configurable builder for creating a [`KiCadClient`].
pub fn builder() -> ClientBuilder { pub fn builder() -> ClientBuilder {
ClientBuilder::new() ClientBuilder::new()
} }
/// Connects with default builder settings.
pub async fn connect() -> Result<Self, KiCadError> { pub async fn connect() -> Result<Self, KiCadError> {
ClientBuilder::new().connect().await ClientBuilder::new().connect().await
} }
/// Returns configured per-request timeout.
pub fn timeout(&self) -> Duration { pub fn timeout(&self) -> Duration {
self.inner.timeout self.inner.timeout
} }
/// Returns resolved KiCad IPC socket URI/path.
pub fn socket_uri(&self) -> &str { pub fn socket_uri(&self) -> &str {
&self.inner.socket_uri &self.inner.socket_uri
} }
/// Sends a health-check request to KiCad.
pub async fn ping(&self) -> Result<(), KiCadError> { pub async fn ping(&self) -> Result<(), KiCadError> {
let command = envelope::pack_any(&common_commands::Ping {}, CMD_PING); let command = envelope::pack_any(&common_commands::Ping {}, CMD_PING);
self.send_command(command).await?; self.send_command(command).await?;
Ok(()) Ok(())
} }
/// Requests KiCad to refresh a specific editor frame.
pub async fn refresh_editor(&self, frame: EditorFrameType) -> Result<(), KiCadError> { pub async fn refresh_editor(&self, frame: EditorFrameType) -> Result<(), KiCadError> {
let command = envelope::pack_any( let command = envelope::pack_any(
&common_commands::RefreshEditor { &common_commands::RefreshEditor {
@ -352,6 +383,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_RUN_ACTION_RESPONSE) response_payload_as_any(response, RES_RUN_ACTION_RESPONSE)
} }
/// Runs a KiCad action by action name and returns mapped status.
pub async fn run_action( pub async fn run_action(
&self, &self,
action: impl Into<String>, action: impl Into<String>,
@ -362,6 +394,7 @@ impl KiCadClient {
Ok(map_run_action_status(response.status)) Ok(map_run_action_status(response.status))
} }
/// Queries KiCad version info for the connected instance.
pub async fn get_version(&self) -> Result<VersionInfo, KiCadError> { pub async fn get_version(&self) -> Result<VersionInfo, KiCadError> {
let command = envelope::pack_any(&common_commands::GetVersion {}, CMD_GET_VERSION); let command = envelope::pack_any(&common_commands::GetVersion {}, CMD_GET_VERSION);
let response = self.send_command(command).await?; let response = self.send_command(command).await?;
@ -394,6 +427,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_PATH_RESPONSE) response_payload_as_any(response, RES_PATH_RESPONSE)
} }
/// Resolves a KiCad binary path by binary name.
pub async fn get_kicad_binary_path( pub async fn get_kicad_binary_path(
&self, &self,
binary_name: impl Into<String>, binary_name: impl Into<String>,
@ -416,6 +450,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_STRING_RESPONSE) response_payload_as_any(response, RES_STRING_RESPONSE)
} }
/// Resolves plugin settings path for a plugin identifier.
pub async fn get_plugin_settings_path( pub async fn get_plugin_settings_path(
&self, &self,
identifier: impl Into<String>, identifier: impl Into<String>,
@ -425,6 +460,7 @@ impl KiCadClient {
Ok(response.response) Ok(response.response)
} }
/// Lists open KiCad documents of the requested type.
pub async fn get_open_documents( pub async fn get_open_documents(
&self, &self,
document_type: DocumentType, document_type: DocumentType,
@ -455,6 +491,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_NET_CLASSES_RESPONSE) response_payload_as_any(response, RES_NET_CLASSES_RESPONSE)
} }
/// Reads project net classes from the current project context.
pub async fn get_net_classes(&self) -> Result<Vec<NetClassInfo>, KiCadError> { pub async fn get_net_classes(&self) -> Result<Vec<NetClassInfo>, KiCadError> {
let payload = self.get_net_classes_raw().await?; let payload = self.get_net_classes_raw().await?;
let response: common_commands::NetClassesResponse = let response: common_commands::NetClassesResponse =
@ -487,6 +524,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_PROTOBUF_EMPTY) response_payload_as_any(response, RES_PROTOBUF_EMPTY)
} }
/// Replaces or merges project net classes, then returns current classes.
pub async fn set_net_classes( pub async fn set_net_classes(
&self, &self,
net_classes: Vec<NetClassInfo>, net_classes: Vec<NetClassInfo>,
@ -506,6 +544,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_TEXT_VARIABLES) response_payload_as_any(response, RES_TEXT_VARIABLES)
} }
/// Reads project text variables.
pub async fn get_text_variables(&self) -> Result<BTreeMap<String, String>, KiCadError> { pub async fn get_text_variables(&self) -> Result<BTreeMap<String, String>, KiCadError> {
let payload = self.get_text_variables_raw().await?; let payload = self.get_text_variables_raw().await?;
let response: common_project::TextVariables = decode_any(&payload, RES_TEXT_VARIABLES)?; let response: common_project::TextVariables = decode_any(&payload, RES_TEXT_VARIABLES)?;
@ -530,6 +569,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_PROTOBUF_EMPTY) response_payload_as_any(response, RES_PROTOBUF_EMPTY)
} }
/// Replaces or merges project text variables, then returns current values.
pub async fn set_text_variables( pub async fn set_text_variables(
&self, &self,
variables: BTreeMap<String, String>, variables: BTreeMap<String, String>,
@ -553,6 +593,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_EXPAND_TEXT_VARIABLES_RESPONSE) response_payload_as_any(response, RES_EXPAND_TEXT_VARIABLES_RESPONSE)
} }
/// Expands `${VAR}`-style text variables using current project context.
pub async fn expand_text_variables( pub async fn expand_text_variables(
&self, &self,
text: Vec<String>, text: Vec<String>,
@ -576,6 +617,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_BOX2) response_payload_as_any(response, RES_BOX2)
} }
/// Computes rendered text extents in nanometer units.
pub async fn get_text_extents(&self, text: TextSpec) -> Result<TextExtents, KiCadError> { pub async fn get_text_extents(&self, text: TextSpec) -> Result<TextExtents, KiCadError> {
let payload = self.get_text_extents_raw(text).await?; let payload = self.get_text_extents_raw(text).await?;
let response: common_types::Box2 = decode_any(&payload, RES_BOX2)?; let response: common_types::Box2 = decode_any(&payload, RES_BOX2)?;
@ -609,6 +651,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_GET_TEXT_AS_SHAPES_RESPONSE) response_payload_as_any(response, RES_GET_TEXT_AS_SHAPES_RESPONSE)
} }
/// Converts text/textbox specs into drawable shape geometry.
pub async fn get_text_as_shapes( pub async fn get_text_as_shapes(
&self, &self,
text: Vec<TextObjectSpec>, text: Vec<TextObjectSpec>,
@ -624,11 +667,15 @@ impl KiCadClient {
.collect() .collect()
} }
/// Returns the current PCB project's path.
///
/// Fails if no PCB is open or if multiple project paths are present.
pub async fn get_current_project_path(&self) -> Result<PathBuf, KiCadError> { pub async fn get_current_project_path(&self) -> Result<PathBuf, KiCadError> {
let docs = self.get_open_documents(DocumentType::Pcb).await?; let docs = self.get_open_documents(DocumentType::Pcb).await?;
select_single_project_path(&docs) select_single_project_path(&docs)
} }
/// Returns `true` when at least one PCB document is open in KiCad.
pub async fn has_open_board(&self) -> Result<bool, KiCadError> { pub async fn has_open_board(&self) -> Result<bool, KiCadError> {
let docs = self.get_open_documents(DocumentType::Pcb).await?; let docs = self.get_open_documents(DocumentType::Pcb).await?;
Ok(!docs.is_empty()) Ok(!docs.is_empty())
@ -642,6 +689,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_BEGIN_COMMIT_RESPONSE) response_payload_as_any(response, RES_BEGIN_COMMIT_RESPONSE)
} }
/// Starts a KiCad commit session used for grouped board edits.
pub async fn begin_commit(&self) -> Result<CommitSession, KiCadError> { pub async fn begin_commit(&self) -> Result<CommitSession, KiCadError> {
let payload = self.begin_commit_raw().await?; let payload = self.begin_commit_raw().await?;
let response: common_commands::BeginCommitResponse = let response: common_commands::BeginCommitResponse =
@ -672,6 +720,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_END_COMMIT_RESPONSE) response_payload_as_any(response, RES_END_COMMIT_RESPONSE)
} }
/// Finalizes a commit session, either committing or dropping staged changes.
pub async fn end_commit( pub async fn end_commit(
&self, &self,
session: CommitSession, session: CommitSession,
@ -699,6 +748,9 @@ impl KiCadClient {
response_payload_as_any(response, RES_CREATE_ITEMS_RESPONSE) response_payload_as_any(response, RES_CREATE_ITEMS_RESPONSE)
} }
/// Creates items in the active PCB document.
///
/// Returns created items as raw protobuf `Any` payloads.
pub async fn create_items( pub async fn create_items(
&self, &self,
items: Vec<prost_types::Any>, items: Vec<prost_types::Any>,
@ -736,6 +788,9 @@ impl KiCadClient {
response_payload_as_any(response, RES_UPDATE_ITEMS_RESPONSE) response_payload_as_any(response, RES_UPDATE_ITEMS_RESPONSE)
} }
/// Updates existing items in the active PCB document.
///
/// Returns updated items as raw protobuf `Any` payloads.
pub async fn update_items( pub async fn update_items(
&self, &self,
items: Vec<prost_types::Any>, items: Vec<prost_types::Any>,
@ -775,6 +830,9 @@ impl KiCadClient {
response_payload_as_any(response, RES_DELETE_ITEMS_RESPONSE) response_payload_as_any(response, RES_DELETE_ITEMS_RESPONSE)
} }
/// Deletes items by id from the active PCB document.
///
/// Returns ids of items deleted by KiCad.
pub async fn delete_items(&self, item_ids: Vec<String>) -> Result<Vec<String>, KiCadError> { pub async fn delete_items(&self, item_ids: Vec<String>) -> Result<Vec<String>, KiCadError> {
let payload = self.delete_items_raw(item_ids).await?; let payload = self.delete_items_raw(item_ids).await?;
let response: common_commands::DeleteItemsResponse = let response: common_commands::DeleteItemsResponse =
@ -836,6 +894,7 @@ impl KiCadClient {
.collect() .collect()
} }
/// Returns nets from the active PCB document.
pub async fn get_nets(&self) -> Result<Vec<BoardNet>, KiCadError> { pub async fn get_nets(&self) -> Result<Vec<BoardNet>, KiCadError> {
let board = self.current_board_document_proto().await?; let board = self.current_board_document_proto().await?;
let command = board_commands::GetNets { let command = board_commands::GetNets {
@ -981,6 +1040,7 @@ impl KiCadClient {
Ok(()) Ok(())
} }
/// Returns a compact summary of the current PCB selection.
pub async fn get_selection_summary(&self) -> Result<SelectionSummary, KiCadError> { pub async fn get_selection_summary(&self) -> Result<SelectionSummary, KiCadError> {
let document = self.current_board_document_proto().await?; let document = self.current_board_document_proto().await?;
let command = common_commands::GetSelection { let command = common_commands::GetSelection {
@ -1023,6 +1083,7 @@ impl KiCadClient {
summarize_item_details(items) summarize_item_details(items)
} }
/// Returns the current selection as decoded typed PCB items.
pub async fn get_selection(&self) -> Result<Vec<PcbItem>, KiCadError> { pub async fn get_selection(&self) -> Result<Vec<PcbItem>, KiCadError> {
let items = self.get_selection_raw().await?; let items = self.get_selection_raw().await?;
decode_pcb_items(items) decode_pcb_items(items)
@ -1154,10 +1215,12 @@ impl KiCadClient {
.collect()) .collect())
} }
/// Returns known KiCad PCB object type codes handled by this crate.
pub fn pcb_object_type_codes() -> &'static [PcbObjectTypeCode] { pub fn pcb_object_type_codes() -> &'static [PcbObjectTypeCode] {
&PCB_OBJECT_TYPES &PCB_OBJECT_TYPES
} }
/// Resolves a human-readable object type name from a KiCad object type code.
pub fn pcb_object_type_name(type_code: i32) -> Option<&'static str> { pub fn pcb_object_type_name(type_code: i32) -> Option<&'static str> {
PCB_OBJECT_TYPES PCB_OBJECT_TYPES
.iter() .iter()
@ -1165,6 +1228,7 @@ impl KiCadClient {
.map(|entry| entry.name) .map(|entry| entry.name)
} }
/// Formats a raw protobuf PCB item payload for debugging/logging.
pub fn debug_any_item(item: &prost_types::Any) -> Result<String, KiCadError> { pub fn debug_any_item(item: &prost_types::Any) -> Result<String, KiCadError> {
any_to_pretty_debug(item) any_to_pretty_debug(item)
} }
@ -1184,6 +1248,7 @@ impl KiCadClient {
summarize_item_details(items) summarize_item_details(items)
} }
/// Fetches and decodes items by KiCad object type codes.
pub async fn get_items_by_type_codes( pub async fn get_items_by_type_codes(
&self, &self,
type_codes: Vec<i32>, type_codes: Vec<i32>,
@ -1216,6 +1281,7 @@ impl KiCadClient {
Ok(rows) Ok(rows)
} }
/// Fetches all known PCB item kinds and decodes each bucket.
pub async fn get_all_pcb_items( pub async fn get_all_pcb_items(
&self, &self,
) -> Result<Vec<(PcbObjectTypeCode, Vec<PcbItem>)>, KiCadError> { ) -> Result<Vec<(PcbObjectTypeCode, Vec<PcbItem>)>, KiCadError> {
@ -1537,6 +1603,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_BOARD_STACKUP_RESPONSE) response_payload_as_any(response, RES_BOARD_STACKUP_RESPONSE)
} }
/// Reads board stackup from the active PCB document.
pub async fn get_board_stackup(&self) -> Result<BoardStackup, KiCadError> { pub async fn get_board_stackup(&self) -> Result<BoardStackup, KiCadError> {
let payload = self.get_board_stackup_raw().await?; let payload = self.get_board_stackup_raw().await?;
let response: board_commands::BoardStackupResponse = let response: board_commands::BoardStackupResponse =
@ -1560,6 +1627,7 @@ impl KiCadClient {
response_payload_as_any(response, RES_BOARD_STACKUP_RESPONSE) response_payload_as_any(response, RES_BOARD_STACKUP_RESPONSE)
} }
/// Writes a board stackup and returns KiCad's resulting stackup state.
pub async fn update_board_stackup( pub async fn update_board_stackup(
&self, &self,
stackup: BoardStackup, stackup: BoardStackup,
@ -1660,6 +1728,7 @@ impl KiCadClient {
Ok(()) Ok(())
} }
/// Reads title block metadata from the active PCB document.
pub async fn get_title_block_info(&self) -> Result<TitleBlockInfo, KiCadError> { pub async fn get_title_block_info(&self) -> Result<TitleBlockInfo, KiCadError> {
let command = common_commands::GetTitleBlockInfo { let command = common_commands::GetTitleBlockInfo {
document: Some(self.current_board_document_proto().await?), document: Some(self.current_board_document_proto().await?),
@ -1760,6 +1829,7 @@ impl KiCadClient {
Ok(()) Ok(())
} }
/// Serializes the active PCB document to KiCad's string format.
pub async fn get_board_as_string(&self) -> Result<String, KiCadError> { pub async fn get_board_as_string(&self) -> Result<String, KiCadError> {
let command = common_commands::SaveDocumentToString { let command = common_commands::SaveDocumentToString {
document: Some(self.current_board_document_proto().await?), document: Some(self.current_board_document_proto().await?),
@ -1773,6 +1843,7 @@ impl KiCadClient {
Ok(payload.contents) Ok(payload.contents)
} }
/// Serializes current selection to KiCad's string format.
pub async fn get_selection_as_string(&self) -> Result<String, KiCadError> { pub async fn get_selection_as_string(&self) -> Result<String, KiCadError> {
let command = common_commands::SaveSelectionToString {}; let command = common_commands::SaveSelectionToString {};
@ -1819,6 +1890,7 @@ impl KiCadClient {
summarize_item_details(items) summarize_item_details(items)
} }
/// Fetches and decodes items by KiCad item id.
pub async fn get_items_by_id(&self, item_ids: Vec<String>) -> Result<Vec<PcbItem>, KiCadError> { pub async fn get_items_by_id(&self, item_ids: Vec<String>) -> Result<Vec<PcbItem>, KiCadError> {
let items = self.get_items_by_id_raw(item_ids).await?; let items = self.get_items_by_id_raw(item_ids).await?;
decode_pcb_items(items) decode_pcb_items(items)

View File

@ -3,67 +3,88 @@ use std::time::Duration;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
/// Error type returned by `kicad-ipc-rs` operations.
pub enum KiCadError { pub enum KiCadError {
/// Invalid local configuration or user input before IPC dispatch.
#[error("invalid configuration: {reason}")] #[error("invalid configuration: {reason}")]
Config { reason: String }, Config { reason: String },
/// KiCad IPC socket could not be found at connect time.
#[error("KiCad IPC socket not available at `{socket_uri}`. Open KiCad and open a project/board first.")] #[error("KiCad IPC socket not available at `{socket_uri}`. Open KiCad and open a project/board first.")]
SocketUnavailable { socket_uri: String }, SocketUnavailable { socket_uri: String },
/// IPC connection failed.
#[error("connection failed for `{socket_uri}`: {reason}")] #[error("connection failed for `{socket_uri}`: {reason}")]
Connection { socket_uri: String, reason: String }, Connection { socket_uri: String, reason: String },
/// Transport send path failed.
#[error("transport send failed: {reason}")] #[error("transport send failed: {reason}")]
TransportSend { reason: String }, TransportSend { reason: String },
/// Transport receive path failed.
#[error("transport receive failed: {reason}")] #[error("transport receive failed: {reason}")]
TransportReceive { reason: String }, TransportReceive { reason: String },
/// Background transport task has stopped.
#[error("transport task is unavailable")] #[error("transport task is unavailable")]
TransportClosed, TransportClosed,
/// Request exceeded configured timeout.
#[error("request timed out after {timeout:?}")] #[error("request timed out after {timeout:?}")]
Timeout { timeout: Duration }, Timeout { timeout: Duration },
/// KiCad returned a non-success API status.
#[error("API status error `{code}`: {message}")] #[error("API status error `{code}`: {message}")]
ApiStatus { code: String, message: String }, ApiStatus { code: String, message: String },
/// KiCad returned a non-success per-item status.
#[error("item request status error `{code}`")] #[error("item request status error `{code}`")]
ItemStatus { code: String }, ItemStatus { code: String },
/// Response payload content was malformed or inconsistent.
#[error("invalid API response: {reason}")] #[error("invalid API response: {reason}")]
InvalidResponse { reason: String }, InvalidResponse { reason: String },
/// Response payload was missing when required.
#[error("API response missing payload for `{expected_type_url}`")] #[error("API response missing payload for `{expected_type_url}`")]
MissingPayload { expected_type_url: String }, MissingPayload { expected_type_url: String },
/// Response payload type did not match expected protobuf type URL.
#[error("unexpected payload type; expected `{expected_type_url}`, got `{actual_type_url}`")] #[error("unexpected payload type; expected `{expected_type_url}`, got `{actual_type_url}`")]
UnexpectedPayloadType { UnexpectedPayloadType {
expected_type_url: String, expected_type_url: String,
actual_type_url: String, actual_type_url: String,
}, },
/// Protobuf encoding failed.
#[error("protobuf encode failed: {0}")] #[error("protobuf encode failed: {0}")]
ProtobufEncode(String), ProtobufEncode(String),
/// Protobuf decoding failed.
#[error("protobuf decode failed: {0}")] #[error("protobuf decode failed: {0}")]
ProtobufDecode(String), ProtobufDecode(String),
/// Blocking runtime worker join failed.
#[error("runtime task join failed: {0}")] #[error("runtime task join failed: {0}")]
RuntimeJoin(String), RuntimeJoin(String),
/// Blocking runtime worker is unavailable.
#[error("blocking runtime is unavailable")] #[error("blocking runtime is unavailable")]
BlockingRuntimeClosed, BlockingRuntimeClosed,
/// Internal mutex poisoning detected.
#[error("mutex poisoned")] #[error("mutex poisoned")]
InternalPoisoned, InternalPoisoned,
/// Operation requires an open PCB document.
#[error("no open PCB document found; open a board in KiCad first")] #[error("no open PCB document found; open a board in KiCad first")]
BoardNotOpen, BoardNotOpen,
/// Multiple project paths were detected where a single path was required.
#[error("multiple project paths found across open PCB docs: {paths:?}")] #[error("multiple project paths found across open PCB docs: {paths:?}")]
AmbiguousProjectPath { paths: Vec<String> }, AmbiguousProjectPath { paths: Vec<String> },
/// Multiple open PCB docs prevent choosing an implicit board context.
#[error("multiple PCB documents are open; unable to choose one board context: {boards:?}")] #[error("multiple PCB documents are open; unable to choose one board context: {boards:?}")]
AmbiguousBoardSelection { boards: Vec<String> }, AmbiguousBoardSelection { boards: Vec<String> },
} }

View File

@ -1,20 +1,82 @@
//! Async-first Rust bindings for the KiCad IPC API. //! # KiCad IPC RS
//! //!
//! Layering: //! **Async-first, pure-Rust IPC bindings for KiCad's official API.**
//! Production-focused Rust API surface, typed models, and a blocking wrapper for sync callers.
//!
//! ## Why this crate?
//!
//! | Capability | `kicad-ipc-rs` | Official Python bindings (`kicad-python`) | Official Rust bindings (`kicad-rs`) |
//! | --- | --- | --- | --- |
//! | Rust-native client API | ✅ Yes | ❌ Python package | ⚠️ Development preview |
//! | Async-first API design | ✅ `KiCadClient` | ⚠️ App-managed event-loop model | ⚠️ Development preview |
//! | Blocking support for sync apps | ✅ `feature = "blocking"` | ✅ Native Python sync usage | ⚠️ Development preview |
//! | Wrapped KiCad command coverage (current proto snapshot) | ✅ 56/56 command wrappers | Unknown | Unknown |
//! | Maintainer focus | ✅ This crate is actively maintained for Rust users | ✅ Official KiCad Python package | ⚠️ Preview status |
//!
//! Evidence and references:
//! - `kicad-python` package: <https://gitlab.com/kicad/code/kicad-python>
//! - `kicad-rs` package (states "development preview with no docs yet"): <https://gitlab.com/kicad/code/kicad-rs>
//! - Coverage matrix and runtime notes: <https://github.com/Milind220/kicad-ipc-rs#kicad-v10-rc11-api-completion-matrix>
//!
//! ## Quickstart (async)
//!
//! ```no_run
//! use kicad_ipc_rs::KiCadClient;
//!
//! #[tokio::main(flavor = "current_thread")]
//! async fn main() -> Result<(), kicad_ipc_rs::KiCadError> {
//! let client = KiCadClient::connect().await?;
//! client.ping().await?;
//! let version = client.get_version().await?;
//! println!("KiCad: {}", version.full_version);
//! Ok(())
//! }
//! ```
//!
//! ## Quickstart (blocking)
//!
//! ```no_run
//! # #[cfg(feature = "blocking")]
//! # fn run() -> Result<(), kicad_ipc_rs::KiCadError> {
//! use kicad_ipc_rs::KiCadClientBlocking;
//! let client = KiCadClientBlocking::connect()?;
//! let version = client.get_version()?;
//! println!("KiCad: {}", version.full_version);
//! # Ok(())
//! # }
//! ```
//!
//! Architecture layers:
//! - transport //! - transport
//! - envelope //! - envelope
//! - command builders //! - command builders
//! - high-level client //! - high-level client
#![warn(missing_docs)]
/// High-level async client and request/response convenience methods.
pub mod client; pub mod client;
/// Low-level command payload builders.
///
/// This module is public for advanced integrations and debugging, but most users
/// should prefer [`crate::client::KiCadClient`] methods.
pub mod commands; pub mod commands;
/// Envelope helpers for command/response packing and unpacking.
///
/// This is primarily an advanced/internal surface.
pub mod envelope; pub mod envelope;
/// Error types returned by this crate.
pub mod error; pub mod error;
mod kicad_api_version; mod kicad_api_version;
/// Stable data models used by typed client APIs.
pub mod model; pub mod model;
/// IPC transport implementation details.
///
/// Most applications should not need to use this module directly.
pub mod transport; pub mod transport;
#[cfg(feature = "blocking")] #[cfg(feature = "blocking")]
/// Blocking wrapper over the async client.
pub mod blocking; pub mod blocking;
pub(crate) mod proto; pub(crate) mod proto;

View File

@ -1,26 +1,38 @@
use std::str::FromStr; use std::str::FromStr;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// KiCad net descriptor.
pub struct BoardNet { pub struct BoardNet {
/// Numeric net code.
pub code: i32, pub code: i32,
/// Net name.
pub name: String, pub name: String,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Board layer descriptor.
pub struct BoardLayerInfo { pub struct BoardLayerInfo {
/// KiCad layer id.
pub id: i32, pub id: i32,
/// Human-readable layer name.
pub name: String, pub name: String,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Enabled layer set for a board.
pub struct BoardEnabledLayers { pub struct BoardEnabledLayers {
/// Number of copper layers configured in the board stack.
pub copper_layer_count: u32, pub copper_layer_count: u32,
/// Enabled board layers.
pub layers: Vec<BoardLayerInfo>, pub layers: Vec<BoardLayerInfo>,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Board origin kind.
pub enum BoardOriginKind { pub enum BoardOriginKind {
/// Grid origin.
Grid, Grid,
/// Drill/place origin.
Drill, Drill,
} }
@ -48,43 +60,66 @@ impl std::fmt::Display for BoardOriginKind {
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// 2D coordinate in nanometer units.
pub struct Vector2Nm { pub struct Vector2Nm {
/// X coordinate in nm.
pub x_nm: i64, pub x_nm: i64,
/// Y coordinate in nm.
pub y_nm: i64, pub y_nm: i64,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Pad-to-net lookup row derived from footprint items.
pub struct PadNetEntry { pub struct PadNetEntry {
/// Footprint reference (e.g. `U1`) when available.
pub footprint_reference: Option<String>, pub footprint_reference: Option<String>,
/// Footprint id when available.
pub footprint_id: Option<String>, pub footprint_id: Option<String>,
/// Pad item id when available.
pub pad_id: Option<String>, pub pad_id: Option<String>,
/// Pad number/text as shown in KiCad.
pub pad_number: String, pub pad_number: String,
/// Net code when connected.
pub net_code: Option<i32>, pub net_code: Option<i32>,
/// Net name when connected.
pub net_name: Option<String>, pub net_name: Option<String>,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Arc geometry in nanometer units.
pub struct ArcStartMidEndNm { pub struct ArcStartMidEndNm {
/// Arc start point.
pub start: Vector2Nm, pub start: Vector2Nm,
/// Arc midpoint.
pub mid: Vector2Nm, pub mid: Vector2Nm,
/// Arc end point.
pub end: Vector2Nm, pub end: Vector2Nm,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Polyline node geometry.
pub enum PolyLineNodeGeometryNm { pub enum PolyLineNodeGeometryNm {
/// Straight segment point.
Point(Vector2Nm), Point(Vector2Nm),
/// Arc segment node.
Arc(ArcStartMidEndNm), Arc(ArcStartMidEndNm),
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Polyline geometry.
pub struct PolyLineNm { pub struct PolyLineNm {
/// Ordered geometry nodes.
pub nodes: Vec<PolyLineNodeGeometryNm>, pub nodes: Vec<PolyLineNodeGeometryNm>,
/// Whether last node closes back to first.
pub closed: bool, pub closed: bool,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Polygon with optional interior holes.
pub struct PolygonWithHolesNm { pub struct PolygonWithHolesNm {
/// Outer outline polygon.
pub outline: Option<PolyLineNm>, pub outline: Option<PolyLineNm>,
/// Interior holes.
pub holes: Vec<PolyLineNm>, pub holes: Vec<PolyLineNm>,
} }

View File

@ -5,21 +5,34 @@ use crate::model::board::{ColorRgba, PolygonWithHolesNm, Vector2Nm};
use crate::proto::kiapi::common::types as common_types; use crate::proto::kiapi::common::types as common_types;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// KiCad semantic version returned by `GetVersion`.
pub struct VersionInfo { pub struct VersionInfo {
/// Major version component.
pub major: u32, pub major: u32,
/// Minor version component.
pub minor: u32, pub minor: u32,
/// Patch version component.
pub patch: u32, pub patch: u32,
/// Full KiCad version string (includes prerelease/build details).
pub full_version: String, pub full_version: String,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// KiCad top-level frame/editor targets used by API commands.
pub enum EditorFrameType { pub enum EditorFrameType {
/// KiCad project manager frame.
ProjectManager, ProjectManager,
/// Schematic editor frame.
SchematicEditor, SchematicEditor,
/// PCB editor frame.
PcbEditor, PcbEditor,
/// Spice simulator frame.
SpiceSimulator, SpiceSimulator,
/// Symbol editor frame.
SymbolEditor, SymbolEditor,
/// Footprint editor frame.
FootprintEditor, FootprintEditor,
/// Drawing-sheet editor frame.
DrawingSheetEditor, DrawingSheetEditor,
} }
@ -72,12 +85,19 @@ impl FromStr for EditorFrameType {
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// KiCad document type selector used by document-scoped APIs.
pub enum DocumentType { pub enum DocumentType {
/// Schematic document.
Schematic, Schematic,
/// Symbol document.
Symbol, Symbol,
/// PCB document.
Pcb, Pcb,
/// Footprint document.
Footprint, Footprint,
/// Drawing-sheet document.
DrawingSheet, DrawingSheet,
/// Project-level document.
Project, Project,
} }
@ -141,59 +161,89 @@ impl FromStr for DocumentType {
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Minimal project information attached to open-document responses.
pub struct ProjectInfo { pub struct ProjectInfo {
/// Project display name, if provided by KiCad.
pub name: Option<String>, pub name: Option<String>,
/// Project filesystem path, if available.
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Descriptor for an open KiCad document.
pub struct DocumentSpecifier { pub struct DocumentSpecifier {
/// KiCad document type.
pub document_type: DocumentType, pub document_type: DocumentType,
/// Board filename when relevant.
pub board_filename: Option<String>, pub board_filename: Option<String>,
/// Owning project metadata.
pub project: ProjectInfo, pub project: ProjectInfo,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Count of selected items for a specific protobuf type URL.
pub struct SelectionTypeCount { pub struct SelectionTypeCount {
/// Protobuf type URL for the selected item type.
pub type_url: String, pub type_url: String,
/// Number of selected items of this type.
pub count: usize, pub count: usize,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Summary of current selection composition.
pub struct SelectionSummary { pub struct SelectionSummary {
/// Total selected item count.
pub total_items: usize, pub total_items: usize,
/// Per-type counts by protobuf type URL.
pub type_url_counts: Vec<SelectionTypeCount>, pub type_url_counts: Vec<SelectionTypeCount>,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Human/debug-friendly selection entry detail.
pub struct SelectionItemDetail { pub struct SelectionItemDetail {
/// Protobuf type URL.
pub type_url: String, pub type_url: String,
/// Decoded/debug string detail.
pub detail: String, pub detail: String,
/// Raw payload length in bytes.
pub raw_len: usize, pub raw_len: usize,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Opaque commit session identifier returned by `begin_commit`.
pub struct CommitSession { pub struct CommitSession {
/// KiCad commit session id.
pub id: String, pub id: String,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Final action to apply when ending a commit session.
pub enum CommitAction { pub enum CommitAction {
/// Persist commit changes.
Commit, Commit,
/// Discard commit changes.
Drop, Drop,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Status result returned by `run_action`.
pub enum RunActionStatus { pub enum RunActionStatus {
/// Action succeeded.
Ok, Ok,
/// Action name or payload was invalid.
Invalid, Invalid,
/// Target editor frame was not open.
FrameNotOpen, FrameNotOpen,
/// Unrecognized status code from KiCad.
Unknown(i32), Unknown(i32),
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Merge strategy for map-like update APIs.
pub enum MapMergeMode { pub enum MapMergeMode {
/// Merge provided entries into existing map.
Merge, Merge,
/// Replace existing map with provided entries.
Replace, Replace,
} }
@ -244,11 +294,17 @@ impl FromStr for CommitAction {
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Title block fields from the active document.
pub struct TitleBlockInfo { pub struct TitleBlockInfo {
/// Title block title.
pub title: String, pub title: String,
/// Title block date.
pub date: String, pub date: String,
/// Revision string.
pub revision: String, pub revision: String,
/// Company field.
pub company: String, pub company: String,
/// Non-empty comment fields.
pub comments: Vec<String>, pub comments: Vec<String>,
} }