Quickstart
View SourceA counter app, running in your terminal, using the four callbacks every Raxol app implements.
Install
Generate a new project:
mix raxol.new my_app
cd my_app
mix deps.get
Or add to an existing project:
# mix.exs
def deps do
[{:raxol, "~> 2.6"}]
endHeadless / CI setup
The tutorial app below needs a real terminal, but building and testing Raxol
does not. Prerequisites: Elixir/OTP (versions in the repo's .tool-versions)
and a C toolchain for the termbox2 NIF (make + cc; on Debian/Ubuntu,
apt-get install build-essential). From a fresh clone:
mix local.hex --force # fresh machines and CI: install Hex without a prompt
mix deps.get
mix compile # builds the termbox2 NIF
SKIP_TERMBOX2_TESTS=true MIX_ENV=test mix test --exclude slow --exclude integration --exclude docker
MIX_ENV=test mix raxol.rate # RATE: render-determinism golden suite
SKIP_TERMBOX2_TESTS=true excludes the tests that need a real local terminal;
CI sets the same variable. Plain mix test without the exclude flags also
runs integration suites that need external services (PostgreSQL for the
workflow checkpoint tests), so stick to the command above. If HOME is
read-only in your sandbox, point MIX_HOME and HEX_HOME at a writable
directory first.
Your first app
Every Raxol app follows The Elm Architecture (TEA) with four callbacks:
defmodule MyApp do
use Raxol.Core.Runtime.Application
# 1. Initialize state
@impl true
def init(_context) do
%{count: 0}
end
# 2. Handle messages
@impl true
def update(message, model) do
case message do
:increment ->
{%{model | count: model.count + 1}, []}
:decrement ->
{%{model | count: model.count - 1}, []}
# Keyboard events
%Raxol.Core.Events.Event{type: :key, data: %{key: :char, char: "="}} ->
{%{model | count: model.count + 1}, []}
%Raxol.Core.Events.Event{type: :key, data: %{key: :char, char: "-"}} ->
{%{model | count: model.count - 1}, []}
%Raxol.Core.Events.Event{type: :key, data: %{key: :char, char: "q"}} ->
{model, [Directive.stop()]}
_ ->
{model, []}
end
end
# 3. Render UI from state
@impl true
def view(model) do
column style: %{padding: 1, gap: 1, align_items: :center} do
[
text("My Counter", style: [:bold]),
box style: %{border: :single, padding: 1, width: 20, justify_content: :center} do
text("Count: #{model.count}", style: [:bold])
end,
row style: %{gap: 1} do
[
button("=", on_click: :increment),
button("-", on_click: :decrement)
]
end,
text("Press =/- or click buttons. q to quit.", style: [:dim])
]
end
end
# 4. Subscriptions (optional)
@impl true
def subscribe(_model), do: []
end
# Start the app
{:ok, pid} = Raxol.start_link(MyApp, [])
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, _reason} -> :ok
endinit/1returns a plain map, which is your entire app stateupdate/2pattern-matches on messages and returns{new_state, commands}. The empty list[]means "no side effects"view/1builds the UI from state using the View DSL macros (column,row,box)Directive.stop()tells the runtime to shut down (theDirectivealias comes fromuse Raxol.Core.Runtime.Application)
Save as lib/my_app.ex and run:
mix run lib/my_app.ex
How it works
+---> view(model) ---> Terminal
|
init(context) --+--> model
|
+---> update(message, model) --+
^ |
| {new_model, cmds} |
+------------------------+init/1sets up your initial state (the "model")view/1renders the UI; it's called after every state changeupdate/2handles messages (keyboard events, button clicks, timers)subscribe/1sets up recurring events (timers, external data)
State flows in one direction. Views are pure functions of state. Side effects go through commands.
View DSL
The View DSL provides macros for building layouts:
# Layout containers
column style: %{gap: 1} do ... end # Vertical stack
row style: %{gap: 2} do ... end # Horizontal stack
# Components
text("Hello", style: [:bold]) # Text with styling
button("Click", on_click: :msg) # Clickable button
text_input(value: v, placeholder: "") # Text input
progress(value: 65, max: 100) # Progress bar
# Containers
box style: %{border: :single, padding: 1} do ... end # Bordered box
# Utilities
divider() # Horizontal line
spacer() # Flexible spaceAdding live updates
Use subscribe/1 to get periodic messages:
@impl true
def subscribe(_model) do
[subscribe_interval(1000, :tick)] # Send :tick every second
end
@impl true
def update(:tick, model) do
{%{model | uptime: model.uptime + 1}, []}
endOTP Supervision
Use --sup when generating to get a proper OTP application:
mix raxol.new my_app --sup
This generates an Application module with a supervision tree. Run with:
mix run --no-halt
If the terminal is left in a bad state
A Raxol app puts your terminal into raw mode, and full-screen apps also switch to
the alternate screen. Both get restored on the way out. But if the app dies hard,
a kill -9 or a VM crash, nothing runs that restore and you land back in a shell
with no echo and no line editing.
Nothing is broken. Reset it:
reset
If you cannot see what you are typing, that still works blind. Type it and hit
Enter. stty sane is the lighter version, and it fixes echo without clearing the
screen.
vim and tmux leave the same mess when you SIGKILL them. It comes with the territory for full-screen terminal programs. Why OTP covers what can take the VM down that way in the first place.
Where to go next
That counter is a complete Raxol app. init/update/view is the whole API, and
everything else builds on this loop.
- Component Gallery: all Components with examples
- Core Concepts: buffers, the rendering pipeline, and how they fit together
- Building Apps: state machines, scrollable lists, keyboard shortcuts
mix raxol.playground browses 40 Component demos interactively, with search and
filtering.
Things to try
SSH serving. Serve your app over SSH. Each connection gets its own process:
mix run examples/ssh/ssh_counter.exs
# Then: ssh localhost -p 2222
Hot code reload. Edit your view function while the app is running:
iex -S mix run examples/dev/hot_reload_demo.exs
# Edit the view/1 function and save; UI updates automatically
Crash isolation. Components run in separate processes. One crash doesn't take down the app:
mix run examples/components/process_component_demo.exs
Working examples to study:
examples/getting_started/counter.exs: the counter from this pageexamples/demo.exs: flagship demo with dashboard, sparklines, live statsexamples/getting_started/todo_app.exs: a keyboard-driven todo list app