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
36pub fn restore_terminal() {
37    let _ = disable_raw_mode();
38    let _ = execute!(
39        io::stdout(),
40        DisableMouseCapture,
41        DisableBracketedPaste,
42        LeaveAlternateScreen,
43    );
44}
45
46fn install_panic_hook() {
47    use std::sync::Once;
48    static INSTALLED: Once = Once::new();
49    INSTALLED.call_once(|| {
50        let prev = std::panic::take_hook();
51        std::panic::set_hook(Box::new(move |info| {
52            restore_terminal();
53            prev(info);
54        }));
55    });
56}
57
58impl TuiApp {
59    pub fn new() -> io::Result<Self> {
60        install_panic_hook();
61        enable_raw_mode()?;
62        let mut stdout = io::stdout();
63        execute!(stdout, EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste)?;
64        let backend = CrosstermBackend::new(stdout);
65        let terminal = Terminal::new(backend)?;
66        Ok(Self { terminal, state: AppState::new(), in_alt: true })
67    }
68
69    pub fn draw(&mut self) -> io::Result<()> {
70        let Self { terminal, state, .. } = self;
71        terminal.draw(|frame| Renderer::draw(frame, state))?;
72        Ok(())
73    }
74
75    pub fn leave_alt(&mut self) {
76        if self.in_alt {
77            disable_raw_mode().ok();
78            execute!(self.terminal.backend_mut(), DisableMouseCapture, DisableBracketedPaste, LeaveAlternateScreen).ok();
79            self.in_alt = false;
80        }
81    }
82
83    pub fn enter_alt(&mut self) {
84        if !self.in_alt {
85            enable_raw_mode().ok();
86            execute!(self.terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture, EnableBracketedPaste).ok();
87            self.terminal.clear().ok();
88            self.in_alt = true;
89        }
90    }
91
92    pub fn is_in_alt(&self) -> bool { self.in_alt }
93
94    pub fn tick_spinner(&mut self) {
95        if self.state.is_streaming {
96            self.state.spinner_tick = self.state.spinner_tick.wrapping_add(1);
97            if self.state.spinner_tick % 8 == 0 {
98                self.state.spinner_frame = self.state.spinner_frame.wrapping_add(1) % 10;
99            }
100        }
101        self.state.toasts.tick();
102    }
103
104    pub fn sync_pause(&mut self, paused: &AtomicBool) {
105        if paused.load(Ordering::Relaxed) {
106            self.leave_alt();
107        } else if !self.in_alt {
108            self.enter_alt();
109        }
110    }
111}
112
113impl Drop for TuiApp {
114    fn drop(&mut self) { self.leave_alt(); }
115}