use std::time::Duration; use anyhow::Context; use clap::Parser; mod app; mod color; mod ui; #[derive(Parser)] #[command(name = "log-viewer", about = "A log viewer TUI")] struct Cli { /// Log files to open files: Vec, } // ── RAII terminal guard ──────────────────────────────────────────── // // Holds the ratatui Terminal and guarantees terminal state restoration // (disable raw mode, leave alternate screen, show cursor) on **every** // exit path: normal return, `?` error propagation, and panic unwind. // // IMPORTANT: `Drop` does **not** run on `std::process::exit` or // `panic = "abort"`. Therefore we must never call `process::exit` // inside the guarded scope — use `?` to return `Err` instead. type Backend = ratatui::backend::CrosstermBackend; struct TerminalGuard { terminal: ratatui::Terminal, } impl TerminalGuard { /// Enable raw mode, enter alternate screen, and create the terminal. /// On partial failure, rolls back any steps that already succeeded. fn enter() -> anyhow::Result { crossterm::terminal::enable_raw_mode() .map_err(|e| anyhow::anyhow!("enable_raw_mode: {e}"))?; let mut stdout = std::io::stdout(); if let Err(e) = crossterm::execute!(stdout, crossterm::terminal::EnterAlternateScreen) { let _ = crossterm::terminal::disable_raw_mode(); return Err(anyhow::anyhow!("EnterAlternateScreen: {e}")); } let backend = ratatui::backend::CrosstermBackend::new(stdout); match ratatui::Terminal::new(backend) { Ok(terminal) => Ok(Self { terminal }), Err(e) => { // Roll back: leave alternate screen + disable raw mode let _ = crossterm::execute!( std::io::stdout(), crossterm::terminal::LeaveAlternateScreen ); let _ = crossterm::terminal::disable_raw_mode(); Err(anyhow::anyhow!("Terminal::new: {e}")) } } } fn terminal(&mut self) -> &mut ratatui::Terminal { &mut self.terminal } } impl Drop for TerminalGuard { fn drop(&mut self) { // Best-effort cleanup; suppress errors (Drop must not panic). let _ = crossterm::terminal::disable_raw_mode(); let _ = crossterm::execute!( self.terminal.backend_mut(), crossterm::terminal::LeaveAlternateScreen ); let _ = self.terminal.show_cursor(); } } // ── main ─────────────────────────────────────────────────────────── fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let mut guard = TerminalGuard::enter()?; let mut app = app::App::new(); app.color_config = log_viewer_core::config::ColorConfig::load(); if let Some(file) = cli.files.first() { app.load_file(file) .with_context(|| format!("loading file {file}"))?; } while !app.should_quit { app.poll_background_indexer(); app.poll_file_watcher(); guard.terminal().draw(|frame| ui::render(frame, &mut app))?; if crossterm::event::poll(Duration::from_millis(100))? { match crossterm::event::read()? { crossterm::event::Event::Key(key) => app.handle_key(key), crossterm::event::Event::Resize(_, _) => {} _ => {} } } } Ok(()) }