Skip to main content

termwright_protocol/
tree.rs

1//! Semantic tree DTOs.
2//!
3//! Unset optionals are omitted from the wire form: the schema is strict, so an
4//! explicit `null` is a validation failure rather than "absent".
5
6use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::roles::{Action, Role};
12
13/// Zero-based viewport cell rectangle.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct Rect {
17    /// Zero-based row of the top edge.
18    pub row: i64,
19    /// Zero-based column of the left edge.
20    pub column: i64,
21    /// Width in cells; zero means nothing is painted.
22    pub width: i64,
23    /// Height in cells; zero means nothing is painted.
24    pub height: i64,
25}
26
27impl Rect {
28    /// Build a rectangle from absolute viewport coordinates.
29    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/// Evidence-qualified fact. Unknown and unsupported are never coerced to false.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
42pub enum Observation<T> {
43    /// The producer knows the value and names the evidence behind it.
44    Known {
45        /// Observed value.
46        value: T,
47        /// Provenance of the observation.
48        evidence: EvidenceProvenance,
49    },
50    /// The fact has no value for the named lifecycle/layout reason.
51    Absent {
52        /// Why no value exists.
53        reason: String,
54        /// Authoritative provenance proving that no value exists.
55        evidence: EvidenceProvenance,
56    },
57    /// The fact may become observable on a later revision.
58    Unknown {
59        /// Why evidence is not currently available.
60        reason: String,
61    },
62    /// The negotiated producer cannot provide this capability.
63    Unsupported {
64        /// Missing wire or framework capability.
65        capability: String,
66        /// Why the capability is unavailable.
67        reason: String,
68    },
69}
70
71/// Whether a semantic value may safely cross normal artifact boundaries.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "lowercase")]
74pub enum SemanticValueSensitivity {
75    /// The application declares the value safe to expose.
76    Public,
77    /// The value is sensitive and must be redacted by default.
78    Sensitive,
79}
80
81/// A semantic value with absence, support and confidentiality preserved.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
84pub enum SemanticValueObservation {
85    /// The value is known for this committed revision.
86    Known {
87        /// Exact plaintext value.
88        value: String,
89        /// Application-declared artifact sensitivity.
90        sensitivity: SemanticValueSensitivity,
91        /// Source proving this value for the committed revision.
92        evidence: EvidenceProvenance,
93    },
94    /// The node authoritatively has no value now.
95    Absent {
96        /// Why the node currently has no semantic value.
97        reason: String,
98        /// Authoritative source proving absence.
99        evidence: EvidenceProvenance,
100    },
101    /// A later paired revision may provide the value.
102    Unknown {
103        /// Retryable revision-domain reason.
104        reason: String,
105    },
106    /// The frozen contract cannot provide semantic values.
107    Unsupported {
108        /// Always `semantic-value` on the v2 wire.
109        capability: String,
110        /// Why the capability is unavailable.
111        reason: String,
112    },
113    /// A value exists but its plaintext is deliberately not transported.
114    Withheld {
115        /// Policy or sensitivity reason for withholding plaintext.
116        reason: String,
117        /// Sensitivity of the omitted value.
118        sensitivity: SemanticValueSensitivity,
119    },
120}
121
122/// Provenance carried by every known physical observation.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase", deny_unknown_fields)]
125pub struct EvidenceProvenance {
126    /// Layer that supplied the fact.
127    pub source: EvidenceSource,
128    /// How that layer obtained the fact.
129    pub method: EvidenceMethod,
130    /// Whether the fact is safe for behavior or diagnostic only.
131    pub strength: EvidenceStrength,
132    /// Stable identity of the provider that made the observation.
133    pub provider_id: String,
134}
135
136/// Layer that supplied a known observation.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "lowercase")]
139pub enum EvidenceSource {
140    /// The UI framework itself.
141    Framework,
142    /// Application-authored information.
143    Application,
144    /// The terminal emulator or terminal grid.
145    Terminal,
146    /// A recognizer derived the fact from another representation.
147    Recognizer,
148    /// The Termwright driver measured the fact.
149    Driver,
150}
151
152/// Method used to produce a known observation.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "kebab-case")]
155pub enum EvidenceMethod {
156    /// Native framework/runtime observation.
157    Native,
158    /// Observation made by installed instrumentation.
159    Instrumented,
160    /// Explicit application declaration.
161    Declared,
162    /// Correlation across independently identified facts.
163    Correlated,
164    /// Direct measurement.
165    Measured,
166    /// Deterministic derivation from stronger facts.
167    Derived,
168    /// Best-effort heuristic; never behavioral authority.
169    Heuristic,
170}
171
172/// Authority of a known observation.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "lowercase")]
175pub enum EvidenceStrength {
176    /// May be used to decide behavior.
177    Authoritative,
178    /// May be shown for diagnosis but not used as behavioral proof.
179    Diagnostic,
180}
181
182/// Display and layout facts for one protocol-v2 semantic node.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185pub struct NodeGeometryObservations {
186    /// Effective display state through the complete ancestor chain.
187    pub displayed: Observation<bool>,
188    /// Layout rectangle before viewport clipping.
189    pub intended_rect: Observation<Rect>,
190    /// Rectangle remaining after framework clipping.
191    pub visible_rect: Observation<Rect>,
192}
193
194/// One non-overlapping rectangle owned by an exact pointer recipient.
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase", deny_unknown_fields)]
197pub struct PointerHitRegion {
198    /// Half-open viewport-cell rectangle.
199    pub rect: Rect,
200    /// Semantic node id receiving a fresh pointer event in this rectangle.
201    pub recipient_id: String,
202}
203
204/// Compressed exact fresh-pointer routing grid for a completed frame.
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(deny_unknown_fields)]
207pub struct PointerHitGrid {
208    /// Non-overlapping recipient rectangles.
209    pub regions: Vec<PointerHitRegion>,
210}
211
212/// One canonical half-open pointer row run.
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase", deny_unknown_fields)]
215pub struct ProviderPointerSpan {
216    /// Viewport row containing the run.
217    pub row: i64,
218    /// Inclusive starting column.
219    pub from: i64,
220    /// Exclusive ending column.
221    pub to: i64,
222}
223
224/// Pointer-only application region; never layout or clipping geometry.
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase", deny_unknown_fields)]
227pub struct ProviderPointerRegion {
228    /// Semantic node that the production router associates with the region.
229    pub recipient_id: String,
230    /// Bounding rectangle used only as pointer-region metadata.
231    pub region_bounds: Rect,
232    /// Exact possibly disjoint owned cells as canonical row spans.
233    pub spans: Vec<ProviderPointerSpan>,
234}
235
236/// Exact viewport cells painted by an application's production painter.
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase", deny_unknown_fields)]
239pub struct ProviderPaintedRegion {
240    /// Semantic node whose production painter produced these cells.
241    pub recipient_id: String,
242    /// Bounding rectangle of all attributed spans.
243    pub region_bounds: Rect,
244    /// Exact possibly disjoint cells as canonical row spans.
245    pub spans: Vec<ProviderPointerSpan>,
246}
247
248/// Production keybinding recipes for one semantic recipient and revision.
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase", deny_unknown_fields)]
251pub struct ProviderActionRecipes {
252    /// Semantic node the application strategy targets.
253    pub recipient_id: String,
254    /// Data-only recipes executed later by Termwright's PTY devices.
255    pub recipes: Vec<PhysicalInputRecipe>,
256}
257
258/// Exact production focus-manager result for one committed revision.
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
261pub enum ProviderFocusState {
262    /// One semantic recipient owns focus.
263    Focused {
264        /// Stable semantic recipient id.
265        #[serde(rename = "recipientId")]
266        recipient_id: String,
267    },
268    /// The production focus manager authoritatively reports no focus owner.
269    None,
270}
271
272/// Production terminal parser configuration for one committed revision.
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase", deny_unknown_fields)]
275pub struct ProviderTerminalInputModes {
276    /// Mouse tracking level accepted by the production parser.
277    pub mouse_tracking: String,
278    /// Mouse report encoding accepted by the production parser.
279    pub mouse_encoding: String,
280    /// Whether the production parser accepts terminal focus reports.
281    pub focus_reporting: String,
282}
283
284/// Revision-bound application evidence. `status` is available, lost, or violation.
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286#[serde(rename_all = "camelCase", deny_unknown_fields)]
287pub struct ProviderRevisionEvidence {
288    /// Stable negotiated provider identity.
289    pub provider_id: String,
290    /// Session this evidence belongs to.
291    pub session_id: String,
292    /// Semantic revision described by the evidence.
293    pub revision: i64,
294    /// `available`, `lost`, or `violation`.
295    pub status: String,
296    /// Provenance present for available evidence.
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub evidence: Option<EvidenceProvenance>,
299    /// Exact pointer regions when the provider supplies them.
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub pointer_regions: Option<Vec<ProviderPointerRegion>>,
302    /// Production focus-manager result when negotiated.
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub focus_state: Option<ProviderFocusState>,
305    /// Production action recipes when negotiated.
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub action_recipes: Option<Vec<ProviderActionRecipes>>,
308    /// Production application viewport facts when negotiated.
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub scroll_states: Option<Vec<ProviderScrollState>>,
311    /// Production painter attribution when negotiated.
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub painted_regions: Option<Vec<ProviderPaintedRegion>>,
314    /// Production terminal parser configuration when negotiated.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub input_modes: Option<ProviderTerminalInputModes>,
317    /// Complete verified production hit grid when negotiated.
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub hit_grid: Option<PointerHitGrid>,
320    /// Diagnostic explanation for lost or violating providers.
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub reason: Option<String>,
323}
324
325/// Revision-bound application viewport state for one semantic recipient.
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase", deny_unknown_fields)]
328pub struct ProviderScrollState {
329    /// Stable semantic recipient id.
330    pub recipient_id: String,
331    /// Logical scroll axis.
332    pub axis: Orientation,
333    /// First visible logical unit.
334    pub offset: i64,
335    /// Visible logical units.
336    pub viewport: i64,
337    /// Total logical units.
338    pub extent: i64,
339}
340
341/// Exact painted cells attached to a semantic node observation.
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase", deny_unknown_fields)]
344pub struct SemanticPaintedRegion {
345    /// Bounding rectangle of all attributed spans.
346    pub region_bounds: Rect,
347    /// Exact possibly disjoint cells as canonical row spans.
348    pub spans: Vec<ProviderPointerSpan>,
349}
350
351/// Whether a tri-state control is on, off, or partially selected.
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(untagged)]
354pub enum Checked {
355    /// Plain on/off.
356    Flag(bool),
357    /// The literal string `"mixed"`.
358    Mixed(MixedState),
359}
360
361/// The `"mixed"` literal, as its own type so serde can keep the schema closed.
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
363pub enum MixedState {
364    /// The literal `"mixed"`.
365    #[serde(rename = "mixed")]
366    Mixed,
367}
368
369/// Layout direction of a composite widget.
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(rename_all = "lowercase")]
372pub enum Orientation {
373    /// Laid out left to right.
374    Horizontal,
375    /// Laid out top to bottom.
376    Vertical,
377}
378
379/// Cursor rendering style.
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
381#[serde(rename_all = "lowercase")]
382pub enum CursorShape {
383    /// A filled block cursor.
384    Block,
385    /// An underline cursor.
386    Underline,
387    /// A vertical bar cursor.
388    Bar,
389}
390
391/// The closed state set. `None` means "not asserted", not "false".
392#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
393#[serde(rename_all = "camelCase", deny_unknown_fields)]
394pub struct State {
395    /// The control refuses interaction.
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub disabled: Option<bool>,
398    /// Keyboard input goes here.
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub focused: Option<bool>,
401    /// The node is selected within its parent set.
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub selected: Option<bool>,
404    /// Checked, unchecked, or mixed.
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub checked: Option<Checked>,
407    /// A disclosure is open.
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub expanded: Option<bool>,
410    /// The node traps interaction while it is present.
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub modal: Option<bool>,
413    /// Content is being loaded or recomputed.
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub busy: Option<bool>,
416    /// Present in the tree but not painted.
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub hidden: Option<bool>,
419    /// Every cell is outside the visible area — scrolled away, not
420    /// undisplayed. Implies [`State::hidden`]; the pair without it is refused
421    /// by validation.
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub offscreen: Option<bool>,
424    /// Value is displayed but cannot be edited.
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub readonly: Option<bool>,
427    /// The text control accepts newlines.
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub multiline: Option<bool>,
430    /// The control requires a value or selection before submission.
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub required: Option<bool>,
433    /// The composite permits more than one selected descendant.
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub multiselectable: Option<bool>,
436    /// Layout direction of a composite widget.
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub orientation: Option<Orientation>,
439    /// Heading or tree depth, starting at 1.
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub level: Option<i64>,
442    /// One-based position among siblings.
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub position_in_set: Option<i64>,
445    /// Number of siblings in the set.
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub set_size: Option<i64>,
448}
449
450impl State {
451    /// Whether every member is unset, in which case the field is omitted.
452    pub fn is_empty(&self) -> bool {
453        *self == State::default()
454    }
455}
456
457/// Maps grapheme offsets of a node's text onto cell coordinates.
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
459#[serde(rename_all = "camelCase", deny_unknown_fields)]
460pub struct TextRange {
461    /// First grapheme offset covered by `rect`.
462    pub start_offset: i64,
463    /// Offset just past the last grapheme covered by `rect`.
464    pub end_offset: i64,
465    /// Cells the offset span occupies.
466    pub rect: Rect,
467}
468
469/// Semantic intent backed by a physical PTY-input recipe.
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
471#[serde(rename_all = "camelCase")]
472pub enum PhysicalInputRecipeAction {
473    /// Move authoritative application focus to the target.
474    Focus,
475    /// Invoke the target's primary action.
476    Activate,
477    /// Change the target's checked/toggled state.
478    Toggle,
479    /// Replace the target's editable value.
480    SetValue,
481}
482
483/// One data-only recipe step; no framework callback can cross this boundary.
484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
485#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
486pub enum PhysicalInputRecipeStep {
487    /// Press one terminal key through the real PTY input device.
488    Press {
489        /// Key descriptor interpreted by Termwright's keyboard device.
490        key: String,
491    },
492    /// Insert the value supplied to the current semantic action at execution time.
493    InsertActionValue,
494}
495
496/// A revision-bound, data-only physical strategy for one semantic intent.
497#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
498#[serde(rename_all = "camelCase", deny_unknown_fields)]
499pub struct PhysicalInputRecipe {
500    /// Semantic intent implemented by this recipe.
501    pub action: PhysicalInputRecipeAction,
502    /// Whether the target must already be focused before executing the steps.
503    pub requires_focus: bool,
504    /// Ordered real keyboard operations. At least one step is required.
505    pub steps: Vec<PhysicalInputRecipeStep>,
506}
507
508/// One accessible node with evidence-qualified geometry.
509#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
510#[serde(rename_all = "camelCase", deny_unknown_fields)]
511pub struct Node {
512    /// Stable identity within the session.
513    pub id: String,
514    /// Parent node, or `None` for a root.
515    #[serde(skip_serializing_if = "Option::is_none")]
516    pub parent_id: Option<String>,
517    /// Semantic role from the current closed set.
518    pub role: Role,
519    /// Accessible name; empty when the node has none.
520    pub name: String,
521    /// Longer description, when one exists.
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub description: Option<String>,
524    /// Current value of a value-bearing node, without collapsing secrets.
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub value: Option<SemanticValueObservation>,
527    /// Asserted state flags; unset members are not claims.
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub state: Option<State>,
530    /// Application-defined JSON state, separate from portable state flags.
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub extended: Option<BTreeMap<String, Value>>,
533    /// Capability hints, never callback endpoints.
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub actions: Option<Vec<Action>>,
536    /// Authoritative physical recipe executed only by Termwright devices.
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub input_recipes: Option<Vec<PhysicalInputRecipe>>,
539    /// Ids of nodes that name this one.
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub labelled_by: Option<Vec<String>>,
542    /// Ids of nodes that describe this one.
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub described_by: Option<Vec<String>>,
545    /// Offset-to-cell mapping for this node's text.
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub text_ranges: Option<Vec<TextRange>>,
548    /// Author-supplied test id.
549    #[serde(skip_serializing_if = "Option::is_none")]
550    pub test_id: Option<String>,
551    /// What the UI framework calls this widget. Required when `role` is
552    /// [`Role::Generic`]: an unrecognised widget must at least name its own
553    /// type, so a reader can tell one unknown thing from another.
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub framework_type: Option<String>,
556    /// This node may own children the framework cannot enumerate.
557    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
558    pub opaque_children: bool,
559    /// Where this node's facts came from, as a whole.
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub p: Option<Provenance>,
562    /// Where individual fields came from, when they differ from `p`.
563    #[serde(skip_serializing_if = "Option::is_none")]
564    pub px: Option<BTreeMap<String, Provenance>>,
565    /// Qualified layout facts for this committed observation.
566    pub geometry: NodeGeometryObservations,
567    /// Production application viewport state, not terminal scrollback.
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub scroll: Option<Observation<ScrollState>>,
570    /// Exact production-painted cells, distinct from layout and pointer ownership.
571    #[serde(skip_serializing_if = "Option::is_none")]
572    pub painted_region: Option<Observation<SemanticPaintedRegion>>,
573}
574
575/// Application viewport state in production-defined logical units.
576#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
577#[serde(rename_all = "camelCase", deny_unknown_fields)]
578pub struct ScrollState {
579    /// Logical scroll axis.
580    pub axis: Orientation,
581    /// First visible logical unit.
582    pub offset: i64,
583    /// Visible logical units.
584    pub viewport: i64,
585    /// Total logical units.
586    pub extent: i64,
587}
588
589/// Where a semantic fact came from. Closed set.
590#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
591#[serde(rename_all = "lowercase")]
592pub enum Provenance {
593    /// What the author wrote.
594    Annotation,
595    /// What our rules concluded.
596    Recognizer,
597    /// What the framework itself reported.
598    Framework,
599    /// What the application's production mechanism reported.
600    Application,
601    /// What matching across sources implied.
602    Correlation,
603    /// A guess that happened to be useful.
604    Heuristic,
605}
606
607impl Node {
608    /// A node with only the required fields set.
609    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    /// Name what the framework calls this widget, which the protocol requires
649    /// for a [`Role::Generic`] node.
650    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    /// Declare that this node may own children the probe cannot enumerate.
656    #[must_use]
657    pub fn with_opaque_children(mut self) -> Self {
658        self.opaque_children = true;
659        self
660    }
661
662    /// Attach this node to a parent.
663    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    /// Set evidence-qualified layout facts.
669    pub fn with_geometry(mut self, geometry: NodeGeometryObservations) -> Self {
670        self.geometry = geometry;
671        self
672    }
673
674    /// Set the state flags, dropping them when nothing is asserted.
675    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    /// Attach application-defined JSON state.
681    pub fn with_extended(mut self, extended: BTreeMap<String, Value>) -> Self {
682        self.extended = Some(extended);
683        self
684    }
685
686    /// Declare which actions the node supports.
687    pub fn with_actions(mut self, actions: Vec<Action>) -> Self {
688        self.actions = Some(actions);
689        self
690    }
691
692    /// Set the author-supplied test id.
693    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/// Terminal cursor position, in viewport cells.
700#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
701#[serde(deny_unknown_fields)]
702pub struct Cursor {
703    /// Zero-based row of the top edge.
704    pub row: i64,
705    /// Zero-based column of the left edge.
706    pub column: i64,
707    /// Whether the terminal is showing the cursor.
708    pub visible: bool,
709    /// Cursor rendering style, when the app sets one.
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub shape: Option<CursorShape>,
712}
713
714/// The whole tree for one committed render.
715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
716#[serde(rename_all = "camelCase", deny_unknown_fields)]
717pub struct Snapshot {
718    /// Snapshot format version; always 2.
719    pub v: u8,
720    /// Session this snapshot belongs to.
721    pub session_id: String,
722    /// Render revision, strictly increasing per session.
723    pub revision: i64,
724    /// Viewport width in cells.
725    pub columns: i64,
726    /// Viewport height in cells.
727    pub rows: i64,
728    /// Cursor position, when the app reports one.
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub cursor: Option<Cursor>,
731    /// Ids of the parentless nodes, in document order.
732    pub root_ids: Vec<String>,
733    /// Every node in the tree.
734    pub nodes: Vec<Node>,
735    /// Qualified coordinate space for all known geometry.
736    pub coordinate_space: Observation<String>,
737    /// Exact fresh-pointer ownership map, or an explicit non-known result.
738    pub hit_grid: Observation<PointerHitGrid>,
739    /// Application evidence collected atomically for this semantic revision.
740    #[serde(default, skip_serializing_if = "Vec::is_empty")]
741    pub provider_evidence: Vec<ProviderRevisionEvidence>,
742}
743
744impl Snapshot {
745    /// An empty snapshot for a viewport. The session id and revision are
746    /// filled in by [`crate::Client::publish`].
747    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    /// Append a node, recording it as a root when it declares no parent.
775    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}