ratatui_crossterm_input/
ratatui_crossterm_input.rs

1use std::io;
2
3use ratatui::{
4    crossterm::event::{self, Event, KeyCode},
5    layout::{Constraint, Layout, Rect},
6    style::{Color, Style, Stylize},
7    text::{Line, ToSpan},
8    widgets::{Block, List, Paragraph},
9    DefaultTerminal, Frame,
10};
11use tui_input::backend::crossterm::EventHandler;
12use tui_input::Input;
13
14fn main() -> io::Result<()> {
15    let mut terminal = ratatui::init();
16    let result = App::default().run(&mut terminal);
17    ratatui::restore();
18    result
19}
20
21/// App holds the state of the application
22#[derive(Debug, Default)]
23struct App {
24    /// Current value of the input box
25    input: Input,
26    /// Current input mode
27    input_mode: InputMode,
28    /// History of recorded messages
29    messages: Vec<String>,
30}
31
32#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
33enum InputMode {
34    #[default]
35    Normal,
36    Editing,
37}
38
39impl App {
40    fn run(mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
41        loop {
42            terminal.draw(|frame| self.render(frame))?;
43
44            let event = event::read()?;
45            if let Event::Key(key) = event {
46                match self.input_mode {
47                    InputMode::Normal => match key.code {
48                        KeyCode::Char('e') => self.start_editing(),
49                        KeyCode::Char('q') => return Ok(()), // exit
50                        _ => {}
51                    },
52                    InputMode::Editing => match key.code {
53                        KeyCode::Enter => self.push_message(),
54                        KeyCode::Esc => self.stop_editing(),
55                        _ => {
56                            self.input.handle_event(&event);
57                        }
58                    },
59                }
60            }
61        }
62    }
63
64    fn start_editing(&mut self) {
65        self.input_mode = InputMode::Editing
66    }
67
68    fn stop_editing(&mut self) {
69        self.input_mode = InputMode::Normal
70    }
71
72    fn push_message(&mut self) {
73        self.messages.push(self.input.value_and_reset());
74    }
75
76    fn render(&self, frame: &mut Frame) {
77        let [header_area, input_area, messages_area] = Layout::vertical([
78            Constraint::Length(1),
79            Constraint::Length(3),
80            Constraint::Min(1),
81        ])
82        .areas(frame.area());
83
84        self.render_help_message(frame, header_area);
85        self.render_input(frame, input_area);
86        self.render_messages(frame, messages_area);
87    }
88
89    fn render_help_message(&self, frame: &mut Frame, area: Rect) {
90        let help_message = Line::from_iter(match self.input_mode {
91            InputMode::Normal => [
92                "Press ".to_span(),
93                "q".bold(),
94                " to exit, ".to_span(),
95                "e".bold(),
96                " to start editing.".to_span(),
97            ],
98            InputMode::Editing => [
99                "Press ".to_span(),
100                "Esc".bold(),
101                " to stop editing, ".to_span(),
102                "Enter".bold(),
103                " to record the message".to_span(),
104            ],
105        });
106        frame.render_widget(help_message, area);
107    }
108
109    fn render_input(&self, frame: &mut Frame, area: Rect) {
110        // keep 2 for borders and 1 for cursor
111        let width = area.width.max(3) - 3;
112        let scroll = self.input.visual_scroll(width as usize);
113        let style = match self.input_mode {
114            InputMode::Normal => Style::default(),
115            InputMode::Editing => Color::Yellow.into(),
116        };
117        let input = Paragraph::new(self.input.value())
118            .style(style)
119            .scroll((0, scroll as u16))
120            .block(Block::bordered().title("Input"));
121        frame.render_widget(input, area);
122
123        if self.input_mode == InputMode::Editing {
124            // Ratatui hides the cursor unless it's explicitly set. Position the  cursor past the
125            // end of the input text and one line down from the border to the input line
126            let x = self.input.visual_cursor().max(scroll) - scroll + 1;
127            frame.set_cursor_position((area.x + x as u16, area.y + 1))
128        }
129    }
130
131    fn render_messages(&self, frame: &mut Frame, area: Rect) {
132        let messages = self
133            .messages
134            .iter()
135            .enumerate()
136            .map(|(i, message)| format!("{}: {}", i, message));
137        let messages = List::new(messages).block(Block::bordered().title("Messages"));
138        frame.render_widget(messages, area);
139    }
140}