tui-canvas 0.8.2

Form/textarea for TUI
Documentation
//! Demonstrates the single-line text input widget in normal mode.
//!
//! Run with:
//! cargo run --example textinput_normal --features "gui,cursor-style,textinput,textmode-normal"

#[cfg(not(feature = "cursor-style"))]
compile_error!(
    "This example requires the 'cursor-style' feature. \
     Run with: cargo run --example textinput_normal --features \"gui,cursor-style,textinput,textmode-normal\""
);

#[cfg(not(feature = "textinput"))]
compile_error!(
    "This example requires the 'textinput' feature. \
     Run with: cargo run --example textinput_normal --features \"gui,cursor-style,textinput,textmode-normal\""
);

use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use ratatui::{
    backend::{Backend, CrosstermBackend},
    layout::{Constraint, Direction, Layout},
    style::{Color, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph},
    Frame, Terminal,
};

use tui_canvas::{
    integration::crossterm_input::{CrosstermInputOptions, CrosstermInputSession},
    CursorManager, TextInput, TextInputEventOutcome, TextInputState,
};

struct TextInputDemo {
    input: TextInputState,
    status: String,
}

impl TextInputDemo {
    fn new() -> Self {
        let mut input = TextInputState::default();
        input.set_placeholder("Type a command and press Enter");

        Self {
            input,
            status: "Enter submits. Ctrl+Q exits.".to_string(),
        }
    }

    fn handle_input(&mut self, key: KeyEvent) {
        match self.input.input(key) {
            TextInputEventOutcome::Submitted => {
                self.status = format!("Submitted: {}", self.input.text());
            }
            TextInputEventOutcome::Handled | TextInputEventOutcome::Ignored => {}
        }
    }

    fn handle_event(&mut self, event: Event) {
        match self.input.handle_event(event) {
            TextInputEventOutcome::Submitted => {
                self.status = format!("Submitted: {}", self.input.text());
            }
            TextInputEventOutcome::Handled | TextInputEventOutcome::Ignored => {}
        }
    }
}

fn handle_key_press(key_event: KeyEvent, app: &mut TextInputDemo) -> bool {
    if (key_event.code == KeyCode::Char('q') && key_event.modifiers.contains(KeyModifiers::CONTROL))
        || (key_event.code == KeyCode::Char('c')
            && key_event.modifiers.contains(KeyModifiers::CONTROL))
        || key_event.code == KeyCode::F(10)
    {
        return false;
    }

    app.handle_input(key_event);
    true
}

fn ui(f: &mut Frame, app: &mut TextInputDemo) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(3),
            Constraint::Length(3),
            Constraint::Min(1),
        ])
        .split(f.area());

    let input_block = Block::default()
        .borders(Borders::ALL)
        .title("Command")
        .border_style(Style::default().fg(Color::Cyan));

    let input_widget = TextInput::default().block(input_block.clone());
    f.render_stateful_widget(input_widget, chunks[0], &mut app.input);

    let (cx, cy) = app.input.cursor(chunks[0], Some(&input_block));
    f.set_cursor_position((cx, cy));

    let status = Paragraph::new(Line::from(Span::raw(app.status.clone())))
        .block(Block::default().borders(Borders::ALL).title("Status"));
    f.render_widget(status, chunks[1]);
}

fn run_app<B: Backend>(
    terminal: &mut Terminal<B>,
    session: &CrosstermInputSession,
) -> anyhow::Result<()> {
    let mut app = TextInputDemo::new();

    loop {
        terminal.draw(|f| ui(f, &mut app))?;

        match session.read_event()? {
            Event::Key(key) => {
                if !handle_key_press(key, &mut app) {
                    return Ok(());
                }
            }
            other => app.handle_event(other),
        }
    }
}

fn main() -> anyhow::Result<()> {
    let mut session =
        CrosstermInputSession::install_with_options(CrosstermInputOptions::tui_defaults())?;
    let backend = CrosstermBackend::new(std::io::stdout());
    let mut terminal = Terminal::new(backend)?;

    let result = run_app(&mut terminal, &session);
    let _ = CursorManager::reset();
    let _ = session.uninstall();
    terminal.show_cursor()?;

    result
}