mcass19
ExRatatui - Elixir bindings for the Rust ratatui terminal UI library
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applications that run under OTP supervision — without blocking the BEAM.
Why?
I wanted to build terminal UIs in Elixir with the same ergonomics we’re used to from LiveView. The existing options in the ecosystem are either stale (ratatouille hasn’t been updated in years) or focused on CLI output rather than full-screen interactive apps. Meanwhile, ratatui is one of the most actively maintained TUI libraries in any language — so bridging it to Elixir felt like the right approach.
What it looks like
The ExRatatui.App behaviour uses LiveView-inspired callbacks — mount/1, render/2, handle_event/2, and handle_info/2:
defmodule MyCounter do
use ExRatatui.App
@impl true
def mount(_opts), do: {:ok, %{count: 0}}
@impl true
def render(state, frame) do
alias ExRatatui.Widgets.Paragraph
alias ExRatatui.Layout.Rect
widget = %Paragraph{text: "Count: #{state.count}"}
[{widget, %Rect{x: 0, y: 0, width: frame.width, height: frame.height}}]
end
@impl true
def handle_event(%ExRatatui.Event.Key{code: "up"}, state),
do: {:noreply, %{state | count: state.count + 1}}
def handle_event(%ExRatatui.Event.Key{code: "q"}, state),
do: {:stop, state}
def handle_event(_event, state),
do: {:noreply, state}
end
# Add to your supervision tree
children = [{MyCounter, []}]
Supervisor.start_link(children, strategy: :one_for_one)
Features
- 5 widgets (so far): Paragraph, Block, List, Table, Gauge — with Block composition on all of them
- Constraint-based layout engine — split areas by percentage, length, min, max, or ratio
- Non-blocking event polling — keyboard, mouse, and resize events on BEAM’s DirtyIo scheduler
- OTP-supervised apps via
ExRatatui.Appbehaviour - Full color support — 17 named colors, RGB, and 256-color indexed
- Headless test backend — render to an in-memory buffer for CI-friendly testing
- Precompiled NIF binaries for Linux, macOS, and Windows — no Rust toolchain needed
Installation
def deps do
[{:ex_ratatui, "~> 0.4"}]
end
Precompiled binaries are downloaded automatically. No Rust toolchain required.
Examples
The repo includes several examples you can run directly:
mix run examples/hello_world.exs— minimal paragraph displaymix run examples/counter.exs— interactive counter with key eventsmix run examples/counter_app.exs— counter using the App behaviourmix run examples/system_monitor.exs— system dashboard (CPU, memory, disk, network, BEAM stats)mix run examples/task_manager.exs— full task manager using all widgetsexamples/task_manager/— a complete supervised Ecto + SQLite CRUD app with a TUI interface
What’s next
More widgets (Tabs, Sparkline, BarChart, Scrollbar), rich text primitives (mixed-style spans within a single widget), custom widgets. Beyond that — a theming system, periodic handle_tick callbacks, viewport modes for inline rendering, single-binary distribution via Burrito, etc etc.
The precompiled NIFs already target ARM and RISC-V, so running TUI apps on Nerves devices over SSH should be a natural fit? I haven’t played with it yet!
The issues list is the place to go — ratatui has a huge surface area, so there’s a lot of room to grow.
Contributions are very welcome!!!
Links
I’d love to hear what people think and what you’d want to build with it.
Most Liked
mcass19
0.6 is out! Headline is a built-in SSH transport that lets you serve any ExRatatui.App module as a remote TUI over OTP :ssh. A single daemon hands each connected client its own isolated session; multiple clients can attach to the same app at the same time without stepping on each other.
What it looks like
The simplest shape. Drop the daemon straight into a supervision tree:
children = [
{MyApp.TUI,
transport: :ssh,
port: 2222,
auto_host_key: true,
auth_methods: ~c"password",
user_passwords: [{~c"admin", ~c"admin"}]}
]
Then from any other machine:
ssh -p 2222 admin@localhost
That’s it. transport: :ssh on ExRatatui.App routes start_link/1 through a new ExRatatui.SSH.Daemon instead of the local terminal path. The app module itself is unchange. It doesn’t know it’s being served over SSH. auto_host_key: true is the other nice bit. More on that on the docs.
Example: phoenix_ex_ratatui_example.
Integrating with nerves_ssh
If you’re already running nerves_ssh on a Nerves device you don’t need a second daemon. ExRatatui.SSH is an :ssh_server_channel, and nerves_ssh takes one through its subsystems: list. There’s a helper for the tuple shape:
config :nerves_ssh,
authorized_keys: [File.read!("/root/.ssh/authorized_keys")],
subsystems: [
:ssh_sftpd.subsystem_spec(cwd: ~c"/"),
ExRatatui.SSH.subsystem(MyApp.TUI)
]
Connect with:
ssh -t nerves.local -s Elixir.MyApp.TUI
The -t is required — OpenSSH doesn’t allocate a PTY by default for subsystem invocations (sftp and similar binary protocols don’t need one), so without it your local terminal stays in cooked mode and keystrokes get line-buffered and locally echoed over the TUI. The subsystem name is the full Elixir module name as a charlist, so two different app modules configured into the same daemon get distinct names and don’t collide.
Example: nerves_ex_ratatui_example.
What’s next
Reducer runtime for non-trivial apps. More distribution capabilites. More widgets. Contributions and TUIs out there are starting to appear :)!
mcass19
Quick update! ExRatatui is now at v0.5 with more widgets. And more in the makings…
Some projects built with it are starting to appear:
- AshTui — Interactive terminal explorer for Ash domains, resources, attributes, actions, and relationships. Two-panel UI with search, tabs, scrollbar, and relationship navigation. Run
mix ash.tuiand you’re in. - Nerves ExRatatui Example — System monitor and LED control TUI running on a Raspberry Pi. Renders directly to the HDMI console. Works on RPi Zero, 3, 4, and 5.
Feedback and contributions very welcome!
mcass19
Hi all! A bunch of stuff shipped since the last update.
Highlights:
- Reducer runtime: Elm-style
update/2+ commands + subscriptions, alongside the original callback runtime. Pick whichever fits your app. - New widgets: Chart, Canvas, Calendar, Sparkline, BarChart. Plus a Widget protocol so you can build composite widgets in pure Elixir, a Focus primitive for multi-panel apps, and rich text (
Span/Line) across every text-bearing widget. - SSH transport: subsystem mode,
nerves_sshintegration, auto host-key bootstrap. Drop the daemon into a supervision tree and you’re in. - Distribution-attach transport: serve any
ExRatatui.Appto remote BEAM nodes over Erlang distribution. - Telemetry across the runtime (mount, events, frames, transport handshakes), and a public
Transportbehaviour for plugging in custom byte-stream carriers. - Livebook:
kino_ex_ratatuiruns TUIs straight from a notebook. Honestly more fun than I expected :D! - New guides covering everything from setup to advanced topics. Starting point: Getting Started — ExRatatui v0.11.1
Readme, changelog, and examples have everything else: GitHub - mcass19/ex_ratatui: Elixir bindings for the Rust ratatui terminal UI library · GitHub .
Would love to hear your feedback and what you end up building with it.
Last Post!
mcass19
Update: fixed in 0.11.1!
The cause was a race over stdin: when running locally (iex but also just elixir or mix run), the BEAM’s own tty reader and crossterm were both pulling from the terminal, so some keys went to the BEAM and never reached the poll loop, causing the dropped arrows/characters.
We now hand the local tty off to crossterm so there’s a single reader. It only kicks in automatically on the :local backend, so no code changes are needed and it doesn’t affect any other transport.
Let me know if you still see drops after upgrading, or if you have any other questions or feedback. Thank you again @shashi!
Popular in Announcing
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









