udokai_tui/
lib.rs

1/// This example is taken from https://raw.githubusercontent.com/fdehau/tui-rs/master/examples/user_input.rs
2use 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
23/// App holds the state of the application
24struct App {
25    /// Current value of the input box
26    input: Input,
27    /// Current input mode
28    input_mode: InputMode,
29    /// History of recorded messages
30    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    // setup terminal
48    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    // create app and run it
55    let app = App::default();
56    let res = run_app(&mut terminal, app, on_input);
57
58    // restore terminal
59    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; // keep 2 for borders and 1 for cursor
156
157    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            // Hide the cursor. `Frame` does this by default, so we don't need to do anything here
169            {}
170
171        InputMode::Editing => {
172            // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
173            f.set_cursor_position((
174                // Put cursor past the end of the input text
175                chunks[1].x
176                    + ((app.input.visual_cursor()).max(scroll) - scroll) as u16
177                    + 1,
178                // Move one line down, from the border to the input line
179                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}