Ratatui - counter
- A copy of the counter app
- Thre is a crash if the counter overflows or underflows!
examples/ratatui/counter/Cargo.toml
[package] name = "counter" version = "0.1.0" edition = "2021" [dependencies] crossterm = "0.28.1" ratatui = "0.29.0"
examples/ratatui/counter/src/main.rs
use std::io; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ buffer::Buffer, layout::Rect, style::Stylize, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Widget}, DefaultTerminal, Frame, }; #[derive(Debug, Default)] pub struct App { counter: u8, exit: bool, } impl App { pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { while !self.exit { terminal.draw(|frame| self.draw(frame))?; self.handle_events()?; } Ok(()) } fn draw(&self, frame: &mut Frame) { frame.render_widget(self, frame.area()); } fn handle_events(&mut self) -> io::Result<()> { match event::read()? { // 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) { match key_event.code { KeyCode::Char('q') => self.exit(), KeyCode::Left => self.decrement_counter(), KeyCode::Right => self.increment_counter(), _ => {} } } fn exit(&mut self) { self.exit = true; } fn increment_counter(&mut self) { self.counter += 1; } fn decrement_counter(&mut self) { self.counter -= 1; } } 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(), "<Left>".blue().bold(), " Increment ".into(), "<Right>".blue().bold(), " Quit ".into(), "<Q> ".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); } } fn main() -> io::Result<()> { let mut terminal = ratatui::init(); let app_result = App::default().run(&mut terminal); ratatui::restore(); app_result } #[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 <Left> Increment <Right> Quit <Q> ━━┛", ]); 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() -> io::Result<()> { let mut app = App::default(); app.handle_key_event(KeyCode::Right.into()); assert_eq!(app.counter, 1); app.handle_key_event(KeyCode::Left.into()); assert_eq!(app.counter, 0); let mut app = App::default(); app.handle_key_event(KeyCode::Char('q').into()); assert!(app.exit); Ok(()) } }