Elixir bindings for the Rust ratatui terminal UI library
Elixir
119
475 commits
updated Sep 22, 2026
Elixir bindings for the Rust ratatui terminal UI library, via Rustler NIFs.
Build rich terminal UIs in Elixir with ratatui's layout engine, widget library, and styling system together with the BEAM superpowers.
Viewport3D) with meshes, lights, materials, a movable camera, and a scene-graph for articulated models, blitted into true-color cellsCodeBlock with curated themes; oversized 8×8 pixel text for titles and banners:telemetry events across runtime, render, and transportsmix ex_ratatui.gen.burrito scaffolds the wiringAdd ex_ratatui to the dependencies in mix.exs:
def deps do
[
{:ex_ratatui, "~> 0.16"}
]
end
Then fetch and compile:
mix deps.get && mix compile
A precompiled NIF binary for the host platform is downloaded automatically. The native library itself is loaded lazily on first use, so compiling a project that depends on ex_ratatui does not require the NIF to be loaded into the compiler VM.
Precompiled NIF binaries are available for Linux (x86_64, aarch64, armv6/hf, riscv64), macOS (x86_64, aarch64), and Windows (x86_64), covering NIF ABI versions 2.16 (OTP 26) and 2.17 (OTP 27/28) — no Rust toolchain needed. OTP 29 raised the ABI to 2.18; since the NIF ABI is backward compatible, OTP 29 loads the 2.17 binary and also needs no Rust toolchain. A native 2.18 build is pending rustler_precompiled support upstream.
The Linux glibc binaries are built against old C libraries on purpose, and the release refuses to publish one that needs more: glibc 2.28 for aarch64, armv6/hf, and riscv64 (so they load on Nerves systems and older distributions), and glibc 2.35 for x86_64. The musl binaries are self-contained.
On musl systems (Alpine, single-binary Burrito payloads, Nerves-style rootfs): the *-unknown-linux-musl artifacts published up to v0.11.2 dynamically link libgcc_s.so.1, which stock musl systems do not ship — apk add libgcc fixes the resulting relocation error. Artifacts from later releases embed the unwinder statically and depend on musl libc alone.
To compile from source instead, install the Rust toolchain and set:
export EX_RATATUI_BUILD=true
Or, without an env var, in your config/config.exs:
config :rustler_precompiled, :force_build, ex_ratatui: true
The application config takes precedence over EX_RATATUI_BUILD, and config :rustler_precompiled, force_build_all: true takes precedence over both.
Either way, forcing a build also requires Rustler in the consuming project's dependencies — ex_ratatui declares :rustler as optional, and optional dependencies are not fetched transitively, so without it the build fails with Rustler.Compiler is not available:
{:rustler, ">= 0.0.0"}
Forcing a build is also required when depending on a git tag instead of Hex ({:ex_ratatui, github: "mcass19/ex_ratatui", tag: "..."}): the checksum file for a release's precompiled artifacts can only be committed after the tag triggers their build, so each tag predates its own checksums and the precompiled path fails with the precompiled NIF file does not exist in the checksum file. The Hex package always carries the complete checksum file.
alias ExRatatui.Layout.Rect
alias ExRatatui.Style
alias ExRatatui.Widgets.{Block, Paragraph}
ExRatatui.run(fn terminal ->
{w, h} = ExRatatui.terminal_size()
paragraph = %Paragraph{
text: "Hello from ExRatatui!\n\nPress any key to exit.",
style: %Style{fg: :green, modifiers: [:bold]},
alignment: :center,
block: %Block{
title: " Hello World ",
borders: [:all],
border_type: :rounded,
border_style: %Style{fg: :cyan}
}
}
ExRatatui.draw(terminal, [{paragraph, %Rect{x: 0, y: 0, width: w, height: h}}])
# Wait for a keypress, then exit
ExRatatui.poll_event(60_000)
end)
Try the examples catalog for more — every widget has a focused, copyable demo, e.g. mix run examples/basics/hello_world.exs.
New here? The Getting Started guide builds a supervised todo app from mix new to a working TUI.
ExRatatui offers two ways to structure a supervised app and several ways to serve it — every combination works, and switching transport doesn't change the app module.
mount/render/handle_event, the default) and the Reducer Runtime (Elm-style single update/2 with first-class commands and subscriptions). The callback guide has a side-by-side comparison.| Guide | Description |
|---|---|
| Getting Started | Walk-through from mix new to a supervised TUI — the place to start |
| Building UIs | Widgets, layout, styles, rich text, and events — everything for render/2 |
| Custom Widgets | Compose primitives into reusable widgets via the ExRatatui.Widget protocol |
| Images | Image rendering across terminals and transports — protocols, resizing, telemetry |
| 3D Rendering | Lit 3D scenes with Viewport3D — meshes, camera, render modes, and pipelines |
| Paste and Clipboard | Bracketed paste behaviour, batch-insert helpers, and an OSC 52 copy snippet |
| Callback Runtime | OTP-supervised apps with mount, render, handle_event, and handle_info callbacks |
| Reducer Runtime | Elm-style apps with init, update, subscriptions, commands, and runtime inspection |
| State Machine Patterns | Multi-screen apps, modals, and conditional UI without the tangle |
| Transports | Canonical feature matrix — what works where across Local / Session / SSH / Distributed / CellSession |
| Running TUIs over SSH | Serve any app as a remote TUI over SSH, standalone or under nerves_ssh |
| Running TUIs over Erlang Distribution | Drive a TUI from a remote BEAM node with zero NIF on the app side |
| Custom Transports | Plug in a custom transport (TCP, Livebook, WebSocket) via the ExRatatui.Transport behaviour |
| Rendering to Non-Terminal Surfaces | Use ExRatatui.CellSession to expose the rendered cell buffer to LiveView, framebuffers, screenshot tools, and other non-ANSI consumers — plus RGB pixel regions for images and 3D on surfaces that own pixels |
| Rendering to a Framebuffer | Put a TUI on a pixel display, such as an e-ink panel or a Linux framebuffer, with raster_ex_ratatui |
| Architecture | The NIF bridge and the per-transport process trees |
| Testing | Headless backend, test_mode, inject_event, and assertion patterns |
| Debugging | Runtime.snapshot, tracing, buffer inspection, and common errors |
| Performance | Render-loop tuning, render?: false, large trees, async effects |
| Telemetry | :telemetry events for runtime, render, transport, and session — logging, metrics, OpenTelemetry |
| Packaging with Burrito | Ship a TUI as a single-file native binary per OS/arch via Burrito — no Erlang or Rust install required for end users |
| Widgets Cheatsheet | One-page reference with every struct and its key fields |
ExRatatui bridges Elixir and Rust through Rustler NIFs: widget structs are encoded across the NIF boundary and decoded into ratatui types for rendering, while terminal events are polled on BEAM's DirtyIo scheduler so nothing blocks Elixir processes. Every transport builds on the same supervised Server process, with full session isolation per connected client.
The Architecture guide has the full picture — the NIF bridge and the per-transport process trees.
mix athena.chat), supporting Ollama, OpenAI-compatible, llama.cpp, and Anthropic backends.Contributions are welcome! See CONTRIBUTING.md for development setup and PR guidelines.
ExRatatui is undeniably inspired by ratatui. Having such a great terminal UI library in Rust was the motivation to bring the same experience to the Elixir world and combine the best of both worlds. Special thanks to the great community behind ratatui.
The ExRatatui logo adapts the ratatui logo designed by Pavel Fomchenkov, with the Elixir drop and ex_ratatui wordmark added by Malena Merlina.
MIT — see LICENSE for details.
Elixir
71.1%
Rust
28.9%
Elixir bindings for the Rust ratatui terminal UI library
Elixir
119
475 commits
updated Sep 22, 2026
Elixir bindings for the Rust ratatui terminal UI library, via Rustler NIFs.
Build rich terminal UIs in Elixir with ratatui's layout engine, widget library, and styling system together with the BEAM superpowers.
Viewport3D) with meshes, lights, materials, a movable camera, and a scene-graph for articulated models, blitted into true-color cellsCodeBlock with curated themes; oversized 8×8 pixel text for titles and banners:telemetry events across runtime, render, and transportsmix ex_ratatui.gen.burrito scaffolds the wiringAdd ex_ratatui to the dependencies in mix.exs:
def deps do
[
{:ex_ratatui, "~> 0.16"}
]
end
Then fetch and compile:
mix deps.get && mix compile
A precompiled NIF binary for the host platform is downloaded automatically. The native library itself is loaded lazily on first use, so compiling a project that depends on ex_ratatui does not require the NIF to be loaded into the compiler VM.
Precompiled NIF binaries are available for Linux (x86_64, aarch64, armv6/hf, riscv64), macOS (x86_64, aarch64), and Windows (x86_64), covering NIF ABI versions 2.16 (OTP 26) and 2.17 (OTP 27/28) — no Rust toolchain needed. OTP 29 raised the ABI to 2.18; since the NIF ABI is backward compatible, OTP 29 loads the 2.17 binary and also needs no Rust toolchain. A native 2.18 build is pending rustler_precompiled support upstream.
The Linux glibc binaries are built against old C libraries on purpose, and the release refuses to publish one that needs more: glibc 2.28 for aarch64, armv6/hf, and riscv64 (so they load on Nerves systems and older distributions), and glibc 2.35 for x86_64. The musl binaries are self-contained.
On musl systems (Alpine, single-binary Burrito payloads, Nerves-style rootfs): the *-unknown-linux-musl artifacts published up to v0.11.2 dynamically link libgcc_s.so.1, which stock musl systems do not ship — apk add libgcc fixes the resulting relocation error. Artifacts from later releases embed the unwinder statically and depend on musl libc alone.
To compile from source instead, install the Rust toolchain and set:
export EX_RATATUI_BUILD=true
Or, without an env var, in your config/config.exs:
config :rustler_precompiled, :force_build, ex_ratatui: true
The application config takes precedence over EX_RATATUI_BUILD, and config :rustler_precompiled, force_build_all: true takes precedence over both.
Either way, forcing a build also requires Rustler in the consuming project's dependencies — ex_ratatui declares :rustler as optional, and optional dependencies are not fetched transitively, so without it the build fails with Rustler.Compiler is not available:
{:rustler, ">= 0.0.0"}
Forcing a build is also required when depending on a git tag instead of Hex ({:ex_ratatui, github: "mcass19/ex_ratatui", tag: "..."}): the checksum file for a release's precompiled artifacts can only be committed after the tag triggers their build, so each tag predates its own checksums and the precompiled path fails with the precompiled NIF file does not exist in the checksum file. The Hex package always carries the complete checksum file.
alias ExRatatui.Layout.Rect
alias ExRatatui.Style
alias ExRatatui.Widgets.{Block, Paragraph}
ExRatatui.run(fn terminal ->
{w, h} = ExRatatui.terminal_size()
paragraph = %Paragraph{
text: "Hello from ExRatatui!\n\nPress any key to exit.",
style: %Style{fg: :green, modifiers: [:bold]},
alignment: :center,
block: %Block{
title: " Hello World ",
borders: [:all],
border_type: :rounded,
border_style: %Style{fg: :cyan}
}
}
ExRatatui.draw(terminal, [{paragraph, %Rect{x: 0, y: 0, width: w, height: h}}])
# Wait for a keypress, then exit
ExRatatui.poll_event(60_000)
end)
Try the examples catalog for more — every widget has a focused, copyable demo, e.g. mix run examples/basics/hello_world.exs.
New here? The Getting Started guide builds a supervised todo app from mix new to a working TUI.
ExRatatui offers two ways to structure a supervised app and several ways to serve it — every combination works, and switching transport doesn't change the app module.
mount/render/handle_event, the default) and the Reducer Runtime (Elm-style single update/2 with first-class commands and subscriptions). The callback guide has a side-by-side comparison.| Guide | Description |
|---|---|
| Getting Started | Walk-through from mix new to a supervised TUI — the place to start |
| Building UIs | Widgets, layout, styles, rich text, and events — everything for render/2 |
| Custom Widgets | Compose primitives into reusable widgets via the ExRatatui.Widget protocol |
| Images | Image rendering across terminals and transports — protocols, resizing, telemetry |
| 3D Rendering | Lit 3D scenes with Viewport3D — meshes, camera, render modes, and pipelines |
| Paste and Clipboard | Bracketed paste behaviour, batch-insert helpers, and an OSC 52 copy snippet |
| Callback Runtime | OTP-supervised apps with mount, render, handle_event, and handle_info callbacks |
| Reducer Runtime | Elm-style apps with init, update, subscriptions, commands, and runtime inspection |
| State Machine Patterns | Multi-screen apps, modals, and conditional UI without the tangle |
| Transports | Canonical feature matrix — what works where across Local / Session / SSH / Distributed / CellSession |
| Running TUIs over SSH | Serve any app as a remote TUI over SSH, standalone or under nerves_ssh |
| Running TUIs over Erlang Distribution | Drive a TUI from a remote BEAM node with zero NIF on the app side |
| Custom Transports | Plug in a custom transport (TCP, Livebook, WebSocket) via the ExRatatui.Transport behaviour |
| Rendering to Non-Terminal Surfaces | Use ExRatatui.CellSession to expose the rendered cell buffer to LiveView, framebuffers, screenshot tools, and other non-ANSI consumers — plus RGB pixel regions for images and 3D on surfaces that own pixels |
| Rendering to a Framebuffer | Put a TUI on a pixel display, such as an e-ink panel or a Linux framebuffer, with raster_ex_ratatui |
| Architecture | The NIF bridge and the per-transport process trees |
| Testing | Headless backend, test_mode, inject_event, and assertion patterns |
| Debugging | Runtime.snapshot, tracing, buffer inspection, and common errors |
| Performance | Render-loop tuning, render?: false, large trees, async effects |
| Telemetry | :telemetry events for runtime, render, transport, and session — logging, metrics, OpenTelemetry |
| Packaging with Burrito | Ship a TUI as a single-file native binary per OS/arch via Burrito — no Erlang or Rust install required for end users |
| Widgets Cheatsheet | One-page reference with every struct and its key fields |
ExRatatui bridges Elixir and Rust through Rustler NIFs: widget structs are encoded across the NIF boundary and decoded into ratatui types for rendering, while terminal events are polled on BEAM's DirtyIo scheduler so nothing blocks Elixir processes. Every transport builds on the same supervised Server process, with full session isolation per connected client.
The Architecture guide has the full picture — the NIF bridge and the per-transport process trees.
mix athena.chat), supporting Ollama, OpenAI-compatible, llama.cpp, and Anthropic backends.Contributions are welcome! See CONTRIBUTING.md for development setup and PR guidelines.
ExRatatui is undeniably inspired by ratatui. Having such a great terminal UI library in Rust was the motivation to bring the same experience to the Elixir world and combine the best of both worlds. Special thanks to the great community behind ratatui.
The ExRatatui logo adapts the ratatui logo designed by Pavel Fomchenkov, with the Elixir drop and ex_ratatui wordmark added by Malena Merlina.
MIT — see LICENSE for details.
Elixir
71.1%
Rust
28.9%