feat(client): add RefreshEditor API and CLI command

This commit is contained in:
Milind Sharma 2026-02-20 16:54:54 +08:00
parent aa406927a5
commit 331910444d
6 changed files with 147 additions and 10 deletions

View File

@ -39,11 +39,11 @@ Legend:
| Section | Proto Commands | Implemented | Coverage | | Section | Proto Commands | Implemented | Coverage |
| --- | ---: | ---: | ---: | | --- | ---: | ---: | ---: |
| Common (base) | 6 | 4 | 67% | | Common (base) | 6 | 4 | 67% |
| Common editor/document | 23 | 11 | 48% | | Common editor/document | 23 | 12 | 52% |
| Project manager | 5 | 3 | 60% | | Project manager | 5 | 3 | 60% |
| Board editor (PCB) | 22 | 13 | 59% | | Board editor (PCB) | 22 | 13 | 59% |
| Schematic editor (dedicated proto commands) | 0 | 0 | n/a | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a |
| **Total** | **56** | **31** | **55%** | | **Total** | **56** | **32** | **57%** |
### Common (base) ### Common (base)
@ -60,7 +60,7 @@ Legend:
| KiCad Command | Status | Rust API | | 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` | | `GetOpenDocuments` | Implemented | `KiCadClient::get_open_documents`, `KiCadClient::get_current_project_path`, `KiCadClient::has_open_board` |
| `SaveDocument` | Not yet | - | | `SaveDocument` | Not yet | - |
| `SaveCopyOfDocument` | Not yet | - | | `SaveCopyOfDocument` | Not yet | - |

View File

@ -107,6 +107,14 @@ Show drill origin:
cargo run --bin kicad-ipc-cli -- board-origin --type drill 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: Start a staged commit and print commit ID:
```bash ```bash

View File

@ -18,7 +18,7 @@ use crate::model::board::{
Vector2Nm, Vector2Nm,
}; };
use crate::model::common::{ use crate::model::common::{
CommitAction, CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox,
ItemHitTestResult, PcbObjectTypeCode, ProjectInfo, SelectionItemDetail, SelectionSummary, ItemHitTestResult, PcbObjectTypeCode, ProjectInfo, SelectionItemDetail, SelectionSummary,
SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents,
TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, 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_EXPAND_TEXT_VARIABLES: &str = "kiapi.common.commands.ExpandTextVariables";
const CMD_GET_TEXT_EXTENTS: &str = "kiapi.common.commands.GetTextExtents"; const CMD_GET_TEXT_EXTENTS: &str = "kiapi.common.commands.GetTextExtents";
const CMD_GET_TEXT_AS_SHAPES: &str = "kiapi.common.commands.GetTextAsShapes"; 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_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocuments";
const CMD_GET_NETS: &str = "kiapi.board.commands.GetNets"; const CMD_GET_NETS: &str = "kiapi.board.commands.GetNets";
const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabledLayers"; const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabledLayers";
@ -293,6 +294,17 @@ impl KiCadClient {
Ok(()) 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<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?;

View File

@ -33,7 +33,7 @@ pub use crate::model::board::{
Vector2Nm, Vector2Nm,
}; };
pub use crate::model::common::{ pub use crate::model::common::{
CommitAction, CommitSession, DocumentSpecifier, DocumentType, ItemBoundingBox, CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox,
ItemHitTestResult, PcbObjectTypeCode, SelectionItemDetail, SelectionSummary, ItemHitTestResult, PcbObjectTypeCode, SelectionItemDetail, SelectionSummary,
SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents,
TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec,

View File

@ -12,6 +12,65 @@ pub struct VersionInfo {
pub full_version: String, 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<Self, Self::Err> {
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)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DocumentType { pub enum DocumentType {
Schematic, Schematic,
@ -336,7 +395,7 @@ impl std::fmt::Display for ItemHitTestResult {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::CommitAction; use super::{CommitAction, EditorFrameType};
use std::str::FromStr; use std::str::FromStr;
#[test] #[test]
@ -349,4 +408,21 @@ mod tests {
fn commit_action_rejects_unknown_values() { fn commit_action_rejects_unknown_values() {
assert!(CommitAction::from_str("rollback").is_err()); 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());
}
} }

View File

@ -6,9 +6,9 @@ use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use kicad_ipc::{ use kicad_ipc::{
BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, KiCadClient, BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, EditorFrameType,
KiCadError, PadstackPresenceState, PcbObjectTypeCode, TextObjectSpec, TextShapeGeometry, KiCadClient, KiCadError, PadstackPresenceState, PcbObjectTypeCode, TextObjectSpec,
TextSpec, Vector2Nm, TextShapeGeometry, TextSpec, Vector2Nm,
}; };
const REPORT_MAX_PAD_NET_ROWS: usize = 2_000; const REPORT_MAX_PAD_NET_ROWS: usize = 2_000;
@ -52,6 +52,9 @@ enum Command {
BoardOrigin { BoardOrigin {
kind: BoardOriginKind, kind: BoardOriginKind,
}, },
RefreshEditor {
frame: EditorFrameType,
},
BeginCommit, BeginCommit,
EndCommit { EndCommit {
id: String, id: String,
@ -320,6 +323,10 @@ async fn run() -> Result<(), KiCadError> {
kind, origin.x_nm, origin.y_nm kind, origin.x_nm, origin.y_nm
); );
} }
Command::RefreshEditor { frame } => {
client.refresh_editor(frame).await?;
println!("refresh_editor=ok frame={}", frame);
}
Command::BeginCommit => { Command::BeginCommit => {
let session = client.begin_commit().await?; let session = client.begin_commit().await?;
println!("commit_id={}", session.id); println!("commit_id={}", session.id);
@ -795,6 +802,23 @@ fn parse_args_from(mut args: Vec<String>) -> Result<(CliConfig, Command), KiCadE
} }
Command::BoardOrigin { kind } 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, "begin-commit" => Command::BeginCommit,
"end-commit" => { "end-commit" => {
let mut id = None; let mut id = None;
@ -1154,7 +1178,7 @@ fn default_config() -> CliConfig {
fn print_help() { fn print_help() {
println!( println!(
"kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--client-name NAME] [--timeout-ms N] <command> [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--type <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 <value> (repeatable)\n text-extents Measure text bounding box\n Options: --text <value>\n text-as-shapes Convert text to rendered shapes\n Options: --text <value> (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 <uuid> ... Show parsed details for specific item IDs\n item-bbox --id <uuid> ... Show bounding boxes for item IDs\n hit-test --id <uuid> --x-nm <x> --y-nm <y> [--tolerance-nm <n>]\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 <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 <uuid> ... --layer-id <i32> [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id <uuid> ... --layer-id <i32> ... [--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 <t>] Show board origin (`grid` default, or `drill`)\n begin-commit Start staged commit and print commit ID\n end-commit --id <uuid> [--action <commit|drop>] [--message <text>]\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> [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--type <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 <value> (repeatable)\n text-extents Measure text bounding box\n Options: --text <value>\n text-as-shapes Convert text to rendered shapes\n Options: --text <value> (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 <uuid> ... Show parsed details for specific item IDs\n item-bbox --id <uuid> ... Show bounding boxes for item IDs\n hit-test --id <uuid> --x-nm <x> --y-nm <y> [--tolerance-nm <n>]\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 <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 <uuid> ... --layer-id <i32> [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id <uuid> ... --layer-id <i32> ... [--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 <t>] Show board origin (`grid` default, or `drill`)\n refresh-editor [--frame <f>] Refresh a specific editor frame (default: pcb)\n begin-commit Start staged commit and print commit ID\n end-commit --id <uuid> [--action <commit|drop>] [--message <text>]\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:?}"), 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:?}"),
}
}
} }