1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::roles::{Action, Role};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct Rect {
17 pub row: i64,
19 pub column: i64,
21 pub width: i64,
23 pub height: i64,
25}
26
27impl Rect {
28 pub fn new(row: i64, column: i64, width: i64, height: i64) -> Self {
30 Self {
31 row,
32 column,
33 width,
34 height,
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
42pub enum Observation<T> {
43 Known {
45 value: T,
47 evidence: EvidenceProvenance,
49 },
50 Absent {
52 reason: String,
54 evidence: EvidenceProvenance,
56 },
57 Unknown {
59 reason: String,
61 },
62 Unsupported {
64 capability: String,
66 reason: String,
68 },
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "lowercase")]
74pub enum SemanticValueSensitivity {
75 Public,
77 Sensitive,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
84pub enum SemanticValueObservation {
85 Known {
87 value: String,
89 sensitivity: SemanticValueSensitivity,
91 evidence: EvidenceProvenance,
93 },
94 Absent {
96 reason: String,
98 evidence: EvidenceProvenance,
100 },
101 Unknown {
103 reason: String,
105 },
106 Unsupported {
108 capability: String,
110 reason: String,
112 },
113 Withheld {
115 reason: String,
117 sensitivity: SemanticValueSensitivity,
119 },
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase", deny_unknown_fields)]
125pub struct EvidenceProvenance {
126 pub source: EvidenceSource,
128 pub method: EvidenceMethod,
130 pub strength: EvidenceStrength,
132 pub provider_id: String,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "lowercase")]
139pub enum EvidenceSource {
140 Framework,
142 Application,
144 Terminal,
146 Recognizer,
148 Driver,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "kebab-case")]
155pub enum EvidenceMethod {
156 Native,
158 Instrumented,
160 Declared,
162 Correlated,
164 Measured,
166 Derived,
168 Heuristic,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "lowercase")]
175pub enum EvidenceStrength {
176 Authoritative,
178 Diagnostic,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185pub struct NodeGeometryObservations {
186 pub displayed: Observation<bool>,
188 pub intended_rect: Observation<Rect>,
190 pub visible_rect: Observation<Rect>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase", deny_unknown_fields)]
197pub struct PointerHitRegion {
198 pub rect: Rect,
200 pub recipient_id: String,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(deny_unknown_fields)]
207pub struct PointerHitGrid {
208 pub regions: Vec<PointerHitRegion>,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase", deny_unknown_fields)]
215pub struct ProviderPointerSpan {
216 pub row: i64,
218 pub from: i64,
220 pub to: i64,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase", deny_unknown_fields)]
227pub struct ProviderPointerRegion {
228 pub recipient_id: String,
230 pub region_bounds: Rect,
232 pub spans: Vec<ProviderPointerSpan>,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase", deny_unknown_fields)]
239pub struct ProviderPaintedRegion {
240 pub recipient_id: String,
242 pub region_bounds: Rect,
244 pub spans: Vec<ProviderPointerSpan>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase", deny_unknown_fields)]
251pub struct ProviderActionRecipes {
252 pub recipient_id: String,
254 pub recipes: Vec<PhysicalInputRecipe>,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
261pub enum ProviderFocusState {
262 Focused {
264 #[serde(rename = "recipientId")]
266 recipient_id: String,
267 },
268 None,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase", deny_unknown_fields)]
275pub struct ProviderTerminalInputModes {
276 pub mouse_tracking: String,
278 pub mouse_encoding: String,
280 pub focus_reporting: String,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286#[serde(rename_all = "camelCase", deny_unknown_fields)]
287pub struct ProviderRevisionEvidence {
288 pub provider_id: String,
290 pub session_id: String,
292 pub revision: i64,
294 pub status: String,
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub evidence: Option<EvidenceProvenance>,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub pointer_regions: Option<Vec<ProviderPointerRegion>>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub focus_state: Option<ProviderFocusState>,
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub action_recipes: Option<Vec<ProviderActionRecipes>>,
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub scroll_states: Option<Vec<ProviderScrollState>>,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub painted_regions: Option<Vec<ProviderPaintedRegion>>,
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub input_modes: Option<ProviderTerminalInputModes>,
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub hit_grid: Option<PointerHitGrid>,
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub reason: Option<String>,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase", deny_unknown_fields)]
328pub struct ProviderScrollState {
329 pub recipient_id: String,
331 pub axis: Orientation,
333 pub offset: i64,
335 pub viewport: i64,
337 pub extent: i64,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase", deny_unknown_fields)]
344pub struct SemanticPaintedRegion {
345 pub region_bounds: Rect,
347 pub spans: Vec<ProviderPointerSpan>,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(untagged)]
354pub enum Checked {
355 Flag(bool),
357 Mixed(MixedState),
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
363pub enum MixedState {
364 #[serde(rename = "mixed")]
366 Mixed,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(rename_all = "lowercase")]
372pub enum Orientation {
373 Horizontal,
375 Vertical,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
381#[serde(rename_all = "lowercase")]
382pub enum CursorShape {
383 Block,
385 Underline,
387 Bar,
389}
390
391#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
393#[serde(rename_all = "camelCase", deny_unknown_fields)]
394pub struct State {
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub disabled: Option<bool>,
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub focused: Option<bool>,
401 #[serde(skip_serializing_if = "Option::is_none")]
403 pub selected: Option<bool>,
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub checked: Option<Checked>,
407 #[serde(skip_serializing_if = "Option::is_none")]
409 pub expanded: Option<bool>,
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub modal: Option<bool>,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 pub busy: Option<bool>,
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub hidden: Option<bool>,
419 #[serde(skip_serializing_if = "Option::is_none")]
423 pub offscreen: Option<bool>,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub readonly: Option<bool>,
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub multiline: Option<bool>,
430 #[serde(skip_serializing_if = "Option::is_none")]
432 pub required: Option<bool>,
433 #[serde(skip_serializing_if = "Option::is_none")]
435 pub multiselectable: Option<bool>,
436 #[serde(skip_serializing_if = "Option::is_none")]
438 pub orientation: Option<Orientation>,
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub level: Option<i64>,
442 #[serde(skip_serializing_if = "Option::is_none")]
444 pub position_in_set: Option<i64>,
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub set_size: Option<i64>,
448}
449
450impl State {
451 pub fn is_empty(&self) -> bool {
453 *self == State::default()
454 }
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
459#[serde(rename_all = "camelCase", deny_unknown_fields)]
460pub struct TextRange {
461 pub start_offset: i64,
463 pub end_offset: i64,
465 pub rect: Rect,
467}
468
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
471#[serde(rename_all = "camelCase")]
472pub enum PhysicalInputRecipeAction {
473 Focus,
475 Activate,
477 Toggle,
479 SetValue,
481}
482
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
485#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
486pub enum PhysicalInputRecipeStep {
487 Press {
489 key: String,
491 },
492 InsertActionValue,
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
498#[serde(rename_all = "camelCase", deny_unknown_fields)]
499pub struct PhysicalInputRecipe {
500 pub action: PhysicalInputRecipeAction,
502 pub requires_focus: bool,
504 pub steps: Vec<PhysicalInputRecipeStep>,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
510#[serde(rename_all = "camelCase", deny_unknown_fields)]
511pub struct Node {
512 pub id: String,
514 #[serde(skip_serializing_if = "Option::is_none")]
516 pub parent_id: Option<String>,
517 pub role: Role,
519 pub name: String,
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub description: Option<String>,
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub value: Option<SemanticValueObservation>,
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub state: Option<State>,
530 #[serde(skip_serializing_if = "Option::is_none")]
532 pub extended: Option<BTreeMap<String, Value>>,
533 #[serde(skip_serializing_if = "Option::is_none")]
535 pub actions: Option<Vec<Action>>,
536 #[serde(skip_serializing_if = "Option::is_none")]
538 pub input_recipes: Option<Vec<PhysicalInputRecipe>>,
539 #[serde(skip_serializing_if = "Option::is_none")]
541 pub labelled_by: Option<Vec<String>>,
542 #[serde(skip_serializing_if = "Option::is_none")]
544 pub described_by: Option<Vec<String>>,
545 #[serde(skip_serializing_if = "Option::is_none")]
547 pub text_ranges: Option<Vec<TextRange>>,
548 #[serde(skip_serializing_if = "Option::is_none")]
550 pub test_id: Option<String>,
551 #[serde(skip_serializing_if = "Option::is_none")]
555 pub framework_type: Option<String>,
556 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
558 pub opaque_children: bool,
559 #[serde(skip_serializing_if = "Option::is_none")]
561 pub p: Option<Provenance>,
562 #[serde(skip_serializing_if = "Option::is_none")]
564 pub px: Option<BTreeMap<String, Provenance>>,
565 pub geometry: NodeGeometryObservations,
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub scroll: Option<Observation<ScrollState>>,
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub painted_region: Option<Observation<SemanticPaintedRegion>>,
573}
574
575#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
577#[serde(rename_all = "camelCase", deny_unknown_fields)]
578pub struct ScrollState {
579 pub axis: Orientation,
581 pub offset: i64,
583 pub viewport: i64,
585 pub extent: i64,
587}
588
589#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
591#[serde(rename_all = "lowercase")]
592pub enum Provenance {
593 Annotation,
595 Recognizer,
597 Framework,
599 Application,
601 Correlation,
603 Heuristic,
605}
606
607impl Node {
608 pub fn new(id: impl Into<String>, role: Role, name: impl Into<String>) -> Self {
610 Self {
611 id: id.into(),
612 parent_id: None,
613 role,
614 name: name.into(),
615 description: None,
616 value: None,
617 state: None,
618 extended: None,
619 actions: None,
620 input_recipes: None,
621 labelled_by: None,
622 described_by: None,
623 text_ranges: None,
624 test_id: None,
625 framework_type: None,
626 opaque_children: false,
627 p: None,
628 px: None,
629 geometry: NodeGeometryObservations {
630 displayed: Observation::Unsupported {
631 capability: "displayed".into(),
632 reason: "framework-unobservable".into(),
633 },
634 intended_rect: Observation::Unsupported {
635 capability: "intended-geometry".into(),
636 reason: "framework-unobservable".into(),
637 },
638 visible_rect: Observation::Unsupported {
639 capability: "clipped-geometry".into(),
640 reason: "framework-unobservable".into(),
641 },
642 },
643 scroll: None,
644 painted_region: None,
645 }
646 }
647
648 pub fn with_framework_type(mut self, framework_type: impl Into<String>) -> Self {
651 self.framework_type = Some(framework_type.into());
652 self
653 }
654
655 #[must_use]
657 pub fn with_opaque_children(mut self) -> Self {
658 self.opaque_children = true;
659 self
660 }
661
662 pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
664 self.parent_id = Some(parent_id.into());
665 self
666 }
667
668 pub fn with_geometry(mut self, geometry: NodeGeometryObservations) -> Self {
670 self.geometry = geometry;
671 self
672 }
673
674 pub fn with_state(mut self, state: State) -> Self {
676 self.state = if state.is_empty() { None } else { Some(state) };
677 self
678 }
679
680 pub fn with_extended(mut self, extended: BTreeMap<String, Value>) -> Self {
682 self.extended = Some(extended);
683 self
684 }
685
686 pub fn with_actions(mut self, actions: Vec<Action>) -> Self {
688 self.actions = Some(actions);
689 self
690 }
691
692 pub fn with_test_id(mut self, test_id: impl Into<String>) -> Self {
694 self.test_id = Some(test_id.into());
695 self
696 }
697}
698
699#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
701#[serde(deny_unknown_fields)]
702pub struct Cursor {
703 pub row: i64,
705 pub column: i64,
707 pub visible: bool,
709 #[serde(skip_serializing_if = "Option::is_none")]
711 pub shape: Option<CursorShape>,
712}
713
714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
716#[serde(rename_all = "camelCase", deny_unknown_fields)]
717pub struct Snapshot {
718 pub v: u8,
720 pub session_id: String,
722 pub revision: i64,
724 pub columns: i64,
726 pub rows: i64,
728 #[serde(skip_serializing_if = "Option::is_none")]
730 pub cursor: Option<Cursor>,
731 pub root_ids: Vec<String>,
733 pub nodes: Vec<Node>,
735 pub coordinate_space: Observation<String>,
737 pub hit_grid: Observation<PointerHitGrid>,
739 #[serde(default, skip_serializing_if = "Vec::is_empty")]
741 pub provider_evidence: Vec<ProviderRevisionEvidence>,
742}
743
744impl Snapshot {
745 pub fn new(columns: i64, rows: i64) -> Self {
748 Self {
749 v: 2,
750 session_id: String::new(),
751 revision: 0,
752 columns,
753 rows,
754 cursor: None,
755 root_ids: Vec::new(),
756 nodes: Vec::new(),
757 coordinate_space: Observation::Known {
758 value: "viewport-cells".into(),
759 evidence: EvidenceProvenance {
760 source: EvidenceSource::Framework,
761 method: EvidenceMethod::Instrumented,
762 strength: EvidenceStrength::Authoritative,
763 provider_id: "termwright-rust-client".into(),
764 },
765 },
766 hit_grid: Observation::Unsupported {
767 capability: "pointer-hit-grid".into(),
768 reason: "framework-unobservable".into(),
769 },
770 provider_evidence: Vec::new(),
771 }
772 }
773
774 pub fn push(&mut self, node: Node) {
776 if node.parent_id.is_none() {
777 self.root_ids.push(node.id.clone());
778 }
779 self.nodes.push(node);
780 }
781}