mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-22 20:53:19 +00:00
23d5fbde56
The thread spawned by `Events` to listen for keyboard inputs had knowlegde of the exit key to exit on its own when it was pressed. It is however a source of confusion (#491) because the exit behavior is wired in both the event handler and the input handling performed by the app. In addition, this is not needed as the thread will exit anyway when the main thread finishes as it is already the case for the "tick" thread. Therefore, this commit removes both the option to configure the exit key in the `Events` handler and the option to temporarily ignore it.
179 lines
6.1 KiB
Rust
179 lines
6.1 KiB
Rust
/// A simple example demonstrating how to handle user input. This is
|
|
/// a bit out of the scope of the library as it does not provide any
|
|
/// input handling out of the box. However, it may helps some to get
|
|
/// started.
|
|
///
|
|
/// This is a very simple example:
|
|
/// * A input box always focused. Every character you type is registered
|
|
/// here
|
|
/// * Pressing Backspace erases a character
|
|
/// * Pressing Enter pushes the current input in the history of previous
|
|
/// messages
|
|
|
|
#[allow(dead_code)]
|
|
mod util;
|
|
|
|
use crate::util::event::{Event, Events};
|
|
use std::{error::Error, io};
|
|
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
|
use tui::{
|
|
backend::TermionBackend,
|
|
layout::{Constraint, Direction, Layout},
|
|
style::{Color, Modifier, Style},
|
|
text::{Span, Spans, Text},
|
|
widgets::{Block, Borders, List, ListItem, Paragraph},
|
|
Terminal,
|
|
};
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
enum InputMode {
|
|
Normal,
|
|
Editing,
|
|
}
|
|
|
|
/// App holds the state of the application
|
|
struct App {
|
|
/// Current value of the input box
|
|
input: String,
|
|
/// Current input mode
|
|
input_mode: InputMode,
|
|
/// History of recorded messages
|
|
messages: Vec<String>,
|
|
}
|
|
|
|
impl Default for App {
|
|
fn default() -> App {
|
|
App {
|
|
input: String::new(),
|
|
input_mode: InputMode::Normal,
|
|
messages: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
|
// Terminal initialization
|
|
let stdout = io::stdout().into_raw_mode()?;
|
|
let stdout = MouseTerminal::from(stdout);
|
|
let stdout = AlternateScreen::from(stdout);
|
|
let backend = TermionBackend::new(stdout);
|
|
let mut terminal = Terminal::new(backend)?;
|
|
|
|
// Setup event handlers
|
|
let events = Events::new();
|
|
|
|
// Create default app state
|
|
let mut app = App::default();
|
|
|
|
loop {
|
|
// Draw UI
|
|
terminal.draw(|f| {
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.margin(2)
|
|
.constraints(
|
|
[
|
|
Constraint::Length(1),
|
|
Constraint::Length(3),
|
|
Constraint::Min(1),
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(f.size());
|
|
|
|
let (msg, style) = match app.input_mode {
|
|
InputMode::Normal => (
|
|
vec![
|
|
Span::raw("Press "),
|
|
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
|
|
Span::raw(" to exit, "),
|
|
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
|
|
Span::raw(" to start editing."),
|
|
],
|
|
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
|
),
|
|
InputMode::Editing => (
|
|
vec![
|
|
Span::raw("Press "),
|
|
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
|
Span::raw(" to stop editing, "),
|
|
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
|
|
Span::raw(" to record the message"),
|
|
],
|
|
Style::default(),
|
|
),
|
|
};
|
|
let mut text = Text::from(Spans::from(msg));
|
|
text.patch_style(style);
|
|
let help_message = Paragraph::new(text);
|
|
f.render_widget(help_message, chunks[0]);
|
|
|
|
let input = Paragraph::new(app.input.as_ref())
|
|
.style(match app.input_mode {
|
|
InputMode::Normal => Style::default(),
|
|
InputMode::Editing => Style::default().fg(Color::Yellow),
|
|
})
|
|
.block(Block::default().borders(Borders::ALL).title("Input"));
|
|
f.render_widget(input, chunks[1]);
|
|
match app.input_mode {
|
|
InputMode::Normal =>
|
|
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
|
{}
|
|
|
|
InputMode::Editing => {
|
|
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
|
|
f.set_cursor(
|
|
// Put cursor past the end of the input text
|
|
chunks[1].x + app.input.width() as u16 + 1,
|
|
// Move one line down, from the border to the input line
|
|
chunks[1].y + 1,
|
|
)
|
|
}
|
|
}
|
|
|
|
let messages: Vec<ListItem> = app
|
|
.messages
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, m)| {
|
|
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
|
|
ListItem::new(content)
|
|
})
|
|
.collect();
|
|
let messages =
|
|
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
|
|
f.render_widget(messages, chunks[2]);
|
|
})?;
|
|
|
|
// Handle input
|
|
if let Event::Input(input) = events.next()? {
|
|
match app.input_mode {
|
|
InputMode::Normal => match input {
|
|
Key::Char('e') => {
|
|
app.input_mode = InputMode::Editing;
|
|
}
|
|
Key::Char('q') => {
|
|
break;
|
|
}
|
|
_ => {}
|
|
},
|
|
InputMode::Editing => match input {
|
|
Key::Char('\n') => {
|
|
app.messages.push(app.input.drain(..).collect());
|
|
}
|
|
Key::Char(c) => {
|
|
app.input.push(c);
|
|
}
|
|
Key::Backspace => {
|
|
app.input.pop();
|
|
}
|
|
Key::Esc => {
|
|
app.input_mode = InputMode::Normal;
|
|
}
|
|
_ => {}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|