pub mod animation;
pub mod app;
pub mod layout;
pub mod renderer;
pub mod results;
pub mod theme;
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use parking_lot::Mutex;
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use app::App;
use layout::render_adaptive;
pub struct TuiManager {
terminal: Arc<Mutex<Terminal<CrosstermBackend<io::Stdout>>>>,
app: Arc<Mutex<App>>,
should_exit: Arc<AtomicBool>,
render_thread: Option<std::thread::JoinHandle<()>>,
}
impl TuiManager {
pub fn new() -> io::Result<Self> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
crate::observability::set_tui_active(true);
let backend = CrosstermBackend::new(stdout);
let terminal = Arc::new(Mutex::new(Terminal::new(backend)?));
let should_exit = Arc::new(AtomicBool::new(false));
let app = Arc::new(Mutex::new(App::new()));
Self::spawn_signal_handler(should_exit.clone());
let render_thread =
Self::spawn_render_thread(terminal.clone(), app.clone(), should_exit.clone());
Ok(Self {
terminal,
app,
should_exit,
render_thread: Some(render_thread),
})
}
fn spawn_signal_handler(exit_flag: Arc<AtomicBool>) {
std::thread::spawn(move || {
while !exit_flag.load(Ordering::Relaxed) {
if let Some(key) = Self::poll_key_event() {
Self::handle_control_key(key);
}
}
});
}
fn poll_key_event() -> Option<KeyEvent> {
event::poll(std::time::Duration::from_millis(100))
.ok()
.filter(|&ready| ready)
.and_then(|_| event::read().ok())
.and_then(|evt| match evt {
Event::Key(key) => Some(key),
_ => None,
})
}
fn handle_control_key(key: KeyEvent) {
let is_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match (key.code, is_ctrl) {
(KeyCode::Char('c'), true) => Self::exit_with_cleanup("Interrupted by user", 130),
(KeyCode::Char('z'), true) => Self::exit_with_cleanup("Suspended by user", 148),
_ => {}
}
}
fn exit_with_cleanup(message: &str, code: i32) {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen);
eprintln!("\n{}", message);
std::process::exit(code);
}
fn spawn_render_thread(
terminal: Arc<Mutex<Terminal<CrosstermBackend<io::Stdout>>>>,
app: Arc<Mutex<App>>,
exit_flag: Arc<AtomicBool>,
) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
const FRAME_DURATION: std::time::Duration = std::time::Duration::from_millis(16);
while !exit_flag.load(Ordering::Relaxed) {
{
let mut terminal = terminal.lock();
let mut app = app.lock();
app.tick();
let _ = terminal.draw(|f| render_adaptive(f, &app));
}
std::thread::sleep(FRAME_DURATION);
}
})
}
pub fn render(&mut self) -> io::Result<()> {
Ok(())
}
pub fn app_mut(&mut self) -> parking_lot::MutexGuard<'_, App> {
self.app.lock()
}
pub fn app(&self) -> Arc<Mutex<App>> {
self.app.clone()
}
pub fn cleanup(&mut self) -> io::Result<()> {
self.should_exit.store(true, Ordering::Relaxed);
if let Some(handle) = self.render_thread.take() {
let _ = handle.join();
}
std::thread::sleep(std::time::Duration::from_millis(50));
disable_raw_mode()?;
let mut terminal = self.terminal.lock();
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
crate::observability::set_tui_active(false);
Ok(())
}
}
impl Drop for TuiManager {
fn drop(&mut self) {
let _ = self.cleanup();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_initialization() {
let app = App::new();
assert_eq!(app.stages.len(), 6); assert_eq!(app.overall_progress, 0.0);
}
}