agent_tui/core/vom/
mod.rs

1pub mod classifier;
2pub mod patterns;
3pub mod segmentation;
4pub mod snapshot;
5
6#[cfg(test)]
7mod integration_tests;
8
9use std::hash::Hash;
10use std::hash::Hasher;
11
12use uuid::Uuid;
13
14use crate::core::screen::ScreenGrid;
15use crate::core::style::CellStyle;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub struct Rect {
19    pub x: u16,
20    pub y: u16,
21    pub width: u16,
22    pub height: u16,
23}
24
25impl Rect {
26    pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
27        Self {
28            x,
29            y,
30            width,
31            height,
32        }
33    }
34
35    pub fn contains(&self, x: u16, y: u16) -> bool {
36        x >= self.x
37            && x < self.x.saturating_add(self.width)
38            && y >= self.y
39            && y < self.y.saturating_add(self.height)
40    }
41}
42
43#[derive(Debug, Clone)]
44pub struct Cluster {
45    pub rect: Rect,
46    pub text: String,
47    pub style: CellStyle,
48    pub is_whitespace: bool,
49}
50
51impl Cluster {
52    pub fn new(x: u16, y: u16, char: char, style: CellStyle) -> Self {
53        Self {
54            rect: Rect::new(x, y, 1, 1),
55            text: char.to_string(),
56            style,
57            is_whitespace: false,
58        }
59    }
60
61    pub fn extend(&mut self, char: char) {
62        self.text.push(char);
63        self.rect.width = self.rect.width.saturating_add(1);
64    }
65
66    pub fn seal(&mut self) {
67        self.is_whitespace = self.text.trim().is_empty();
68    }
69}
70
71#[derive(Debug, Clone)]
72pub struct Component {
73    pub id: Uuid,
74    pub role: Role,
75    pub bounds: Rect,
76    pub text_content: String,
77    pub visual_hash: u64,
78    pub selected: bool,
79}
80
81impl Component {
82    pub fn new(role: Role, bounds: Rect, text_content: String, visual_hash: u64) -> Self {
83        Self {
84            id: Uuid::new_v4(),
85            role,
86            bounds,
87            text_content,
88            visual_hash,
89            selected: false,
90        }
91    }
92
93    pub fn with_selected(
94        role: Role,
95        bounds: Rect,
96        text_content: String,
97        visual_hash: u64,
98        selected: bool,
99    ) -> Self {
100        Self {
101            id: Uuid::new_v4(),
102            role,
103            bounds,
104            text_content,
105            visual_hash,
106            selected,
107        }
108    }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112pub enum Role {
113    Button,
114    Tab,
115    Input,
116    StaticText,
117    Panel,
118    Checkbox,
119    MenuItem,
120    Status,
121    ToolBlock,
122    PromptMarker,
123    ProgressBar,
124    Link,
125    ErrorMessage,
126    DiffLine,
127    CodeBlock,
128}
129
130impl Role {
131    pub fn is_interactive(&self) -> bool {
132        matches!(
133            self,
134            Role::Button
135                | Role::Tab
136                | Role::Input
137                | Role::Checkbox
138                | Role::MenuItem
139                | Role::PromptMarker
140                | Role::Link
141        )
142    }
143}
144
145impl std::fmt::Display for Role {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        match self {
148            Role::Button => write!(f, "button"),
149            Role::Tab => write!(f, "tab"),
150            Role::Input => write!(f, "input"),
151            Role::StaticText => write!(f, "text"),
152            Role::Panel => write!(f, "panel"),
153            Role::Checkbox => write!(f, "checkbox"),
154            Role::MenuItem => write!(f, "menuitem"),
155            Role::Status => write!(f, "status"),
156            Role::ToolBlock => write!(f, "toolblock"),
157            Role::PromptMarker => write!(f, "prompt"),
158            Role::ProgressBar => write!(f, "progressbar"),
159            Role::Link => write!(f, "link"),
160            Role::ErrorMessage => write!(f, "error"),
161            Role::DiffLine => write!(f, "diff"),
162            Role::CodeBlock => write!(f, "codeblock"),
163        }
164    }
165}
166
167pub use classifier::ClassifyOptions;
168pub use classifier::classify;
169pub use segmentation::segment_buffer;
170
171pub fn analyze(buffer: &impl ScreenGrid, cursor: &super::CursorPosition) -> Vec<Component> {
172    let clusters = segment_buffer(buffer);
173    classify(clusters, cursor, &ClassifyOptions::default())
174}
175
176pub fn hash_cluster(cluster: &Cluster) -> u64 {
177    let mut hasher = std::collections::hash_map::DefaultHasher::new();
178    cluster.rect.hash(&mut hasher);
179    cluster.text.hash(&mut hasher);
180    cluster.style.hash(&mut hasher);
181    hasher.finish()
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_rect_contains() {
190        let rect = Rect::new(10, 5, 20, 10);
191        assert!(rect.contains(10, 5));
192        assert!(rect.contains(15, 8));
193        assert!(!rect.contains(30, 5));
194        assert!(!rect.contains(10, 15));
195        assert!(!rect.contains(5, 5));
196    }
197
198    #[test]
199    fn test_cluster_extend() {
200        let mut cluster = Cluster::new(0, 0, 'H', CellStyle::default());
201        cluster.extend('i');
202        cluster.seal();
203        assert_eq!(cluster.text, "Hi");
204        assert_eq!(cluster.rect.width, 2);
205        assert!(!cluster.is_whitespace);
206    }
207
208    #[test]
209    fn test_cluster_whitespace() {
210        let mut cluster = Cluster::new(0, 0, ' ', CellStyle::default());
211        cluster.extend(' ');
212        cluster.seal();
213        assert!(cluster.is_whitespace);
214    }
215
216    #[test]
217    fn test_role_display() {
218        assert_eq!(Role::Button.to_string(), "button");
219        assert_eq!(Role::Tab.to_string(), "tab");
220        assert_eq!(Role::Input.to_string(), "input");
221    }
222}