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}