pub mod store; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use miette::{Diagnostic, IntoDiagnostic, Result}; use ratatui::{ DefaultTerminal, Frame, buffer::Buffer, layout::Rect, style::Stylize, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Widget}, }; use store::{Context, Store}; use thiserror::Error; #[derive(Default)] pub struct App { context: Context, counter: u8, exit: bool, } #[derive(Diagnostic, Debug, Error, PartialEq)] #[error("An overflow error occurred")] #[diagnostic()] pub struct OverflowError { #[help] _msg: String, } #[cfg(not(tarpaulin_include))] fn main() -> Result<()> { ratatui::run(|term| App::default().init_context()?.run(term)) } impl App { // Initializes context pub fn init_context(&mut self) -> Result<&mut Self> { match &mut self.context { Context::SQLite(s) => { s.init()?; } } Ok(self) } /// runs the application's main loop until the user quits #[cfg(not(tarpaulin_include))] pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> { while !self.exit { terminal.draw(|frame| self.draw(frame)).into_diagnostic()?; self.handle_events()?; } Ok(()) } fn draw(&self, frame: &mut Frame) { frame.render_widget(self, frame.area()); } fn handle_events(&mut self) -> Result<()> { 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 => { self.handle_key_event(key_event) } _ => Ok(()), } } fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> { match key_event.code { KeyCode::Char('q') => self.exit()?, KeyCode::Left => self.decrement_counter()?, KeyCode::Right => self.increment_counter()?, _ => {} }; Ok(()) } fn exit(&mut self) -> Result<()> { self.exit = true; Ok(()) } fn decrement_counter(&mut self) -> Result<(), OverflowError> { if self.counter == 0 { Err(OverflowError { _msg: "Cannot count below 0".into(), }) } else { self.counter -= 1; Ok(()) } } fn increment_counter(&mut self) -> Result<(), OverflowError> { if self.counter == u8::MAX { Err(OverflowError { _msg: "Cannot count above u8::MAX".into(), }) } else { self.counter += 1; Ok(()) } } } impl Widget for &App { 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.counter.to_string().yellow(), ])]); Paragraph::new(counter_text) .centered() .block(block) .render(area, buf); } } #[cfg(test)] mod tests { use super::*; use ratatui::style::Style; #[test] fn render() { let app = App::default(); let mut buf = Buffer::empty(Rect::new(0, 0, 50, 4)); app.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(); let res = app.handle_key_event(KeyCode::Right.into()); assert!(res.is_ok()); assert_eq!(app.counter, 1); let res = app.handle_key_event(KeyCode::Left.into()); assert!(res.is_ok()); assert_eq!(app.counter, 0); } #[test] fn handle_key_event_inc_n() { let mut app = App::default(); let n = 8; for i in 1..=n { let res = app.handle_key_event(KeyCode::Right.into()); assert!(res.is_ok()); assert_eq!(app.counter, i); } assert_eq!(app.counter, n); } #[test] fn handle_key_event_inc_n_dec() { let mut app = App::default(); let n = 8; for i in 1..=n { let res = app.handle_key_event(KeyCode::Right.into()); assert!(res.is_ok()); assert_eq!(app.counter, i); } assert_eq!(app.counter, n); let res = app.handle_key_event(KeyCode::Left.into()); assert!(res.is_ok()); assert_eq!(app.counter, n - 1); } #[test] fn handle_key_event_exit() { let mut app = App::default(); let res = app.handle_key_event(KeyCode::Char('q').into()); assert!(res.is_ok()); assert!(app.exit); } #[test] fn handle_key_event_dec_error() { let mut app = App::default(); let res = app.handle_key_event(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(); app.counter = u8::MAX; let res = app.handle_key_event(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, }, ); } }