brep_app/automation/hit_keys.rs
1//! The hit-key registry: each panel documents the key prefixes it publishes
2//! in its hit-rect blob, beside the publisher. `brep://widgets` is generated
3//! from [`hit_key_docs`], and a `test-mcp` script checks that every key the
4//! running app publishes matches a documented prefix.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct HitKeyDoc {
7 /// The panel name as `Registry::hit_blob_panel` derives it.
8 pub panel: &'static str,
9 /// A key prefix (`step:`), or a whole key (`form:return`).
10 pub prefix: &'static str,
11 pub meaning: &'static str,
12 /// The APP COMMAND that does what a click here does, when one exists
13 /// ([`crate::automation::command::registry`]).
14 ///
15 /// A host may expose it under another tool name — the MCP server owns the
16 /// file halves, so `doc_load` reaches a caller as `document_open` — and the
17 /// generated widget docs translate it. This side names the command, because
18 /// this side is the only one that can check the name is real.
19 ///
20 /// This is what makes "every button is reachable without a pointer" a
21 /// CHECKED property rather than a claim: the registration sits beside the
22 /// widget it describes, the generated widget docs carry it, and
23 /// `every_named_command_exists` fails on a name the registry does not have.
24 /// `None` is the honest answer for a widget with no command equivalent (a
25 /// history row, a form field) — those are driven by clicking their rect.
26 pub command: Option<&'static str>,
27}
28
29/// Every panel's registrations. Adding a panel means adding its static here —
30/// and the `brep-mcp test` hit-key script fails on any published key that no
31/// registration covers, so a forgotten line is caught by the gate.
32pub fn hit_key_docs() -> Vec<&'static HitKeyDoc> {
33 let sets: &[&[HitKeyDoc]] = &[
34 crate::panels::history::HIT_KEYS,
35 crate::panels::toolbar::HIT_KEYS,
36 crate::panels::workbench_toolbar::HIT_KEYS,
37 crate::panels::file::HIT_KEYS,
38 crate::panels::scene::HIT_KEYS,
39 crate::panels::selection::HIT_KEYS,
40 crate::panels::part_properties::HIT_KEYS,
41 crate::panels::context_bar::HIT_KEYS,
42 crate::panels::mode_bar::HIT_KEYS,
43 crate::panels::expressions::HIT_KEYS,
44 crate::panels::document_tabs::HIT_KEYS,
45 crate::panels::settings::HIT_KEYS,
46 crate::panels::info::HIT_KEYS,
47 crate::panels::info_windows::HIT_KEYS,
48 crate::panels::interference::HIT_KEYS,
49 crate::panels::auto_constraints::HIT_KEYS,
50 crate::panels::step_parts::HIT_KEYS,
51 crate::panels::wire_harness::HIT_KEYS,
52 crate::panels::bug_report::HIT_KEYS,
53 crate::panels::pmi::HIT_KEYS,
54 crate::panels::bom::HIT_KEYS,
55 crate::panels::assembly_constraints::HIT_KEYS,
56 crate::panels::spline_anchors::HIT_KEYS,
57 crate::panels::stl_import::HIT_KEYS,
58 crate::recovery::HIT_KEYS,
59 crate::viewport::HIT_KEYS,
60 ];
61 sets.iter().flat_map(|s| s.iter()).collect()
62}
63
64/// Does `panel/key` match a registered prefix?
65pub fn documented(panel: &str, key: &str) -> bool {
66 hit_key_docs()
67 .iter()
68 .any(|d| d.panel == panel && (key == d.prefix || (d.prefix.ends_with(':') && key.starts_with(d.prefix))))
69}
70
71/// The docs as JSON: `[{panel, prefix, meaning, command}]`.
72pub fn hit_key_docs_json() -> serde_json::Value {
73 serde_json::Value::Array(
74 hit_key_docs()
75 .iter()
76 .map(|d| serde_json::json!({ "panel": d.panel, "prefix": d.prefix, "meaning": d.meaning, "command": d.command }))
77 .collect(),
78 )
79}
80
81// BREP private tests: 2c7b8a92a203de40