TermUI.Input behaviour (TermUI v1.0.0)

View Source

Behaviour defining the input abstraction for TermUI.

This module establishes a unified interface for reading terminal input, regardless of whether the application is running with the Raw backend or the TTY backend.

Input Modes

TermUI supports two input approaches:

Character Mode (Default)

Both Raw and TTY handlers request one character at a time. Raw mode receives each keystroke immediately. TTY mode leaves the terminal in cooked mode, so the shell or terminal driver may buffer input until Enter. After delivery, both handlers normalize navigation keys into the same TermUI events.

This is the primary input mode used by most widgets:

  • Menu, PickList, Table - navigation with arrows, selection with Enter
  • Dialog, AlertDialog - button navigation with Tab
  • Tabs, TreeView - keyboard navigation

Line Mode (TextInput.Line Only)

The TermUI.Input.LineReader module provides line-based input using IO.gets/1. This is only used by the TextInput.Line widget, which benefits from shell line editing features:

  • Backspace, delete, cursor movement
  • Command history (if shell supports it)
  • Input submitted on Enter

Most applications should use character mode. Line mode is a specialized feature for free-form text entry where shell editing is desirable.

Implementing the Behaviour

Input handlers must implement three callbacks:

  • poll/2 - Read input with optional timeout
  • mode/1 - Return the input mode (:raw or :tty)
  • stop/1 - Cleanup and release resources

Example Implementation

defmodule MyApp.CustomInput do
  @behaviour TermUI.Input

  @impl true
  def poll(state, timeout) do
    # Read input, return {:ok, event}, :timeout, or :eof
    {:ok, event, state}
  end

  @impl true
  def mode(_state), do: :tty

  @impl true
  def stop(_state), do: :ok
end

Built-in Handlers

Use TermUI.Input.Selector to automatically choose the appropriate handler based on the active backend.

Summary

Types

Result of an input polling operation.

Key event returned from input polling.

Input mode indicator.

Result of poll/2 including updated state.

Opaque state maintained by the input handler.

Callbacks

Return the input mode for this handler.

Poll for input with an optional timeout.

Stop the input handler and release any resources.

Types

input_result()

@type input_result() ::
  {:ok, key_event() | TermUI.Event.Mouse.t() | TermUI.Event.Paste.t()}
  | :timeout
  | :eof

Result of an input polling operation.

  • {:ok, key_event()} - A key event was received
  • {:ok, Event.Mouse.t()} - A mouse event was received
  • {:ok, Event.Paste.t()} - A paste event was received (bracketed paste)
  • :timeout - No input within the timeout period
  • :eof - End of input stream

key_event()

@type key_event() :: TermUI.Event.Key.t()

Key event returned from input polling.

This is the standard key event type from TermUI.Event.Key.

mode()

@type mode() :: :raw | :tty

Input mode indicator.

  • :raw - Raw mode with full terminal control
  • :tty - TTY mode with shell present

poll_result()

@type poll_result() :: {input_result(), state()}

Result of poll/2 including updated state.

state()

@type state() :: term()

Opaque state maintained by the input handler.

Each handler implementation defines its own state structure.

Callbacks

mode(state)

@callback mode(state()) :: mode()

Return the input mode for this handler.

Returns :raw or :tty to indicate which mode the handler operates in. This allows components to adapt their behavior if needed. Most widgets handle the normalized events identically, although event delivery timing differs.

Use Cases

Most widgets do not need to check the mode—input events are normalized across both handlers. However, some specialized components might use this:

  • Displaying mode indicator in status bar
  • Adjusting behavior for mode-specific features
  • Debugging and logging

Examples

mode = MyInput.mode(state)
# => :raw or :tty

poll(state, timeout)

@callback poll(state(), timeout :: non_neg_integer()) :: poll_result()

Poll for input with an optional timeout.

Reads input from the terminal and returns a parsed event. The timeout specifies the maximum time to wait for input in milliseconds.

Parameters

  • state - Handler-specific state (escape sequence buffer, etc.)
  • timeout - Maximum wait time in milliseconds (0 for non-blocking)

Returns

  • {{:ok, event}, new_state} - An event was received
  • {:timeout, new_state} - No input within timeout
  • {:eof, new_state} - End of input stream

Timeout Semantics

The timeout is best-effort:

  • Raw mode: Supports non-blocking reads; timeout is honored accurately
  • TTY mode: Uses blocking IO.getn/2; timeout may not be honored

Direct callers should not use input polling to schedule application work; use timers in another process. TermUI.Runtime already places blocking TTY polling in a dedicated reader process, so its render and command timers keep running while input waits.

Escape Sequences

Handlers are responsible for buffering and parsing escape sequences. Multi-byte sequences (arrow keys, function keys) should be assembled before returning an event. Incomplete sequences should be buffered in the state and completed on subsequent calls.

Examples

# Non-blocking poll
{result, new_state} = MyInput.poll(state, 0)

# Wait up to 100ms
{result, new_state} = MyInput.poll(state, 100)

# Process result
case result do
  {:ok, %Event.Key{key: :enter}} -> handle_enter()
  {:ok, %Event.Key{key: :up}} -> handle_up()
  :timeout -> continue_animation()
  :eof -> shutdown()
end

stop(state)

@callback stop(state()) :: :ok

Stop the input handler and release any resources.

This callback is called during runtime shutdown to allow the handler to perform cleanup operations such as:

  • Restoring terminal IO options
  • Stopping any background processes
  • Closing file descriptors or ports

The function should be idempotent—calling it multiple times should have the same effect as calling it once.

Parameters

  • state - Handler-specific state

Returns

  • :ok - Cleanup completed successfully

Examples

:ok = MyInput.stop(state)

Implementation Notes

  • Raw handler: Typically a no-op since InputReader is managed separately
  • TTY handler: Should restore IO options (echo, binary mode)
  • Custom handlers: Implement any necessary cleanup

This callback is always called during runtime shutdown, even if the handler was never successfully started or has already been stopped due to EOF.