The Problem
Building an "AI coding assistant" CLI that streams LLM responses directly to the terminal sounds simpleβuntil you try it. Existing TUI frameworks are designed for static layouts (menus, dashboards) that update sporadically. When used for high-frequency streaming (50+ tokens/second), they suffer from:
| Issue | Symptom |
|---|---|
| Flickering | clear() + redraw() on every character creates strobing artifacts. |
| Blocking | Render calls starve the input handler, making Ctrl+C unresponsive. |
| Inefficiency | Diffing the entire 80x24 grid for 1 new character is O(nΒ²) waste. |
| State Desync | Direct stdout writes conflict with the framework's internal cursor tracking. |
Flywheel was designed from the ground up to solve this.
Features
| Feature | Description |
|---|---|
| π Zero-Flicker Rendering | Double-buffered diffing outputs only the delta between frames. No screen clears. |
| β‘ Sub-Millisecond Input Latency | Actor model decouples input polling from rendering. Ctrl+C always works. |
| π― Fast Path Optimization | For simple character appends, bypass the buffer entirelyβemit ANSI codes directly. |
| π Infinite Scrollback | StreamWidget stores 100k+ lines efficiently with "sticky scroll" UX. |
| π¨ True Color (24-bit RGB) | Full RGB attribute support for syntax highlighting and theming. |
| π¦ Safe Rust Core | Core library is #![forbid(unsafe_code)]. FFI module uses unsafe as required by C ABI. |
| π C FFI | Stable extern "C" interface for Python, Node.js, Go, and C/C++ bindings. |
Quickstart
Installation
[]
= "0.1"
Minimal Example
use ;
Run the Demo
This showcases:
- 100% GPU-free flicker elimination at 60 FPS
- 3000+ characters/second matrix generation
- Real-time input handling with cursor blinking
- Live CPU/Memory usage display
Architecture
Flywheel implements a 3-Actor Pipeline:
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Input Actor ββββββΆβ Main Thread ββββββΆβ Renderer Actor β
β (crossterm) β β (Your Code) β β (stdout) β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β β β
Keyboard Buffer Updates ANSI Sequences
Mouse Events Widget Logic Diff Output
Resize State Management Cursor Control
Core Axioms
| Axiom | Principle |
|---|---|
| A: Double Buffering | Next buffer holds pending changes. Current buffer holds what's on screen. Diffing produces minimal escape sequences. |
| B: Append-Optimized | StreamWidget::append() returns FastPath or SlowPath. Fast path bypasses diffing for O(1) writes. |
| C: Thread Isolation | Only Renderer Actor touches stdout. Zero contention. Zero deadlocks. |
| D: Event-Driven | Main loop uses recv_timeout() for input events. No polling. No sleeping. Sub-ms latency. |
Fast Path vs Slow Path
let result = stream.append;
match result
The append_fast_into() helper encapsulates this:
let mut raw_output = Vecnew;
stream.append_fast_into;
engine.write_raw; // Sends RawOutput command to Renderer
API Reference
Engine
The central coordinator. Manages terminal lifecycle and actor threads.
// Initialization
let mut engine = new?; // Default config
let mut engine = with_config?; // Custom FPS, mouse, etc.
// Dimensions
engine.width; // Terminal columns
engine.height; // Terminal rows
// Event Loop
engine.is_running; // Check if still alive
engine.poll_input; // Non-blocking: Vec<InputEvent>
engine.input_receiver.recv_timeout; // Blocking: for event-driven loops
// Rendering
engine.buffer_mut; // Get mutable reference to the Next buffer
engine.request_update; // Send buffer to Renderer (diff-based)
engine.request_redraw; // Send buffer to Renderer (full redraw)
engine.write_raw; // Bypass buffer, write ANSI directly (Fast Path)
// Lifecycle
engine.stop; // Signal shutdown
StreamWidget
A scrolling text viewport optimized for streaming content.
let mut stream = new;
// Styling
stream.set_fg; // Orange text
stream.set_bg; // Dark background
stream.set_bold;
// Content (Recommended API)
stream.push; // Automatic Fast/Slow path handling
stream.newline;
stream.clear;
// Low-level API (for advanced use cases)
stream.append; // Returns AppendResult, manual handling
stream.append_fast_into; // Manual Fast Path with raw output
// Scrolling (Sticky Scroll: auto-scroll only if at bottom)
stream.scroll_up;
stream.scroll_down;
// Rendering
stream.render; // Write to Buffer
stream.needs_redraw; // Check if dirty
Buffer
Low-level grid of cells representing the terminal screen.
let mut buffer = new;
buffer.set;
buffer.get; // Option<&Cell>
buffer.draw_text;
buffer.fill_rect;
buffer.clear;
InputEvent
Events received from the terminal.
match event
V2 Widgets
Flywheel V2 introduces a proper widget system with composable UI components.
TextInput
Single-line text input with cursor, editing, and navigation:
use ;
let mut input = new;
// Configure
input.set_content;
input.set_focused;
// Handle input events
if input.handle_input
// Render
input.render;
// Get content
let text = input.content;
StatusBar
Three-section status bar (left, center, right):
use ;
let mut status = new;
status.set_all;
// Or set individually
status.set_left;
status.set_center;
status.set_right;
status.render;
ProgressBar
Animated horizontal progress indicator:
use ;
let mut progress = new;
progress.set_progress; // 50%
progress.set_label;
progress.increment; // +10%
progress.render;
Widget Trait
All widgets implement the Widget trait:
Examples
Event-Driven Loop with TickerActor (Recommended)
Use the V2 TickerActor for non-blocking frame pacing:
use ;
use select;
use Duration;
let engine = new?;
let ticker = spawn; // 60 FPS
while engine.is_running
ticker.join;
Legacy Event Loop
For simpler applications without the ticker:
use RecvTimeoutError;
use Duration;
let target_fps = from_micros; // 60 FPS
let mut last_tick = now;
while engine.is_running
C FFI Usage
int
Performance
Benchmarked on Apple Silicon (criterion, release build):
Cell Operations
| Operation | Time |
|---|---|
| Cell equality (same) | 2.09 ns |
| Cell equality (diff grapheme) | 650 ps |
| Cell equality (diff color) | 921 ps |
| Cell from ASCII char | 1.73 ns |
| Cell from CJK char | 2.56 ns |
Buffer Diffing (200Γ50 = 10,000 cells)
| Scenario | Time | Notes |
|---|---|---|
| Identical buffers | 33.3 Β΅s | No-op diff |
| Single cell change | 33.6 Β΅s | Minimal output |
| Line change (200 cells) | 33.7 Β΅s | Optimized cursor moves |
| Full change (10K cells) | 289 Β΅s | ~2.9M cells/second |
| Full render | 318 Β΅s | No diffing |
RopeBuffer (Chunked Storage)
| Operation | Time | Notes |
|---|---|---|
| Append single char | 2.69 ns | O(1) amortized |
| Newline | 9.29 ns | Creates new line |
| Append 80 cells | 178 ns | Full line |
| Push complete line | 93 ns | Pre-built line |
| Get line (50K lines) | 537 ps | O(1) chunk lookup |
| Visible lines iterator | 194 ns | 50 lines |
| Push 100K lines | 404 Β΅s | ~247M lines/second |
Scaling
| Buffer Size | Diff Time (full change) |
|---|---|
| 80Γ24 (1,920 cells) | 55 Β΅s |
| 120Γ40 (4,800 cells) | 140 Β΅s |
| 200Γ50 (10,000 cells) | 291 Β΅s |
| 300Γ80 (24,000 cells) | 693 Β΅s |
Run Benchmarks
Flywheel vs Ratatui (Head-to-Head)
| Operation | Flywheel | Ratatui | Speedup |
|---|---|---|---|
| Buffer Creation (80Γ24) | 546 ns | 2.02 Β΅s | 3.7Γ |
| Buffer Creation (200Γ50) | 3.17 Β΅s | 9.96 Β΅s | 3.1Γ |
| Cell Write | 1.32 ns | 1.53 ns | 1.2Γ |
| Buffer Fill (80Γ24) | 796 ns | 3.20 Β΅s | 4.0Γ |
| Buffer Fill (200Γ50) | 4.17 Β΅s | 16.1 Β΅s | 3.9Γ |
| Buffer Diff (80Γ24) | 8.05 Β΅s | 21.6 Β΅s | 2.7Γ |
| Buffer Diff (200Γ50) | 39.2 Β΅s | 109 Β΅s | 2.8Γ |
| Cell Clone/Copy | 1.77 ns | 2.03 ns | 1.1Γ |
| Text Render (47 chars) | 91.1 ns | 137 ns | 1.5Γ |
Comparison
| Feature | Flywheel | ratatui | crossterm (raw) |
|---|---|---|---|
| Zero-flicker streaming | β | β | β |
| Non-blocking input | β | β | β |
| Fast Path optimization | β | β | N/A |
| Sticky scroll | β | β | N/A |
| Actor-based rendering | β | β | β |
| Widget system | β | β | β |
| RopeBuffer (1M+ lines) | β | β | N/A |
| C FFI | β | β | β |
Roadmap
V2.0 β Complete
- Buffer synchronization fix (ghost character elimination)
- Async-friendly TickerActor
- RopeBuffer for 1M+ line documents
- Widget system (TextInput, StatusBar, ProgressBar)
- Comprehensive documentation and benchmarks
Future
- V2.1: Layout containers (VSplit, HSplit, Stack)
- V2.2: Focus management system
- V3.0: WASM target for browser terminals
- V3.1: Plugin system for custom widgets
License
MIT