TermUI.Widgets.TextInput.Line (TermUI v1.0.0)

View Source

Line-based text input widget using shell line editing.

This widget provides a simple text input experience using IO.gets/1 through the TermUI.Input.LineReader module. Unlike the standard event-driven TextInput widget, this widget delegates to the shell for line editing, providing familiar shell features.

When to Use TextInput.Line

Use TextInput.Line when you need:

  • Free-form text entry: User types arbitrary text and submits with Enter
  • Shell line editing: Backspace, cursor movement, command history
  • Simple input flow: Just prompt → read → validate → done

Use the standard TextInput widget when you need:

  • Event-driven key handling (delivery depends on the selected backend)
  • Custom key bindings or input transformations
  • Real-time validation as the user types
  • Multi-line text editing

Shell Line Editing Features

When using TextInput.Line, the shell provides (depending on terminal):

  • Backspace: Delete character before cursor
  • Delete: Delete character at cursor
  • Left/Right arrows: Move cursor within line
  • Home/End: Jump to start/end of line
  • Ctrl+A/E: Jump to start/end (Emacs-style)
  • Ctrl+K: Kill to end of line
  • Up/Down arrows: Command history (if shell supports)

These features are provided by the shell, not by TermUI.

TTY Mode Compatibility

This widget is designed for TTY mode where shell line editing is available. It should not be mixed with a running Raw runtime, which already owns local terminal input.

Standard TextInput Works in TTY Mode

The standard TermUI.Widgets.TextInput consumes the same normalized key events in either backend. In cooked TTY mode those events may be buffered until Enter; Raw mode provides character-at-a-time delivery. Use TextInput.Line when you intentionally want a blocking shell line read.

Usage

# Create input props
props = TextInput.Line.new(
  prompt: "Enter name: ",
  label: "User Name",
  placeholder: "Type your name"
)

# Initialize state
{:ok, state} = TextInput.Line.init(props)

# Read input (blocks until Enter)
case TextInput.Line.read(state) do
  {:ok, value, new_state} ->
    IO.puts("You entered: #{value}")
    new_state

  {:error, reason, new_state} ->
    IO.puts("Invalid input: #{reason}")
    new_state

  {:eof, new_state} ->
    IO.puts("EOF received")
    new_state
end

With Validation

validator = fn input ->
  if String.length(input) >= 3 do
    :ok
  else
    {:error, "Name must be at least 3 characters"}
  end
end

props = TextInput.Line.new(
  prompt: "Enter name: ",
  validator: validator
)

Comparison with TextInput

FeatureTextInput.LineTextInput
Input styleLine-based (Enter to submit)Event-driven; backend-dependent delivery
Line editingShell-providedWidget-handled
Real-time validationNoYes
Multi-lineNoYes (optional)
Custom key bindingsNoYes
BlockingYes (blocks during read)No (event-driven)

Blocking I/O Behavior (Architectural Note)

Unlike other TermUI widgets which are event-driven, TextInput.Line uses blocking I/O when reading input. This is an intentional design choice:

  1. Why blocking? Shell line editing requires the terminal to be in line mode, where the shell buffers input until Enter is pressed. This is fundamentally different from raw mode's character-by-character input.

  2. Process implications: When read/1 or handle_focus/1 is called, the calling process blocks until input is complete. This means:

    • The widget cannot respond to other events during input
    • UI updates (like animations) will pause
    • Other processes are unaffected
  3. Best practices:

    • Use TextInput.Line for simple, sequential input flows
    • For concurrent input handling, spawn a separate process for input
    • For real-time UI during input, use the standard TextInput widget

This behavior is intentional and will not change. The blocking nature enables shell line editing features that are not possible with event-driven input.

Summary

Types

Result of a read operation.

t()

TextInput.Line state structure.

Validator function type.

Functions

Clears focus and calls the on_blur callback if configured.

Clears the current value and any error.

Clears the current error.

Checks if the widget is currently focused.

Gets the current error message, if any.

Gets the label, if any.

Gets the placeholder text.

Gets the prompt.

Gets the current value.

Handles focus gain by initiating a line read.

Checks if the widget has an error.

Initializes TextInput.Line state from props.

Creates new TextInput.Line props.

Reads a line of input from the user.

Renders the widget state as a render node tree.

Sets the focus state directly.

Sets the value programmatically.

Types

read_result()

@type read_result() ::
  {:ok, term(), t()} | {:error, term(), t()} | {:cancelled, t()} | {:eof, t()}

Result of a read operation.

  • {:ok, value, state} - Successfully read and validated input
  • {:error, reason, state} - Read succeeded but validation failed
  • {:cancelled, state} - Input was cancelled (Ctrl+C)
  • {:eof, state} - End of input stream

t()

@type t() :: %TermUI.Widgets.TextInput.Line{
  error: String.t() | nil,
  focused: boolean(),
  label: String.t() | nil,
  on_blur: (t() -> any()) | nil,
  placeholder: String.t(),
  prompt: String.t(),
  validator: validator() | nil,
  value: String.t()
}

TextInput.Line state structure.

  • :prompt - Text displayed before input cursor
  • :value - Current or last entered value
  • :label - Optional label displayed above input
  • :validator - Optional validation function
  • :placeholder - Text shown when value is empty
  • :error - Current validation error message, if any
  • :focused - Whether the widget currently has focus
  • :on_blur - Optional callback when widget loses focus or completes input

validator()

@type validator() :: (String.t() -> :ok | {:ok, term()} | {:error, term()})

