1pub mod animation;
31pub mod app;
32pub mod layout;
33pub mod renderer;
34pub mod results;
35pub mod theme;
36
37use crossterm::{
38 event::{self, Event, KeyCode, KeyModifiers},
39 execute,
40 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
41};
42use ratatui::{backend::CrosstermBackend, Terminal};
43use std::io;
44use std::sync::atomic::{AtomicBool, Ordering};
45use std::sync::Arc;
46
47use app::App;
48use layout::render_adaptive;
49
50pub struct TuiManager {
52 terminal: Arc<std::sync::Mutex<Terminal<CrosstermBackend<io::Stdout>>>>,
53 app: Arc<std::sync::Mutex<App>>,
54 should_exit: Arc<AtomicBool>,
55 render_thread: Option<std::thread::JoinHandle<()>>,
56}
57
58impl TuiManager {
59 pub fn new() -> io::Result<Self> {
61 enable_raw_mode()?;
62 let mut stdout = io::stdout();
63 execute!(stdout, EnterAlternateScreen)?;
64
65 let backend = CrosstermBackend::new(stdout);
66 let terminal = Terminal::new(backend)?;
67
68 let should_exit = Arc::new(AtomicBool::new(false));
69 let terminal = Arc::new(std::sync::Mutex::new(terminal));
70 let app = Arc::new(std::sync::Mutex::new(App::new()));
71
72 let exit_flag = should_exit.clone();
74 std::thread::spawn(move || {
75 loop {
76 if exit_flag.load(Ordering::Relaxed) {
77 break;
78 }
79
80 if event::poll(std::time::Duration::from_millis(100)).unwrap_or(false) {
82 if let Ok(Event::Key(key)) = event::read() {
83 if key.code == KeyCode::Char('c')
85 && key.modifiers.contains(KeyModifiers::CONTROL)
86 {
87 let _ = disable_raw_mode();
89 let _ = execute!(io::stdout(), LeaveAlternateScreen);
90 eprintln!("\nInterrupted by user");
91 std::process::exit(130); }
93 if key.code == KeyCode::Char('z')
94 && key.modifiers.contains(KeyModifiers::CONTROL)
95 {
96 let _ = disable_raw_mode();
98 let _ = execute!(io::stdout(), LeaveAlternateScreen);
99 eprintln!("\nSuspended by user");
100 std::process::exit(148); }
102 }
103 }
104 }
105 });
106
107 let render_terminal = terminal.clone();
109 let render_app = app.clone();
110 let render_exit_flag = should_exit.clone();
111
112 let render_thread = std::thread::spawn(move || {
113 let frame_duration = std::time::Duration::from_millis(16); loop {
116 if render_exit_flag.load(Ordering::Relaxed) {
117 break;
118 }
119
120 if let (Ok(mut terminal), Ok(mut app)) = (render_terminal.lock(), render_app.lock())
122 {
123 app.tick();
124 let _ = terminal.draw(|f| render_adaptive(f, &app));
125 }
126
127 std::thread::sleep(frame_duration);
128 }
129 });
130
131 Ok(Self {
132 terminal,
133 app,
134 should_exit,
135 render_thread: Some(render_thread),
136 })
137 }
138
139 pub fn render(&mut self) -> io::Result<()> {
141 Ok(())
144 }
145
146 pub fn app_mut(&mut self) -> std::sync::MutexGuard<'_, App> {
148 self.app.lock().unwrap()
149 }
150
151 pub fn app(&self) -> Arc<std::sync::Mutex<App>> {
153 self.app.clone()
154 }
155
156 pub fn cleanup(&mut self) -> io::Result<()> {
158 self.should_exit.store(true, Ordering::Relaxed);
160
161 if let Some(handle) = self.render_thread.take() {
163 let _ = handle.join();
164 }
165
166 std::thread::sleep(std::time::Duration::from_millis(50));
168
169 disable_raw_mode()?;
170 if let Ok(mut terminal) = self.terminal.lock() {
171 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
172 terminal.show_cursor()?;
173 }
174 Ok(())
175 }
176}
177
178impl Drop for TuiManager {
179 fn drop(&mut self) {
180 let _ = self.cleanup();
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_app_initialization() {
190 let app = App::new();
191 assert_eq!(app.stages.len(), 6); assert_eq!(app.overall_progress, 0.0);
193 }
194}