parley_ratatui
parley_ratatui is a Ratatui backend and renderer that turns a Ratatui
Buffer into a Vello scene or texture. It is intended for applications that
want to build terminal-style UI with Ratatui while presenting it inside a GPU
renderer, game engine, or offscreen texture pipeline.
The crate is built around three layers:
ParleyBackendrecords Ratatui draw calls into an in-memoryBuffer.TerminalRendererconverts thatBufferinto a VelloScene, preserving text shaping, modifiers, colors, cursor state, and blink state.GpuRendererowns Vello GPU renderer state and renders the scene into awgpu::TextureView.
The examples show a Bevy bridge that renders Ratatui UI into an offscreen Vello
texture, reads that texture back asynchronously, and updates a Bevy Image.
Supported Rendering Features
The renderer is designed to preserve terminal rendering correctness for:
- Unicode grapheme clusters and combining marks
- CJK and double-width cells
- Emoji and font fallback
- Box drawing, block elements, and powerline symbols
- Ratatui foreground/background colors
- ANSI indexed colors and truecolor
Color::Rgb BOLD,DIM,ITALIC,UNDERLINED,CROSSED_OUT,REVERSED,HIDDENSLOW_BLINKandRAPID_BLINKwhen using elapsed-time rendering APIs- Cursor visibility and cursor position
Installation
Add the crate to your project:
[]
= "0.1"
During local development in this repository, run the examples with:
Basic Usage
Create a Ratatui terminal with ParleyBackend, draw widgets into it, then render
the backend buffer into a texture.
use Terminal;
use ;
use wgpu;
use ;
# async
Prefer reusing GpuRenderer, TerminalRenderer, TextureTarget, and readback
state across frames. The convenience methods on TerminalRenderer create a new
GpuRenderer internally and are best for simple one-shot rendering.
Core Types
ParleyBackend
ParleyBackend implements Ratatui's Backend trait and stores the latest
terminal content in memory.
let mut terminal = new?;
Useful methods:
ParleyBackend::new(width, height)creates a fixed-size terminal buffer.backend.buffer()returns the current RatatuiBuffer.backend.cursor_position()returns Ratatui's current cursor position.backend.cursor_visible()returns whether Ratatui requested the cursor.backend.resize(width, height)resizes the backing buffer.
TerminalRenderer
TerminalRenderer owns text shaping state, layout caches, reusable Vello scene
state, and per-frame scratch data.
let mut renderer = new;
Useful methods:
metrics()returns measured cell metrics.texture_size_for_buffer(buffer)converts a Ratatui buffer size to pixels.build_scene(buffer, cursor, cursor_visible)returns a Vello scene reference.build_scene_with_elapsed(...)also evaluates slow/rapid blink state.render_to_texture(...)is a one-shot convenience API.render_to_rgba8(...)is a one-shot blocking readback API.render_to_rgba8_into(...)writes into caller-ownedVec<u8>storage.register_font(...),register_font_data(...), andregister_font_family(...)register bundled fonts after construction.set_font_family(...)changes the primary family and clears layout caches.
GpuRenderer
GpuRenderer wraps Vello's GPU renderer. Reuse one instance per wgpu::Device.
let mut gpu_renderer = new?;
Useful methods:
render_to_texture(...)render_to_texture_with_elapsed(...)render_to_rgba8(...)render_to_rgba8_into(...)render_to_rgba8_with_elapsed(...)render_to_rgba8_with_elapsed_into(...)
Use the *_with_elapsed variants when the UI contains SLOW_BLINK or
RAPID_BLINK and you want blink state to update over time.
TextureTarget
TextureTarget owns the destination wgpu::Texture and TextureView.
let target = new;
The target texture is created with these usages:
TEXTURE_BINDINGCOPY_SRCRENDER_ATTACHMENTSTORAGE_BINDING
Readback APIs currently support Rgba8Unorm and Rgba8UnormSrgb.
TextureReadback
TextureReadback is a reusable blocking readback helper. It reuses the staging
buffer and writes into caller-owned output storage, but it still waits for the
GPU before returning.
let mut readback = new;
let mut rgba = Vecnew;
gpu_renderer.render_to_rgba8_into?;
Use this for screenshots, tests, export, or simple integrations. For interactive
apps, prefer AsyncTextureReadback.
AsyncTextureReadback
AsyncTextureReadback pipelines GPU-to-CPU texture copies. It avoids blocking
the current frame while the GPU completes the readback.
Typical frame loop:
let mut readback = new;
let mut rgba = Vecnew;
// At the start of a frame, poll the oldest pending readback.
if readback.try_read_rgba8_into?
// Render the new frame.
gpu_renderer.render_to_texture?;
// Queue readback for a future frame.
let queued = readback.submit?;
if !queued
This introduces up to one frame of latency, but it avoids a CPU/GPU synchronization stall.
Font Configuration
FontOptions controls the primary font family, size, optional line height, and
bundled font registration.
let font = FontOptions ;
let renderer = new;
The family string is parsed as a CSS-style font family list. The renderer also appends these generic fallbacks internally:
ui-monospacemonospacesystem-uiemoji
Builder-Style Font Options
FontOptions has small builder-style helpers:
let font = default
.with_family
.with_bundled_font_data;
Available helpers:
with_family(family)sets the primary family.with_bundled_font(font)registers aBundledFont.with_bundled_font_data(data)registersinclude_bytes!-style data.with_bundled_font_family(family_name, data)registers data underfamily_nameand selects that family as the primary font.
Bundled Fonts
Use BundledFont when you need to ship fonts with your application.
use ;
let font = default
.with_family
.with_bundled_font;
For the common case, use with_bundled_font_family:
let font = default.with_bundled_font_family;
include_bytes! uses the zero-copy static path. If you load a font file at
runtime, use BundledFont::from_vec(bytes).
let bytes = read?;
let font = default
.with_family
.with_bundled_font;
Registering after construction is also supported:
let mut renderer = new;
let count = renderer.register_font_family;
if count == 0
Runtime registration clears layout caches and recomputes text metrics if at least one font was registered.
Font Fallback and Unicode
Parley and Fontique handle shaping and fallback. The renderer additionally seeds fallbacks for scripts that Fontique does not already cover on the current platform. This matters for CJK, Korean, Arabic, Devanagari, emoji, and other non-Latin text.
For best coverage, use a font stack that includes:
- A monospace terminal font for ASCII and UI glyphs
- A CJK font if your app displays Japanese, Chinese, or Korean text
- An emoji font for emoji and emoji ZWJ sequences
- A symbol or Nerd Font if your UI uses powerline/private-use glyphs
Example:
let font = default
.with_family;
Theme and Color Configuration
Theme controls default foreground/background colors, cursor color, and the
16-color ANSI palette.
use ;
let theme = Theme ;
Ratatui styles are resolved as follows:
Color::Resetand missing colors useTheme::foregroundorTheme::background.- Named ANSI colors use
Theme::palette. Color::Indexed(0..=15)usesTheme::palette.Color::Indexed(16..=231)maps to the 6x6x6 ANSI color cube.Color::Indexed(232..=255)maps to grayscale ramp colors.Color::Rgb(r, g, b)is preserved as truecolor.Modifier::DIMdims resolved colors.Modifier::REVERSEDswaps resolved foreground and background.
Cursor and Blink
Ratatui cursor state is stored by ParleyBackend. Pass it into render calls:
let cursor = Some;
let cursor_visible = terminal.backend.cursor_visible;
For blinking modifiers, use elapsed-time APIs:
gpu_renderer.render_to_texture_with_elapsed?;
SLOW_BLINK and RAPID_BLINK are resolved by hiding foreground text and
decorations during the hidden phase.
Bevy Integration
The examples use this flow:
- Draw Ratatui widgets into
ParleyBackend. - Render the buffer into a Vello-owned offscreen
TextureTarget. - Queue an asynchronous readback from that texture.
- Copy completed readback bytes into a Bevy
Image.
The examples intentionally borrow terminal.backend().buffer() directly. Avoid
cloning Ratatui buffers in frame loops.
The current Bevy examples use a separate Vello wgpu::Device. Bevy 0.18 and
Vello 0.8 currently use different wgpu versions in this dependency graph, so
passing a Vello-created wgpu::Texture directly into Bevy's render world is not
available through the public types used by these examples. The async readback
bridge is the optimized fallback: it reuses staging buffers and avoids blocking
on the GPU every frame, but it is still a GPU-to-CPU-to-Bevy upload path.
For a deeper Bevy integration, render on Bevy's render-world device and write
directly to a Bevy-managed texture. That requires a custom Bevy render plugin
and version-compatible wgpu access.
Performance Guidance
Runtime Practices
Prefer this in frame loops:
- Reuse
TerminalRenderer. - Reuse
GpuRenderer. - Reuse
TextureTargetuntil the terminal pixel size changes. - Reuse
TextureReadbackorAsyncTextureReadback. - Use
render_to_textureif the destination can consume a texture directly. - Use
render_to_rgba8_intoinstead ofrender_to_rgba8when you need CPU bytes. - Prefer
AsyncTextureReadbackfor interactive bridges. - Borrow
terminal.backend().buffer()directly. - Avoid cloning Ratatui
Buffers. - Avoid constructing a new
GpuRendererthroughTerminalRendererconvenience methods each frame.
Prefer this:
let buffer = terminal.backend.buffer;
gpu_renderer.render_to_texture?;
Avoid this in frame loops:
let buffer = terminal.backend.buffer.clone;
let rgba = renderer.render_to_rgba8?;
Debug Build Performance
Text shaping, Vello, WGPU, and Bevy are expensive in unoptimized debug builds.
For this repository, Cargo.toml includes:
[]
= 1
[]
= 3
This keeps the local crate easier to debug while compiling dependencies with optimization.
Cargo profile settings only apply from the workspace root or final binary crate.
They do not propagate from a library dependency. Downstream applications should
put the same settings in their own root Cargo.toml if they want similar debug
runtime performance:
[]
= 1
[]
= 3
If rebuild time matters more than debug runtime speed, consider using a custom profile in your application instead:
[]
= "dev"
= 1
[]
= 3
Then run:
Profiling
For profiling, use a release-like profile with debug symbols:
[]
= "release"
= true
= false
Then profile your application with its normal workload. Useful timing buckets:
- Ratatui
Terminal::draw TerminalRenderer::build_scene_with_elapsed- Vello
GpuRenderer::render_to_texture - Readback submit/poll/copy
- Destination texture or image upload
Examples
bevy_texture
Demonstrates a Unicode/style matrix rendered into a Bevy sprite. It exercises modifiers, truecolor, CJK, emoji sequences, combining marks, box drawing, and blink.
bevy_colors_rgb
Demonstrates high-frequency truecolor updates using upper-half block glyphs. This is useful for checking whether the bridge can keep up with dense color changes.
Limitations
- Readback APIs support
Rgba8UnormandRgba8UnormSrgb. - The Bevy examples use CPU image data as the bridge between Vello and Bevy.
- Runtime font registration invalidates layout caches and recomputes metrics.
TextureTargetmust be recreated when the terminal pixel size changes.- The renderer assumes terminal-style cell layout. It shapes Unicode text, but Ratatui still owns the cell grid and cell contents.
Development Checks
Run these before sending changes: