Cellium is a Terminal User Interface (TUI) framework for Erlang/OTP. It provides a declarative way to build interactive terminal applications using an architecture inspired by the Elm Architecture.
Applications built with Cellium implement the cellium behaviour, which follows a strictly decoupled pattern:
- Model: The application state.
- Update: A function that transforms the model in response to messages (keyboard input, resize events, or internal timers).
- View: A function that transforms the model into a UI representation using a tuple-based DSL.
To create an application, implement the following callbacks:
init(Args): Initializes the application model.update(Model, Msg): Processes events and returns the updated model.render(Model): Returns the UI structure as a DSL tree.
-module(counter).
-behaviour(cellium).
-include("cellium.hrl").
-export([init/1, update/2, render/1, start/0]).
init(_Args) ->
InitialCount = 0,
Model = #{
count => InitialCount,
widget_states => #{
display => #{text => io_lib:format("Count: ~p", [InitialCount])}
}
},
{ok, Model}.
update(Model = #{count := Count, widget_states := States}, Msg) ->
case Msg of
{button_clicked, plus_btn} ->
NewCount = Count + 1,
Model#{
count => NewCount,
widget_states => States#{display => #{text => io_lib:format("Count: ~p", [NewCount])}}
};
{button_clicked, minus_btn} ->
NewCount = Count - 1,
Model#{
count => NewCount,
widget_states => States#{display => #{text => io_lib:format("Count: ~p", [NewCount])}}
};
{key, _, _, _, _, <<"q">>} -> cellium:stop(), Model;
_ -> Model
end.
render(_Model) ->
{vbox, [{padding, 1}], [
{header, [], "Counter Example"},
{text, [{id, display}]},
{hbox, [{size, 1}], [
{button, [{id, minus_btn}, {size, 5}], "-"},
{spacer, [{size, 2}]},
{button, [{id, plus_btn}, {size, 5}], "+"}
]},
{text, [], "Press Tab to focus, Space/Enter to click, 'q' to quit"}
]}.
start() ->
cellium:start(#{module => ?MODULE}).Cellium simplifies interactive UIs by automatically managing the state of complex widgets. This "Component Pattern" is inspired by modern frameworks like React (Controlled Components) and Phoenix LiveView (LiveComponents).
Instead of manually threading every keypress and update:
- Event Routing: When a widget has focus, the framework automatically routes keyboard events to that widget's internal handler.
- State Storage: Internal state is stored in a
widget_statesmap within your Model, keyed by the widget'sid. - Automatic Injection: During rendering, the DSL lookups the stored state by ID and merges it into the widget properties.
text: Simple text display.button: Interactive clickable button. Emits{button_clicked, Id}.text_input: Multi-line or single-line text editor with cursor management.checkbox: Boolean toggle with label.list: Vertically scrollable list of selectable items.
tree: Hierarchical navigation with support for expansion/collapsing.{tree, [{id, my_tree}], [{"Root", [{"Child 1", []}, {"Child 2", []}]}]}radiogroup: Mutually exclusive selection group. Supportshorizontalandverticallayouts.{radiogroup, [{id, rg1}, {orientation, horizontal}], [opt1, opt2, opt3]}progress_bar: Visual task completion tracker using block characters ([████░░░░]).{progress_bar, [{id, pb1}, {progress, 0.75}, {width, 20}]}gauge: Labeled percentage indicator, useful for levels or volume.{gauge, [{id, g1}, {label, <<"Vol">>}, {value, 50}, {width, 30}]}
box: Simple rectangular container with a border.frame: Container with a border and an optional title.vbox/hbox: Layout containers for vertical and horizontal stacking.tabs: Multi-tab interface for switching between views.
Cellium provides a screen management system for applications with multiple views. The screen module handles lifecycle, transitions, and automatic focus cleanup.
SearchScreen = screen:new(search_screen,
cellium_dsl:from_dsl({vbox, [], [
{text_input, [{id, search_box}, {focusable, true}]},
{list, [{id, results_list}, {focusable, true}]}
]})),
% Transition between screens (automatic cleanup)
NewScreen = screen:transition(OldScreen, SearchScreen)Cellium uses a flexible layout engine that calculates absolute coordinates based on container constraints.
- Space Distribution: Use
{size, N}for fixed height/width, or{expand, true}to fill remaining space. - Padding: Apply
{padding, 1}to any container to create inner margins. Containers likebox,frame, anddialogdefault to 1-character padding. - Borders: Automatically switch between single-line (
┌─┐) and double-line (╔═╗) based on focus state.
- DSL:
render/1returns a high-level tuple tree. - Processing:
cellium_dslconverts tuples into internal widget maps and injects state. - Layout:
layoutengine calculates absolute (x, y) coordinates and dimensions. - Styling:
cssengine applies visual themes. - Rendering:
viewprocess uses thenative_terminaldriver to draw to the screen.
Cellium includes a powerful "Snapshot" testing pattern that allows you to verify widget rendering without a real terminal.
- Declaration: Define the widget in DSL.
- Rendering: Pipe the widget into a virtual buffer.
- Assertion: Compare the buffer against an "Idealized" ASCII string.
Use the cellium_test_utils:assert_snapshot/2 and show_render/2 helpers to see the output during your tests.
rebar3 compile
make run example=widgets_gallerysrc/: Core framework source code.test/: Snapshot tests and unit tests.examples/: Sample applications.include/: Common header files.
- API docs: https://wmealing.github.io/cellium/api-reference.html
- Discussion: https://wmealing.github.io/
