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}