Skip to main content

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