OpenTUI Rust
A Rust port of anomalyco/opentui (TypeScript), with native Rust performance and extended features.
High-performance terminal UI library with alpha blending, scissoring, and double-buffered rendering.
# Add to your project
TL;DR
The Problem
Building terminal UIs in Rust means choosing between:
- High-level frameworks (ratatui, cursive) that are opinionated and heavy
- Low-level crates (crossterm, termion) that require manual buffer management
- Neither offers true alpha blending, layered composition, or sub-millisecond rendering
The Solution
OpenTUI is a rendering engine, not a framework. It gives you:
- Cell-based buffers with real RGBA alpha blending
- Scissor clipping for nested viewports
- Double-buffered rendering with diff detection (only changed cells update)
- Rope-based text editing with undo/redo
- Zero opinions about your application structure
Why OpenTUI?
| Feature | OpenTUI | ratatui | crossterm |
|---|---|---|---|
| Alpha blending | RGBA Porter-Duff | No | No |
| Scissor clipping | Stack-based | Manual | No |
| Diff rendering | Automatic | Manual | Manual |
| Text editing | Rope + undo | No | No |
| Grapheme support | Full | Partial | No |
| Framework lock-in | None | Widget system | None |
| Binary size | ~200KB | ~500KB | ~100KB |
Quick Example
use ;
Alpha Blending
// 50% transparent red over blue background
let bg = BLUE;
let overlay = RED.with_alpha;
buffer.clear;
buffer.set_blended;
// Result: purple-ish cell with proper Porter-Duff compositing
Scissor Clipping
use ClipRect;
// Only draw within this rectangle
buffer.push_scissor;
// This text is clipped to the scissor rect
buffer.draw_text;
buffer.pop_scissor;
Opacity Stacks
// Everything drawn at 50% opacity
buffer.push_opacity;
buffer.draw_text;
buffer.pop_opacity;
Design Philosophy
1. Rendering Engine, Not Framework
OpenTUI provides primitives: buffers, cells, colors, text. You decide how to structure your app. No widget trees, no layout systems, no event loops forced on you.
2. Correctness Over Convenience
- Real alpha blending using Porter-Duff "over" compositing
- Proper grapheme handling via
unicode-segmentation - Accurate character widths via
unicode-width - Immutable rope for text that doesn't corrupt on edits
3. Performance by Default
- Diff rendering: Only changed cells generate ANSI output
- Synchronized output: Uses
\x1b[?2026hto eliminate flicker - Zero allocations on hot paths (cell updates, blending)
- SIMD-friendly memory layout (contiguous cell arrays)
4. Terminal Respect
- Automatic cleanup on drop (restores cursor, exits alt screen)
- Proper mouse protocol handling
- True color support with graceful fallback
- Works in SSH, tmux, and embedded terminals
5. Port of Battle-Tested Code
Based on OpenTUI's Zig core (~15,900 LOC), which powers production terminal applications. This isn't a weekend experiment.
Comparison
| Library | Abstraction | Alpha | Scissor | Diff | Text Edit | Use Case |
|---|---|---|---|---|---|---|
| OpenTUI | Rendering engine | Yes | Yes | Yes | Yes | Custom TUI apps |
| ratatui | Widget framework | No | No | Partial | No | Standard TUIs |
| crossterm | Terminal I/O | No | No | No | No | Low-level control |
| termion | Terminal I/O | No | No | No | No | Low-level control |
| cursive | Dialog framework | No | No | Yes | Partial | Form-based apps |
| tui-rs | Widget framework | No | No | Partial | No | Dashboards |
Choose OpenTUI when you need:
- Compositing layers with transparency
- Pixel-perfect control over rendering
- High-performance text editing
- No framework opinions
Choose ratatui when you need:
- Quick prototyping with widgets
- Standard TUI patterns (tables, lists, tabs)
- Large community and examples
Installation
From crates.io
From Source
Cargo.toml
[]
= "0.1"
Quick Start
1. Create a Renderer
use Renderer;
use io;
2. Draw to the Buffer
use ;
let buffer = renderer.buffer;
// Clear with background
buffer.clear;
// Draw styled text
buffer.draw_text;
buffer.draw_text;
// Draw a box
buffer.draw_box;
3. Present Frame
// Diff-based update (fast)
renderer.present?;
// Force full redraw
renderer.invalidate;
renderer.present?;
4. Handle Input (bring your own)
OpenTUI doesn't include an event loop. Use crossterm or termion:
use ;
loop
Demo Showcase
The demo_showcase binary demonstrates OpenTUI's full capability set in an interactive terminal application.
Running the Demo
# Interactive mode (explore with keyboard/mouse)
# Guided tour mode (auto-plays through features)
# Tour mode with auto-exit (for scripting/CI)
What It Demonstrates
The demo showcases every major OpenTUI feature:
- Alpha blending — Glass-like overlays, semi-transparent panels
- Scissor clipping — Nested scroll regions, viewport masking
- Opacity stacks — Hierarchical transparency
- Diff rendering — Only changed cells update (watch the stats panel)
- Grapheme handling — CJK, emoji, ZWJ sequences rendered correctly
- OSC 8 hyperlinks — Clickable URLs in supported terminals
- Hit testing — Mouse hover/click detection
- Pixel buffers — Animated graphics using block characters
Keybindings
| Key | Action |
|---|---|
Tab |
Cycle focus between panels |
↑/↓ |
Navigate sidebar sections |
Enter |
Select section |
F1 |
Toggle help overlay |
Ctrl+P |
Command palette |
Ctrl+D |
Debug/inspector panel |
T |
Start/restart tour |
Esc |
Close overlay / exit tour |
Q |
Quit |
CLI Flags
Interactive:
| Flag | Description |
|---|---|
--fps <N> |
Target frame rate (default: 60) |
--tour |
Start in guided tour mode |
--exit-after-tour |
Exit when tour completes |
--max-frames <N> |
Hard frame limit (safety bound) |
--seed <N> |
Random seed for deterministic behavior |
--threaded |
Use threaded renderer |
Headless/Testing:
| Flag | Description |
|---|---|
--headless-smoke |
Run without TTY (for CI) |
--headless-dump-json |
Output frame stats as JSON |
--headless-size <WxH> |
Set virtual terminal size (e.g., 80x24) |
Terminal Behavior:
| Flag | Description |
|---|---|
--no-mouse |
Disable mouse tracking |
--no-alt-screen |
Don't use alternate screen buffer |
--no-cap-queries |
Skip terminal capability detection |
--cap-preset <name> |
Force capability preset (minimal, no_hyperlinks, etc.) |
Recommended Terminals
For the best visual experience, use a terminal that supports:
- True color (24-bit RGB)
- Synchronized output (eliminates flicker)
- OSC 8 hyperlinks (clickable URLs)
- Unicode (grapheme clusters, emoji)
Recommended: kitty, WezTerm, Ghostty, Alacritty, iTerm2
Verification Scripts
# Quick validation (format + clippy + tests)
# Fast check (skip headless tests)
# Run PTY E2E tests with artifact collection
API Reference
Core Types
| Type | Purpose |
|---|---|
Rgba |
RGBA color with f32 components, alpha blending |
Style |
Foreground, background, and text attributes |
TextAttributes |
Bold, italic, underline, etc. (bitflags) |
Cell |
Single terminal cell (char + colors + attributes) |
CellContent |
Char, grapheme cluster, empty, or continuation |
GraphemeId |
Packed grapheme ID + width encoding for cells |
GraphemePool |
Interned grapheme storage with ref counting |
LinkPool |
Hyperlink URL storage for OSC 8 output |
Buffer Operations
| Method | Description |
|---|---|
OptimizedBuffer::new(w, h) |
Create buffer with dimensions |
buffer.set(x, y, cell) |
Write cell (respects scissor/opacity) |
buffer.set_blended(x, y, cell) |
Write with alpha blending |
buffer.get(x, y) |
Read cell at position |
buffer.clear(bg) |
Fill entire buffer |
buffer.fill_rect(x, y, w, h, bg) |
Fill rectangle |
buffer.draw_text(x, y, text, style) |
Draw UTF-8 string |
buffer.draw_box(x, y, w, h, style) |
Draw box border |
buffer.draw_buffer(x, y, src) |
Composite another buffer |
buffer.push_scissor(rect) |
Push clipping rectangle |
buffer.pop_scissor() |
Pop clipping rectangle |
buffer.push_opacity(f32) |
Push opacity multiplier |
buffer.pop_opacity() |
Pop opacity multiplier |
Renderer Operations
| Method | Description |
|---|---|
Renderer::new(w, h) |
Create renderer, setup terminal |
renderer.buffer() |
Get back buffer for drawing |
renderer.present() |
Swap buffers, render diff |
renderer.present_force() |
Force full redraw |
renderer.resize(w, h) |
Handle terminal resize |
renderer.set_cursor(x, y, visible) |
Position/show cursor |
renderer.set_title(title) |
Set terminal title |
renderer.register_hit_area(...) |
Register mouse hit zone |
renderer.hit_test(x, y) |
Test mouse position |
Threaded Renderer
Use the threaded renderer when you want terminal I/O off the main thread:
use ThreadedRenderer;
let mut renderer = new?;
renderer.buffer.draw_text;
renderer.present?;
renderer.shutdown?;
Grapheme Pools and Hyperlinks
Use the grapheme pool for multi-codepoint graphemes so they can be resolved back to their full UTF-8 sequence during rendering:
let = renderer.buffer_with_pool;
let grapheme = "\u{0061}\u{0301}"; // "a" + combining acute accent
buffer.draw_text_with_pool;
Hyperlinks are stored in a link pool and referenced by link ID in text styles:
let link_id = renderer.link_pool.alloc;
let style = fg.with_underline.with_link;
renderer.buffer.draw_text;
Color Operations
// Creation
new // f32 RGBA
rgb // f32 RGB (opaque)
from_rgb_u8 // u8 RGB
from_hex // Hex string
from_hsv // HSV (h: 0-360)
// Operations
color.blend_over // Porter-Duff "over"
color.with_alpha // Set alpha
color.multiply_alpha // Multiply alpha
color.lerp // Linear interpolation
color.to_rgb_u8 // Convert to (u8, u8, u8)
Text Module
| Type | Purpose |
|---|---|
TextBuffer |
Styled text storage with rope backend |
TextBufferView |
Viewport with wrapping and selection |
EditBuffer |
Editable text with cursor and undo/redo |
WrapMode |
None, Char, or Word wrapping |
HighlightedBuffer |
Text buffer wrapper with syntax highlighting |
SyntaxStyleRegistry |
Style registry for token kinds |
TokenizerRegistry |
Language tokenizer registry |
Theme |
TokenKind → Style mapping |
Architecture
┌─────────────────────────────────────┐
│ Your Application │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ OpenTUI │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Renderer │───▶│ Buffer │◀───│ Text │ │
│ │ │ │ │ │ │ │
│ │ • Double buf │ │ • Cells │ │ • Rope │ │
│ │ • Diff detect│ │ • Scissor │ │ • Segments │ │
│ │ • Hit grid │ │ • Opacity │ │ • Highlights │ │
│ │ • Sync output│ │ • Drawing │ │ • Edit/Undo │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Terminal │ │ Cell │ │ Unicode │ │
│ │ │ │ │ │ │ │
│ │ • ANSI codes │ │ • Char/Graph │ │ • Graphemes │ │
│ │ • Mouse │ │ • Style │ │ • Width calc │ │
│ │ • Cursor │ │ • Blending │ │ • Segmentat. │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ stdout (ANSI TTY) │
└─────────────────────────────────────┘
Module Breakdown
opentui_rust/
├── lib.rs # Public API exports
├── color.rs # RGBA type, blending, conversions
├── style.rs # TextAttributes, Style builder
├── cell.rs # Cell type, CellContent enum
├── grapheme_pool.rs# Grapheme pool + ID encoding
├── link.rs # Hyperlink pool (OSC 8)
├── ansi/ # ANSI escape sequence generation
│ ├── mod.rs
│ ├── sequences.rs
│ └── output.rs # Buffered ANSI writer
├── buffer/ # OptimizedBuffer
│ ├── mod.rs
│ ├── scissor.rs # ClipRect, ScissorStack
│ ├── opacity.rs # OpacityStack
│ └── drawing.rs # Text/box drawing
├── text/ # Text editing
│ ├── mod.rs
│ ├── rope.rs # Rope wrapper (ropey)
│ ├── segment.rs # StyledSegment
│ ├── buffer.rs # TextBuffer
│ ├── view.rs # TextBufferView
│ ├── edit.rs # EditBuffer
│ └── editor.rs # EditorView
├── renderer/ # Display rendering
│ ├── mod.rs
│ ├── diff.rs # Buffer diffing
│ ├── hitgrid.rs # Mouse hit testing
│ └── threaded.rs # Threaded renderer
├── terminal/ # Terminal abstraction
│ ├── mod.rs
│ ├── capabilities.rs
│ ├── cursor.rs
│ └── mouse.rs
├── unicode/ # Unicode handling
│ ├── mod.rs
│ ├── grapheme.rs
│ └── width.rs
└── highlight/ # Syntax highlighting
├── mod.rs
├── highlighted_buffer.rs
├── languages/ # Language tokenizers
├── syntax.rs
├── theme.rs
├── token.rs
└── tokenizer.rs
Troubleshooting
Terminal doesn't restore after crash
If your program panics, the terminal may be left in a bad state:
# Reset terminal
# Or
For robust cleanup, install a panic hook:
use panic;
let original_hook = take_hook;
set_hook;
Characters display with wrong width
Some terminals report incorrect widths for certain Unicode characters. Try:
// Use wcwidth-based calculation (POSIX compatible)
set_width_method;
// Or Unicode Standard Annex #11 (more accurate for CJK)
set_width_method;
Flickering on slow terminals
OpenTUI uses synchronized output (\x1b[?2026h) which most modern terminals support. If you see flicker:
- Update your terminal emulator
- Try a different terminal (kitty, alacritty, wezterm)
- Reduce frame rate
Colors look wrong
Ensure your terminal supports true color:
If not supported, colors will be approximated to 256-color palette.
High CPU usage
Check that you're not calling present() in a tight loop:
// Bad: spins CPU
loop
// Good: wait for events
loop
Limitations
- No built-in event loop: You provide your own (use crossterm/termion)
- No widgets: OpenTUI is a rendering engine, not a widget toolkit
- No layout system: You calculate positions yourself
- Nightly Rust required: Uses edition 2024 features
- No Windows ConPTY: Windows support is terminal-dependent
- Text-only: No image protocols (sixel, kitty graphics) yet
FAQ
Q: Why not just use ratatui?
A: ratatui is excellent for standard TUI patterns. OpenTUI is for when you need lower-level control: alpha blending, precise clipping, custom rendering pipelines, or want to build your own widget system.
Q: Is this production-ready?
A: The core rendering is solid (ported from battle-tested Zig code). The Rust API is still stabilizing. Pin your version and expect some churn.
Q: Why f32 for colors instead of u8?
A: Alpha blending math is more accurate with floats. Final output converts to u8 for ANSI codes. The performance difference is negligible.
Q: Can I use this with async?
A: Yes, but Renderer isn't Send. Keep it on one thread and send drawing commands via channels.
Q: Why require nightly Rust?
A: Edition 2024 provides better ergonomics. We'll support stable once edition 2024 stabilizes.
Q: How do I handle terminal resize?
A: Listen for SIGWINCH (Unix) or use crossterm's resize event, then call renderer.resize(w, h).
About Contributions
Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via gh and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
License
MIT License. See LICENSE for details.
Acknowledgments
- Original OpenTUI Zig implementation for the battle-tested architecture
- ropey for the rope data structure
- unicode-segmentation for grapheme clustering
- unicode-width for display width calculation