TermUI.Widgets.TextInput.Line (TermUI v1.0.0)
View SourceLine-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
endWith 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
| Feature | TextInput.Line | TextInput |
|---|---|---|
| Input style | Line-based (Enter to submit) | Event-driven; backend-dependent delivery |
| Line editing | Shell-provided | Widget-handled |
| Real-time validation | No | Yes |
| Multi-line | No | Yes (optional) |
| Custom key bindings | No | Yes |
| Blocking | Yes (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:
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.
Process implications: When
read/1orhandle_focus/1is 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
Best practices:
- Use
TextInput.Linefor simple, sequential input flows - For concurrent input handling, spawn a separate process for input
- For real-time UI during input, use the standard
TextInputwidget
- Use
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
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
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
@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 function type.
Should return:
:ok- Input is valid{:ok, transformed}- Input is valid, use transformed value{:error, reason}- Input is invalid
Functions
Clears focus and calls the on_blur callback if configured.
Examples
state = TextInput.Line.blur(state)
Clears the current value and any error.
Examples
state = TextInput.Line.clear(state)
Clears the current error.
Examples
state = TextInput.Line.clear_error(state)
Checks if the widget is currently focused.
Examples
TextInput.Line.focused?(state) # => true or false
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
Gets the label, if any.
Examples
label = TextInput.Line.get_label(state)
Gets the placeholder text.
Examples
placeholder = TextInput.Line.get_placeholder(state)
Gets the prompt.
Examples
prompt = TextInput.Line.get_prompt(state)
Gets the current value.
Examples
value = TextInput.Line.get_value(state)
@spec handle_focus(t()) :: read_result()
Handles focus gain by initiating a line read.
When the widget gains focus, this function:
- Sets the focused state to true
- Initiates a blocking line read
- Returns the result with updated state
- 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, ...}}
Checks if the widget has an error.
Examples
if TextInput.Line.has_error?(state) do
IO.puts("Please fix the error")
end
Initializes TextInput.Line state from props.
Examples
props = TextInput.Line.new(prompt: "Name: ")
{:ok, state} = TextInput.Line.init(props)
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
)
@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
@spec render(t()) :: TermUI.Component.RenderNode.t()
Renders the widget state as a render node tree.
The render output consists of:
- Label (if provided) - displayed on first line
- Prompt + value (or placeholder if empty) - the input line
- 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)
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)
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")