Skip to main content

stynx_code_tui/
lib.rs

1pub mod clipboard;
2pub mod dialogs;
3pub mod event;
4pub mod layout;
5pub mod persistence;
6pub mod render;
7pub mod state;
8pub mod theme;
9pub mod tool_ui_impl;
10pub mod util;
11pub mod widgets;
12
13pub use event::event_handler::{EventHandler, UiAction};
14pub use render::renderer::Renderer;
15pub use state::{
16    AppState, ConversationState, DialogOption, DisplayMessage, DisplayToolUse, InputMode,
17    InputState, ModalKind, ModalState, PermissionChoice, SelectKind, ToolUseStatus,
18};
19
20use std::io;
21use std::sync::atomic::{AtomicBool, Ordering};
22
23use crossterm::{
24    event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture},
25    execute,
26    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
27};
28use ratatui::{Terminal, backend::CrosstermBackend};
29
30pub struct TuiApp {
31    terminal: Terminal<CrosstermBackend<io::Stdout>>,
32    pub state: AppState,
33    in_alt: bool,
34}
35
36/// Restore the terminal to a usable state. Safe to call multiple times.
37pub fn restore_terminal() {
38    let _ = disable_raw_mode();
39    let _ = execute!(
40        io::stdout(),
41        DisableMouseCapture,
42        DisableBracketedPaste,
43        LeaveAlternateScreen,
44    );
45}
46
47fn install_panic_hook() {
48    use std::sync::Once;
49    static INSTALLED: Once = Once::new();
50    INSTALLED.call_once(|| {
51        let prev = std::panic::take_hook();
52        std::panic::set_hook(Box::new(move |info| {
53            restore_terminal();
54            prev(info);
55        }));
56    });
57}
58
59impl TuiApp {
60    pub fn new() -> io::Result<Self> {
61        install_panic_hook();
62        enable_raw_mode()?;
63        let mut stdout = io::stdout();
64        execute!(stdout, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste)?;
65        let backend = CrosstermBackend::new(stdout);
66        let terminal = Terminal::new(backend)?;
67        Ok(Self { terminal, state: AppState::new(), in_alt: true })
68    }
69
70    pub fn draw(&mut self) -> io::Result<()> {
71        let Self { terminal, state, .. } = self;
72        terminal.draw(|frame| Renderer::draw(frame, state))?;
73        Ok(())
74    }
75
76    pub fn leave_alt(&mut self) {
77        if self.in_alt {
78            disable_raw_mode().ok();
79            execute!(self.terminal.backend_mut(), DisableMouseCapture, DisableBracketedPaste, LeaveAlternateScreen).ok();
80            self.in_alt = false;
81        }
82    }
83
84    pub fn enter_alt(&mut self) {
85        if !self.in_alt {
86            enable_raw_mode().ok();
87            execute!(self.terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste).ok();
88            self.terminal.clear().ok();
89            self.in_alt = true;
90        }
91    }
92
93    pub fn is_in_alt(&self) -> bool { self.in_alt }
94
95    pub fn tick_spinner(&mut self) {
96        if self.state.is_streaming {
97            self.state.spinner_tick = self.state.spinner_tick.wrapping_add(1);
98            if self.state.spinner_tick % 8 == 0 {
99                self.state.spinner_frame = self.state.spinner_frame.wrapping_add(1) % 10;
100            }
101        }
102        self.state.toasts.tick();
103    }
104
105    pub fn sync_pause(&mut self, paused: &AtomicBool) {
106        if paused.load(Ordering::Relaxed) {
107            self.leave_alt();
108        } else if !self.in_alt {
109            self.enter_alt();
110        }
111    }
112}
113
114impl Drop for TuiApp {
115    fn drop(&mut self) { self.leave_alt(); }
116}