TermUI.Input.TTY (TermUI v1.0.0)

View Source

TTY mode input handler implementing the TermUI.Input behaviour.

This module requests one character at a time using IO.getn/2 for IEx compatibility. The terminal remains in cooked mode, so the shell or terminal driver may still buffer those characters until Enter is pressed.

Features

  • IEx Compatible: Reads through the active shell's IO server without replacing its cooked mode
  • Single-character reads: Processes one requested character at a time
  • Normalized keyboard parsing: Arrow keys, Tab, Enter, and function keys are parsed once their bytes are delivered
  • 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

IEx Compatibility

IEx owns the active shell, so the TTY backend reads through that shell's IO server instead of trying to replace it with a Raw shell. This allows a TermUI application to receive input and return cleanly to the IEx prompt.

How Arrow Keys and Special Keys Work

IO.getn/2 requests one character, but it does not disable the operating system's canonical input mode. Delivery therefore depends on the active shell and terminal driver. Some environments deliver each key immediately; others buffer input until Enter. Once delivered:

  • Arrow keys: Are normalized after delivery (↑↓←→)
  • Tab: Can drive field/button navigation after delivery
  • Enter: Is normalized when delivered
  • Function keys: Are parsed when their escape bytes are delivered
  • Ctrl combinations: Retain the active shell's cooked-mode behavior; some combinations may be handled by the terminal rather than emitted as events

Most keyboard-driven widgets remain usable, with line buffering as the main compatibility difference from Raw mode.

Usage

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

# Poll for input (timeout is noted but not honored - blocking I/O)
case TermUI.Input.TTY.poll(state, 100) do
  {{:ok, event}, new_state} -> handle_event(event, new_state)
  {:eof, new_state} -> handle_shutdown(new_state)
end

Timeout Semantics

Important: The timeout parameter is accepted for API compatibility but is not honored in TTY mode. :io.get_chars/2 is blocking and will wait indefinitely for input. Direct callers must account for this:

  • Do not rely on :timeout results
  • Poll in a dedicated process if other work must continue
  • TermUI.Runtime already uses that dedicated-reader pattern, so Elm command timers and rendering can continue in TTY mode

Comparison with Raw Input Handler

FeatureTTY (Input.TTY)Raw (Input.Raw)
IEx CompatibleYesNo
Timeout supportNo (blocking)Yes (Task-based)
Non-blocking pollNoYes
Escape sequencesYesYes
Arrow/Tab/EnterYes (delivery may be buffered)Yes
Mouse parsingParser supports supplied sequences; runtime does not enable reportingRuntime enables reporting except on WSL/ConPTY

When to Use TTY Mode

TTY mode is appropriate when:

  • You want to run TUI applications inside IEx
  • You don't need timeout-based polling
  • You want simpler deployment (no raw mode setup)
  • A direct caller can isolate blocking input in its own process
  • You're building simple interactive scripts

Use Raw when immediate input or polling timeouts are required. Animations and periodic messages can still run under TermUI.Runtime in TTY mode because the blocking read is isolated from the runtime process.

Escape Sequence Handling

When an escape sequence spans multiple reads (e.g., arrow keys send multiple bytes), the partial sequence is buffered and completed in the dedicated input reader process before an event is returned.

TTY IO requests cannot be cancelled safely: a timed-out request can still consume a later byte. Sequence completion therefore stays synchronous. In a cooked terminal, a lone Escape is emitted when the containing line is submitted.

Security

This module implements several security measures to prevent resource exhaustion:

  • Buffer size limit: Input buffer is limited by InputBuffer.apply_limit/2 (1KB max, truncates to 256 bytes when exceeded). This prevents memory exhaustion from malformed or malicious escape sequences.

  • Event queue limit: Maximum 1000 events can be queued. Excess events are dropped with a warning. This prevents memory exhaustion from rapid input.

  • Rate-limited logging: Buffer overflow warnings use rate-limited logging (via InputBuffer) to prevent log flooding attacks.

  • Dedicated blocking reader: Partial sequences are completed outside the runtime process, so blocking TTY delivery cannot stall rendering or cleanup.

For concurrent usage, each handler instance maintains independent state, so memory usage scales linearly with the number of concurrent handlers.

Summary

Types

t()

State for the TTY input handler.

Functions

Returns the input mode for this handler.

Creates a new TTY input handler state.

Polls for input.

Stops the TTY input handler and restores IO options.

Types

t()

@type t() :: %TermUI.Input.TTY{
  buffer: binary(),
  event_queue: [TermUI.Event.t()],
  io_opts_restored: boolean(),
  io_opts_set: boolean(),
  original_opts: term()
}

State for the TTY input handler.

  • :buffer - Binary buffer for partial escape sequences
  • :event_queue - Queue of parsed events waiting to be returned
  • :io_opts_restored - Whether IO options have been restored
  • :io_opts_set - Whether IO options have been set

Functions

mode(tty)

@spec mode(t()) :: :tty

Returns the input mode for this handler.

Always returns :tty for the TTY input handler.

Examples

mode = TTY.mode(state)
# => :tty

new()

@spec new() :: t()

Creates a new TTY input handler state.

Configures the IO server for TTY input (echo: false, binary: false).

Examples

state = TermUI.Input.TTY.new()

poll(state, timeout)

Polls for input.

Note: The timeout parameter is accepted for API compatibility but is not honored in TTY mode. :io.get_chars/2 is blocking and will wait indefinitely for input. This function will not return :timeout in normal operation.

Parameters

  • state - Current handler state
  • timeout - Maximum wait time in milliseconds (ignored in TTY mode)

Returns

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

Examples

# Note: timeout is ignored, this will block until input
{result, state} = TTY.poll(state, 100)

stop(tty)

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

Stops the TTY input handler and restores IO options.

Examples

:ok = TTY.stop(state)