flywheel-compositor 0.1.2

A zero-flicker terminal compositor for Agentic CLIs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
<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:

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

| 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

---

<p align="center">
  <sub>Built with ❤️ for the AI-native CLI era.</sub>
</p>