feat: handle crashes better for the terminal state

This commit is contained in:
Lukas Wölfer
2026-01-24 08:46:31 +01:00
parent e8e68682d8
commit 15c792fc21

View File

@@ -1,6 +1,7 @@
use std::io::{self, Read, Stdout}; use std::io::{self, Read};
use std::time::Duration; use std::time::Duration;
use crossterm::cursor::Show;
use crossterm::event::{self, Event, KeyCode}; use crossterm::event::{self, Event, KeyCode};
use crossterm::execute; use crossterm::execute;
use crossterm::terminal::{ use crossterm::terminal::{
@@ -12,6 +13,33 @@ use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::widgets::{Block, Borders, List, ListItem, ListState}; use ratatui::widgets::{Block, Borders, List, ListItem, ListState};
// RAII guard to ensure terminal is restored on panic/unwind
struct TerminalModeGuard {
// track whether we still need to clean up
active: bool,
}
impl TerminalModeGuard {
fn new() -> Self {
TerminalModeGuard { active: true }
}
fn cleanup(&mut self) {
if !self.active {
return;
}
let _ = disable_raw_mode();
let mut stdout = std::io::stdout();
let _ = execute!(stdout, LeaveAlternateScreen, Show);
self.active = false;
}
}
impl Drop for TerminalModeGuard {
fn drop(&mut self) {
self.cleanup();
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut input = String::new(); let mut input = String::new();
io::stdin().read_to_string(&mut input)?; io::stdin().read_to_string(&mut input)?;
@@ -27,6 +55,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = std::io::stdout(); let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen)?; execute!(stdout, EnterAlternateScreen)?;
let mut _mode_guard = TerminalModeGuard::new();
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
@@ -63,11 +94,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
f.render_stateful_widget(list, chunks[0], &mut state); f.render_stateful_widget(list, chunks[0], &mut state);
})?; })?;
// Event handling
if event::poll(Duration::from_millis(100))? { if event::poll(Duration::from_millis(100))? {
match event::read()? { match event::read()? {
Event::Key(key) => { Event::Key(key) => match key.code {
match key.code {
KeyCode::Char('q') => break, KeyCode::Char('q') => break,
KeyCode::Up => { KeyCode::Up => {
if let Some(i) = state.selected() if let Some(i) = state.selected()
@@ -88,24 +117,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
&& i < marked.len() && i < marked.len()
{ {
assert!(i < marked.len()); assert!(i < marked.len());
if marked[i] { if !marked[i] {
marked[i] = false;
} else {
marked[i] = true;
// move cursor down
let next = lines.len().min(i + 1); let next = lines.len().min(i + 1);
state.select(Some(next)); state.select(Some(next));
} }
marked[i] = !marked[i];
} }
} }
KeyCode::Char('c') KeyCode::Char('c') if key.modifiers.contains(event::KeyModifiers::CONTROL) => {
if key.modifiers.contains(event::KeyModifiers::CONTROL) =>
{
break; break;
} }
_ => {} _ => {}
} },
}
Event::Mouse(s) => { Event::Mouse(s) => {
if let event::MouseEventKind::Down(event::MouseButton::Left) = s.kind { if let event::MouseEventKind::Down(event::MouseButton::Left) = s.kind {
let area = terminal.size()?; let area = terminal.size()?;
@@ -122,9 +145,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
} }
disable_raw_mode()?; _mode_guard.cleanup();
let mut stdout: Stdout = std::io::stdout();
execute!(stdout, LeaveAlternateScreen)?;
terminal.show_cursor()?; terminal.show_cursor()?;
Ok(()) Ok(())