Skip to main content

tuillem_tui/
lib.rs

1pub mod app;
2pub mod commands;
3pub mod control;
4pub mod conversation;
5pub mod help;
6pub mod input;
7pub mod settings;
8pub mod sidebar;
9pub mod theme;
10
11use std::time::Duration;
12
13use crossterm::{
14    event::{
15        self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
16        Event as CrosstermEvent, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
17        PushKeyboardEnhancementFlags,
18    },
19    execute,
20    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
21};
22use ratatui::{Terminal, backend::CrosstermBackend};
23use tokio::sync::mpsc;
24use tuillem_core::actions::Event;
25
26use tracing::debug;
27
28use crate::app::App;
29
30pub async fn run(
31    mut app: App,
32    mut event_rx: mpsc::UnboundedReceiver<Event>,
33    mouse_enabled: bool,
34) -> anyhow::Result<()> {
35    // Check minimum terminal size (60x20)
36    let (cols, rows) = crossterm::terminal::size()?;
37    if cols < 60 || rows < 20 {
38        anyhow::bail!("Terminal too small ({cols}x{rows}). Minimum size is 60x20.");
39    }
40
41    // Setup terminal
42    enable_raw_mode()?;
43    let mut stdout = std::io::stdout();
44
45    // Enable keyboard enhancement for Shift+Enter detection
46    let keyboard_enhanced = execute!(
47        stdout,
48        PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES)
49    )
50    .is_ok();
51
52    if mouse_enabled {
53        execute!(
54            stdout,
55            EnterAlternateScreen,
56            EnableMouseCapture,
57            EnableBracketedPaste
58        )?;
59    } else {
60        execute!(stdout, EnterAlternateScreen, EnableBracketedPaste)?;
61    }
62    let backend = CrosstermBackend::new(stdout);
63    let mut terminal = Terminal::new(backend)?;
64
65    // Main loop
66    loop {
67        // Force full redraw if needed (e.g. after external editor)
68        if app.needs_redraw {
69            terminal.clear()?;
70            app.needs_redraw = false;
71        }
72
73        // Draw
74        terminal.draw(|frame| {
75            app.draw(frame);
76        })?;
77
78        // Process backend events (non-blocking)
79        let mut event_count = 0;
80        while let Ok(event) = event_rx.try_recv() {
81            debug!("TUI received event: {:?}", event);
82            app.apply_event(&event);
83            event_count += 1;
84        }
85        if event_count > 0 {
86            debug!("TUI processed {} events this frame", event_count);
87        }
88
89        // Poll terminal events with 16ms timeout (~60fps)
90        if event::poll(Duration::from_millis(16))? {
91            match event::read()? {
92                CrosstermEvent::Key(key) => {
93                    app.handle_key_event(key);
94                }
95                CrosstermEvent::Mouse(mouse) => {
96                    app.handle_mouse_event(mouse);
97                }
98                CrosstermEvent::Paste(text) => {
99                    app.handle_paste(text);
100                }
101                _ => {}
102            }
103        }
104
105        if app.should_quit {
106            break;
107        }
108    }
109
110    // Cleanup
111    if keyboard_enhanced {
112        let _ = execute!(terminal.backend_mut(), PopKeyboardEnhancementFlags);
113    }
114    disable_raw_mode()?;
115    if mouse_enabled {
116        execute!(
117            terminal.backend_mut(),
118            DisableMouseCapture,
119            DisableBracketedPaste,
120            LeaveAlternateScreen
121        )?;
122    } else {
123        execute!(
124            terminal.backend_mut(),
125            DisableBracketedPaste,
126            LeaveAlternateScreen
127        )?;
128    }
129    terminal.show_cursor()?;
130
131    Ok(())
132}