RasterExRatatui
Render ExRatatui apps on pixel displays such as e-ink panels, with helpers for Linux framebuffers.
A terminal paints glyphs for us. A panel with nothing but pixels does not, so something has to turn every cell (a symbol, a foreground, a background) into pixels, and blit the bitmaps that Viewport3D and Image render. RasterExRatatui is that something. It sits on top of an ExRatatui.CellSession, keeps the cell grid and the pixel regions of the current frame, and hands the device only the rectangles that changed, already packed in the panel's pixel format.
Flow
flowchart LR
app("ExRatatui app") --> server["ExRatatui.Server"]
server -- "cell diff<br/>cells + pixel regions" --> raster["Raster<br/>font × palette × format × scale"]
raster -- "changed rectangles<br/>%Patch{}" --> push[["push/2"]]
push --> panel[/"the panel"/]
input[/"keyboard, touch<br/>Input.Devices"/] -- "key and mouse events" --> server
A surface (or a Session in a process the device already has) runs the app and owns this loop; the panel only ever sees packed pixels, in its own orientation.
Features
- A framebuffer is one line —
use RasterExRatatui.Framebuffer.Surface, app: MyApp: geometry and format from sysfs, a font scale to match, the console detached, the keyboard found and found again, the app restarted when it quits. - Any other panel is one module —
use RasterExRatatui.Surface, return the panel geometry frominit/1, write pixels inpush/2. The surface process starts the app, folds diffs, rasterises, and forwards input. - Rotation —
rotate: 90turns the app for a panel mounted on its side; the panel is still written in its own orientation. - Patches, not frames — only changed cells (and changed pixel regions) are rasterised and pushed;
Raster.frame/1renders a full buffer for panels that want one. - Pixel regions —
Viewport3DandImagearrive as RGB bitmaps and are scaled onto their cell rect at the panel's native resolution. - Pixel formats —
Mono(1-bit panels: tone rules for cells, Bayer dither for regions),RGB565, andXRGB8888, with a palette for named, indexed, and RGB colours. - Fonts — a built-in 6×8 bitmap font with box drawing, blocks, and braille, an integer
scale:for large panels, and aFontbehaviour to bring another. - Keyboards and touch —
Input.Devicesfinds, grabs, and re-finds evdev keyboards and touch panels;Input.EvdevandInput.Touchturn their events into the key and mouse events a terminal would send, taps landing on the right cell at any rotation. - Device helpers —
Framebufferfor Linux fbdev: geometry from sysfs, stride-aware writes. - Own process —
Sessionruns the app on a raster from a process the consumer already has;Raster,Grid, fonts, and formats underneath are plain functions. - Snapshots —
Raster.to_png/1writes what the panel shows as a PNG, for tests and debugging.
Installation
Add raster_ex_ratatui to the dependencies in mix.exs:
def deps do
[
{:raster_ex_ratatui, "~> 0.3"}
]
end
Prerequisites
- Elixir 1.17+
- ex_ratatui 0.15 or later (pixel regions:
CellSession.new/3withfont_size:; native bitmap rotation for turned panels)
Quick start
The app needs no change: it is a plain use ExRatatui.App that renders widgets exactly as it would in a terminal, so build and try it there first.
A Linux framebuffer (a Raspberry Pi with a display, on Nerves) is one module and a child in the supervision tree:
defmodule MyDevice.Surface do
use RasterExRatatui.Framebuffer.Surface, app: MyDevice.App, rotate: 90
end
It waits for /dev/fb0, reads the panel's size and depth from sysfs, picks the pixel format and a font scale, keeps the kernel console off the display, reads the first USB keyboard (and the touch panel, with touch: true) through input_event in the deps, and restarts the app when it quits. Every default is an option or an override. Nerves Quick Start goes from mix nerves.new to the panel; Linux Framebuffers has the details.
Any other panel (an SPI LCD, an e-ink controller) answers three questions in its own surface: how big it is, how its pixels are packed, and how bytes reach it.
defmodule MyDevice.Surface do
use RasterExRatatui.Surface, app: MyDevice.App, format: RasterExRatatui.PixelFormat.RGB565
@impl true
def init(_opts), do: {:ok, [size: {480, 320}], MyDevice.LCD.open!()}
@impl true
def push(patches, lcd) do
Enum.each(patches, &MyDevice.LCD.write(lcd, &1.x, &1.y, &1.width, &1.height, &1.data))
lcd
end
end
Input is whatever reads the hardware, turned into ExRatatui.Event structs and handed to RasterExRatatui.Surface.send_event/2. Building a Surface covers whole-frame panels, slow refreshes, crashes, rotation, and testing on the host. A device already driven from its own process starts a RasterExRatatui.Session there instead of a surface.
Examples
The examples folder has a headless snapshot script, a benchmark, a Nerves project for a Raspberry Pi display with keyboard and touch, and an e-ink name badge; its README says what each one shows and how to run it.
Guides
| Guide | Description |
|---|---|
| Nerves Quick Start | From mix nerves.new to an app on a Raspberry Pi's display with keyboard and touch, the build traps, and first checks |
| Building a Surface | The contract: geometry, push, input, crashes, rotation, testing, Session for an own process, and a 1-bit e-ink consumer |
| Fonts | The glyph layout, the built-in 6×8 font, scale, and bringing a font |
| Pixel Formats | Mono tone rules and dithering, colour palettes, writing a format |
| Linux Framebuffers | Framebuffer.Surface and what it is made of: device geometry, writes, the kernel console, keyboards, and touch |
| Telemetry | Rasterisation, push, and input events with a Telemetry.Metrics example |
Ecosystem
- ex_ratatui — The core terminal UI library this builds on.
- phoenix_ex_ratatui — Run TUIs in the browser within Phoenix LiveView.
- kino_ex_ratatui — Run TUIs inside Livebook notebooks.
Contributing
See CONTRIBUTING.md for development setup and guidelines.
License
MIT — see LICENSE.