TermUI.Elm behaviour (TermUI v1.0.0)

View Source

The Elm Architecture implementation for TermUI components.

This module provides the core callbacks for implementing components using The Elm Architecture pattern: update/2 for state changes and view/1 for rendering.

The Pattern

  1. Events arrive from terminal input
  2. event_to_msg/2 converts events to component-specific messages
  3. update/2 transforms state based on messages, returns new state + commands
  4. view/1 renders current state to a render tree
  5. Commands execute asynchronously, sending result messages back

TermUI.Runtime runs one Elm root module and owns its state. Stateful widgets are explicit state machines embedded in that root state; the runtime does not automatically start a process or route focus for each widget.

Usage

defmodule Counter do
  use TermUI.Elm

  alias TermUI.Event

  def init(_opts), do: %{count: 0}

  def event_to_msg(%Event.Key{key: :up}, _state), do: {:msg, :increment}
  def event_to_msg(%Event.Key{key: :down}, _state), do: {:msg, :decrement}
  def event_to_msg(_, _), do: :ignore

  def update(:increment, state), do: {%{state | count: state.count + 1}, []}
  def update(:decrement, state), do: {%{state | count: state.count - 1}, []}

  def view(state) do
    text("Count: #{state.count}")
  end
end

Summary

Callbacks

Converts an event to a component-specific message.

Initializes component state from options.

Updates component state based on a message.

Renders the current state to a render tree.

Functions

Normalizes init result to standard form.

Normalizes update result to standard form.

Validates that an update function is pure (best effort).

Types

command()

@type command() :: term()

event_to_msg_result()

@type event_to_msg_result() :: {:msg, msg()} | :ignore | :propagate

init_result()

@type init_result() ::
  state()
  | {state(), [command()]}
  | {:ok, state()}
  | {:ok, state(), [command()]}

msg()

@type msg() :: TermUI.Message.t()

render_tree()

@type render_tree() :: term()

state()

@type state() :: term()

update_result()

@type update_result() :: {state(), [command()]} | {state()} | :noreply

Callbacks

event_to_msg(t, state)

@callback event_to_msg(TermUI.Event.t(), state()) :: event_to_msg_result()

Converts an event to a component-specific message.

This callback transforms raw terminal events into domain-specific messages that have semantic meaning for the component.

Parameters

  • event - The terminal event (Key, Mouse, Resize, etc.)
  • state - Current component state

Returns

  • {:msg, message} - Event converted to a message for update
  • :ignore - Event not handled by this component
  • :propagate - Leave the event unhandled. The 1.0 single-root runtime has no parent to receive it, so this currently has the same practical effect as :ignore.

init(opts)

(optional)
@callback init(opts :: keyword()) :: init_result()

Initializes component state from options.

Called once when the component is created.

Parameters

  • opts - Options passed to the component

Returns

Initial state for the component.

update(msg, state)

@callback update(msg(), state()) :: update_result()

Updates component state based on a message.

This is the core logic of the component. It receives the current state and a message, and returns the new state plus any commands to execute.

Keep update functions pure when practical—state transitions remain easier to test when side effects are represented by returned commands. The runtime does not enforce purity, and application-owned processes may instead report results through TermUI.Runtime.send_message/3 or the optional root handle_info/2 extension.

Parameters

  • msg - The message to handle
  • state - Current component state

Returns

  • {new_state, commands} - New state and commands to execute
  • {new_state} - Shorthand for {new_state, []}
  • :noreply - Keep state unchanged, no commands

Examples

def update(:increment, state) do
  {%{state | count: state.count + 1}, []}
end

def update({:load_file, path}, state) do
  cmd = Command.file_read(path, :file_loaded)
  {%{state | loading: true}, [cmd]}
end

def update(:noop, _state), do: :noreply

view(state)

@callback view(state()) :: render_tree()

Renders the current state to a render tree.

View functions must be pure—given the same state, they always produce the same output. View functions should be fast because the runtime invokes them whenever the application is dirty and needs a render.

Parameters

  • state - Current component state

Returns

A render tree structure that will be processed into terminal output.

Examples

def view(state) do
  box(border: true) do
    text("Count: #{state.count}")
  end
end

Functions

normalize_init_result(state)

@spec normalize_init_result(init_result()) :: {state(), [command()]}

Normalizes init result to standard form.

Supports plain state as well as state with startup commands.

normalize_update_result(arg1, old_state)

@spec normalize_update_result(update_result(), state()) :: {state(), [command()]}

Normalizes update result to standard form.

Converts shorthand forms to the full {state, commands} tuple.

validate_update_purity(module)

@spec validate_update_purity(module()) :: :ok | {:warnings, [String.t()]}

Validates that an update function is pure (best effort).

This 1.0 compatibility hook does not perform static or runtime analysis and currently always returns :ok. Purity is a recommended application design, not a runtime-enforced property.