fork for console input problem https://github.com/mcass19/ex_ratatui
  • Elixir 53.7%
  • Rust 46.3%
Find a file
2026-04-09 12:43:53 +00:00
.github chore(deps): bump the github-actions group with 2 updates (#22) 2026-03-02 20:30:35 +01:00
assets fix(docs): trim demo video 2026-02-19 17:28:25 +01:00
examples fix: use Elixir.List in task_manager example (#21) 2026-02-24 09:49:05 +01:00
lib Redraw after inline scrollback effects 2026-04-09 12:43:53 +00:00
native/ex_ratatui Clear viewport on geometry change 2026-04-09 01:11:57 +00:00
test Unify inline post-render effects 2026-04-09 02:15:26 +00:00
.formatter.exs docs: complete examples 2026-02-17 18:43:05 +01:00
.gitignore ci: improvements 2026-02-21 20:54:25 +01:00
CHANGELOG.md Remove fullscreen runtime 2026-04-06 14:18:06 +00:00
checksum-Elixir.ExRatatui.Native.exs chore: add NIF checksum file for v0.4.2 2026-03-06 10:11:35 +01:00
CONTRIBUTING.md docs: update readme and contributing 2026-02-23 12:57:44 +01:00
LICENSE chore: sync Cargo.toml metadata and fix CI cache key 2026-02-21 20:35:17 +01:00
mix.exs Switch from rustler_precompiled to rustler (source build) 2026-03-19 01:28:11 +00:00
mix.lock feat: add type specs and dialyzer to CI 2026-02-21 20:54:15 +01:00
README.md feat: add clear widget 2026-03-06 10:05:51 +01:00

ExRatatui

Hex.pm Docs CI License

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 — without blocking the BEAM.

ExRatatui Demo

Features

  • 6 built-in widgets (for now!): Paragraph, Block, List, Table, Gauge, Clear
  • Constraint-based layout engine (percentage, length, min, max, ratio)
  • Non-blocking keyboard, mouse, and resize event polling
  • OTP-supervised TUI apps via ExRatatui.App behaviour with LiveView-inspired callbacks
  • Full color support: named, RGB, and 256-color indexed
  • Text modifiers: bold, italic, underlined, and more
  • Headless test backend for CI-friendly rendering verification
  • Precompiled NIF binaries — no Rust toolchain needed
  • Runs on BEAM's DirtyIo scheduler — never blocks your processes

Examples

Example Run Description
hello_world.exs mix run examples/hello_world.exs Minimal paragraph display
counter.exs mix run examples/counter.exs Interactive counter with key events
counter_app.exs mix run examples/counter_app.exs Counter using ExRatatui.App behaviour
system_monitor.exs mix run examples/system_monitor.exs Linux system dashboard — CPU, memory, disk, network, BEAM stats (Linux/Nerves only)
task_manager.exs mix run examples/task_manager.exs Full task manager with all widgets
task_manager/ See README Supervised Ecto + SQLite CRUD app

Installation

Add ex_ratatui to your dependencies in mix.exs:

def deps do
  [
    {:ex_ratatui, "~> 0.4"}
  ]
end

Then fetch and compile:

mix deps.get && mix compile

A precompiled NIF binary for your platform will be downloaded automatically.

Prerequisites

  • Elixir 1.17+

Precompiled NIF binaries are available for Linux (x86_64, aarch64, armv6/hf, riscv64), macOS (x86_64, aarch64), and Windows (x86_64). No Rust toolchain needed.

To compile from source instead, install the Rust toolchain and set:

export EX_RATATUI_BUILD=true

Quick Start

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 for more, e.g. mix run examples/hello_world.exs.

OTP App Behaviour

For supervised TUI applications, use the ExRatatui.App behaviour — a LiveView-inspired callback interface that manages the terminal lifecycle under OTP:

defmodule MyApp.TUI do
  use ExRatatui.App

  @impl true
  def mount(_opts) do
    {:ok, %{count: 0}}
  end

  @impl true
  def render(state, frame) do
    alias ExRatatui.Widgets.Paragraph
    alias ExRatatui.Layout.Rect

    widget = %Paragraph{text: "Count: #{state.count}"}
    rect = %Rect{x: 0, y: 0, width: frame.width, height: frame.height}
    [{widget, rect}]
  end

  @impl true
  def handle_event(%ExRatatui.Event.Key{code: "q"}, state) do
    {:stop, state}
  end

  def handle_event(%ExRatatui.Event.Key{code: "up"}, state) do
    {:noreply, %{state | count: state.count + 1}}
  end

  def handle_event(_event, state) do
    {:noreply, state}
  end
end

Add it to your supervision tree:

children = [{MyApp.TUI, []}]
Supervisor.start_link(children, strategy: :one_for_one)

Callbacks

Callback Description
mount/1 Called once on startup. Return {:ok, initial_state}
render/2 Called after every state change. Receives state and %Frame{} with terminal dimensions. Return [{widget, rect}]
handle_event/2 Called on terminal events. Return {:noreply, state} or {:stop, state}
handle_info/2 Called for non-terminal messages (e.g., PubSub). Optional — defaults to {:noreply, state}
terminate/2 Called on shutdown with reason and final state. Optional — default is a no-op

See the task_manager example for a full Ecto-backed app using this behaviour.

How It Works

ExRatatui bridges Elixir and Rust through Rustler NIFs (Native Implemented Functions):

Elixir structs -> encode to maps -> Rust NIF -> decode to ratatui types -> render to terminal
Terminal events -> Rust NIF (DirtyIo) -> encode to tuples -> Elixir Event structs
  • Rendering: Elixir widget structs are encoded as string-keyed maps, passed across the NIF boundary, and decoded into ratatui widget types for rendering.
  • Events: The poll_event NIF runs on BEAM's DirtyIo scheduler, so event polling never blocks normal Elixir processes.
  • Terminal state: Each process holds its own terminal reference via Rust ResourceArc, supporting two backends — a real crossterm terminal and a headless test backend for CI. The terminal is automatically restored when the reference is garbage collected.
  • Layout: Ratatui's constraint-based layout engine is exposed directly, computing split rectangles on the Rust side and returning them as Elixir tuples.

Precompiled binaries are provided via rustler_precompiled so users don't need the Rust toolchain.

Widgets

Paragraph

Text display with alignment, wrapping, and scrolling.

%Paragraph{
  text: "Hello, world!\nSecond line.",
  style: %Style{fg: :cyan, modifiers: [:bold]},
  alignment: :center,
  wrap: true
}

Block

Container with borders and title. Can wrap any other widget via the :block field.

%Block{
  title: "My Panel",
  borders: [:all],
  border_type: :rounded,
  border_style: %Style{fg: :blue}
}

# Compose with other widgets:
%Paragraph{
  text: "Inside a box",
  block: %Block{title: "Title", borders: [:all]}
}

List

Selectable list with highlight support.

%List{
  items: ["Elixir", "Rust", "Haskell"],
  highlight_style: %Style{fg: :yellow, modifiers: [:bold]},
  highlight_symbol: " > ",
  selected: 0,
  block: %Block{title: " Languages ", borders: [:all]}
}

Table

Table with headers, rows, and column width constraints.

%Table{
  rows: [["Alice", "30"], ["Bob", "25"]],
  header: ["Name", "Age"],
  widths: [{:length, 15}, {:length, 10}],
  highlight_style: %Style{fg: :yellow},
  selected: 0
}

Gauge

Progress bar.

%Gauge{
  ratio: 0.75,
  label: "75%",
  gauge_style: %Style{fg: :green}
}

Clear

Resets all cells in its area to empty (space) characters. Useful for rendering overlays on top of existing content.

%Clear{}

Layout

Split areas into sub-regions using constraints:

alias ExRatatui.Layout
alias ExRatatui.Layout.Rect

area = %Rect{x: 0, y: 0, width: 80, height: 24}

# Three-row layout: header, body, footer
[header, body, footer] = Layout.split(area, :vertical, [
  {:length, 3},
  {:min, 0},
  {:length, 1}
])

# Split body into sidebar + main
[sidebar, main] = Layout.split(body, :horizontal, [
  {:percentage, 30},
  {:percentage, 70}
])

Constraint types: {:percentage, n}, {:length, n}, {:min, n}, {:max, n}, {:ratio, num, den}.

Events

Poll for keyboard, mouse, and resize events without blocking the BEAM:

case ExRatatui.poll_event(100) do
  %Event.Key{code: "q", kind: "press"} ->
    :quit

  %Event.Key{code: "up", kind: "press"} ->
    :move_up

  %Event.Key{code: "j", kind: "press", modifiers: ["ctrl"]} ->
    :ctrl_j

  %Event.Resize{width: w, height: h} ->
    {:resized, w, h}

  nil ->
    :timeout
end

Styles

# Named colors
%Style{fg: :green, bg: :black}

# RGB
%Style{fg: {:rgb, 255, 100, 0}}

# 256-color indexed
%Style{fg: {:indexed, 42}}

# Modifiers
%Style{modifiers: [:bold, :dim, :italic, :underlined, :crossed_out, :reversed]}

Testing

ExRatatui includes a headless test backend for CI-friendly rendering verification. Each test terminal is independent, enabling async: true tests:

test "renders a paragraph" do
  terminal = ExRatatui.init_test_terminal(40, 10)

  paragraph = %Paragraph{text: "Hello!"}
  :ok = ExRatatui.draw(terminal, [{paragraph, %Rect{x: 0, y: 0, width: 40, height: 10}}])

  content = ExRatatui.get_buffer_content(terminal)
  assert content =~ "Hello!"
end

Contributing

Contributions are welcome! See CONTRIBUTING.md for development setup and PR guidelines.

License

MIT — see LICENSE for details.