{"id":43109030,"url":"https://github.com/ccheshirecat/flywheel","last_synced_at":"2026-02-04T22:01:15.609Z","repository":{"id":335422435,"uuid":"1145323771","full_name":"ccheshirecat/flywheel","owner":"ccheshirecat","description":"A high-performance, Rust-native rendering engine purpose-built for streaming LLM outputs at 60+ FPS without tearing, flickering, or input lag.","archived":false,"fork":false,"pushed_at":"2026-01-30T09:31:33.000Z","size":190,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-03T07:48:04.537Z","etag":null,"topics":["agent","ai","claude","cli","compositor","rendering","terminal","tui"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ccheshirecat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-29T17:20:21.000Z","updated_at":"2026-02-02T20:11:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ccheshirecat/flywheel","commit_stats":null,"previous_names":["ccheshirecat/flywheel"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ccheshirecat/flywheel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccheshirecat%2Fflywheel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccheshirecat%2Fflywheel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccheshirecat%2Fflywheel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccheshirecat%2Fflywheel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ccheshirecat","download_url":"https://codeload.github.com/ccheshirecat/flywheel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ccheshirecat%2Fflywheel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29057116,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-03T20:13:53.544Z","status":"ssl_error","status_checked_at":"2026-02-03T20:13:40.507Z","response_time":96,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","ai","claude","cli","compositor","rendering","terminal","tui"],"created_at":"2026-01-31T18:17:55.542Z","updated_at":"2026-02-04T22:01:15.587Z","avatar_url":"https://github.com/ccheshirecat.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ch1 align=\"center\"\u003eFlywheel\u003c/h1\u003e\n  \u003cp align=\"center\"\u003e\n    \u003cstrong\u003eThe Zero-Flicker Terminal Compositor for Agentic CLIs\u003c/strong\u003e\n  \u003c/p\u003e\n  \u003cp align=\"center\"\u003e\n    A high-performance, Rust-native rendering engine purpose-built for streaming LLM outputs at 60+ FPS without tearing, flickering, or input lag.\n  \u003c/p\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#quickstart\"\u003eQuickstart\u003c/a\u003e •\n  \u003ca href=\"#features\"\u003eFeatures\u003c/a\u003e •\n  \u003ca href=\"#architecture\"\u003eArchitecture\u003c/a\u003e •\n  \u003ca href=\"#api-reference\"\u003eAPI\u003c/a\u003e •\n  \u003ca href=\"#examples\"\u003eExamples\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n## The Problem\n\nBuilding 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:\n\n| Issue | Symptom |\n|-------|---------|\n| **Flickering** | `clear()` + `redraw()` on every character creates strobing artifacts. |\n| **Blocking** | Render calls starve the input handler, making `Ctrl+C` unresponsive. |\n| **Inefficiency** | Diffing the entire 80x24 grid for 1 new character is O(n²) waste. |\n| **State Desync** | Direct `stdout` writes conflict with the framework's internal cursor tracking. |\n\n**Flywheel was designed from the ground up to solve this.**\n\n---\n\n## Features\n\n| Feature | Description |\n|---------|-------------|\n| 🚀 **Zero-Flicker Rendering** | Double-buffered diffing outputs only the *delta* between frames. No screen clears. |\n| ⚡ **Sub-Millisecond Input Latency** | Actor model decouples input polling from rendering. `Ctrl+C` always works. |\n| 🎯 **Fast Path Optimization** | For simple character appends, bypass the buffer entirely—emit ANSI codes directly. |\n| 📜 **Infinite Scrollback** | `StreamWidget` stores 100k+ lines efficiently with \"sticky scroll\" UX. |\n| 🎨 **True Color (24-bit RGB)** | Full RGB attribute support for syntax highlighting and theming. |\n| 🦀 **Safe Rust Core** | Core library is `#![forbid(unsafe_code)]`. FFI module uses `unsafe` as required by C ABI. |\n| 🔌 **C FFI** | Stable `extern \"C\"` interface for Python, Node.js, Go, and C/C++ bindings. |\n\n---\n\n## Quickstart\n\n### Installation\n\n```toml\n[dependencies]\nflywheel-compositor = \"0.1\"\n```\n\n### Minimal Example\n\n```rust\nuse flywheel::{Engine, StreamWidget, Rect, Rgb};\n\nfn main() -\u003e std::io::Result\u003c()\u003e {\n    let mut engine = Engine::new()?;\n    let mut stream = StreamWidget::new(Rect::new(0, 0, engine.width(), engine.height()));\n\n    // Simulate LLM streaming\n    for token in [\"Hello, \", \"world! \", \"This \", \"is \", \"Flywheel.\"] {\n        stream.set_fg(Rgb::new(0, 255, 128)); // Green text\n        \n        // Just push. The engine handles Fast/Slow path automatically.\n        stream.push(\u0026engine, token);\n        \n        std::thread::sleep(std::time::Duration::from_millis(100));\n    }\n\n    // Event loop\n    while engine.is_running() {\n        for event in engine.poll_input() {\n            match event {\n                flywheel::InputEvent::Key { code: flywheel::KeyCode::Esc, .. } =\u003e engine.stop(),\n                _ =\u003e {}\n            }\n        }\n    }\n\n    Ok(())\n}\n```\n\n### Run the Demo\n\n```bash\ncargo run --example streaming_demo --release\n```\n\nThis showcases:\n- 100% GPU-free flicker elimination at 60 FPS\n- 3000+ characters/second matrix generation\n- Real-time input handling with cursor blinking\n- Live CPU/Memory usage display\n\n---\n\n## Architecture\n\nFlywheel implements a **3-Actor Pipeline**:\n\n```\n┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐\n│   Input Actor   │────▶│  Main Thread    │────▶│ Renderer Actor  │\n│  (crossterm)    │     │  (Your Code)    │     │    (stdout)     │\n└─────────────────┘     └─────────────────┘     └─────────────────┘\n        │                       │                       │\n    Keyboard            Buffer Updates           ANSI Sequences\n    Mouse Events        Widget Logic             Diff Output\n    Resize              State Management         Cursor Control\n```\n\n### Core Axioms\n\n| Axiom | Principle |\n|-------|-----------|\n| **A: Double Buffering** | `Next` buffer holds pending changes. `Current` buffer holds what's on screen. Diffing produces minimal escape sequences. |\n| **B: Append-Optimized** | `StreamWidget::append()` returns `FastPath` or `SlowPath`. Fast path bypasses diffing for O(1) writes. |\n| **C: Thread Isolation** | Only `Renderer Actor` touches `stdout`. Zero contention. Zero deadlocks. |\n| **D: Event-Driven** | Main loop uses `recv_timeout()` for input events. No polling. No sleeping. Sub-ms latency. |\n\n### Fast Path vs Slow Path\n\n```rust\nlet result = stream.append(\"x\");\n\nmatch result {\n    AppendResult::FastPath { row, start_col, .. } =\u003e {\n        // Character appended within viewport, no wrapping.\n        // Emit: MoveTo(row, col) + SetColor + PrintChar\n        // Cost: ~20 bytes to stdout\n    }\n    AppendResult::SlowPath =\u003e {\n        // Wrapping or scrolling required.\n        // Full frame rendered via diffing engine.\n        // Cost: ~200 bytes to stdout (only changed cells)\n    }\n}\n```\n\nThe `append_fast_into()` helper encapsulates this:\n\n```rust\nlet mut raw_output = Vec::new();\nstream.append_fast_into(\"x\", \u0026mut raw_output);\nengine.write_raw(raw_output); // Sends RawOutput command to Renderer\n```\n\n---\n\n## API Reference\n\n### `Engine`\n\nThe central coordinator. Manages terminal lifecycle and actor threads.\n\n```rust\n// Initialization\nlet mut engine = Engine::new()?;                    // Default config\nlet mut engine = Engine::with_config(config)?;     // Custom FPS, mouse, etc.\n\n// Dimensions\nengine.width();   // Terminal columns\nengine.height();  // Terminal rows\n\n// Event Loop\nengine.is_running();                                // Check if still alive\nengine.poll_input();                                // Non-blocking: Vec\u003cInputEvent\u003e\nengine.input_receiver().recv_timeout(duration);     // Blocking: for event-driven loops\n\n// Rendering\nengine.buffer_mut();        // Get mutable reference to the Next buffer\nengine.request_update();    // Send buffer to Renderer (diff-based)\nengine.request_redraw();    // Send buffer to Renderer (full redraw)\nengine.write_raw(bytes);    // Bypass buffer, write ANSI directly (Fast Path)\n\n// Lifecycle\nengine.stop();              // Signal shutdown\n```\n\n### `StreamWidget`\n\nA scrolling text viewport optimized for streaming content.\n\n```rust\nlet mut stream = StreamWidget::new(Rect::new(x, y, width, height));\n\n// Styling\nstream.set_fg(Rgb::new(255, 128, 0));  // Orange text\nstream.set_bg(Rgb::new(20, 20, 20));   // Dark background\nstream.set_bold(true);\n\n// Content (Recommended API)\nstream.push(\u0026engine, \"Hello\");          // Automatic Fast/Slow path handling\nstream.newline();\nstream.clear();\n\n// Low-level API (for advanced use cases)\nstream.append(\"text\");                  // Returns AppendResult, manual handling\nstream.append_fast_into(\"x\", \u0026mut buf); // Manual Fast Path with raw output\n\n// Scrolling (Sticky Scroll: auto-scroll only if at bottom)\nstream.scroll_up(lines);\nstream.scroll_down(lines);\n\n// Rendering\nstream.render(\u0026mut buffer);             // Write to Buffer\nstream.needs_redraw();                  // Check if dirty\n```\n\n### `Buffer`\n\nLow-level grid of cells representing the terminal screen.\n\n```rust\nlet mut buffer = Buffer::new(80, 24);\n\nbuffer.set(x, y, Cell::new('A').with_fg(Rgb::RED));\nbuffer.get(x, y);                      // Option\u003c\u0026Cell\u003e\nbuffer.draw_text(x, y, \"text\", fg, bg);\nbuffer.fill_rect(x, y, w, h, cell);\nbuffer.clear();\n```\n\n### `InputEvent`\n\nEvents received from the terminal.\n\n```rust\nmatch event {\n    InputEvent::Key { code, modifiers } =\u003e { /* KeyCode::Char, Esc, Enter, etc. */ }\n    InputEvent::MouseClick { x, y, button } =\u003e { /* Left, Right, Middle */ }\n    InputEvent::MouseScroll { x, y, delta } =\u003e { /* +1 up, -1 down */ }\n    InputEvent::Resize { width, height } =\u003e { /* Terminal resized */ }\n    InputEvent::Shutdown =\u003e { /* SIGTERM or similar */ }\n    _ =\u003e {}\n}\n```\n\n---\n\n## V2 Widgets\n\nFlywheel V2 introduces a proper widget system with composable UI components.\n\n### `TextInput`\n\nSingle-line text input with cursor, editing, and navigation:\n\n```rust\nuse flywheel::{TextInput, Widget, Rect};\n\nlet mut input = TextInput::new(Rect::new(0, 23, 80, 1));\n\n// Configure\ninput.set_content(\"Initial text\");\ninput.set_focused(true);\n\n// Handle input events\nif input.handle_input(\u0026event) {\n    // Event was consumed by the widget\n}\n\n// Render\ninput.render(buffer);\n\n// Get content\nlet text = input.content();\n```\n\n### `StatusBar`\n\nThree-section status bar (left, center, right):\n\n```rust\nuse flywheel::{StatusBar, Widget, Rect};\n\nlet mut status = StatusBar::new(Rect::new(0, 0, 80, 1));\nstatus.set_all(\"Flywheel\", \"v2.0\", \"60 FPS\");\n\n// Or set individually\nstatus.set_left(\"App Name\");\nstatus.set_center(\"Status\");\nstatus.set_right(\"12:34\");\n\nstatus.render(buffer);\n```\n\n### `ProgressBar`\n\nAnimated horizontal progress indicator:\n\n```rust\nuse flywheel::{ProgressBar, Widget, Rect, ProgressStyle};\n\nlet mut progress = ProgressBar::new(Rect::new(0, 5, 60, 1));\nprogress.set_progress(0.5);  // 50%\nprogress.set_label(\"Loading\");\nprogress.increment(0.1);     // +10%\n\nprogress.render(buffer);\n```\n\n### Widget Trait\n\nAll widgets implement the `Widget` trait:\n\n```rust\npub trait Widget {\n    fn bounds(\u0026self) -\u003e Rect;\n    fn set_bounds(\u0026mut self, bounds: Rect);\n    fn render(\u0026self, buffer: \u0026mut Buffer);\n    fn handle_input(\u0026mut self, event: \u0026InputEvent) -\u003e bool;\n    fn needs_redraw(\u0026self) -\u003e bool;\n    fn clear_redraw(\u0026mut self);\n}\n```\n\n---\n\n## Examples\n\n### Event-Driven Loop with TickerActor (Recommended)\n\nUse the V2 `TickerActor` for non-blocking frame pacing:\n\n```rust\nuse flywheel::{Engine, TickerActor, InputEvent, KeyCode};\nuse crossbeam_channel::select;\nuse std::time::Duration;\n\nlet engine = Engine::new()?;\nlet ticker = TickerActor::spawn(Duration::from_micros(16_666)); // 60 FPS\n\nwhile engine.is_running() {\n    select! {\n        recv(engine.input_receiver()) -\u003e result =\u003e {\n            if let Ok(event) = result {\n                match event {\n                    InputEvent::Key { code: KeyCode::Esc, .. } =\u003e engine.stop(),\n                    _ =\u003e handle_input(event),\n                }\n            }\n        }\n        recv(ticker.receiver()) -\u003e _ =\u003e {\n            // Tick: generate content, update animations\n            generate_content(\u0026mut stream);\n            stream.render(engine.buffer_mut());\n            engine.request_update();\n        }\n    }\n}\n\nticker.join();\n```\n\n### Legacy Event Loop\n\nFor simpler applications without the ticker:\n\n```rust\nuse crossbeam_channel::RecvTimeoutError;\nuse std::time::Duration;\n\nlet target_fps = Duration::from_micros(16_666); // 60 FPS\nlet mut last_tick = Instant::now();\n\nwhile engine.is_running() {\n    let timeout = target_fps.saturating_sub(last_tick.elapsed());\n    \n    match engine.input_receiver().recv_timeout(timeout) {\n        Ok(event) =\u003e {\n            // Handle input IMMEDIATELY\n            handle_input(event);\n            redraw_ui(\u0026mut engine);\n            engine.request_update();\n        }\n        Err(RecvTimeoutError::Timeout) =\u003e {\n            // Tick: generate content, update animations\n            last_tick = Instant::now();\n            generate_content(\u0026mut stream);\n            stream.render(engine.buffer_mut());\n            engine.request_update();\n        }\n        Err(_) =\u003e break,\n    }\n}\n```\n\n### C FFI Usage\n\n```c\n#include \"flywheel.h\"\n\nint main() {\n    FlywheelEngine* engine = flywheel_engine_new();\n    FlywheelStream* stream = flywheel_stream_new(0, 0, 80, 24);\n\n    flywheel_stream_set_fg(stream, 0, 255, 128);\n    flywheel_stream_append(stream, \"Hello from C!\");\n    flywheel_stream_render(stream, flywheel_engine_buffer(engine));\n    flywheel_engine_request_update(engine);\n\n    // Event loop...\n\n    flywheel_stream_destroy(stream);\n    flywheel_engine_destroy(engine);\n    return 0;\n}\n```\n\n---\n\n## Performance\n\nBenchmarked on Apple Silicon (criterion, release build):\n\n### Cell Operations\n\n| Operation | Time |\n|-----------|------|\n| Cell equality (same) | 2.09 ns |\n| Cell equality (diff grapheme) | 650 ps |\n| Cell equality (diff color) | 921 ps |\n| Cell from ASCII char | 1.73 ns |\n| Cell from CJK char | 2.56 ns |\n\n### Buffer Diffing (200×50 = 10,000 cells)\n\n| Scenario | Time | Notes |\n|----------|------|-------|\n| Identical buffers | 33.3 µs | No-op diff |\n| Single cell change | 33.6 µs | Minimal output |\n| Line change (200 cells) | 33.7 µs | Optimized cursor moves |\n| Full change (10K cells) | 289 µs | ~2.9M cells/second |\n| Full render | 318 µs | No diffing |\n\n### RopeBuffer (Chunked Storage)\n\n| Operation | Time | Notes |\n|-----------|------|-------|\n| Append single char | 2.69 ns | O(1) amortized |\n| Newline | 9.29 ns | Creates new line |\n| Append 80 cells | 178 ns | Full line |\n| Push complete line | 93 ns | Pre-built line |\n| Get line (50K lines) | 537 ps | O(1) chunk lookup |\n| Visible lines iterator | 194 ns | 50 lines |\n| Push 100K lines | 404 µs | ~247M lines/second |\n\n### Scaling\n\n| Buffer Size | Diff Time (full change) |\n|-------------|-------------------------|\n| 80×24 (1,920 cells) | 55 µs |\n| 120×40 (4,800 cells) | 140 µs |\n| 200×50 (10,000 cells) | 291 µs |\n| 300×80 (24,000 cells) | 693 µs |\n\n### Run Benchmarks\n\n```bash\ncargo bench --bench cell_benchmark\ncargo bench --bench diff_benchmark\ncargo bench --bench rope_benchmark\ncargo bench --bench comparison_benchmark  # Flywheel vs Ratatui\n```\n\n### Flywheel vs Ratatui (Head-to-Head)\n\n| Operation | Flywheel | Ratatui | Speedup |\n|-----------|----------|---------|---------|\n| **Buffer Creation (80×24)** | 546 ns | 2.02 µs | **3.7×** |\n| **Buffer Creation (200×50)** | 3.17 µs | 9.96 µs | **3.1×** |\n| **Cell Write** | 1.32 ns | 1.53 ns | **1.2×** |\n| **Buffer Fill (80×24)** | 796 ns | 3.20 µs | **4.0×** |\n| **Buffer Fill (200×50)** | 4.17 µs | 16.1 µs | **3.9×** |\n| **Buffer Diff (80×24)** | 8.05 µs | 21.6 µs | **2.7×** |\n| **Buffer Diff (200×50)** | 39.2 µs | 109 µs | **2.8×** |\n| **Cell Clone/Copy** | 1.77 ns | 2.03 ns | **1.1×** |\n| **Text Render (47 chars)** | 91.1 ns | 137 ns | **1.5×** |\n\n---\n\n## Comparison\n\n| Feature | Flywheel | ratatui | crossterm (raw) |\n|---------|----------|---------|-----------------|\n| Zero-flicker streaming | ✅ | ❌ | ❌ |\n| Non-blocking input | ✅ | ❌ | ✅ |\n| Fast Path optimization | ✅ | ❌ | N/A |\n| Sticky scroll | ✅ | ❌ | N/A |\n| Actor-based rendering | ✅ | ❌ | ❌ |\n| Widget system | ✅ | ✅ | ❌ |\n| RopeBuffer (1M+ lines) | ✅ | ❌ | N/A |\n| C FFI | ✅ | ❌ | ❌ |\n\n---\n\n## Roadmap\n\n### V2.0 ✅ Complete\n- [x] Buffer synchronization fix (ghost character elimination)\n- [x] Async-friendly TickerActor\n- [x] RopeBuffer for 1M+ line documents\n- [x] Widget system (TextInput, StatusBar, ProgressBar)\n- [x] Comprehensive documentation and benchmarks\n\n### Future\n- [ ] **V2.1**: Layout containers (VSplit, HSplit, Stack)\n- [ ] **V2.2**: Focus management system\n- [ ] **V3.0**: WASM target for browser terminals\n- [ ] **V3.1**: Plugin system for custom widgets\n\n---\n\n## License\n\nMIT\n\n---\n\n\u003cp align=\"center\"\u003e\n  \u003csub\u003eBuilt with ❤️ for the AI-native CLI era.\u003c/sub\u003e\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fccheshirecat%2Fflywheel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fccheshirecat%2Fflywheel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fccheshirecat%2Fflywheel/lists"}