From c2c696872b99432b2e148bdb2e16e6a0002674bd Mon Sep 17 00:00:00 2001 From: Kevin Hoerr Date: Tue, 24 Feb 2026 22:20:37 +0000 Subject: main.rs: Counter tutorial --- src/main/rust/main.rs | 142 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 9 deletions(-) (limited to 'src/main') diff --git a/src/main/rust/main.rs b/src/main/rust/main.rs index 77ab480..4796c46 100644 --- a/src/main/rust/main.rs +++ b/src/main/rust/main.rs @@ -1,19 +1,143 @@ -use ratatui::{DefaultTerminal, Frame}; +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; +use ratatui::{ + DefaultTerminal, Frame, + buffer::Buffer, + layout::Rect, + style::Stylize, + symbols::border, + text::{Line, Text}, + widgets::{Block, Paragraph, Widget}, +}; use std::io::Result; +#[derive(Debug, Default)] +pub struct App { + counter: u8, + exit: bool, +} + fn main() -> Result<()> { - ratatui::run(app) + ratatui::run(|term| App::default().run(term)) } -fn app(terminal: &mut DefaultTerminal) -> Result<()> { - loop { - terminal.draw(render)?; - if crossterm::event::read()?.is_key_press() { - break Ok(()); +impl App { + /// runs the application's main loop until the user quits + pub fn run(&mut self, terminal: &mut DefaultTerminal) -> 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) -> 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 decrement_counter(&mut self) { + self.counter -= 1; + } + + fn increment_counter(&mut self) { + self.counter += 1; } } -fn render(frame: &mut Frame) { - frame.render_widget("hello world", frame.area()); +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() { + 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); + } } -- cgit v1.3