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 crossterm::cursor::Show;
use crossterm::event::{self, Event, KeyCode};
use crossterm::execute;
use crossterm::terminal::{
@@ -12,6 +13,33 @@ use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::style::Style;
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>> {
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
@@ -27,6 +55,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let mut _mode_guard = TerminalModeGuard::new();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
@@ -63,49 +94,41 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
f.render_stateful_widget(list, chunks[0], &mut state);
})?;
// Event handling
if event::poll(Duration::from_millis(100))? {
match event::read()? {
Event::Key(key) => {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Up => {
if let Some(i) = state.selected()
&& i > 0
{
state.select(Some(i - 1));
}
}
KeyCode::Down => {
if let Some(i) = state.selected()
&& i + 1 < lines.len()
{
state.select(Some(i + 1));
}
}
KeyCode::Char(' ') => {
if let Some(i) = state.selected()
&& i < marked.len()
{
assert!(i < marked.len());
if marked[i] {
marked[i] = false;
} else {
marked[i] = true;
// move cursor down
let next = lines.len().min(i + 1);
state.select(Some(next));
}
}
}
KeyCode::Char('c')
if key.modifiers.contains(event::KeyModifiers::CONTROL) =>
Event::Key(key) => match key.code {
KeyCode::Char('q') => break,
KeyCode::Up => {
if let Some(i) = state.selected()
&& i > 0
{
break;
state.select(Some(i - 1));
}
_ => {}
}
}
KeyCode::Down => {
if let Some(i) = state.selected()
&& i + 1 < lines.len()
{
state.select(Some(i + 1));
}
}
KeyCode::Char(' ') => {
if let Some(i) = state.selected()
&& i < marked.len()
{
assert!(i < marked.len());
if !marked[i] {
let next = lines.len().min(i + 1);
state.select(Some(next));
}
marked[i] = !marked[i];
}
}
KeyCode::Char('c') if key.modifiers.contains(event::KeyModifiers::CONTROL) => {
break;
}
_ => {}
},
Event::Mouse(s) => {
if let event::MouseEventKind::Down(event::MouseButton::Left) = s.kind {
let area = terminal.size()?;
@@ -122,9 +145,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
disable_raw_mode()?;
let mut stdout: Stdout = std::io::stdout();
execute!(stdout, LeaveAlternateScreen)?;
_mode_guard.cleanup();
terminal.show_cursor()?;
Ok(())