tui-input

repository·main·Indexed 41 days ago

https://github.com/sayanarijit/tui-input

A TUI input library for Rust supporting multiple backends including Ratatui, tui-rs, Crossterm, and Termion. Version 0.15.4 provides an Input struct to manage text state, cursor positioning (handling bytes, codepoints, graphemes, and display columns), and a system of InputRequests to handle text editing operations like navigation, deletion, and character insertion.

Tokens
3.8K
Snippets
13
Records
16
Agent score
52%

What's inside tui-input

  1. Install tui-input

    main

    Add tui-input to your Cargo.toml depending on your preferred TUI backend and framework.

    • For Ratatui with Crossterm (default): Use the standard dependency.
    • For Direct Crossterm (without Ratatui): Enable the crossterm feature and disable default features.
    • For Termion: Enable the termion feature and disable default features.
    # ratatui::crossterm
    tui-input = "*"
    
    # Direct crossterm
    tui-input = { version = "*", features = ["crossterm"], default-features = false }
    
    # termion
    tui-input = { version = "*", features = ["termion"], default-features = false }
  2. Understand string units in tui-input

    main

    The tui-input library distinguishes between four different ways to measure string length and position. Understanding these is critical for correctly positioning cursors and handling text:

    • bytes: UTF-8 encoding indices. Used primarily for internal operations.
    • codepoints: Unicode scalar values (what str::chars() yields). This is the unit used by Input::cursor() and InputRequest::SetCursor.
    • graphemes: User-perceived characters (e.g., emojis with modifiers). Movement and deletion requests like GoToNextChar, DeletePrevChar, GoToNextWord, etc., operate on grapheme boundaries, meaning one request might skip multiple codepoints.
    • display columns: The actual width of the text in terminal cells (per unicode-width). Use Input::visual_cursor() and Input::visual_scroll() to get these values for rendering.

    Example: The string 🤦🏼‍♂️ consists of 17 bytes, 5 codepoints, 1 grapheme, and 2 display columns.

  3. Run tui-input examples

    main

    You can run the provided examples using cargo run with specific feature flags to test different backends:

    • Ratatui (Crossterm): Run the ratatui_crossterm_input example.
    • Direct Crossterm: Run the crossterm_input example with the crossterm feature enabled and default features disabled.
    • Termion: Run the termion_input example with the termion feature enabled and default features disabled.
    # Run the ratatui example
    cargo run --example ratatui_crossterm_input
    
    # Run the example with crossterm as backend.
    cargo run --example crossterm_input --features crossterm --no-default-features
    
    # Run the example with termion as backend.
    cargo run --example termion_input --features termion --no-default-features
  4. Available features in tui-input

    main

    The library provides several feature flags to control backend support and serialization:

    • ratatui-crossterm (default): Integration with Ratatui using the Crossterm backend.
    • ratatui-termion: Integration with Ratatui using the Termion backend.
    • crossterm: Direct support for the Crossterm backend.
    • termion: Direct support for the Termion backend.
    • serde: Support for serialization/deserialization.
  5. Manage input state with InputResponse and StateChanged

    main
    When processing input, the library returns an InputResponse. This response includes information about whether the internal state of the input field has changed, indicated by the StateChanged enum. This allows you to efficiently decide whether a UI re-render is necessary.
  6. Use the Input type for TUI text input

    main

    The Input type is the primary interface for managing text input in a TUI. It is designed to work with different backends (like ratatui, crossterm, or termion) to handle user keystrokes and state changes. You can interact with it using InputRequest to send events and InputResponse to receive the updated state and any changes that occurred.

    use tui_input::{Input, InputRequest, InputResponse, StateChanged};
    
    // Example conceptual usage:
    // let mut input = Input::new();
    // let response: InputResponse = input.handle_request(InputRequest::Key(key));
  7. Render the input UI using `write`

    main

    The write function renders the input text to a terminal buffer at a specific position. It handles cursor positioning by using the Reverse attribute to highlight the character at the current cursor index.

    Parameters:

    • stdout: A mutable reference to a type implementing std::io::Write.
    • value: The string slice representing the current input text.
    • cursor: The current character index of the cursor.
    • (x, y): A tuple of (u16, u16) representing the screen coordinates for the start of the input.
    • width: The maximum width of the input field.

    Note: The function uses crossterm commands to move the cursor and set text attributes.

    use std::io::stdout;
    use tui_input::write;
    
    let mut stdout = stdout();
    write(&mut stdout, "Hello World", 5, (10, 5), 20).unwrap();
  8. Handle InputRequest to update text state

    main

    To modify the text or move the cursor, call input.handle(request). This method returns an InputResponse (which is Option<StateChanged>).

    If the response is Some(StateChanged), the value field indicates if the text content changed, and the cursor field indicates if the cursor position changed. This allows you to efficiently decide whether to re-render your TUI.

    Available InputRequests

    • SetCursor(usize): Move cursor to a specific codepoint.
    • InsertChar(char): Insert a character at the cursor.
    • GoToPrevChar / GoToNextChar: Move cursor by one grapheme.
    • GoToPrevWord / GoToNextWord: Move cursor by one word boundary.
    • GoToStart / GoToEnd: Jump to the beginning or end of the string.
    • DeletePrevChar / DeleteNextChar: Delete one grapheme.
    • DeletePrevWord / DeleteNextWord: Delete one word.
    • DeleteLine: Deletes the entire line and adds it to the yank buffer.
    • DeleteTillEnd: Deletes from the cursor to the end of the line.
    • DeleteFromStart: Deletes from the start of the line to the cursor.
    • Yank: Pastes the contents of the internal yank buffer at the cursor.
    use tui_input::{Input, InputRequest, StateChanged};
    
    let mut input: Input = "Hello Worl".into();
    
    let req = InputRequest::InsertChar('d');
    let resp = input.handle(req);
    
    // resp is Some(StateChanged { value: true, cursor: true })
    assert_eq!(resp, Some(StateChanged { value: true, cursor: true }));
    assert_eq!(input.cursor(), 11);
    assert_eq!(input.value(), "Hello World");
  9. Convert termion events to InputRequest

    main

    The to_input_request function maps termion::event::Event types to crate::input::InputRequest variants. This allows you to translate raw terminal events into the high-level input commands used by tui-input.

    Supported mappings include:

    • DeletePrevChar: Key::Backspace or Key::Ctrl('h')
    • DeleteNextChar: Key::Delete
    • GoToPrevChar: Key::Left or Key::Ctrl('b')
    • GoToNextChar: Key::Right or Key::Ctrl('f')
    • DeleteLine: Key::Ctrl('u')
    • DeletePrevWord: Key::Ctrl('w')
    • Yank: Key::Ctrl('y')
    • GoToStart: Key::Ctrl('a') or Key::Home
    • GoToEnd: Key::Ctrl('e') or Key::End
    • InsertChar(c): Any Key::Char(c) (excluding \t)

    Note: Key::Char('\t') returns None.

    use termion::event::Event;
    // Assuming InputRequest is available in your scope
    let request = to_input_request(&evt);
  10. Render the input UI using termion

    main

    The write function renders the input text and cursor position directly to a std::io::Write implementation (like stdout) using termion commands. It handles cursor positioning, text wrapping/clipping based on width, and visual cursor indication using the Invert style.

    Parameters:

    • stdout: A mutable reference to the writer.
    • value: The string content to display.
    • cursor: The current character index of the cursor.
    • (x, y): The (u16, u16) terminal coordinates where rendering starts.
    • width: The maximum width of the input field.

    Behavior:

    • It uses Goto(x + 1, y + 1) to position the cursor.
    • The character at the cursor position is rendered with Invert style.
    • It handles text clipping by calculating a start offset if the string length exceeds the available width.
    use std::io::stdout;
    
    write(
        &mut stdout(),
        "hello world",
        5,          // cursor index
        (10, 10),   // x, y position
        20          // width
    )?;
  11. Use the Input struct to manage text state

    main

    The Input struct is the primary buffer for managing text and cursor position. It supports common text editing operations and maintains a 'yank' buffer for cut/delete operations.

    Initialization

    • Input::new(value: String): Creates a new input with the cursor at the end.
    • Input::from(value: &str): Creates a new input from a string slice.
    • input.with_value(value: String): Builder pattern to set the value.
    • input.with_cursor(cursor: usize): Builder pattern to set the cursor position (in codepoints).

    Accessing State

    • input.value(): Returns the current text as a &str.
    • input.cursor(): Returns the current cursor position in codepoints.
    • input.visual_cursor(): Returns the cursor position in display columns (useful for terminal rendering).
    • input.to_string(): Converts the input to a String (via From implementation).
    use tui_input::Input;
    
    let input: Input = "Hello World".into();
    
    assert_eq!(input.cursor(), 11);
    assert_eq!(input.value(), "Hello World");
  12. Implement `EventHandler` for Crossterm events

    main

    The EventHandler trait provides a bridge between crossterm events and the Input state machine. By implementing this trait, you can pass raw terminal events directly into an Input instance.

    If you are using the Input struct provided by the library, the trait is already implemented for you. You can call handle_event with a CrosstermEvent, which internally converts the event to an InputRequest and updates the input state, returning Some(StateChanged) if the state was modified.

    use crossterm::event::Event as CrosstermEvent;
    use tui_input::EventHandler;
    
    // Assuming `input` is an instance of `tui_input::Input`
    // and `evt` is a `crossterm::event::Event`
    if let Some(changed) = input.handle_event(&evt) {
        // The input state has changed, trigger a re-render
    }