mcass19

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.App behaviour
  • 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 display
  • mix run examples/counter.exs — interactive counter with key events
  • mix run examples/counter_app.exs — counter using the App behaviour
  • mix run examples/system_monitor.exs — system dashboard (CPU, memory, disk, network, BEAM stats)
  • mix run examples/task_manager.exs — full task manager using all widgets
  • examples/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.

https://github.com/mcass19/ex_ratatui

Most Liked

mcass19

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

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.tui and 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

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_ssh integration, auto host-key bootstrap. Drop the daemon into a supervision tree and you’re in.
  • Distribution-attach transport: serve any ExRatatui.App to remote BEAM nodes over Erlang distribution.
  • Telemetry across the runtime (mount, events, frames, transport handshakes), and a public Transport behaviour for plugging in custom byte-stream carriers.
  • Livebook: kino_ex_ratatui runs 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

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!

Where Next?

Popular in Announcing Top

Qqwy
Hello everyone, I wrote a small library today called MapDiff. It returns a map listing the (smallest amount of) changes to get from map...
New
OvermindDL1
I created a new library (rather I pulled out a couple files from my big project), it manages an operating system PID file for the BEAM. ...
New
josevalim
Hi everyone, We would like to announce that Plataformatec is working on a new MySQL driver called MyXQL. Our goal is to eventually integ...
New
maltoe
Hello! Came here to announce ChromicPDF, a pet project PDF generator I’ve been working on for the past few months. Why another PDF gener...
New
dominicletz
Hi, I thought I had posted my library before but seems I hadn’t. The project is still in early stages but it’s growing and so I think it...
New
wojtekmach
Hey everyone! Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New
markmark206
simple_feature_flags is a tiny package that lets you turn features on or off based on which environment (e.g. localhost, staging, product...
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54921 245
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 49084 226
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New

We're in Beta

About us Mission Statement