Validator function type.

Should return:

  • :ok - Input is valid
  • {:ok, transformed} - Input is valid, use transformed value
  • {:error, reason} - Input is invalid

Functions

blur(state)

@spec blur(t()) :: t()

Clears focus and calls the on_blur callback if configured.

Examples

state = TextInput.Line.blur(state)

clear(state)

@spec clear(t()) :: t()

Clears the current value and any error.

Examples

state = TextInput.Line.clear(state)

clear_error(state)

@spec clear_error(t()) :: t()

Clears the current error.

Examples

state = TextInput.Line.clear_error(state)

focused?(line)

@spec focused?(t()) :: boolean()

Checks if the widget is currently focused.

Examples

TextInput.Line.focused?(state)  # => true or false

get_error(line)

@spec get_error(t()) :: String.t() | nil

Gets the current error message, if any.

Examples

case TextInput.Line.get_error(state) do
  nil -> IO.puts("No error")
  error -> IO.puts("Error: #{error}")
end

get_label(line)

@spec get_label(t()) :: String.t() | nil

Gets the label, if any.

Examples

label = TextInput.Line.get_label(state)

get_placeholder(line)

@spec get_placeholder(t()) :: String.t()

Gets the placeholder text.

Examples

placeholder = TextInput.Line.get_placeholder(state)

get_prompt(line)

@spec get_prompt(t()) :: String.t()

Gets the prompt.

Examples

prompt = TextInput.Line.get_prompt(state)

get_value(line)

@spec get_value(t()) :: String.t()

Gets the current value.

Examples

value = TextInput.Line.get_value(state)

handle_focus(state)

@spec handle_focus(t()) :: read_result()

Handles focus gain by initiating a line read.

When the widget gains focus, this function:

  1. Sets the focused state to true
  2. Initiates a blocking line read
  3. Returns the result with updated state
  4. Calls on_blur callback if configured

The function blocks until the user presses Enter or cancels with Ctrl+C.

Return Values

  • {:ok, value, state} - Successfully read and validated input
  • {:error, reason, state} - Validation failed
  • {:cancelled, state} - User cancelled with Ctrl+C (EOF)

Examples

{:ok, state} = TextInput.Line.init(TextInput.Line.new(prompt: "> "))
result = TextInput.Line.handle_focus(state)
# User types "hello" and presses Enter
# => {:ok, "hello", %TextInput.Line{value: "hello", focused: false, ...}}

has_error?(line)

@spec has_error?(t()) :: boolean()

Checks if the widget has an error.

Examples

if TextInput.Line.has_error?(state) do
  IO.puts("Please fix the error")
end

init(props)

@spec init(map()) :: {:ok, t()}

Initializes TextInput.Line state from props.

Examples

props = TextInput.Line.new(prompt: "Name: ")
{:ok, state} = TextInput.Line.init(props)

new(opts \\ [])

@spec new(keyword()) :: map()

Creates new TextInput.Line props.

Options

  • :prompt - Text to display before input (default: "")
  • :value - Initial value (default: "")
  • :label - Optional label to display above input (default: nil)
  • :validator - Validation function (default: nil)
  • :placeholder - Text shown when value is empty (default: "")
  • :on_blur - Callback when widget loses focus or completes input (default: nil)

Examples

# Simple input
TextInput.Line.new(prompt: "Name: ")

# With label and placeholder
TextInput.Line.new(
  prompt: "> ",
  label: "Enter your name",
  placeholder: "Type here..."
)

# With validation
TextInput.Line.new(
  prompt: "Age: ",
  validator: fn input ->
    case Integer.parse(input) do
      {age, ""} when age > 0 -> {:ok, age}
      _ -> {:error, "Please enter a valid positive number"}
    end
  end
)

read(state)

@spec read(t()) :: read_result()

Reads a line of input from the user.

This function blocks until the user presses Enter or EOF is received. The shell provides line editing features during input.

If a validator is configured, it will be applied to the input. The result depends on validation:

  • Valid input: {:ok, value, new_state} - value may be transformed by validator
  • Invalid input: {:error, reason, new_state} - error is stored in state
  • EOF: {:eof, new_state}

Examples

case TextInput.Line.read(state) do
  {:ok, value, state} ->
    IO.puts("Got: #{value}")
    state

  {:error, reason, state} ->
    IO.puts("Error: #{reason}")
    state

  {:eof, state} ->
    IO.puts("EOF")
    state
end

render(state)

@spec render(t()) :: TermUI.Component.RenderNode.t()

Renders the widget state as a render node tree.

The render output consists of:

  1. Label (if provided) - displayed on first line
  2. Prompt + value (or placeholder if empty) - the input line
  3. Error message (if present) - displayed below in error styling

Examples

state = %TextInput.Line{prompt: "> ", value: "hello", label: "Name"}
node = TextInput.Line.render(state)

Styling

  • Label: default foreground color
  • Prompt: default foreground color
  • Value: default foreground color
  • Placeholder: dim/muted style (bright_black)
  • Error: error style (red)

set_focused(state, focused)

@spec set_focused(t(), boolean()) :: t()

Sets the focus state directly.

Typically you should use handle_focus/1 instead, which initiates a read. This function is useful for testing or manual focus management.

Examples

state = TextInput.Line.set_focused(state, true)

set_value(state, value)

@spec set_value(t(), String.t()) :: t()

Sets the value programmatically.

This does not trigger validation. Use read/1 to get validated input.

Examples

state = TextInput.Line.set_value(state, "new value")