opentui_rust 0.2.1

High-performance terminal UI rendering engine with alpha blending and diffed buffers
Documentation

OpenTUI Rust

License: MIT Rust Edition codecov

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
cargo add opentui_rust

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 opentui_rust::{Renderer, OptimizedBuffer, Style, Rgba, Cell};

fn main() -> std::io::Result<()> {
    // Create renderer (enters alt screen, hides cursor)
    let mut renderer = Renderer::new(80, 24)?;

    // Draw to back buffer
    let buffer = renderer.buffer();
    buffer.clear(Rgba::BLACK);
    buffer.draw_text(10, 5, "Hello, OpenTUI!", Style::fg(Rgba::GREEN));

    // Present (only changed cells are written)
    renderer.present()?;

    // Renderer::drop() restores terminal automatically
    Ok(())
}

Alpha Blending

// 50% transparent red over blue background
let bg = Rgba::BLUE;
let overlay = Rgba::RED.with_alpha(0.5);

buffer.clear(bg);
buffer.set_blended(10, 5, Cell::new('X', Style::fg(overlay)));
// Result: purple-ish cell with proper Porter-Duff compositing

Scissor Clipping

use opentui_rust::buffer::ClipRect;

// Only draw within this rectangle
buffer.push_scissor(ClipRect::new(10, 10, 20, 10));

// This text is clipped to the scissor rect
buffer.draw_text(0, 0, "This won't appear outside the clip region", Style::default());

buffer.pop_scissor();

Opacity Stacks

// Everything drawn at 50% opacity
buffer.push_opacity(0.5);
buffer.draw_text(0, 0, "Semi-transparent", Style::fg(Rgba::WHITE));
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[?2026h to 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

cargo add opentui_rust

From Source

git clone https://github.com/Dicklesworthstone/opentui_rust
cd opentui_rust
cargo build --release

Cargo.toml

[dependencies]
opentui_rust = "0.1"

Quick Start

1. Create a Renderer

use opentui_rust::Renderer;
use std::io;

fn main() -> io::Result<()> {
    // Automatically enters alt screen, hides cursor, enables mouse
    let mut renderer = Renderer::new(80, 24)?;

    // Your rendering loop here...

    Ok(())
    // Renderer::drop() cleans up automatically
}

2. Draw to the Buffer

use opentui_rust::{Style, Rgba};

let buffer = renderer.buffer();

// Clear with background
buffer.clear(Rgba::from_hex("#1a1a2e").unwrap());

// Draw styled text
buffer.draw_text(5, 2, "Title", Style::bold().fg(Rgba::WHITE));
buffer.draw_text(5, 4, "Normal text", Style::fg(Rgba::from_hex("#888888").unwrap()));

// Draw a box
buffer.draw_box(2, 1, 40, 10, opentui_rust::buffer::BoxStyle::single());

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 crossterm::event::{self, Event, KeyCode};

loop {
    // Draw
    renderer.buffer().draw_text(0, 0, "Press 'q' to quit", Style::default());
    renderer.present()?;

    // Handle input
    if event::poll(std::time::Duration::from_millis(100))? {
        if let Event::Key(key) = event::read()? {
            if key.code == KeyCode::Char('q') {
                break;
            }
        }
    }
}

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)
cargo run --bin demo_showcase

# Guided tour mode (auto-plays through features)
cargo run --bin demo_showcase -- --tour

# Tour mode with auto-exit (for scripting/CI)
cargo run --bin demo_showcase -- --tour --exit-after-tour

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)
./scripts/demo_showcase_verify.sh

# Fast check (skip headless tests)
./scripts/demo_showcase_verify.sh --quick

# Run PTY E2E tests with artifact collection
./scripts/demo_showcase_e2e_pty.sh

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 opentui_rust::renderer::ThreadedRenderer;

let mut renderer = ThreadedRenderer::new(80, 24)?;
renderer.buffer().draw_text(1, 1, "Threaded!", Style::fg(Rgba::GREEN));
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 (buffer, pool) = renderer.buffer_with_pool();
let grapheme = "\u{0061}\u{0301}"; // "a" + combining acute accent
buffer.draw_text_with_pool(pool, 0, 0, grapheme, Style::fg(Rgba::WHITE));

Hyperlinks are stored in a link pool and referenced by link ID in text styles:

let link_id = renderer.link_pool().alloc("https://example.com");
let style = Style::fg(Rgba::BLUE).with_underline().with_link(link_id);
renderer.buffer().draw_text(0, 1, "example.com", style);

Color Operations

// Creation
Rgba::new(1.0, 0.0, 0.0, 1.0)  // f32 RGBA
Rgba::rgb(1.0, 0.0, 0.0)       // f32 RGB (opaque)
Rgba::from_rgb_u8(255, 0, 0)   // u8 RGB
Rgba::from_hex("#FF0000")      // Hex string
Rgba::from_hsv(0.0, 1.0, 1.0)  // HSV (h: 0-360)

// Operations
color.blend_over(other)         // Porter-Duff "over"
color.with_alpha(0.5)           // Set alpha
color.multiply_alpha(0.5)       // Multiply alpha
color.lerp(other, 0.5)          // 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
reset
# Or
stty sane

For robust cleanup, install a panic hook:

use std::panic;

let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
    // Restore terminal before printing panic
    let _ = crossterm::terminal::disable_raw_mode();
    let _ = crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen);
    original_hook(info);
}));

Characters display with wrong width

Some terminals report incorrect widths for certain Unicode characters. Try:

// Use wcwidth-based calculation (POSIX compatible)
unicode::set_width_method(WidthMethod::WcWidth);

// Or Unicode Standard Annex #11 (more accurate for CJK)
unicode::set_width_method(WidthMethod::Unicode);

Flickering on slow terminals

OpenTUI uses synchronized output (\x1b[?2026h) which most modern terminals support. If you see flicker:

  1. Update your terminal emulator
  2. Try a different terminal (kitty, alacritty, wezterm)
  3. Reduce frame rate

Colors look wrong

Ensure your terminal supports true color:

echo $COLORTERM  # Should be "truecolor" or "24bit"

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 {
    renderer.present()?;
}

// Good: wait for events
loop {
    renderer.present()?;
    if !event::poll(Duration::from_millis(16))? {
        continue;  // ~60 FPS max
    }
    // Handle event...
}

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 (with OpenAI/Anthropic Rider). 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