Elm Architecture Implementation
August 28, 2026 ยท View on GitHub
An application root uses TermUI.Elm and implements four callbacks:
defmodule Counter do
use TermUI.Elm
alias TermUI.Command
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: "q"}, _state), do: {:msg, :quit}
def event_to_msg(_event, _state), do: :ignore
def update(:increment, state), do: {%{state | count: state.count + 1}, []}
def update(:quit, state), do: {state, [Command.quit()]}
def view(state), do: text("Count: #{state.count}")
end
Callback contracts
init/1 receives the merged runtime option list after the GenServer-only
:name option has been removed. It may return plain state, {state, commands},
{:ok, state}, or {:ok, state, commands}.
event_to_msg/2 returns {:msg, message}, :ignore, or :propagate.
The primary runtime has no parent component, so :propagate is currently
ignored.
update/2 returns {state, commands}, {state}, or :noreply. Commands must
be a list. The runtime compares old/new state to set its dirty flag.
view/1 returns any tree accepted by TermUI.Runtime.NodeRenderer. Both
TermUI.Component.Helpers render-node structs and non-conflicting tuple helpers
from TermUI.Elm.Helpers are imported by use TermUI.Elm.
Message queue
TermUI.MessageQueue is a standalone queue module used directly by
TermUI.Runtime; there is no TermUI.Runtime.MessageQueue module or process.
Events become messages, command results become messages, and public
send_message(runtime, :root, message) can enqueue a message directly.
Commands
The supported command constructors are:
TermUI.Command.timer/2TermUI.Command.interval/2TermUI.Command.file_read/2TermUI.Command.send_after/3TermUI.Command.quit/1TermUI.Command.none/0TermUI.Command.with_timeout/2
Timers deliver the configured message. File reads deliver
{on_result, {:ok, contents}} or {on_result, {:error, reason}}. A command
timeout delivers {:error, :timeout} for file reads and one-shot timers;
intervals and delayed sends do not enforce with_timeout/2 in 1.0. The public
runtime does not expose an interval cancellation handle; intervals end when
root commands are cancelled during shutdown.
There is no generic function-command constructor in 1.0. For arbitrary HTTP or
other effects, use an application-owned process/Task and send the result to the
runtime with TermUI.Runtime.send_message(runtime, :root, result) or arrange a
root handle_info/2 message. Avoid doing blocking I/O inside update/2.
Stateful widgets
Stateful widgets do not become Elm runtime children. Initialize them into root state, forward relevant events, and render their resulting state:
props = TermUI.Widgets.TextInput.new(placeholder: "Name")
{:ok, input} = TermUI.Widgets.TextInput.init(props)
{:ok, input} = TermUI.Widgets.TextInput.handle_event(event, input)
node = TermUI.Widgets.TextInput.render(input, %{x: 0, y: 0, width: 40, height: 1})
The root owns focus decisions and widget state composition.
Optional root messages
If a root module exports handle_info/2, the runtime forwards unknown process
messages to it. Return a new state or {new_state, commands}. This callback is
an extension implemented by the runtime, not a required TermUI.Elm callback.
Next: Creating Widgets.