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() {
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}