TermUI.Runtime (TermUI v1.0.0)

View Source

The central runtime orchestrator for TermUI applications.

The runtime implements The Elm Architecture dispatch loop:

  1. Receive event from terminal
  2. Deliver it to the single root Elm module
  3. Call the root's event_to_msg
  4. Call the root's update with the resulting message
  5. Collect commands from update
  6. Mark component dirty
  7. On render timer, call view and render

The components field in runtime state currently contains only the reserved :root entry. TermUI.ComponentServer, TermUI.EventRouter, and the other process-oriented component services are separate lower-level APIs; they are not mounted or consulted by this runtime loop.

Usage

# Start with a root component
{:ok, runtime} = Runtime.start_link(root: MyApp.Root)

# Send events (usually from terminal input)
Runtime.send_event(runtime, Event.key(:enter))

# Shutdown gracefully
Runtime.shutdown(runtime)

Summary

Functions

Gets the current backend mode.

Gets the detected terminal capabilities.

Returns a child specification for starting the runtime in a supervisor.

Delivers a command result back to the runtime.

Marks the runtime dirty and performs an immediate render.

Gets the current runtime state (for testing/debugging).

Starts the runtime and blocks until it shuts down.

Sends an event to the runtime for processing.

Sends a message to a runtime component ID.

Initiates graceful shutdown of the runtime.

Starts the runtime with the given options.

Synchronously waits for all pending events and messages to be processed.

Types

option()

@type option() ::
  {:root, module()}
  | {:name, GenServer.name()}
  | {:render_interval, pos_integer()}
  | {:backend, :auto | :raw | :tty | module() | {module(), keyword()}}
  | {:skip_terminal, boolean()}
  | {:use_input_handler, boolean()}
  | {:iex_mode, boolean()}

Functions

backend_mode()

@spec backend_mode() :: TermUI.Runtime.State.backend_mode()

Gets the current backend mode.

Returns the node-global local backend context: :raw, :tty, :skip, or nil. Explicit custom/SSH runtimes do not publish a mode here; query their host/session state instead.

Examples

:raw = Runtime.backend_mode()
:tty = Runtime.backend_mode()

capabilities()

@spec capabilities() :: TermUI.Runtime.State.capabilities() | nil

Gets the detected terminal capabilities.

Returns a map with keys:

  • :colors - Color depth (:true_color, :color_256, :color_16, :monochrome)
  • :unicode - Boolean indicating Unicode support
  • :dimensions - {rows, cols} tuple or nil
  • :terminal - Boolean indicating terminal presence

Returns the node-global local Raw/TTY capability map, or nil when no local context is published. Explicit custom/SSH runtimes keep capabilities in their session/backend state and do not publish them here.

Examples

%{colors: :true_color, unicode: true} = Runtime.capabilities()

child_spec(init_arg)

@spec child_spec([option()]) :: Supervisor.child_spec()

Returns a child specification for starting the runtime in a supervisor.

Options

Same as start_link/1:

  • :root - The root component module (required)
  • :name - GenServer name (optional)
  • :render_interval - Milliseconds between renders (default: 16)
  • :backend - :auto, :raw, :tty, a backend module, or {backend_module, backend_opts}
  • :skip_terminal - Skip terminal initialization (default: false)
  • :use_input_handler - Use the TermUI.Input handler path (default: false)

Examples

children = [
  {TermUI.Runtime, root: MyApp.Root, name: :my_runtime}
]

Supervisor.start_link(children, strategy: :one_for_one)

command_result(runtime, component_id, command_id, result)

@spec command_result(GenServer.server(), term(), term(), term()) :: :ok

Delivers a command result back to the runtime.

force_render(runtime)

@spec force_render(GenServer.server()) :: :ok

Marks the runtime dirty and performs an immediate render.

get_state(runtime)

@spec get_state(GenServer.server()) :: TermUI.Runtime.State.t()

Gets the current runtime state (for testing/debugging).

run(opts)

@spec run([option()]) :: :ok | {:error, term()}

Starts the runtime and blocks until it shuts down.

This is the main entry point for running a TUI application. It starts the runtime, takes over the terminal, and blocks the calling process until the application exits (e.g., user presses quit key).

Options

Same as start_link/1.

Example

# In your application entry point:
TermUI.Runtime.run(root: MyApp.Root)
# This blocks until the app exits

send_event(runtime, event)

@spec send_event(GenServer.server(), TermUI.Event.t()) :: :ok

Sends an event to the runtime for processing.

send_message(runtime, component_id, message)

@spec send_message(GenServer.server(), term(), term()) :: :ok

Sends a message to a runtime component ID.

In the 1.0 runtime, only :root is registered. Messages sent to any other ID are ignored.

shutdown(runtime)

@spec shutdown(GenServer.server()) :: :ok

Initiates graceful shutdown of the runtime.

start_link(opts)

@spec start_link([option()]) :: GenServer.on_start()

Starts the runtime with the given options.

Options

  • :root - The root component module (required)
  • :name - GenServer name (optional)
  • :render_interval - Milliseconds between renders (default: 16)
  • :backend - :auto (default), :raw, :tty, a backend module, or {backend_module, backend_opts}
  • :skip_terminal - Skip terminal initialization (default: false, for testing)
  • :use_input_handler - Use the TermUI.Input handler path instead of the legacy local input reader (default: false)

Backend Selection

The :backend option controls which terminal backend is used:

  • :auto (default) - Attempts raw mode first, falls back to TTY if unavailable
  • :raw - Forces raw mode (requires OTP 28+, errors if unavailable)
  • :tty - Forces TTY mode (line-based input, no raw mode attempt)

When the caller is an IEx evaluator, :auto resolves directly to :tty so the existing shell keeps ownership of the terminal. An explicit :raw or custom backend selection is preserved.

Examples

# Auto-detect backend (default behavior)
{:ok, runtime} = Runtime.start_link(root: MyApp.Root)

# Force TTY mode
{:ok, runtime} = Runtime.start_link(root: MyApp.Root, backend: :tty)

# Query backend mode at runtime
:raw = Runtime.backend_mode()

# Query capabilities (useful for TTY mode)
%{colors: :true_color, unicode: true} = Runtime.capabilities()

sync(runtime, timeout \\ 5000)

@spec sync(GenServer.server(), timeout()) :: :ok

Synchronously waits for all pending events and messages to be processed.

This is primarily useful for testing to avoid race conditions from Process.sleep. It processes all queued messages and returns when complete.

Example

Runtime.send_event(runtime, Event.key(:up))
Runtime.send_event(runtime, Event.key(:up))
Runtime.sync(runtime)  # Wait for both events to be processed
state = Runtime.get_state(runtime)
assert state.root_state.count == 2