# ElixirOpentui
A terminal UI framework for Elixir with a high-performance Zig NIF backend.
Build rich, interactive terminal applications using an Elm-inspired
init/handle_event/render architecture and a declarative View DSL.
This project is a port of [OpenTUI](https://github.com/anomalyco/opentui) to idiomatic
Elixir. The Zig NIF backend uses OpenTUI's Zig implementation directly — the rope data
structure, text buffer, editor view, grapheme handling, and frame buffer are all vendored
from their codebase. Huge thanks to the OpenTUI team at [Anomaly](https://github.com/anomalyco)
for building and open-sourcing such a solid foundation. This project wouldn't exist
without their work.
<!-- TODO: Add asciinema recording or screenshot of widget_gallery demo -->
## What it looks like
```elixir
defmodule Counter do
alias ElixirOpentui.Color
def init(_cols, _rows) do
%{count: 0}
end
def handle_event(%{type: :key, key: "c", ctrl: true}, _state), do: :quit
def handle_event(%{type: :key, key: :up}, state), do: {:cont, %{state | count: state.count + 1}}
def handle_event(%{type: :key, key: :down}, state), do: {:cont, %{state | count: max(0, state.count - 1)}}
def handle_event(_event, state), do: {:cont, state}
def render(state) do
import ElixirOpentui.View
panel id: :main, title: "Counter", width: 30, height: 7,
border: true, fg: Color.rgb(200, 200, 200), bg: Color.rgb(20, 20, 35) do
text(content: "Count: #{state.count}", fg: Color.rgb(100, 220, 100), bg: Color.rgb(20, 20, 35))
text(content: "")
text(content: "Up/Down to change", fg: Color.rgb(100, 100, 100), bg: Color.rgb(20, 20, 35))
text(content: "Ctrl+C to quit", fg: Color.rgb(100, 100, 100), bg: Color.rgb(20, 20, 35))
end
end
def focused_id(_state), do: nil
end
ElixirOpentui.Demo.DemoRunner.run(Counter)
```
Save that as `counter.exs` and run it with `mix run counter.exs`. You get a bordered panel
with a live counter you can increment and decrement with the arrow keys.
## Features
- **15+ widgets** — text input, select, checkbox, scroll box, tabs, textarea, code viewer, markdown renderer, diff viewer, and more
- **Flexbox-inspired layout** — rows, columns, padding, margin, grow/shrink, alignment, percentage sizing
- **Zig NIF rendering backend** — double-buffered, diff-based terminal output for minimal flicker
- **Pure Elixir fallback** — everything works without the NIF too, just slower
- **Animation system** — timeline-based with 25 easing functions, ~30 FPS live mode
- **Syntax highlighting** — via Makeup, supports Elixir and TypeScript
- **Markdown rendering** — via Earmark, headings, lists, code blocks, blockquotes
- **Full input handling** — keyboard, mouse (SGR 1006), paste, Kitty keyboard protocol
- **Terminal capability detection** — progressive enhancement based on what the terminal supports
## Installation
Add `elixir_opentui` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:elixir_opentui, "~> 0.1.0"}
]
end
```
Then fetch and set up:
```bash
mix deps.get
mix zig.get # downloads the Zig toolchain (required for NIF compilation)
mix compile
```
### Requirements
- **Elixir** ~> 1.19
- **OTP 28+** — uses `:shell.start_interactive/1` for raw terminal mode
- A terminal emulator that supports ANSI escape sequences (basically all of them)
### Optional dependencies
These are optional and only needed if you use the corresponding widgets:
```elixir
{:makeup, "~> 1.2", optional: true} # for Code widget syntax highlighting
{:makeup_elixir, "~> 1.0", optional: true} # Elixir syntax highlighting
{:earmark, "~> 1.4", optional: true} # for Markdown widget
```
## How it works
Every app follows the same pattern: **init** sets up your state, **handle_event** responds
to keyboard/mouse input by returning updated state (or `:quit`), and **render** builds a
declarative UI tree using the View DSL. The framework diffs the output and only redraws
what changed.
Under the hood, the rendering pipeline goes: element tree → flexbox layout → paint to
cell buffer → diff against previous frame → emit minimal ANSI escape sequences. The Zig
NIF handles the buffer and diff operations for speed, but there's a pure Elixir fallback
if you'd rather not compile native code.
## Demos
The `demo/` directory has 17 runnable examples. Here are some highlights:
```bash
mix run demo/widget_gallery.exs # all widgets in one view
mix run demo/text_area_demo.exs # multi-line editor with undo/redo
mix run demo/code_demo.exs # syntax-highlighted code viewer
mix run demo/markdown_demo.exs # markdown renderer
mix run demo/diff_demo.exs # unified and split diff views
mix run demo/animation_demo.exs # timeline-based animations
mix run demo/breakout.exs # breakout game
mix run demo/space_dodge.exs # space dodge game
```
All demos use `Ctrl+C` to exit.
## Available widgets
| Widget | What it does |
|--------|-------------|
| `text` | Static text display |
| `label` | Single-line label |
| `input` / `TextInput` | Single-line text input with cursor, scroll, Emacs bindings |
| `textarea` / `TextArea` | Multi-line editor backed by a Zig NIF rope data structure |
| `select` / `Select` | Dropdown list with vim keys and fast scroll |
| `checkbox` / `Checkbox` | Boolean toggle |
| `scroll_box` / `ScrollBox` | Scrollable container |
| `tab_select` / `TabSelect` | Horizontal tab bar |
| `code` / `Code` | Syntax-highlighted code display |
| `markdown` / `Markdown` | Rendered markdown |
| `diff` / `Diff` | Unified and split diff views |
| `line_number` / `LineNumber` | Line number gutter with signs and colors |
| `ascii_font` / `AsciiFont` | Decorative ASCII art text |
## View DSL
The View DSL gives you macros for building UI trees:
```elixir
import ElixirOpentui.View
import ElixirOpentui.Color
box direction: :row, gap: 2 do
panel id: :left, title: "Left", width: 20, border: true do
text(content: "Hello", fg: green())
end
panel id: :right, title: "Right", width: 20, border: true do
checkbox(id: :toggle, checked: true, label: "Dark mode")
end
end
```
## Styling
Style properties cover layout, colors, borders, and text attributes:
```elixir
box width: {:percent, 50},
height: 10,
padding: {1, 2, 1, 2},
bg: Color.rgb(30, 30, 50),
fg: Color.white(),
border: true,
border_style: :rounded,
border_title: "My Panel" do
text(content: "Styled content", bold: true, fg: Color.rgb(100, 220, 100))
end
```
**Layout**: `width`, `height`, `min_width`, `max_width`, `flex_grow`, `flex_shrink`, `flex_basis`, `padding`, `margin`, `gap`, `direction` (`:row`/`:column`), `justify_content`, `align_items`, `align_self`
**Visual**: `fg`, `bg`, `opacity`, `bold`, `italic`, `underline`, `dim`, `inverse`, `border`, `border_style` (`:single`/`:double`/`:rounded`/`:heavy`)
## Project status
- **~88% feature parity** with OpenTUI v0.1.77
- **~1200 tests** across 41 test files
- Phases 1-11 complete (architecture, layout, rendering, terminal I/O, widgets, animation, advanced input)
- Phase 12 (DX & polish) in progress
## License
MIT