1use ratatui::prelude::*;
3use ratatui::{
4 crossterm::{
5 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
6 execute,
7 terminal::{
8 disable_raw_mode, enable_raw_mode, EnterAlternateScreen,
9 LeaveAlternateScreen,
10 },
11 },
12 widgets::{Block, Borders, List, ListItem, Paragraph},
13};
14use std::{error::Error, io};
15use tui_input::backend::crossterm::EventHandler;
16use tui_input::Input;
17
18enum InputMode {
19 Normal,
20 Editing,
21}
22
23struct App {
25 input: Input,
27 input_mode: InputMode,
29 messages: Vec<String>,
31}
32
33impl Default for App {
34 fn default() -> Self {
35 App {
36 input: Input::default(),
37 input_mode: InputMode::Editing,
38 messages: Vec::new(),
39 }
40 }
41
42}
43
44pub fn show(
45 on_input: impl FnMut(String) -> Vec<String> + 'static,
46) -> Result<(), Box<dyn Error>> {
47 enable_raw_mode()?;
49 let mut stdout = io::stdout();
50 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
51 let backend = CrosstermBackend::new(stdout);
52 let mut terminal = Terminal::new(backend)?;
53
54 let app = App::default();
56 let res = run_app(&mut terminal, app, on_input);
57
58 disable_raw_mode()?;
60 execute!(
61 terminal.backend_mut(),
62 LeaveAlternateScreen,
63 DisableMouseCapture
64 )?;
65 terminal.show_cursor()?;
66
67 if let Err(err) = res {
68 log::error!("{:?}", err)
69 }
70
71 Ok(())
72}
73
74fn run_app<B: Backend>(
75 terminal: &mut Terminal<B>,
76 mut app: App,
77 mut on_input: impl FnMut(String) -> Vec<String> + 'static,
78) -> io::Result<()> {
79 loop {
80 terminal.draw(|f| ui(f, &app))?;
81
82 if let Event::Key(key) = event::read()? {
83 match app.input_mode {
84 InputMode::Normal => match key.code {
85 KeyCode::Char('e') => {
86 app.input_mode = InputMode::Editing;
87 }
88 KeyCode::Char('q') => {
89 return Ok(());
90 }
91 _ => {}
92 },
93 InputMode::Editing => match key.code {
94 KeyCode::Enter => {
95 app.input.reset();
96 }
97 KeyCode::Esc => {
98 app.input_mode = InputMode::Normal;
99 }
100 _ => {
101 app.input.handle_event(&Event::Key(key));
102 let msgs = on_input(app.input.value().to_string());
103
104 if !msgs.is_empty() {
105 app.messages.extend(msgs);
106 }
107
108 }
109 },
110 }
111 }
112 }
113}
114
115fn ui(f: &mut Frame, app: &App) {
116 let chunks = Layout::default()
117 .direction(Direction::Vertical)
118 .margin(2)
119 .constraints(
120 [
121 Constraint::Length(1),
122 Constraint::Length(3),
123 Constraint::Min(1),
124 ]
125 .as_ref(),
126 )
127 .split(f.area());
128
129 let (msg, style) = match app.input_mode {
130 InputMode::Normal => (
131 vec![
132 Span::raw("Press "),
133 Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
134 Span::raw(" to exit, "),
135 Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
136 Span::raw(" to start editing."),
137 ],
138 Style::default().add_modifier(Modifier::RAPID_BLINK),
139 ),
140 InputMode::Editing => (
141 vec![
142 Span::raw("Press "),
143 Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
144 Span::raw(" to stop editing, "),
145 Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
146 Span::raw(" to record the message"),
147 ],
148 Style::default(),
149 ),
150 };
151 let text = Text::from(Line::from(msg)).style(style);
152 let help_message = Paragraph::new(text);
153 f.render_widget(help_message, chunks[0]);
154
155 let width = chunks[0].width.max(3) - 3; let scroll = app.input.visual_scroll(width as usize);
158 let input = Paragraph::new(app.input.value())
159 .style(match app.input_mode {
160 InputMode::Normal => Style::default(),
161 InputMode::Editing => Style::default().fg(Color::Yellow),
162 })
163 .scroll((0, scroll as u16))
164 .block(Block::default().borders(Borders::ALL).title("Input"));
165 f.render_widget(input, chunks[1]);
166 match app.input_mode {
167 InputMode::Normal =>
168 {}
170
171 InputMode::Editing => {
172 f.set_cursor_position((
174 chunks[1].x
176 + ((app.input.visual_cursor()).max(scroll) - scroll) as u16
177 + 1,
178 chunks[1].y + 1,
180 ))
181 }
182 }
183
184 let messages: Vec<ListItem> = app
185 .messages
186 .iter()
187 .enumerate()
188 .map(|(i, m)| {
189 let content = vec![Line::from(Span::raw(format!("{}: {}", i, m)))];
190 ListItem::new(content)
191 })
192 .collect();
193 let messages = List::new(messages)
194 .block(Block::default().borders(Borders::ALL).title("Messages"));
195 f.render_widget(messages, chunks[2]);
196}