Compare commits

...

2 Commits

Author SHA1 Message Date
Lukas Wölfer
0081e79492 refactor: move terminal guard to own file 2026-01-24 08:50:06 +01:00
Lukas Wölfer
15c792fc21 feat: handle crashes better for the terminal state 2026-01-24 08:46:31 +01:00
2 changed files with 70 additions and 47 deletions

View File

@@ -1,17 +1,18 @@
use std::io::{self, Read, Stdout}; use std::io::{self, Read};
use std::time::Duration; use std::time::Duration;
use crossterm::event::{self, Event, KeyCode}; use crossterm::event::{self, Event, KeyCode};
use crossterm::execute; use crossterm::execute;
use crossterm::terminal::{ use crossterm::terminal::{EnterAlternateScreen, enable_raw_mode};
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal; use ratatui::Terminal;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Direction, Layout}; 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};
mod terminal_guard;
use terminal_guard::TerminalModeGuard;
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 +28,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)?;
@@ -47,8 +51,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, l)| { .map(|(i, l)| {
let prefix = if marked[i] { "[x]" } else { "[ ]" }; let prefix = if marked[i] { 'x' } else { ' ' };
ListItem::new(format!("{} {}", prefix, l)) ListItem::new(format!("[{prefix}] {l}"))
}) })
.collect(); .collect();
@@ -63,49 +67,41 @@ 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() && i > 0
&& 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) =>
{ {
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) => { 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 +118,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(())

29
src/terminal_guard.rs Normal file
View File

@@ -0,0 +1,29 @@
use crossterm::cursor::Show;
use crossterm::execute;
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode};
pub struct TerminalModeGuard {
active: bool,
}
impl TerminalModeGuard {
pub fn new() -> Self {
Self { active: true }
}
pub 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();
}
}