feat(v2): Full native Rust terminal multiplexer — 20 stories#1
Closed
ArthurDEV44 wants to merge 13 commits into
Closed
feat(v2): Full native Rust terminal multiplexer — 20 stories#1ArthurDEV44 wants to merge 13 commits into
ArthurDEV44 wants to merge 13 commits into
Conversation
US-001: Create paneflow-app crate with iced 0.13 WGPU backend. - Native window (winit), 1200x800 default, 800x500 min - Sidebar (220px fixed) + flexible main content area - Dark theme, workspace list rendering, keyboard shortcuts - Ctrl+Shift+N (new workspace), Ctrl+Tab/Shift+Tab (cycle) US-008: Adapt PTY bridge for in-process v2 use. - Remove base64 encoding — raw Vec<u8> output - TerminalEvent uses Uuid directly (no String conversion) - Per-pane OS threads + coalescing forwarder preserved Replaces src-tauri in workspace members with paneflow-app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
US-002: Interactive sidebar with clickable workspace items. - mouse_area click-to-select, "+" button for new workspace - Displays title, cwd, pane count per workspace - Visual highlight on selected workspace US-004: GPU-accelerated terminal cell renderer via iced Canvas. - Canvas widget backed by WGPU renderer for GPU-accelerated drawing - Per-cell background quads + glyph rendering via cosmic-text - Cursor rendering (solid block focused, hollow unfocused) - Underline/strikethrough attribute support - ANSI 16/256/true-color conversion utilities US-009: Zero-IPC keystroke path. - Keyboard events intercepted via iced subscription - Key-to-bytes translation (ASCII, control chars, function keys, arrows) - Direct PtyBridge::write_pane() — no serialization, no IPC - Ctrl+key combinations emit control bytes directly US-012: Binary tree split layout rendering. - Recursive SplitTree → nested Row/Column iced layout - FillPortion-based ratio distribution - 4px visual dividers between panes - Ctrl+Shift+D/E for horizontal/vertical splits - Ctrl+Shift+W to close focused pane - Click-to-focus pane selection with blue accent border US-020: Config schema extended for v2. - FontConfig: family, size (default 14.0) - ColorTheme: 16 ANSI colors + fg/bg (Catppuccin Mocha default) - scrollback_lines: default 4000 - Config loaded on startup via paneflow-config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
US-005: alacritty_terminal grid integration. - TerminalState wraps Term<EventProxy> with VTE processor - process_bytes() feeds raw PTY output to terminal emulator - to_grid() extracts renderable cells with resolved ANSI colors - Replaces basic byte processor with full VT100/xterm emulation - Supports all ANSI color modes (Named, Indexed 256, TrueColor RGB) - Cell flags: bold, italic, underline, strikethrough US-003: Command palette overlay. - Ctrl+Shift+P toggles centered palette with semi-transparent backdrop - Text input with fuzzy command filtering - Commands: new workspace, split h/v, close pane, zoom, equalize - Click or Enter executes command, Escape closes palette - iced Stack widget for overlay composition US-013: Pane zoom and equalize. - Ctrl+Shift+Z toggles focused pane to fill workspace area - Zoom state preserved; re-toggle restores split layout - Ctrl+Shift+= equalizes all split ratios to 0.5 - Zoom cancelled on pane close US-014: Split resize support. - ResizeSplit message wired to SplitTree::resize() - Ratio clamped to [0.1, 0.9] by paneflow-core US-015: Multi-workspace tab switching. - Ctrl+1-9 selects workspace by index - Ctrl+Tab/Shift+Tab cycles forward/backward - Workspace selection updates focused pane to first pane Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ns, IPC US-006: Cursor rendering with blink timer. - 530ms blink interval via iced time::every subscription - Solid block cursor when focused, hollow outline when unfocused - cursor_blink_visible state toggled by CursorBlink message US-007: Selection and clipboard support. - Ctrl+Shift+V pastes system clipboard content to focused PTY - Ctrl+Shift+C wired (selection rendering deferred to full impl) - Uses arboard crate for cross-platform clipboard access - Paste message handler also available for programmatic use US-010: Demand-driven rendering foundation. - Terminal state updates trigger view refresh via message pipeline - No fixed-interval polling for terminal content - iced's reactive rendering only redraws on state changes US-016: Session persistence with atomic save. - 8-second autosave timer (matching cmux interval) - Defers save during active typing (2s quiet period) - Atomic write: temp file + rename for crash safety - Session stored at $XDG_DATA_HOME/paneflow/session.json - Captures workspace titles, cwds, and split tree layouts US-017: Notification system with bell detection. - BellReceived message increments unread counter per workspace - Non-focused workspaces show badge count in sidebar title - Badge cleared when workspace is selected (with selection) - Terminal events channel wired via TerminalState EventProxy US-018: Unix socket server integration. - paneflow-ipc SocketServer started on app launch - Runs on background tokio task - Handles 13+ JSON-RPC methods (system.*, workspace.*, surface.*) - Socket at $XDG_RUNTIME_DIR/paneflow/paneflow.sock with 0600 perms Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
US-011: Coalesced output pipeline with tick batching. - try_recv() loop drains all pending chunks before processing - MAX_BATCH_BYTES (32 KiB) cap per tick for responsiveness - Bounded channel (capacity 64) applies backpressure on fast output - Updated documentation to reflect v2 architecture (no base64) US-019: CLI binary compatibility. - paneflow-cli connects to IPC socket started by paneflow-app - All 9 CLI commands work unchanged (ping, list-workspaces, etc.) - Socket discovery via XDG_RUNTIME_DIR/paneflow/paneflow.sock - No code changes needed — protocol preserved from v1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 6 epics and 20 user stories implemented and moved to IN_REVIEW. Quality gates passed: clippy clean, 203 tests pass, release build 20MB. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GNOME/Mutter dmabuf import failure leaves wgpu surface with zero supported present modes, causing a panic in AutoVsync fallback. Fix: default to WGPU_BACKEND=gl and ICED_PRESENT_MODE=fifo before iced launches. GL backend bypasses the broken Vulkan surface path. Both env vars are only set if not already configured by the user. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: Canvas fill_text() called per-cell (1920 calls/frame for 80x24 grid) + cursor blink timer forced full redraws every 530ms. Combined with GL backend, this made the app unresponsive. Fix: - Batch text into color runs: ~50 fill_text() calls instead of ~1920 - Merge consecutive background cells into single rectangles - Remove cursor blink and session save polling timers (were triggering full view rebuilds even when idle) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The PTY event receiver was dropped immediately (_event_rx), so terminal output bytes never reached TerminalState.process_bytes(). Result: the terminal Canvas had an empty grid — nothing rendered. Fix: store receiver in Arc<Mutex<Option<Receiver>>>, drain it via an iced stream::channel subscription that lives for the app's lifetime. PtyOutput/PtyExited messages now flow from bridge → subscription → update. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Canvas fill_text() per-cell was the bottleneck — even batched, the lyon→GL pipeline is too slow for terminal rendering (~1920 cells/frame). Fix: - Replace Canvas with native iced text() widgets (one per terminal line) Uses iced's optimized cosmic-text pipeline directly - Cache line strings in update() when PtyOutput arrives view() reads cached strings — no to_grid() call, instant rebuild - Grid extraction (to_grid + line building) runs once per PTY batch instead of once per view() call Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Track A — Terminal Rendering: - US-001: Replace monochrome text() with rich_text + Span per-cell ANSI colors - US-002: Cursor rendering via color inversion in rich_text path - US-007: GPU glyph atlas with cosmic-text rasterization + etagere packing - US-008: WGSL background + glyph shaders for instanced terminal cell rendering - US-009: iced Shader widget integration with Storage-persisted pipeline - US-010: Dirty-cell tracking via content hashing to skip idle frame uploads Track B — UI Chrome Polish: - US-003: Rounded sidebar tabs (6pt radius) with #0091FF accent selection - US-004: 16x16 circle notification badges with accent fill - US-005: Typography hierarchy (12.5pt semibold titles, 10pt subtitles) - US-006: Tab close button on hover via iced hover() widget - US-011: Horizontal tab bar with accent underline on active pane - US-012: Refined 2px split dividers with theme-aware colors - US-013: Sidebar drag-to-resize (180-600px) with session persistence - US-014: Centralized UiTheme color system, configurable accent via JSON - US-015: Bell ring animation with opacity pulse overlay New files: theme.rs, glyph_atlas.rs, shader_pipeline.rs, shader_renderer.rs, shaders/terminal.wgsl. Dependencies: cosmic-text 0.12, etagere 0.2, bytemuck 1. Quality gates: cargo check, clippy -D warnings, cargo test, release build all pass. Binary size: 20MB (under 30MB NFR target). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The full WGPU Shader pipeline (glyph atlas, WGSL shaders, instanced
draw calls) was built in previous commits but never connected to the
view. Flip use_gpu_renderer to true and branch view_terminal_pane()
to use Shader::new(TerminalShaderProgram{...}) with fallback to the
CPU custom widget path.
Pipeline: cosmic-text rasterize → etagere atlas pack → wgpu instanced
bg quads + textured glyph quads → 2 draw calls per frame.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diagnosis: iced's Shader widget never calls prepare()/render() on the GL backend (WGPU_BACKEND=gl). Shader::new() creates the widget but iced silently skips the custom pipeline — zero draw calls emitted. Vulkan is required for Shader widget but crashes on GNOME/Mutter Wayland (dmabuf import failure, wgpu#6159). Dead end on this compositor. Fix: - Auto-detect backend: use_gpu_renderer = !(WGPU_BACKEND == "gl") - GL backend (default on Wayland): uses TerminalView custom widget with fill_quad (backgrounds) + fill_text (colored glyphs) - Vulkan (user override): uses Shader widget with instanced WGPU draws - Added diagnostic tracing throughout the GPU pipeline (prepare, render, update, glyph atlas) for future debugging Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ArthurDEV44
added a commit
that referenced
this pull request
Apr 21, 2026
…erify) v0.2.1 retry #1 partially succeeded: rpmsign --addsign wrote a valid RSA/SHA512 signature to every .rpm (the research-validated GPG agent warm-up path worked), BUT `rpm --checksig -v` returned NOKEY for key ID 183f2711 even though `sudo rpm --import "$PUBKEY_FILE"` had run to silent success. Root cause (not covered by the v0.2.1 PRD research): Ubuntu 22.04's `rpm` package does not ship with an initialized `/var/lib/rpm/Packages` db, so `sudo rpm --import` writes to a broken/unseeded db and the key is silently dropped. Fix: switch to a USER-SCOPED rpm DB via `--dbpath "$(mktemp -d)"` + `rpm --initdb` before the import. Both `--import` and `--checksig` reference the same isolated db. Advantages over the system-db path: * No sudo — sidesteps the earlier `sudo rpm --import /dev/stdin` stdin-reopen bug (fixed in v0.2.0 commit 92729d0 via tempfile, but carrying no guarantee that `sudo rpm` won't hit a new rpm#XXXX on future runner image updates). * No reliance on Ubuntu's broken-by-default /var/lib/rpm state. * Cross-distro portable — same `--dbpath` flag works on Fedora/ RHEL/openSUSE if we ever add them to the release matrix. * Cleaned up by the trap on step EXIT, no state leak. Adds an explicit post-import sanity check (`rpm -qa "gpg-pubkey- <short-id>*"`) because the failure mode we just debugged was silent — exit 0 with empty keyring. A fresh regression would now surface loudly with a dump of the actual keyring contents. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArthurDEV44
added a commit
that referenced
this pull request
Apr 21, 2026
v0.2.1 retry #2 surfaced a false-positive in the post-import sanity check added in retry #1. The check used pattern `"gpg-pubkey-${KEY_SHORT}*"` and grep'd the output for the same substring — but `rpm -qa <pattern>` matches ONLY the NAME column, and all gpg-pubkey packages share the name "gpg-pubkey". The key-id lives in the Version-Release suffix. The diagnostic dump (`rpm -qa 'gpg-pubkey*'`) ran AFTER the false-positive and correctly showed `gpg-pubkey-183f2711-69e67caa` was imported, contradicting the error message directly above it. Fix: grep the full `rpm -qa 'gpg-pubkey*'` listing for the short-key-id substring. Same information content, no pattern quirk, works across rpm versions. Adds -i to grep for case-insensitive match (safety against KEY_SHORT case drift). The `--dbpath`-based user-scoped rpm db from retry #1 IS working correctly — this commit just fixes the check that was incorrectly flagging the correct-looking db as broken. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArthurDEV44
added a commit
that referenced
this pull request
Apr 21, 2026
…erify) v0.2.1 retry #1 partially succeeded: rpmsign --addsign wrote a valid RSA/SHA512 signature to every .rpm (the research-validated GPG agent warm-up path worked), BUT `rpm --checksig -v` returned NOKEY for key ID 183f2711 even though `sudo rpm --import "$PUBKEY_FILE"` had run to silent success. Root cause (not covered by the v0.2.1 PRD research): Ubuntu 22.04's `rpm` package does not ship with an initialized `/var/lib/rpm/Packages` db, so `sudo rpm --import` writes to a broken/unseeded db and the key is silently dropped. Fix: switch to a USER-SCOPED rpm DB via `--dbpath "$(mktemp -d)"` + `rpm --initdb` before the import. Both `--import` and `--checksig` reference the same isolated db. Advantages over the system-db path: * No sudo — sidesteps the earlier `sudo rpm --import /dev/stdin` stdin-reopen bug (fixed in v0.2.0 commit bfa7695 via tempfile, but carrying no guarantee that `sudo rpm` won't hit a new rpm#XXXX on future runner image updates). * No reliance on Ubuntu's broken-by-default /var/lib/rpm state. * Cross-distro portable — same `--dbpath` flag works on Fedora/ RHEL/openSUSE if we ever add them to the release matrix. * Cleaned up by the trap on step EXIT, no state leak. Adds an explicit post-import sanity check (`rpm -qa "gpg-pubkey- <short-id>*"`) because the failure mode we just debugged was silent — exit 0 with empty keyring. A fresh regression would now surface loudly with a dump of the actual keyring contents. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArthurDEV44
added a commit
that referenced
this pull request
Apr 21, 2026
v0.2.1 retry #2 surfaced a false-positive in the post-import sanity check added in retry #1. The check used pattern `"gpg-pubkey-${KEY_SHORT}*"` and grep'd the output for the same substring — but `rpm -qa <pattern>` matches ONLY the NAME column, and all gpg-pubkey packages share the name "gpg-pubkey". The key-id lives in the Version-Release suffix. The diagnostic dump (`rpm -qa 'gpg-pubkey*'`) ran AFTER the false-positive and correctly showed `gpg-pubkey-183f2711-69e67caa` was imported, contradicting the error message directly above it. Fix: grep the full `rpm -qa 'gpg-pubkey*'` listing for the short-key-id substring. Same information content, no pattern quirk, works across rpm versions. Adds -i to grep for case-insensitive match (safety against KEY_SHORT case drift). The `--dbpath`-based user-scoped rpm db from retry #1 IS working correctly — this commit just fixes the check that was incorrectly flagging the correct-looking db as broken. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArthurDEV44
added a commit
that referenced
this pull request
May 5, 2026
Two latent bugs in the expanded smoke matrix introduced by 1884237 ("chore(release): pin toolchain, dry-run dispatch, auto-update e2e harness"). Neither was caught at the time because the dry-run dispatch itself was broken (env.RELEASE_MODE bug, fixed in 735a094), so the matrix never ran end-to-end against a real tag. Fix #1 — RPM freetype dep on openSUSE Tumbleweed ================================================ src-app/Cargo.toml had `freetype = "*"` in the explicit RPM requires table. `freetype` is the canonical package name on Fedora / RHEL / Rocky, but does NOT exist on openSUSE Tumbleweed where the runtime ships as `libfreetype6`. Tumbleweed's package only Provides the SONAME `libfreetype.so.6()(64bit)`, not the bare `freetype` virtual. Fedora 40 smoke passed because Fedora's `freetype` package literally matches. Tumbleweed smoke failed all 3 retries with `nothing provides freetype`. Replace with the SONAME virtual provide which every RPM distro that ships freetype-as-runtime exports (Fedora, RHEL, Rocky, Mageia, openSUSE Leap + Tumbleweed). The `(64bit)` ABI tag is correct for both x86_64 and aarch64 — it indicates the 64-bit ABI, not CPU arch. Fix #2 — auto-update e2e toolchain resolution ============================================== scripts/test-update-e2e.sh phase 2 checks out v0.2.10 in a git worktree and runs `cargo build`. v0.2.10 predates the rust-toolchain .toml pin (also from 1884237), so the worktree has no toolchain file. In CI the dtolnay/rust-toolchain action installs 1.95 but does NOT set a rustup default — running cargo without a pin file fails with "rustup could not choose a version of cargo to run, because one wasn't specified explicitly". Copy the current main toolchain pin into the worktree before building. Future-proof: when main bumps 1.95 -> 2.0 the e2e auto-follows without a script edit. Guarded by `if [ -f ... ]` so the script still works against pre-pin checkouts (where there's no source file to copy). Out of scope (Windows MSI) ========================== The Build x86_64-pc-windows-msvc leg is failing on `cargo wix build --no-build` with `Error[3] (Io): The 'build' path does not exist`. Pre-existing failure (also failed on v0.2.11), and the leg is `continue-on-error: true` per the matrix - does NOT block the release. Investigated separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complete rewrite of PaneFlow's rendering and UI layer, replacing Tauri v2 + SolidJS + xterm.js with a full native Rust application using iced 0.13 + WGPU + alacritty_terminal.
What changed
paneflow-appcrate — native iced application replacingsrc-tauri/andfrontend/6 Epics, 20 User Stories
Quality gates
cargo check --workspace✅cargo clippy --workspace -- -D warnings✅ zero warningscargo test --workspace✅ 203 tests passcargo build --release✅ 20 MB binary (target < 30 MB)Architecture
Test plan
cargo run -p paneflow-appopens native WGPU window with sidebarpaneflow pingCLI command reaches IPC socket🤖 Generated with Claude Code