mod app; pub mod store; use app::App; use miette::{IntoDiagnostic, Result}; use ratatui::{ Frame, Terminal, backend::Backend, buffer::Buffer, layout::Rect, style::Stylize, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Widget}, }; 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()?; let mut app = App::default(); app.init_context()?; // 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(()) } fn run_app(term: &mut Terminal, app: &mut App) -> Result<()> { while !app.get_exit_status() { term.draw(|frame| draw(frame, app)) .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "idk")) .into_diagnostic()?; 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 => { handle_key_event(app, key_event)?; } _ => {} }; } Ok(()) } fn draw(frame: &mut Frame, app: &App) { let run = Runtime { app }; frame.render_widget(&run, frame.area()); } /// Process user input and run associated app functions fn handle_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(()) } struct Runtime<'a> { app: &'a App, } impl<'a> Widget for &Runtime<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let title = Line::from(" Counter App Tutorial ".bold()); let instructions = Line::from(vec![ " Decrement ".into(), "".blue().bold(), " Increment ".into(), "".blue().bold(), " Quit ".into(), " ".blue().bold(), ]); let block = Block::bordered() .title(title.centered()) .title_bottom(instructions.centered()) .border_set(border::THICK); let counter_text = Text::from(vec![Line::from(vec![ "Value: ".into(), self.app.display_counter().yellow(), ])]); Paragraph::new(counter_text) .centered() .block(block) .render(area, buf); } } #[cfg(test)] mod tests { use super::*; use app::OverflowError; use ratatui::style::Style; #[test] fn render() { let mut app = App::default(); assert!(app.init_context().is_ok()); let runtime = Runtime { app: &app }; let mut buf = Buffer::empty(Rect::new(0, 0, 50, 4)); runtime.render(buf.area, &mut buf); let mut expected = Buffer::with_lines(vec![ "┏━━━━━━━━━━━━━ Counter App Tutorial ━━━━━━━━━━━━━┓", "┃ Value: 0 ┃", "┃ ┃", "┗━ Decrement Increment Quit ━━┛", ]); let title_style = Style::new().bold(); let counter_style = Style::new().yellow(); let key_style = Style::new().blue().bold(); expected.set_style(Rect::new(14, 0, 22, 1), title_style); expected.set_style(Rect::new(28, 1, 1, 1), counter_style); expected.set_style(Rect::new(13, 3, 6, 1), key_style); expected.set_style(Rect::new(30, 3, 7, 1), key_style); expected.set_style(Rect::new(43, 3, 4, 1), key_style); assert_eq!(buf, expected); } #[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, }, ); } }