From 924f989c0564629836b614db7742736d1663b1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20W=C3=B6lfer?= Date: Thu, 29 Jan 2026 21:09:13 +0100 Subject: [PATCH] feat: anyhow error handling --- Cargo.lock | 1 + Cargo.toml | 1 + src/event_source.rs | 10 +++++----- src/main.rs | 33 ++++++++++++++++++++------------- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7290441..b5ed1a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,6 +117,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" name = "chkr" version = "0.1.0" dependencies = [ + "anyhow", "crossterm", "insta", "ratatui", diff --git a/Cargo.toml b/Cargo.toml index a578902..d48c76d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] crossterm = "0.29" ratatui = { version = "0.30", features = ["crossterm"] } +anyhow = "1.0" [dev-dependencies] insta = "1.33" diff --git a/src/event_source.rs b/src/event_source.rs index 451a6f6..d0eb2f7 100644 --- a/src/event_source.rs +++ b/src/event_source.rs @@ -1,5 +1,5 @@ +use anyhow::Result; use crossterm::event::{self, Event}; -use std::error::Error; use std::time::Duration; /// An abstraction around event sourcing so we can inject events in tests. @@ -7,21 +7,21 @@ pub trait EventSource { /// Polls for an event for up to `timeout`, returning `Ok(true)` if an event is /// available, `Ok(false)` if the timeout elapsed without an event, or an `Err` /// if polling failed. - fn poll(&mut self, timeout: Duration) -> Result>; + fn poll(&mut self, timeout: Duration) -> Result; /// Reads the next available event, or returns an `Err` if reading fails. - fn read(&mut self) -> Result>; + fn read(&mut self) -> Result; } /// Production implementation that delegates to `crossterm::event`. pub struct CrosstermEventSource; impl EventSource for CrosstermEventSource { - fn poll(&mut self, timeout: Duration) -> Result> { + fn poll(&mut self, timeout: Duration) -> Result { Ok(event::poll(timeout).map_err(Box::new)?) } - fn read(&mut self) -> Result> { + fn read(&mut self) -> Result { Ok(event::read().map_err(Box::new)?) } } diff --git a/src/main.rs b/src/main.rs index 470746d..32db5cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ +/// A simple terminal checklist application. mod event_source; mod terminal_guard; /// UI components (todo list) mod todo_list; +use anyhow::{Context as _, Result, anyhow, bail}; use core::time::Duration; use crossterm::event::{self, EnableMouseCapture, Event, KeyCode}; use crossterm::execute; @@ -15,10 +17,11 @@ use std::io::{self, Read as _}; use terminal_guard::TerminalModeGuard; use todo_list::TodoList; -#[allow(clippy::wildcard_enum_match_arm)] -fn main() -> Result<(), Box> { +fn main() -> Result<()> { let mut input = String::new(); - io::stdin().read_to_string(&mut input)?; + io::stdin() + .read_to_string(&mut input) + .context("reading stdin")?; if input.trim().is_empty() { eprintln!("Provide text via stdin (pipe or heredoc). Example: \n cat file.txt | chkr"); @@ -31,31 +34,34 @@ fn main() -> Result<(), Box> { .collect(); let mut todo = TodoList::with_lines(lines); - enable_raw_mode()?; + enable_raw_mode().context("enable raw mode")?; let mut stdout = std::io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).context("enter alternate screen")?; let mode_guard = TerminalModeGuard; let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; + let mut terminal = Terminal::new(backend).context("create terminal")?; - run_app(&mut terminal, &mut CrosstermEventSource, &mut todo)?; + run_app(&mut terminal, &mut CrosstermEventSource, &mut todo) + .map_err(|e| anyhow!("running app: {e}"))?; drop(mode_guard); - terminal.show_cursor()?; + terminal.show_cursor().context("show cursor")?; Ok(()) } /// Run the app loop using an abstract `EventSource` so tests can inject events. +/// # Errors +/// If terminal drawing or event sourcing fails. pub fn run_app( terminal: &mut Terminal, event_source: &mut E, todo: &mut TodoList, -) -> Result<(), Box> +) -> Result<()> where - ::Error: 'static, + ::Error: 'static + Sync + Send, { loop { terminal.draw(|f| { @@ -63,6 +69,7 @@ where })?; if event_source.poll(Duration::from_millis(100))? { + #[allow(clippy::wildcard_enum_match_arm)] match event_source.read()? { Event::Key(key) => match key.code { KeyCode::Char('q') => break, @@ -79,7 +86,7 @@ where KeyCode::Char(' ') => { if let Some(i) = todo.state.selected() { let Some(marked_cell) = todo.marked.get_mut(i) else { - return Err("Index out of bounds".into()); + bail!("Index out of bounds"); }; if !*marked_cell { let next = todo.lines.len().min(i + 1); @@ -140,11 +147,11 @@ mod tests { } impl crate::event_source::EventSource for MockEventSource { - fn poll(&mut self, _timeout: Duration) -> Result> { + fn poll(&mut self, _timeout: Duration) -> Result { Ok(!self.events.is_empty()) } - fn read(&mut self) -> Result> { + fn read(&mut self) -> Result { Ok(self.events.pop_front().expect("no events left")) } }