fix(tui): RAII TerminalGuard prevents terminal corruption on error exit (closes #8)

The previous code called std::process::exit(1) on file load failure,
bypassing all terminal restoration (disable_raw_mode, LeaveAlternateScreen,
show_cursor). This left the user's shell in a broken state with no echo
and no visible cursor.

Introduce TerminalGuard with Drop-based cleanup that fires on every exit
path: normal return, ? error propagation, and panic unwind. The guard
also handles partial initialization rollback (e.g. raw mode enabled but
alternate screen fails). Remove all process::exit calls from the guarded
scope.
This commit is contained in:
dailz
2026-06-04 16:11:16 +08:00
parent d40d70c600
commit b7938e069d

View File

@@ -1,3 +1,6 @@
use std::time::Duration;
use anyhow::Context;
use clap::Parser;
mod app;
@@ -11,44 +14,93 @@ struct Cli {
files: Vec<String>,
}
// ── 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<std::io::Stdout>;
struct TerminalGuard {
terminal: ratatui::Terminal<Backend>,
}
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<Self> {
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<Backend> {
&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();
crossterm::terminal::enable_raw_mode()?;
let mut stdout = std::io::stdout();
crossterm::execute!(stdout, crossterm::terminal::EnterAlternateScreen)?;
let backend = ratatui::backend::CrosstermBackend::new(stdout);
let mut terminal = ratatui::Terminal::new(backend)?;
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()
&& let Err(e) = app.load_file(file)
{
eprintln!("Error loading file: {e}");
std::process::exit(1);
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();
terminal.draw(|frame| ui::render(frame, &mut app))?;
if crossterm::event::poll(std::time::Duration::from_millis(100))? {
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(_w, _h) => {}
crossterm::event::Event::Resize(_, _) => {}
_ => {}
}
}
}
crossterm::terminal::disable_raw_mode()?;
crossterm::execute!(
terminal.backend_mut(),
crossterm::terminal::LeaveAlternateScreen
)?;
terminal.show_cursor()?;
Ok(())
}