rext_tui/
lib.rs

1pub mod error;
2
3use crate::error::RextTuiError;
4use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
5use ratatui::{
6    DefaultTerminal, Frame,
7    style::Stylize,
8    symbols::border,
9    text::{Line, Text},
10    widgets::{Block, Paragraph},
11};
12
13/// The main application which holds the state and logic of the application.
14#[derive(Debug, Default)]
15pub struct App {
16    /// Is the application running?
17    pub running: bool,
18    pub counter: u8,
19}
20
21impl App {
22    /// Construct a new instance of [`App`].
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    fn increment_counter(&mut self) {
28        self.counter += 1;
29    }
30
31    fn decrement_counter(&mut self) {
32        self.counter -= 1;
33    }
34
35    /// Run the application's main loop.
36    pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<(), RextTuiError> {
37        self.running = true;
38        while self.running {
39            terminal.draw(|frame| self.render(frame))?;
40            self.handle_crossterm_events()?;
41        }
42        Ok(())
43    }
44
45    /// Renders the user interface.
46    ///
47    /// This is where you add new widgets. See the following resources for more information:
48    ///
49    /// - <https://docs.rs/ratatui/latest/ratatui/widgets/index.html>
50    /// - <https://github.com/ratatui/ratatui/tree/main/ratatui-widgets/examples>
51    fn render(&mut self, frame: &mut Frame) {
52        let title = Line::from("Rext").bold().yellow().centered();
53        let instructions = Line::from(vec![
54            " Decrement ".into(),
55            "<Left>".bold().blue(),
56            " Increment ".into(),
57            "<Right>".bold().blue(),
58            " Quit ".into(),
59            "<Esc>".bold().blue(),
60        ]);
61
62        let block = Block::bordered()
63            .title(title.centered())
64            .title_bottom(instructions.centered())
65            .border_set(border::THICK);
66
67        let text = Text::from(vec![Line::from(vec![
68            "Value: ".into(),
69            self.counter.to_string().yellow(),
70        ])]);
71
72        frame.render_widget(Paragraph::new(text).block(block).centered(), frame.area());
73    }
74
75    /// Reads the crossterm events and updates the state of [`App`].
76    ///
77    /// If your application needs to perform work in between handling events, you can use the
78    /// [`event::poll`] function to check if there are any events available with a timeout.
79    ///
80    /// # Errors
81    ///
82    /// Returns a [`RextTuiError::ReadEvent`] if reading from crossterm's event stream fails.
83    /// This wraps the underlying [`std::io::Error`] from crossterm's [`event::read()`].
84    fn handle_crossterm_events(&mut self) -> Result<(), RextTuiError> {
85        // event::read() returns std::io::Error which gets converted into RextTuiError::ReadEvent
86        match event::read()? {
87            // it's important to check KeyEventKind::Press to avoid handling key release events
88            Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key_event(key),
89            Event::Mouse(_) => {}
90            Event::Resize(_, _) => {}
91            _ => {}
92        }
93        Ok(())
94    }
95
96    /// Handles the key events and updates the state of [`App`].
97    pub fn on_key_event(&mut self, key: KeyEvent) {
98        match (key.modifiers, key.code) {
99            (_, KeyCode::Esc | KeyCode::Char('q'))
100            | (KeyModifiers::CONTROL, KeyCode::Char('c') | KeyCode::Char('C')) => self.quit(),
101            (_, KeyCode::Left) => self.decrement_counter(),
102            (_, KeyCode::Right) => self.increment_counter(),
103            _ => {}
104        }
105    }
106
107    /// Set running to false to quit the application.
108    fn quit(&mut self) {
109        self.running = false;
110    }
111}