ratatui_crossterm_input/
ratatui_crossterm_input.rs1use 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#[derive(Debug, Default)]
23struct App {
24 input: Input,
26 input_mode: InputMode,
28 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(()), _ => {}
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 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 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}