agent_tui/detection/
framework.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Framework {
9 Ink,
11 Blessed,
13 BubbleTea,
15 Textual,
17 Ncurses,
19 Inquirer,
21 Prompts,
23 Charm,
25 Ratatui,
27 CrosstermRust,
29 Unknown,
31}
32
33impl Framework {
34 pub fn as_str(&self) -> &'static str {
36 match self {
37 Framework::Ink => "ink",
38 Framework::Blessed => "blessed",
39 Framework::BubbleTea => "bubbletea",
40 Framework::Textual => "textual",
41 Framework::Ncurses => "ncurses",
42 Framework::Inquirer => "inquirer",
43 Framework::Prompts => "prompts",
44 Framework::Charm => "charm",
45 Framework::Ratatui => "ratatui",
46 Framework::CrosstermRust => "crossterm",
47 Framework::Unknown => "unknown",
48 }
49 }
50}
51
52pub fn detect_framework(screen: &str) -> Framework {
54 if is_inquirer(screen) {
56 return Framework::Inquirer;
57 }
58
59 if is_ink(screen) {
61 return Framework::Ink;
62 }
63
64 if is_prompts(screen) {
66 return Framework::Prompts;
67 }
68
69 if is_bubbletea(screen) {
71 return Framework::BubbleTea;
72 }
73
74 if is_textual(screen) {
76 return Framework::Textual;
77 }
78
79 if is_blessed(screen) {
81 return Framework::Blessed;
82 }
83
84 if is_ratatui(screen) {
86 return Framework::Ratatui;
87 }
88
89 if is_ncurses(screen) {
91 return Framework::Ncurses;
92 }
93
94 Framework::Unknown
95}
96
97fn is_ink(screen: &str) -> bool {
99 let braille_spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
107 let has_braille = braille_spinners.iter().any(|s| screen.contains(s));
108
109 let has_select_pointer = screen.contains("❯") || screen.contains("›");
111
112 let has_ink_select = has_select_pointer
114 && screen
115 .lines()
116 .any(|l| l.trim().starts_with('❯') || l.trim().starts_with('›'));
117
118 let has_question_pattern = screen.contains("?") && has_select_pointer;
120
121 if has_braille {
123 return true;
124 }
125
126 if has_ink_select {
128 return true;
129 }
130
131 has_question_pattern
133}
134
135fn is_inquirer(screen: &str) -> bool {
137 let circle_lines = screen
147 .lines()
148 .filter(|l| l.trim().starts_with('◯') || l.trim().starts_with('◉'))
149 .count();
150 let has_inquirer_select = circle_lines >= 2;
151
152 let has_inquirer_checkbox = screen.contains("◻") || screen.contains("◼");
153
154 let has_inquirer_confirm = screen.contains("(Y/n)") || screen.contains("(y/N)");
155
156 has_inquirer_select || has_inquirer_checkbox || has_inquirer_confirm
157}
158
159fn is_prompts(screen: &str) -> bool {
161 let has_prompts_toggle = screen.contains("◉") && screen.contains("○");
167 let has_prompts_select =
168 screen.contains("›") && screen.lines().any(|l| l.contains("←") || l.contains("→"));
169
170 has_prompts_toggle || has_prompts_select
171}
172
173fn is_bubbletea(screen: &str) -> bool {
175 let charm_spinners = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
182 let has_charm_spinner = charm_spinners.iter().any(|s| screen.contains(s));
183
184 let has_help_bar = screen.contains("q: quit")
186 || screen.contains("ctrl+c")
187 || screen.contains("esc: back")
188 || screen.contains("enter: select");
189
190 let has_text_input = screen.contains("│") && screen.lines().any(|l| l.contains(">"));
192
193 has_charm_spinner || (has_help_bar && has_text_input)
194}
195
196fn is_textual(screen: &str) -> bool {
198 let has_textual_footer = screen.lines().last().is_some_and(|l| {
205 l.contains("^q") || l.contains("^c") || l.contains("F1") || l.contains("ESC")
206 });
207
208 let has_heavy_borders = screen.contains("┏") && screen.contains("┓") && screen.contains("┗");
210
211 let has_data_table = screen.contains("│") && screen.contains("─") && screen.contains("┼");
213
214 has_textual_footer || has_heavy_borders || has_data_table
215}
216
217fn is_blessed(screen: &str) -> bool {
219 let has_scrollbar = screen.contains("▒") || screen.contains("░");
226
227 let has_blessed_input =
229 screen.contains("[") && screen.contains("]") && screen.contains("_____");
230
231 let has_blessed_box = screen.contains("┌─") && screen.contains("─┐") && screen.contains("└─");
233
234 (has_scrollbar && has_blessed_box) || has_blessed_input
235}
236
237fn is_ratatui(screen: &str) -> bool {
239 let block_chars = ["█", "▓", "▒", "░", "▏", "▎", "▍", "▌", "▋", "▊", "▉"];
246 let block_count = block_chars.iter().filter(|c| screen.contains(*c)).count();
247
248 let sparkline_chars = ["▁", "▂", "▃", "▄", "▅", "▆", "▇"];
250 let has_sparkline = sparkline_chars
251 .iter()
252 .filter(|c| screen.contains(*c))
253 .count()
254 >= 3;
255
256 block_count >= 3 || has_sparkline
257}
258
259fn is_ncurses(screen: &str) -> bool {
261 let has_function_keys = screen.contains("F1")
267 || screen.contains("F2")
268 || screen.contains("F10")
269 || screen.contains("^X");
270
271 let has_simple_box = screen.contains("+-")
273 && screen.contains("-+")
274 && (screen.contains("|") || screen.contains("│"));
275
276 let has_menu_bar = screen
278 .lines()
279 .next()
280 .is_some_and(|l| l.contains("File") || l.contains("Help"));
281
282 (has_function_keys && has_simple_box) || has_menu_bar
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_detect_ink() {
291 let screen = "? Select a color\n ❯ Red\n Blue\n Green";
292 assert_eq!(detect_framework(screen), Framework::Ink);
293 }
294
295 #[test]
296 fn test_detect_inquirer() {
297 let screen = "? Choose an option (Y/n)\n ◯ Option 1\n ◉ Option 2";
298 assert_eq!(detect_framework(screen), Framework::Inquirer);
299 }
300
301 #[test]
302 fn test_detect_bubbletea() {
303 let screen = "My App\n\n> Select an item\n\nq: quit | enter: select";
304 assert!(matches!(
305 detect_framework(screen),
306 Framework::BubbleTea | Framework::Unknown
307 ));
308 }
309
310 #[test]
311 fn test_detect_unknown() {
312 let screen = "Hello World";
313 assert_eq!(detect_framework(screen), Framework::Unknown);
314 }
315
316 #[test]
317 fn test_detect_ratatui() {
318 let screen = "Progress: [████████░░░░░░░░] 50%\nSparkline: ▁▂▃▄▅▆▇█";
319 assert_eq!(detect_framework(screen), Framework::Ratatui);
320 }
321}