Skip to main content

agent_tui/detection/
framework.rs

1//! TUI framework auto-detection
2//!
3//! Detects which TUI framework is being used based on visual patterns
4//! and common UI signatures in the terminal output.
5
6/// Known TUI frameworks
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Framework {
9    /// Ink (React for CLI)
10    Ink,
11    /// Blessed (Node.js curses-like)
12    Blessed,
13    /// Bubble Tea (Go)
14    BubbleTea,
15    /// Textual (Python)
16    Textual,
17    /// ncurses/curses
18    Ncurses,
19    /// Inquirer.js
20    Inquirer,
21    /// Prompts (Node.js)
22    Prompts,
23    /// Charm (Go libraries)
24    Charm,
25    /// Ratatui (Rust)
26    Ratatui,
27    /// Crossterm/Termion (Rust)
28    CrosstermRust,
29    /// Unknown framework
30    Unknown,
31}
32
33impl Framework {
34    /// Get the framework name as a string
35    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
52/// Detect the TUI framework based on screen content and patterns
53pub fn detect_framework(screen: &str) -> Framework {
54    // Check for Inquirer patterns first (more specific with (Y/n) confirm patterns)
55    if is_inquirer(screen) {
56        return Framework::Inquirer;
57    }
58
59    // Check for Ink patterns
60    if is_ink(screen) {
61        return Framework::Ink;
62    }
63
64    // Check for Prompts patterns
65    if is_prompts(screen) {
66        return Framework::Prompts;
67    }
68
69    // Check for Bubble Tea / Charm patterns
70    if is_bubbletea(screen) {
71        return Framework::BubbleTea;
72    }
73
74    // Check for Textual patterns
75    if is_textual(screen) {
76        return Framework::Textual;
77    }
78
79    // Check for Blessed patterns
80    if is_blessed(screen) {
81        return Framework::Blessed;
82    }
83
84    // Check for Ratatui patterns
85    if is_ratatui(screen) {
86        return Framework::Ratatui;
87    }
88
89    // Check for ncurses patterns (generic)
90    if is_ncurses(screen) {
91        return Framework::Ncurses;
92    }
93
94    Framework::Unknown
95}
96
97/// Check for Ink framework patterns
98fn is_ink(screen: &str) -> bool {
99    // Ink uses specific patterns:
100    // - Braille spinners: ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏
101    // - Select indicators: ❯, › (required for select components)
102    // - Checkbox: ◉, ◯ (but only when combined with select indicators)
103    // - Specific question/answer patterns
104
105    // Braille spinners are strongly Ink-specific
106    let braille_spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
107    let has_braille = braille_spinners.iter().any(|s| screen.contains(s));
108
109    // Select pointer indicators
110    let has_select_pointer = screen.contains("❯") || screen.contains("›");
111
112    // Ink select component: pointer indicator with text
113    let has_ink_select = has_select_pointer
114        && screen
115            .lines()
116            .any(|l| l.trim().starts_with('❯') || l.trim().starts_with('›'));
117
118    // Ink question pattern: "? Question text" with pointer indicator
119    let has_question_pattern = screen.contains("?") && has_select_pointer;
120
121    // Strong indicator: braille spinner
122    if has_braille {
123        return true;
124    }
125
126    // Ink select: pointer indicator at start of line
127    if has_ink_select {
128        return true;
129    }
130
131    // Ink question with pointer
132    has_question_pattern
133}
134
135/// Check for Inquirer.js patterns
136fn is_inquirer(screen: &str) -> bool {
137    // Inquirer.js specific patterns:
138    // - Green checkmarks: ✔
139    // - Cyan pointers: ❯
140    // - Yellow warning: ⚠
141    // - Radio buttons: ◯, ◉
142    // - Specific spacing and formatting
143    // - Question mark at start of line followed by content
144
145    // Inquirer often has ◯ and ◉ on separate lines (one per option)
146    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
159/// Check for Prompts (Node.js) patterns
160fn is_prompts(screen: &str) -> bool {
161    // Prompts uses specific patterns:
162    // - Toggle: ◉ / ○
163    // - Select: › (no-color mode) or colored version
164    // - Confirm: yes/no toggle
165
166    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
173/// Check for Bubble Tea patterns
174fn is_bubbletea(screen: &str) -> bool {
175    // Bubble Tea / Charm patterns:
176    // - Often uses lipgloss styling
177    // - Glamour markdown rendering
178    // - Specific spinner characters
179    // - Common UI patterns from charm libraries
180
181    let charm_spinners = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
182    let has_charm_spinner = charm_spinners.iter().any(|s| screen.contains(s));
183
184    // Bubble Tea apps often have help text at the bottom with specific formatting
185    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    // Bubble Tea text inputs often show cursor with │
191    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
196/// Check for Textual (Python) patterns
197fn is_textual(screen: &str) -> bool {
198    // Textual patterns:
199    // - Uses rich box drawing characters
200    // - Specific footer bar patterns
201    // - CSS-like styling results in specific visual patterns
202
203    // Textual apps often have a footer with keybindings
204    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    // Textual uses specific box drawing for borders
209    let has_heavy_borders = screen.contains("┏") && screen.contains("┓") && screen.contains("┗");
210
211    // Textual data tables have specific patterns
212    let has_data_table = screen.contains("│") && screen.contains("─") && screen.contains("┼");
213
214    has_textual_footer || has_heavy_borders || has_data_table
215}
216
217/// Check for Blessed patterns
218fn is_blessed(screen: &str) -> bool {
219    // Blessed patterns:
220    // - Uses ACS characters for box drawing
221    // - Specific scrollbar patterns
222    // - Form widget patterns
223
224    // Blessed scrollbars use specific characters
225    let has_scrollbar = screen.contains("▒") || screen.contains("░");
226
227    // Blessed forms have specific input patterns
228    let has_blessed_input =
229        screen.contains("[") && screen.contains("]") && screen.contains("_____");
230
231    // Blessed uses specific box styles
232    let has_blessed_box = screen.contains("┌─") && screen.contains("─┐") && screen.contains("└─");
233
234    (has_scrollbar && has_blessed_box) || has_blessed_input
235}
236
237/// Check for Ratatui patterns
238fn is_ratatui(screen: &str) -> bool {
239    // Ratatui patterns:
240    // - Rust-style panic messages if crashed
241    // - Specific block rendering patterns
242    // - Often uses specific Unicode block characters
243
244    // Ratatui gauge/progress uses block characters
245    let block_chars = ["█", "▓", "▒", "░", "▏", "▎", "▍", "▌", "▋", "▊", "▉"];
246    let block_count = block_chars.iter().filter(|c| screen.contains(*c)).count();
247
248    // Ratatui sparklines use specific characters
249    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
259/// Check for ncurses patterns (generic curses-based apps)
260fn is_ncurses(screen: &str) -> bool {
261    // Generic ncurses patterns:
262    // - ACS line drawing characters
263    // - Function key hints (F1, F2, etc.)
264    // - Specific menu patterns
265
266    let has_function_keys = screen.contains("F1")
267        || screen.contains("F2")
268        || screen.contains("F10")
269        || screen.contains("^X");
270
271    // ncurses often uses simple ASCII box drawing or ACS
272    let has_simple_box = screen.contains("+-")
273        && screen.contains("-+")
274        && (screen.contains("|") || screen.contains("│"));
275
276    // htop, top, mc style menus
277    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}