Extras overview
ftui-extras is the crate where FrankenTUI puts everything that is either
large, feature-gated, or SIMD-heavy. Visual effects live here
(Metaballs, Plasma, Doom Fire). So does Markdown rendering, Mermaid
diagrams, tree-sitter syntax highlighting, chart widgets, and the image
protocol bridges for iTerm2 and Kitty. Nothing in extras is required to
ship a working TUI — it is the “if you want this, here is how” shelf.
That design choice is load-bearing. Not every app wants a 482 KB Mermaid
renderer; not every app wants Markdown with every GFM extension. Keeping
those on the extras shelf means a minimal ftui-widgets app stays lean and
compiles fast, and the heavy modules are behind feature flags you opt into.
Source crate: ftui-extras/.
Why a separate crate
Three reasons:
- Optimization profile. Extras is compiled at
opt-level = 3even in debug builds. Visual effects and text-effects modules contain hot inner loops with SIMD-friendly shapes (Braille packing, plasma wave sums, metaball iso-surface sampling).opt-level = 0runs them at single-digit FPS;opt-level = 3holds 60. - Feature gating. 20+ feature flags select which modules you pull
in. Default is just
canvasandcharts; you opt intomarkdown,diagram,syntax,visual-fx,doom,quake,image,live,logging,clipboard, and so on. - Crate budget. Extras alone is several MB of source. Keeping it
separate lets the core crates (
ftui-core,ftui-render,ftui-widgets) stay compact and individually publishable.
What’s inside
| Module | Feature | Approx size | Purpose |
|---|---|---|---|
canvas | canvas (default) | 56 KB | Pixel-level drawing primitives (lines, arcs, fills) |
charts | charts (default) | 64 KB | Bar / line / pie charts built on canvas |
visual_fx + effects/* | visual-fx | 300 KB | Metaballs, Plasma, Doom Fire, Screen Melt, etc. |
visual_fx/gpu | fx-gpu | 26 KB | Optional wgpu acceleration for supported effects |
markdown | markdown | 121 KB | GFM + tables + code fences → styled text |
mermaid + mermaid_* | diagram | ~1.2 MB | Mermaid parser + layout + render |
syntax | syntax | 176 KB | Tree-sitter-backed syntax highlighting spans |
text_effects | text-effects | 374 KB | Animated gradients, fades, ASCII art |
theme | theme | 162 KB | Colour themes + palette tokens (incl. TableTheme) |
forms | forms | 104 KB | Form layout + validation widgets |
export | export | 51 KB | Buffer → HTML / SVG / plain text |
image | image | 34 KB | iTerm2 / Kitty image protocol bridges |
live | live | 44 KB | Live-updating display primitives |
logging | logging | 46 KB | tracing subscriber that writes into a widget log ring |
clipboard | clipboard | 62 KB | OSC 52 clipboard plumbing |
terminal | terminal | — | Embedded terminal emulator for PTY panes |
doom | doom | — | Doom Fire + Melt effects (classic demoscene) |
quake | quake | — | Quake-console drop-down overlay |
How to enable features
A minimal Cargo.toml that turns on Markdown and visual FX:
[dependencies]
ftui-extras = { version = "0.3", features = ["markdown", "visual-fx"] }Defaults (canvas, charts) are still on unless you set
default-features = false.
Feature dependency graph
A few features imply others:
You can ask Cargo for exactly the set you want and nothing else.
The ThemeInputs boundary
Visual effects never reach into the global theme. They consume a single
value type, ThemeInputs, and treat it as pure data
(visual_fx.rs:186):
pub struct ThemeInputs {
pub bg_base: PackedRgba, // deepest background
pub bg_surface: PackedRgba, // cards / panels
pub bg_overlay: PackedRgba, // legibility scrim
pub fg_primary: PackedRgba, // primary text / lines
pub fg_muted: PackedRgba, // secondary text / lines
pub accent_primary: PackedRgba,
pub accent_secondary: PackedRgba,
pub accent_slots: [PackedRgba; 4],
}Why this matters:
- Deterministic. Same inputs → same output pixels.
- Testable. Unit-test an effect with a fixture theme and compare byte buffers.
- No global state. An effect cannot accidentally depend on “the current theme” via an implicit lookup.
You build a ThemeInputs from whatever theme system you use (the
ftui-extras::theme module ships a converter), then hand it to the effect.
Quality tiers and degradation
Every visual effect honours a FxQuality
tier:
pub enum FxQuality {
Off, // skip entirely
Minimal, // one or two ops, near-static
Reduced, // fewer iterations, simpler math
Full, // normal detail
}The tier is derived from the frame’s DegradationLevel:
| Degradation | FxQuality |
|---|---|
EssentialOnly | Off |
NoStyling | Reduced |
SimpleBorders | Full |
Full | Full |
Plus an area-based clamp: even at Full, if the target rect exceeds
roughly 16 K cells (≈ 160×100), the tier drops one step to keep the
effect’s cost bounded. You never have a Plasma effect unexpectedly
spending 40 ms on a giant viewport.
Effects are optional by design — they exist to delight, not to inform. When the frame budget is tight, they go first. Primary content always wins.
Integration surface
From your runtime, extras look like any other widget or renderer. A typical visual FX integration:
use ftui_extras::visual_fx::{effects::MetaballsFx, ThemeInputs, FxQuality};
pub struct Model {
metaballs: MetaballsFx,
theme_inputs: ThemeInputs,
}
fn view(&self, frame: &mut ftui_render::frame::Frame) {
let area = frame.buffer.bounds();
let quality = FxQuality::from_degradation(frame.degradation());
self.metaballs.render_into(area, frame, &self.theme_inputs, quality);
}No registration, no global init. The effect owns its state (ball positions, plasma phase, particle trails) and is a plain struct you store on your model.
Performance
- Visual FX modules are compiled with
opt-level = 3regardless of the crate-level profile, so you do not need a release build to see smooth animation. ThemeInputsis a plain struct, cloneable, 80 bytes on 64-bit. Pass by reference.- The SIMD shapes (especially Plasma’s sum-of-sines) vectorise cleanly on x86_64 AVX2 and aarch64 NEON. There is no runtime SIMD dispatch — the compiler emits what it can.
Pitfalls
- Turning on
diagramfor a minimal app. Mermaid alone is ~1.2 MB of Rust source; link times grow noticeably. Don’t enable what you don’t use. - Ignoring the area clamp. If you force
FxQuality::Fullon a viewport that is 600×80, you will miss frame budgets. Let the clamp do its job. - Reaching into global theme from a custom effect. Don’t. Funnel the
inputs through
ThemeInputsso the effect stays pure. - Running visual FX during integration tests. Effects are non-deterministic by default (time-driven). Snapshot tests that render an effect will flap; either stub the time source or skip them.
Where next
How sub-character pixel density works — Braille, half-blocks, quadrants.
VFX rasterizerExhaustive catalog of effects with their equations and how to view each one.
Visual FX catalogThe visual_effects view runs every effect through the
FTUI_HARNESS_VIEW environment variable.
Per-crate matrix of every feature flag.
Feature flags