fix: decouple project commands from GetOpenDocuments and add KIPRJMOD fallback (#16)
* fix(api): use project doc context for text vars and add KIPRJMOD fallback * fix(api): tighten project-path fallback and unhandled detection
This commit is contained in:
parent
0b078379bd
commit
9fbf833174
|
|
@ -79,7 +79,7 @@ Write current net classes back with selected merge mode:
|
|||
cargo run --features blocking --bin kicad-ipc-cli -- set-net-classes --merge-mode merge
|
||||
```
|
||||
|
||||
List text variables for current board document:
|
||||
List text variables for current project:
|
||||
|
||||
```bash
|
||||
cargo run --features blocking --bin kicad-ipc-cli -- text-variables
|
||||
|
|
@ -400,7 +400,7 @@ Notes:
|
|||
- Report output is intentionally capped for very large boards to avoid multi-GB files.
|
||||
- For full raw payloads, use targeted commands such as `items-raw --debug`, `pad-shape-polygon --debug`, and `padstack-presence --debug`.
|
||||
|
||||
Get current project path (derived from open PCB docs):
|
||||
Get current project path (from open PCB docs, or `KIPRJMOD` when `GetOpenDocuments` is unavailable):
|
||||
|
||||
```bash
|
||||
cargo run --features blocking --bin kicad-ipc-cli -- project-path
|
||||
|
|
|
|||
146
src/client.rs
146
src/client.rs
|
|
@ -34,6 +34,7 @@ use crate::transport::Transport;
|
|||
|
||||
const KICAD_API_SOCKET_ENV: &str = "KICAD_API_SOCKET";
|
||||
const KICAD_API_TOKEN_ENV: &str = "KICAD_API_TOKEN";
|
||||
const KIPRJMOD_ENV: &str = "KIPRJMOD";
|
||||
|
||||
const CMD_PING: &str = "kiapi.common.commands.Ping";
|
||||
const CMD_GET_VERSION: &str = "kiapi.common.commands.GetVersion";
|
||||
|
|
@ -536,7 +537,7 @@ impl KiCadClient {
|
|||
|
||||
pub async fn get_text_variables_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
||||
let command = common_commands::GetTextVariables {
|
||||
document: Some(self.current_board_document_proto().await?),
|
||||
document: Some(project_document_proto()),
|
||||
};
|
||||
let response = self
|
||||
.send_command(envelope::pack_any(&command, CMD_GET_TEXT_VARIABLES))
|
||||
|
|
@ -557,7 +558,7 @@ impl KiCadClient {
|
|||
merge_mode: MapMergeMode,
|
||||
) -> Result<prost_types::Any, KiCadError> {
|
||||
let command = common_commands::SetTextVariables {
|
||||
document: Some(self.current_board_document_proto().await?),
|
||||
document: Some(project_document_proto()),
|
||||
variables: Some(common_project::TextVariables {
|
||||
variables: variables.into_iter().collect(),
|
||||
}),
|
||||
|
|
@ -584,7 +585,7 @@ impl KiCadClient {
|
|||
text: Vec<String>,
|
||||
) -> Result<prost_types::Any, KiCadError> {
|
||||
let command = common_commands::ExpandTextVariables {
|
||||
document: Some(self.current_board_document_proto().await?),
|
||||
document: Some(project_document_proto()),
|
||||
text,
|
||||
};
|
||||
let response = self
|
||||
|
|
@ -667,12 +668,13 @@ impl KiCadClient {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the current PCB project's path.
|
||||
/// Returns the current project path.
|
||||
///
|
||||
/// Fails if no PCB is open or if multiple project paths are present.
|
||||
/// First queries open PCB documents. If KiCad reports `GetOpenDocuments` as unhandled,
|
||||
/// this falls back to the `KIPRJMOD` environment variable when available.
|
||||
pub async fn get_current_project_path(&self) -> Result<PathBuf, KiCadError> {
|
||||
let docs = self.get_open_documents(DocumentType::Pcb).await?;
|
||||
select_single_project_path(&docs)
|
||||
let docs = self.get_open_documents(DocumentType::Pcb).await;
|
||||
resolve_current_project_path(docs)
|
||||
}
|
||||
|
||||
/// Returns `true` when at least one PCB document is open in KiCad.
|
||||
|
|
@ -2079,6 +2081,14 @@ fn model_document_to_proto(document: &DocumentSpecifier) -> common_types::Docume
|
|||
}
|
||||
}
|
||||
|
||||
fn project_document_proto() -> common_types::DocumentSpecifier {
|
||||
common_types::DocumentSpecifier {
|
||||
r#type: DocumentType::Project.to_proto(),
|
||||
project: Some(common_types::ProjectSpecifier::default()),
|
||||
identifier: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_spec_to_proto(text: TextSpec) -> common_types::Text {
|
||||
common_types::Text {
|
||||
position: text.position_nm.map(vector2_nm_to_proto),
|
||||
|
|
@ -3705,6 +3715,35 @@ fn select_single_project_path(docs: &[DocumentSpecifier]) -> Result<PathBuf, KiC
|
|||
Ok(PathBuf::from(first))
|
||||
}
|
||||
|
||||
fn resolve_current_project_path(
|
||||
docs_result: Result<Vec<DocumentSpecifier>, KiCadError>,
|
||||
) -> Result<PathBuf, KiCadError> {
|
||||
match docs_result {
|
||||
Ok(docs) => select_single_project_path(&docs),
|
||||
Err(err) if is_get_open_documents_unhandled(&err) => {
|
||||
project_path_from_environment().ok_or(err)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn project_path_from_environment() -> Option<PathBuf> {
|
||||
let value = std::env::var(KIPRJMOD_ENV).ok()?;
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(PathBuf::from(trimmed))
|
||||
}
|
||||
|
||||
fn is_get_open_documents_unhandled(err: &KiCadError) -> bool {
|
||||
matches!(
|
||||
err,
|
||||
KiCadError::ApiStatus { code, .. } if code == "AS_UNHANDLED"
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_socket_uri(explicit: Option<&str>) -> String {
|
||||
if let Some(socket) = explicit {
|
||||
return normalize_socket_uri(socket);
|
||||
|
|
@ -3781,13 +3820,14 @@ mod tests {
|
|||
any_to_pretty_debug, board_editor_appearance_settings_to_proto, board_stackup_to_proto,
|
||||
commit_action_to_proto, decode_pcb_item, 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,
|
||||
is_get_open_documents_unhandled, 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, project_document_proto,
|
||||
project_path_from_environment, resolve_current_project_path, 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, KIPRJMOD_ENV, PCB_OBJECT_TYPES,
|
||||
};
|
||||
use crate::error::KiCadError;
|
||||
use crate::model::board::{
|
||||
|
|
@ -3799,6 +3839,9 @@ mod tests {
|
|||
};
|
||||
use prost::Message;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn normalize_socket_uri_adds_ipc_scheme() {
|
||||
|
|
@ -3812,6 +3855,13 @@ mod tests {
|
|||
assert_eq!(normalized, "ipc:///tmp/kicad/api.sock");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_document_proto_uses_project_type() {
|
||||
let document = project_document_proto();
|
||||
assert_eq!(document.r#type, DocumentType::Project.to_proto());
|
||||
assert!(document.identifier.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_single_project_path_picks_unique_path() {
|
||||
let docs = vec![DocumentSpecifier {
|
||||
|
|
@ -3863,6 +3913,74 @@ mod tests {
|
|||
assert!(matches!(result, Err(KiCadError::BoardNotOpen)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_current_project_path_reads_env_when_open_docs_unhandled() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex should lock");
|
||||
std::env::set_var(KIPRJMOD_ENV, "/tmp/kicad-env-project");
|
||||
|
||||
let result = resolve_current_project_path(Err(KiCadError::ApiStatus {
|
||||
code: "AS_UNHANDLED".to_string(),
|
||||
message:
|
||||
"no handler available for request of type kiapi.common.commands.GetOpenDocuments"
|
||||
.to_string(),
|
||||
}))
|
||||
.expect("KIPRJMOD fallback should resolve project path");
|
||||
|
||||
assert_eq!(result, PathBuf::from("/tmp/kicad-env-project"));
|
||||
std::env::remove_var(KIPRJMOD_ENV);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_current_project_path_keeps_original_error_without_env() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex should lock");
|
||||
std::env::remove_var(KIPRJMOD_ENV);
|
||||
|
||||
let err = resolve_current_project_path(Err(KiCadError::ApiStatus {
|
||||
code: "AS_UNHANDLED".to_string(),
|
||||
message:
|
||||
"no handler available for request of type kiapi.common.commands.GetOpenDocuments"
|
||||
.to_string(),
|
||||
}))
|
||||
.expect_err("without env fallback should keep original unhandled error");
|
||||
|
||||
assert!(matches!(err, KiCadError::ApiStatus { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_current_project_path_does_not_fallback_when_no_board_docs() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex should lock");
|
||||
std::env::set_var(KIPRJMOD_ENV, "/tmp/kicad-env-project");
|
||||
|
||||
let err = resolve_current_project_path(Ok(Vec::new()))
|
||||
.expect_err("no-board docs should remain BoardNotOpen");
|
||||
assert!(matches!(err, KiCadError::BoardNotOpen));
|
||||
|
||||
std::env::remove_var(KIPRJMOD_ENV);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_path_from_environment_ignores_empty_values() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex should lock");
|
||||
std::env::set_var(KIPRJMOD_ENV, " ");
|
||||
assert!(project_path_from_environment().is_none());
|
||||
std::env::remove_var(KIPRJMOD_ENV);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_get_open_documents_unhandled_matches_expected_shape() {
|
||||
let unhandled = KiCadError::ApiStatus {
|
||||
code: "AS_UNHANDLED".to_string(),
|
||||
message: String::new(),
|
||||
};
|
||||
assert!(is_get_open_documents_unhandled(&unhandled));
|
||||
|
||||
let other = KiCadError::ApiStatus {
|
||||
code: "AS_BAD_REQUEST".to_string(),
|
||||
message: "bad request".to_string(),
|
||||
};
|
||||
assert!(!is_get_open_documents_unhandled(&other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_single_board_document_errors_on_multiple_open_boards() {
|
||||
let docs = vec![
|
||||
|
|
|
|||
|
|
@ -2110,14 +2110,14 @@ COMMANDS:
|
|||
plugin-settings-path [--identifier <id>]
|
||||
Resolve writeable plugin settings directory (default: kicad-ipc-rust)
|
||||
open-docs [--type <type>] List open docs (default type: pcb)
|
||||
project-path Get current project path from open PCB docs
|
||||
project-path Get current project path from open PCB docs (or KIPRJMOD fallback)
|
||||
board-open Exit non-zero if no PCB doc is open
|
||||
net-classes List project netclass definitions
|
||||
set-net-classes [--merge-mode <merge|replace>]
|
||||
Write current netclass set back with selected merge mode
|
||||
text-variables List text variables for current board document
|
||||
text-variables List text variables for current project
|
||||
set-text-variables [--merge-mode <merge|replace>] [--var <name=value> ...]
|
||||
Set text variables for current board document
|
||||
Set text variables for current project
|
||||
expand-text-variables Expand variables in provided text values
|
||||
Options: --text <value> (repeatable)
|
||||
text-extents Measure text bounding box
|
||||
|
|
|
|||
Loading…
Reference in New Issue