An open API service indexing awesome lists of open source software.

https://github.com/ccheshirecat/flywheel

A high-performance, Rust-native rendering engine purpose-built for streaming LLM outputs at 60+ FPS without tearing, flickering, or input lag.
https://github.com/ccheshirecat/flywheel

agent ai claude cli compositor rendering terminal tui

Last synced: 5 months ago
JSON representation

A high-performance, Rust-native rendering engine purpose-built for streaming LLM outputs at 60+ FPS without tearing, flickering, or input lag.

Awesome Lists containing this project

README

          


Flywheel



The Zero-Flicker Terminal Compositor for Agentic CLIs



A high-performance, Rust-native rendering engine purpose-built for streaming LLM outputs at 60+ FPS without tearing, flickering, or input lag.


Quickstart
Features
Architecture
API
Examples

---

## 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

```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

| 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

```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
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

| 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

```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)

| 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
- [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

---


Built with ❤️ for the AI-native CLI era.