TermUI.Input.Raw (TermUI v1.0.0)

View Source

Raw mode input handler implementing the TermUI.Input behaviour.

This module provides synchronous input polling with timeout support for applications running with the Raw backend. It reads single characters from stdin and parses escape sequences into TermUI.Event structs.

TermUI.Runtime selects this handler for Raw mode only when use_input_handler: true. The default Raw path uses the asynchronous TermUI.Terminal.InputReader instead.

Features

  • Non-blocking input: Supports timeout-based polling (including 0ms for non-blocking checks)
  • Escape sequence parsing: Handles arrow keys, function keys, mouse events, and other terminal escape sequences
  • Buffer management: Maintains partial escape sequences between poll calls
  • Security: Buffer and queue size limits prevent memory exhaustion

Usage

# Create initial state
state = TermUI.Input.Raw.new()

# Poll for input with 100ms timeout
case TermUI.Input.Raw.poll(state, 100) do
  {{:ok, event}, new_state} -> handle_event(event, new_state)
  {:timeout, new_state} -> handle_idle(new_state)
  {:eof, new_state} -> handle_shutdown(new_state)
end

How It Works

The module spawns a Task to read from stdin using :io.get_chars/2 (Erlang's IO module directly). This is critical for compatibility with raw mode activated via :shell.start_interactive({:noshell, :raw}), which redirects standard input. Elixir's IO.getn/2 wrapper cannot access the redirected input, but :io.get_chars/2 works correctly.

Since :io.get_chars/2 blocks until input is available, using a Task allows us to implement timeout semantics via Task.yield/2.

When an escape sequence spans multiple reads (e.g., arrow keys send multiple bytes), the partial sequence is buffered and completed on subsequent polls.

Escape Sequence Timeout

When a partial escape sequence is detected (e.g., lone ESC), the handler waits up to 50ms for completion. This matches standard terminal emulator behavior and distinguishes ESC key presses from escape sequences. The 50ms timeout is the same value used by TermUI.Terminal.InputReader.

Comparison with InputReader

Unlike TermUI.Terminal.InputReader which is a GenServer that asynchronously sends events to a target process, this module provides synchronous polling suitable for use with the TermUI.Input behaviour interface. This module uses direct :io.get_chars/2 calls wrapped in Tasks for timeout support, rather than delegating to InputReader, because InputReader's async message-based design is incompatible with the synchronous polling contract.

Both modules use the same underlying approach (:io.get_chars/2) for reading from stdin, ensuring compatibility with raw mode's redirected input.

Summary

Types

t()

State for the Raw input handler.

Functions

Returns the input mode for this handler.

Creates a new Raw input handler state.

Polls for input with the specified timeout.

Stops the Raw input handler.

Types

t()

@type t() :: %TermUI.Input.Raw{buffer: binary(), event_queue: [TermUI.Event.t()]}

State for the Raw input handler.

  • :buffer - Binary buffer for partial escape sequences
  • :event_queue - Queue of parsed events waiting to be returned

Functions

mode(raw)

@spec mode(t()) :: :raw

Returns the input mode for this handler.

Always returns :raw for the Raw input handler.

Examples

mode = Raw.mode(state)
# => :raw

new()

@spec new() :: t()

Creates a new Raw input handler state.

Examples

state = TermUI.Input.Raw.new()

poll(state, timeout)

Polls for input with the specified timeout.

Reads input from stdin and parses it into events. The timeout specifies the maximum time to wait for input in milliseconds. Use 0 for non-blocking polls.

Parameters

  • state - Current handler state
  • timeout - Maximum wait time in milliseconds

Returns

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

Examples

# Non-blocking check
{result, state} = Raw.poll(state, 0)

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

stop(raw)

@spec stop(t()) :: :ok

Stops the Raw input handler.

For the Raw handler, this is a no-op since the InputReader GenServer is managed separately by the Runtime. This function exists for compatibility with the TermUI.Input behaviour.

Examples

:ok = Raw.stop(state)