mod app; mod logging; mod ui; use app::{App, AppMode}; use miette::{IntoDiagnostic, Result, miette}; use ratatui::{Terminal, backend::Backend}; use ratatui::crossterm::{ event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, }, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use ratatui::prelude::CrosstermBackend; #[cfg(not(tarpaulin_include))] fn main() -> Result<()> { enable_raw_mode().into_diagnostic()?; let mut stdout = std::io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture).into_diagnostic()?; let backend = CrosstermBackend::new(stdout); let mut term = Terminal::new(backend).into_diagnostic()?; logging::initialize_logging().into_diagnostic()?; tracing::event!(tracing::Level::DEBUG, "App initialization started."); let mut app = App::default(); app.init_context()?; tracing::event!(tracing::Level::INFO, "App started."); // could be error, so don't unwrap; let term restore first let run = run_app(&mut term, &mut app); // restore terminal disable_raw_mode().into_diagnostic()?; execute!( term.backend_mut(), LeaveAlternateScreen, DisableMouseCapture ) .into_diagnostic()?; term.show_cursor().into_diagnostic()?; run?; Ok(()) } #[cfg(not(tarpaulin_include))] fn run_app(term: &mut Terminal, app: &mut App) -> Result<()> { while !app.get_exit_status() { term.draw(|frame| ui::draw(frame, app)) .map_err(|_| miette!("Unrecoverable error occurred"))?; match event::read().into_diagnostic()? { // it's important to check that the event is a key press event as // crossterm also emits key release and repeat events on Windows. Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { // if error occurred during input processing, set error in the app state match handle_key_event(app, key_event) { Err(e) => app.set_error(e)?, _ => {} } } _ => {} }; } Ok(()) } /// Process user input and run associated app functions fn handle_key_event(app: &mut App, key_event: KeyEvent) -> Result<()> { tracing::event!( tracing::Level::TRACE, event = key_event.code.to_string(), mode = app.get_app_mode().to_string(), "Received key event." ); match app.get_app_mode() { AppMode::Counter => handle_counter_key_event(app, key_event), // no keyevent handling for init, exit modes AppMode::Init => Ok(()), AppMode::Exit => Ok(()), }?; // any new key event, we should clear the existing error app.clear_error()?; Ok(()) } fn handle_counter_key_event(app: &mut App, key_event: KeyEvent) -> Result<()> { match key_event.code { KeyCode::Char('q') => app.exit()?, KeyCode::Left => app.decrement_counter()?, KeyCode::Right => app.increment_counter()?, _ => {} }; Ok(()) } #[cfg(test)] mod tests { use super::*; use app::OverflowError; #[test] fn handle_key_event_app_init() { let mut app = App::default(); let res = handle_key_event(&mut app, KeyCode::Right.into()); assert!(res.is_ok()); assert_eq!(*app.get_app_mode(), AppMode::Init); // Function returns "Error"; app never init'd assert_eq!(app.display_counter(), "Error"); } #[test] fn handle_key_event_app_exit() { let mut app = App::default(); assert!(app.exit().is_ok()); let res = handle_key_event(&mut app, KeyCode::Right.into()); assert!(res.is_ok()); assert_eq!(*app.get_app_mode(), AppMode::Exit); // Function returns "Error"; app never init'd assert_eq!(app.display_counter(), "Error"); } #[test] fn handle_key_event_inc_dec() { let mut app = App::default(); assert!(app.init_context().is_ok()); let res = handle_key_event(&mut app, KeyCode::Right.into()); assert!(res.is_ok()); assert_eq!(app.display_counter(), "1"); let res = handle_key_event(&mut app, KeyCode::Left.into()); assert!(res.is_ok()); assert_eq!(app.display_counter(), "0"); } #[test] fn handle_key_event_inc_n() { let mut app = App::default(); assert!(app.init_context().is_ok()); let n = 8; for i in 1..=n { let res = handle_key_event(&mut app, KeyCode::Right.into()); assert!(res.is_ok()); assert_eq!(app.display_counter(), i.to_string()); } assert_eq!(app.display_counter(), n.to_string()); } #[test] fn handle_key_event_inc_n_dec() { let mut app = App::default(); assert!(app.init_context().is_ok()); let n = 8; for i in 1..=n { let res = handle_key_event(&mut app, KeyCode::Right.into()); assert!(res.is_ok()); assert_eq!(app.display_counter(), i.to_string()); } assert_eq!(app.display_counter(), n.to_string()); let res = handle_key_event(&mut app, KeyCode::Left.into()); assert!(res.is_ok()); assert_eq!(app.display_counter(), (n - 1).to_string()); } #[test] fn handle_key_event_exit() { let mut app = App::default(); assert!(app.init_context().is_ok()); let res = handle_key_event(&mut app, KeyCode::Char('q').into()); assert!(res.is_ok()); assert!(app.get_exit_status()); } #[test] fn handle_key_event_dec_error() { let mut app = App::default(); assert!(app.init_context().is_ok()); let res = handle_key_event(&mut app, KeyCode::Left.into()); assert!(res.is_err()); let res_err = res.unwrap_err(); let casted_val = res_err .downcast_ref::() .expect("something bad occurred"); let expected_err_msg: String = "Cannot count below 0".into(); assert_eq!(casted_val._msg, expected_err_msg); assert_eq!( *casted_val, OverflowError { _msg: expected_err_msg, }, ); } #[test] fn handle_key_event_inc_error() { let mut app = App::default(); assert!(app.init_context().is_ok()); // no pub set_counter exposed, so we manually set this for _ in 0..u8::MAX { assert!(app.increment_counter().is_ok()); } let res = handle_key_event(&mut app, KeyCode::Right.into()); assert!(res.is_err()); let res_err = res.unwrap_err(); let casted_val = res_err .downcast_ref::() .expect("something bad occurred"); let expected_err_msg: String = "Cannot count above u8::MAX".into(); assert_eq!(casted_val._msg, expected_err_msg); assert_eq!( *casted_val, OverflowError { _msg: expected_err_msg, }, ); } }