<p align="center">
<h1 align="center">Flywheel</h1>
<p align="center">
<strong>The Zero-Flicker Terminal Compositor for Agentic CLIs</strong>
</p>
<p align="center">
A high-performance, Rust-native rendering engine purpose-built for streaming LLM outputs at 60+ FPS without tearing, flickering, or input lag.
</p>
</p>
<p align="center">
<a href="#quickstart">Quickstart</a> •
<a href="#features">Features</a> •
<a href="#architecture">Architecture</a> •
<a href="#api-reference">API</a> •
<a href="#examples">Examples</a>
</p>
---
## 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:
| **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
| 🚀 **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
```toml
[dependencies]
flywheel-compositor = "0.1"
```
### Minimal Example
```rust
use flywheel::{Engine, StreamWidget, Rect, Rgb};
fn main() -> std::io::Result<()> {
let mut engine = Engine::new()?;
let mut stream = StreamWidget::new(Rect::new(0, 0, engine.width(), engine.height()));
// Simulate LLM streaming
for token in ["Hello, ", "world! ", "This ", "is ", "Flywheel."] {
stream.set_fg(Rgb::new(0, 255, 128)); // Green text
// Just push. The engine handles Fast/Slow path automatically.
stream.push(&engine, token);
std::thread::sleep(std::time::Duration::from_millis(100));
}
// Event loop
while engine.is_running() {
for event in engine.poll_input() {
match event {
flywheel::InputEvent::Key { code: flywheel::KeyCode::Esc, .. } => engine.stop(),
_ => {}
}
}
}
Ok(())
}
```
### Run the Demo
```bash
cargo run --example streaming_demo --release
```
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
| **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
```rust
let result = stream.append("x");
match result {
AppendResult::FastPath { row, start_col, .. } => {
// Character appended within viewport, no wrapping.
// Emit: MoveTo(row, col) + SetColor + PrintChar
// Cost: ~20 bytes to stdout
}
AppendResult::SlowPath => {
// Wrapping or scrolling required.
// Full frame rendered via diffing engine.
// Cost: ~200 bytes to stdout (only changed cells)
}
}
```
The `append_fast_into()` helper encapsulates this:
```rust
let mut raw_output = Vec::new();
stream.append_fast_into("x", &mut raw_output);
engine.write_raw(raw_output); // Sends RawOutput command to Renderer
```
---
## API Reference
### `Engine`
The central coordinator. Manages terminal lifecycle and actor threads.
```rust
// Initialization
let mut engine = Engine::new()?; // Default config
let mut engine = Engine::with_config(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(duration); // 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(bytes); // Bypass buffer, write ANSI directly (Fast Path)
// Lifecycle
engine.stop(); // Signal shutdown
```
### `StreamWidget`
A scrolling text viewport optimized for streaming content.
```rust
let mut stream = StreamWidget::new(Rect::new(x, y, width, height));
// Styling
stream.set_fg(Rgb::new(255, 128, 0)); // Orange text
stream.set_bg(Rgb::new(20, 20, 20)); // Dark background
stream.set_bold(true);
// Content (Recommended API)
stream.push(&engine, "Hello"); // Automatic Fast/Slow path handling
stream.newline();
stream.clear();
// Low-level API (for advanced use cases)
stream.append("text"); // Returns AppendResult, manual handling
stream.append_fast_into("x", &mut buf); // Manual Fast Path with raw output
// Scrolling (Sticky Scroll: auto-scroll only if at bottom)
stream.scroll_up(lines);
stream.scroll_down(lines);
// Rendering
stream.render(&mut buffer); // Write to Buffer
stream.needs_redraw(); // Check if dirty
```
### `Buffer`
Low-level grid of cells representing the terminal screen.
```rust
let mut buffer = Buffer::new(80, 24);
buffer.set(x, y, Cell::new('A').with_fg(Rgb::RED));
buffer.get(x, y); // Option<&Cell>
buffer.draw_text(x, y, "text", fg, bg);
buffer.fill_rect(x, y, w, h, cell);
buffer.clear();
```
### `InputEvent`
Events received from the terminal.
```rust
match event {
InputEvent::Key { code, modifiers } => { /* KeyCode::Char, Esc, Enter, etc. */ }
InputEvent::MouseClick { x, y, button } => { /* Left, Right, Middle */ }
InputEvent::MouseScroll { x, y, delta } => { /* +1 up, -1 down */ }
InputEvent::Resize { width, height } => { /* Terminal resized */ }
InputEvent::Shutdown => { /* SIGTERM or similar */ }
_ => {}
}
```
---
## V2 Widgets
Flywheel V2 introduces a proper widget system with composable UI components.
### `TextInput`
Single-line text input with cursor, editing, and navigation:
```rust
use flywheel::{TextInput, Widget, Rect};
let mut input = TextInput::new(Rect::new(0, 23, 80, 1));
// Configure
input.set_content("Initial text");
input.set_focused(true);
// Handle input events
if input.handle_input(&event) {
// Event was consumed by the widget
}
// Render
input.render(buffer);
// Get content
let text = input.content();
```
### `StatusBar`
Three-section status bar (left, center, right):
```rust
use flywheel::{StatusBar, Widget, Rect};
let mut status = StatusBar::new(Rect::new(0, 0, 80, 1));
status.set_all("Flywheel", "v2.0", "60 FPS");
// Or set individually
status.set_left("App Name");
status.set_center("Status");
status.set_right("12:34");
status.render(buffer);
```
### `ProgressBar`
Animated horizontal progress indicator:
```rust
use flywheel::{ProgressBar, Widget, Rect, ProgressStyle};
let mut progress = ProgressBar::new(Rect::new(0, 5, 60, 1));
progress.set_progress(0.5); // 50%
progress.set_label("Loading");
progress.increment(0.1); // +10%
progress.render(buffer);
```
### Widget Trait
All widgets implement the `Widget` trait:
```rust
pub trait Widget {
fn bounds(&self) -> Rect;
fn set_bounds(&mut self, bounds: Rect);
fn render(&self, buffer: &mut Buffer);
fn handle_input(&mut self, event: &InputEvent) -> bool;
fn needs_redraw(&self) -> bool;
fn clear_redraw(&mut self);
}
```
---
## Examples
### Event-Driven Loop with TickerActor (Recommended)
Use the V2 `TickerActor` for non-blocking frame pacing:
```rust
use flywheel::{Engine, TickerActor, InputEvent, KeyCode};
use crossbeam_channel::select;
use std::time::Duration;
let engine = Engine::new()?;
let ticker = TickerActor::spawn(Duration::from_micros(16_666)); // 60 FPS
while engine.is_running() {
select! {
recv(engine.input_receiver()) -> result => {
if let Ok(event) = result {
match event {
InputEvent::Key { code: KeyCode::Esc, .. } => engine.stop(),
_ => handle_input(event),
}
}
}
recv(ticker.receiver()) -> _ => {
// Tick: generate content, update animations
generate_content(&mut stream);
stream.render(engine.buffer_mut());
engine.request_update();
}
}
}
ticker.join();
```
### Legacy Event Loop
For simpler applications without the ticker:
```rust
use crossbeam_channel::RecvTimeoutError;
use std::time::Duration;
let target_fps = Duration::from_micros(16_666); // 60 FPS
let mut last_tick = Instant::now();
while engine.is_running() {
let timeout = target_fps.saturating_sub(last_tick.elapsed());
match engine.input_receiver().recv_timeout(timeout) {
Ok(event) => {
// Handle input IMMEDIATELY
handle_input(event);
redraw_ui(&mut engine);
engine.request_update();
}
Err(RecvTimeoutError::Timeout) => {
// Tick: generate content, update animations
last_tick = Instant::now();
generate_content(&mut stream);
stream.render(engine.buffer_mut());
engine.request_update();
}
Err(_) => break,
}
}
```
### C FFI Usage
```c
#include "flywheel.h"
int main() {
FlywheelEngine* engine = flywheel_engine_new();
FlywheelStream* stream = flywheel_stream_new(0, 0, 80, 24);
flywheel_stream_set_fg(stream, 0, 255, 128);
flywheel_stream_append(stream, "Hello from C!");
flywheel_stream_render(stream, flywheel_engine_buffer(engine));
flywheel_engine_request_update(engine);
// Event loop...
flywheel_stream_destroy(stream);
flywheel_engine_destroy(engine);
return 0;
}
```
---
## Performance
Benchmarked on Apple Silicon (criterion, release build):
### Cell Operations
| 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)
| 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)
| 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
| 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
```bash
cargo bench --bench cell_benchmark
cargo bench --bench diff_benchmark
cargo bench --bench rope_benchmark
cargo bench --bench comparison_benchmark # Flywheel vs Ratatui
```
### Flywheel vs Ratatui (Head-to-Head)
| **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
| 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
- [x] Buffer synchronization fix (ghost character elimination)
- [x] Async-friendly TickerActor
- [x] RopeBuffer for 1M+ line documents
- [x] Widget system (TextInput, StatusBar, ProgressBar)
- [x] 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
---
<p align="center">
<sub>Built with ❤️ for the AI-native CLI era.</sub>
</p>