feat(terminal-engine): full xterm.js → alacritty cutover (Phase 1+2+3)#150
feat(terminal-engine): full xterm.js → alacritty cutover (Phase 1+2+3)#150alxjrvs wants to merge 46 commits into
Conversation
Captures the alacritty_terminal vs libghostty-vt analysis that motivates this branch's xterm.js → alacritty_terminal conversion. Covers API surface, feature matrix, maturity/build axes, gnar-term-specific migration impact, and the TerminalEngine trait escape hatch for a future libghostty-vt swap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cycle-1) Phase 1 MVP cycle-1 — Foundation. Adds alacritty_terminal 0.26 as a direct Rust dependency, creates the src-tauri/src/terminal_engine/ module skeleton, wires it into lib.rs. The module currently holds only an extern-crate-form unit test that asserts the dep resolves; cycle-2 will populate it with the TerminalEngine trait and AlacrittyEngine impl per plan.md. AC-1: alacritty_terminal is a direct dependency, mac + linux build. Plan + ADRs: docs/implement/2026-05-16-alacritty-terminal-engine/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ycle-2) Implements AC-2. Adds the TerminalEngine trait, AlacrittyEngine concrete impl wrapping alacritty_terminal::Term + vte::ansi::Processor, Phase 1 grid types in types.rs (cycle-3 will add serde + TS mirror). Four unit tests exercise feed/damage/resize/snapshot/cursor. Per ADR-0001 (trait shape) and ADR-0002 (wire format) in docs/implement/2026-05-16-alacritty-terminal-engine/scaffold/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cycle-2 of 2026-05-16-alacritty-terminal-engine: TerminalEngine trait + AlacrittyEngine impl, types, 5 unit tests. SHA a0667ed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…trip (cycle-3) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ycle-5) Adds a pure canvas-2d rendering layer for the Alacritty terminal engine: - alacritty-renderer.ts: Renderer class with paintSnapshot/paintDiff/paintCursor, 16-color ANSI palette, SGR bold/underline/inverse, dirty-rect-only repaints. - alacritty-renderer.test.ts: 16 vitest tests (MockContext proxy records canvas operations; asserts call sequences for snapshot, diff, cursor, palette, attrs). - AlacrittyTerminalSurface.svelte: canvas mount, Tauri Channel subscription, Phase-1 key encoding (ASCII, Enter, Backspace, Tab, arrows), detach on destroy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ChannelMessage> (cycle-4) - Add PtyBridge (pty_bridge.rs): owns AlacrittyEngine per pane, emits TerminalChannelMessage::Snapshot on attach and ::Diff on feed. - Add MessageSink trait for testability; TauriChannelSink is the production impl, TestSink (Vec-backed) is used in unit tests. - Add alacritty_commands.rs: attach_alacritty_engine, feed_alacritty_engine, resize_alacritty_engine Tauri commands registered in invoke_handler\!. - Extend AppState.bridges: HashMap<u32, PtyBridge> — xterm.js path untouched. - Ordering invariant: Snapshot always precedes any Diff (structural guarantee). - Resize emits Snapshot (not Diff) because alacritty_terminal reflows entire grid on resize. - Channel drop errors swallowed silently with inline comment explaining rationale. - Fix pre-existing clippy doc_markdown warnings in alacritty_tests.rs and ipc_tests.rs from cycle-3. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… (cycle-6) - Add terminalEngine?: "xterm" | "alacritty" to GnarTermConfig (optional; absent defaults to "xterm", preserving existing behavior) - Export getTerminalEngine() getter matching getMcpSetting() idiom; guards defensively so invalid config values fall back to "xterm" - PaneView.svelte reads $configStore.terminalEngine reactively and mounts AlacrittyTerminalSurface when "alacritty", TerminalSurface otherwise; xterm branch is byte-identical to prior call site - 10 tests in terminal-engine-flag.test.ts covering default, explicit values, invalid fallback, session stability, and pane dispatch contract Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…otes (cycle-7) Add e2e_tests.rs with a single integration test that exercises the full pipeline from cycles 2-4: AlacrittyEngine feed→damage→cursor (cycle-2), GridDiff construction + serde round-trip (cycle-3), TerminalChannelMessage JSON shape verification (cycle-4). 20 Rust tests pass; 2174 vitest tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… for clippy Resolves a clippy::doc-markdown warning surfaced under `-D warnings` that was not caught in the cycle-7 worktree's local clippy run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…xes (cycle-8) - F-001: add detach_alacritty_engine Tauri command + kill_pty safety net - F-002: wire PTY reader thread to bridge feed (Arc<Mutex> bridges, xterm first) - F-003: set textBaseline=top in Renderer constructor and AlacrittyTerminalSurface - F-004: replace color(display-p3) fallback with #000000 for WebKitGTK compat - F-005: replace cfg(debug_assertions) eprintln with log::debug in pty_bridge - F-006: add log::warn for get_size fallback in attach_alacritty_engine - F-007: console.error + remove stale cycle-4 comment + resize-mismatch message - F-008: extract encodeKey to alacritty-key-encoder.ts with 20 unit tests - F-009: add selectTerminalComponent + pane-view-dispatch.test.ts (5 tests) - F-010: fix void cursor-ordering test — real cursorIdx > textIdx assertion - F-011: return after resize-mismatch warn; re-trigger snapshot via resize command - F-012: PaneView uses getTerminalEngine() normalizer instead of raw configStore - F-013: ResizeObserver wires canvas resize to resize_alacritty_engine invoke - F-018: Rgb wire-contract test locks array serialisation format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ned mutex (re-review) Address NF-001 from the cycle-8 re-review: assigning canvasEl.width/height resets the 2D context state including textBaseline. The snapshot handler constructed the Renderer before the resize, so its constructor's textBaseline="top" was wiped before paintSnapshot. The ResizeObserver path never restored baseline after the resize at all. Both produced glyph misalignment that F-003 was meant to prevent. Snapshot handler: assign canvas dimensions first, then construct the Renderer so its constructor's ctx setup is the last write. ResizeObserver: re-apply textBaseline + font directly after dimension assignment. Also address F-002 re-review NEEDS-WORK: replace the silent `if let Ok(mut bridges) = bridges_arc.lock()` with a match arm that logs poisoned-mutex errors at error level — without it, a panic in another thread would silently kill all future bridge feeds with no diagnostic evidence. Bumps write_pty keystroke-loss console.warn to console.error for consistency with the other failure paths in the component. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(cycle-9) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the silent-discard send_event with an Arc<Mutex<VecDeque>> queue. Add AlacrittyEngine::drain_events and wire it into PtyBridge::feed_and_emit with debug logging. PtyWrite routing to the PTY writer is deferred (TODO). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…feed snapshot (cycle-11) Add cols/rows fields to PtyBridge, set at construction and updated in resize_and_emit. feed_and_emit now reads cached u16 values instead of calling engine.snapshot() (O(rows x cols) cell iteration + allocation) on every PTY byte-feed. Delete the viewport_dims helper. Add invariant test: cached_viewport_dims_survive_resize_and_match_diff_payload. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Phase 2 hand-off wave landed (wave 7) — three additional cycles addressing the deferred items from
Updated stats: 26 cargo + 2209 vitest tests passing; clippy The test plan above is unchanged — the additions are behind the same feature flag ( |
…lacritty alignment (cycle-12) - PtyBridge: add pty_writer field; route PtyWrite and ColorRequest events back to the PTY so programs don't hang on OSC color queries - alacritty-key-encoder: Phase-2 coverage (F1-F12, Home/End/PageUp/PageDown, Insert/Delete, Ctrl+letter, Alt-prefix, modifier-encoded sequences) - cursor_position: use Term::renderable_content().cursor (RenderableCursor); add CursorShapeTag enum and shape field to CursorPos; renderer dispatches on beam/underline/hollow_block/hidden - Attr coverage: ATTR_DIM/HIDDEN/STRIKEOUT/WIDE_CHAR constants + flag mapping; zerowidth combining marks preserved; renderer handles dim/hidden/strikeout AC-3: pty_write_event_routed_to_writer AC-4: cursor_shape_beam_renders_thin_vertical_bar AC-6: color_request_response_shape_contains_osc_prefix Tests: 34 Rust, 2248 TypeScript, 0 failing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… after cycle-12 cycle-12 wired the PTY write-back path; this test's doc comment still pointed at the resolved TODO. Tightens the comment to reflect the layered split: the test asserts buffering (cycle-10), routing is asserted in pty_bridge_tests (cycle-12). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
TODO-resolution + alacritty-alignment wave landed (wave 8 — cycle-12):
Stats: 34 cargo + 2248 vitest tests passing (up from 26 + 2209), all 7 pre-commit hooks green, clippy Intentionally deferred (out-of-scope per intent.md): OSC 52 clipboard plumbing ( The alignment audit from earlier (F-1 through F-9) is documented in cycles/cycle-12.md alongside the rationale for which findings landed here vs. which remain for the migration's full Phase 2 run. |
…aware snapshots (cycle-15) Fixes audit F-5: snapshot() and damage() now correctly reflect the visible viewport when display_offset > 0 (user scrolled up into scrollback history). - snapshot(): replaced Line(0)..Line(rows) direct indexing with Grid::display_iter(), which yields cells adjusted for the current display_offset. Viewport row is computed as: point.line.0 + display_offset. - damage() Partial arm: LineDamageBounds.line from TermDamageIterator is already viewport-relative (iterator adds display_offset per upstream source line 208). map_row() call now converts back to grid-relative via Line(b.line - display_offset). - damage() Full arm: viewport rows now mapped with display_offset correction. - AlacrittyEngine::scroll_up_by(lines): new pub method proxying grid_mut().scroll_display(Scroll::Delta(lines as i32)) for test driving. - Two new tests: snapshot_respects_display_offset_when_scrolled, snapshot_viewport_dimensions_invariant_under_scroll. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ipboard (cycle-13) - Add ClipboardAccess trait (write_text/read_text) to decouple bridge from Tauri - Add AppHandleClipboard production wrapper in alacritty_commands.rs - Add clipboard field to PtyBridge; route ClipboardStore/Load events in feed_and_emit - Set Osc52::CopyPaste in AlacrittyEngine so load queries are not silently dropped - Add TestClipboard + 3 tests: store routing, load→PTY response, graceful no-access Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…palette resolution (cycle-14)
Resolves audit F-1: `map_color` previously collapsed all `Color::Named`
variants to `Indexed(u8)`, erasing semantic identity (Foreground \!= White,
Background \!= Black, Cursor lost entirely, Dim variants wrong).
- Add `NamedSlot` enum (13 variants) covering the semantic `NamedColor`
range (indices 256-268: Foreground, Background, Cursor, DimBlack..
DimWhite, BrightForeground, DimForeground).
- Extend `ColorIndex` with `Named(NamedSlot)` third variant; serde shape
becomes `{ kind: "Named", value: "foreground" }` on the wire.
- Update `map_color`: palette range (Black=0..BrightWhite=15) stays
`Indexed`; semantic range emits `Named(NamedSlot)`.
- Mirror in `terminal-ipc.ts`: add `NamedSlot` union + `Named` variant.
- Extend `Renderer` with optional `palette` option; `resolveColor` accepts
a palette for named-slot lookup, falling back to `DEFAULT_NAMED_PALETTE`.
- Verified: cycle-12 `ColorRequest` handler already handles indices 256-268
correctly via `colors[index]`; no change to `pty_bridge.rs` needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds theme-bridge.ts and cell-metrics.ts under src/lib/components/alacritty/. theme-bridge subscribes to the Svelte theme store, maps ThemeDef to the renderer's Palette shape, and notifies the renderer on every theme change. cell-metrics measures monospace glyph dimensions via off-screen canvas and re-fires on font-size store changes for PtyBridge resize plumbing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- osc7.rs: parse_osc7() stateless parser (file:// URL decode, host strip) - osc7.rs: Osc7Emitter callback wrapper + scan() raw-byte scanner for PtyBridge integration - link-extractor.ts: extractLinks(GridSnapshot) → LinkMatch[] (URL + file path regexes) - link-overlay.ts: attachLinkOverlay(canvas, opts) → LinkOverlayHandle; debounced hover + modifier-click dispatch via open_url/open_with_default_app Tauri commands - mod.rs: register pub mod osc7 (minimal required change; cycle-21 owns full mod.rs wiring) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…scroll anchor (cycle-19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add SelectionState, SelectionMode, IpcSelectionRange, and SelectionTextProvider trait in selection.rs; 8 Rust tests covering cell/semantic/lines selection, copy-on-selection-change flag, shift-click extend, serde round-trip, and mode mapping. - Add mouse-handler.ts with attachMouse(): pixel→cell translation, single/double/triple click mode dispatch, shift-click extend, and outside-canvas clear; 10 TS tests including mouse_drag_selection. - Add osc52-read.ts with handleClipboardRead() and encodeOsc52Response(); 8 TS tests including osc52_read_round_trip_through_clipboard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ge (cycle-16) - Add search.rs: SearchQuery/SearchMatch types + find_next/find_prev wrapping Term::regex_search_right/left with literal-escape, case- sensitive (?-i/?i), and ASCII whole-word (?-u:\b) support - Add search_find_next/prev/clear Tauri command stubs (registration deferred to cycle-21) - Add search-bridge.ts: attachSearch(paneId) -> SearchHandle with findNext/findPrev/clear methods invoking the Tauri command surface - 10 Rust tests (all regex_search-named) + 8 vitest tests; 59 Rust / 2266 TS passing AC-1: regex_search_finds_case_sensitive_literal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nchor) into run-branch
… default (cycle-21) Delete TerminalSurface.svelte and xterm.d.ts; update all test fixtures and mocks to reflect the post-cutover TerminalSurface shape (no terminal, fitAddon, searchAddon, termElement fields). Sweep verified zero hits for @xterm/|searchAddon|FitAddon|WebglAddon|xterm.d.ts across src/. Rust clippy doc_markdown warnings fixed in search.rs, osc7_tests.rs, and selection_tests.rs. 214 test files passing, 2412 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cycle-21 removed @xterm/* from package.json but didn't regenerate package-lock.json; running npm install dropped the four orphaned @xterm transitive nodes (addon-fit, addon-search, addon-webgl, xterm itself). npm ls is now empty for those names. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Complete migration of gnar-term's terminal state engine from xterm.js to
alacritty_terminal. Phase 1 (engine + IPC + renderer behind a feature flag), Phase 2 (parity bridges for search, selection/clipboard, links, input, theme/font), and Phase 3 (xterm.js removal + default flip) all shipped in this single branch.After this PR merges,
@xterm/*is gone fromsrc/and frompackage-lock.json.AlacrittyTerminalSurface.svelteis the sole terminal renderer. TheterminalEngineflag is removed (no escape hatch — the alacritty path is the only path).What landed (21 cycles, 3 orchestrator hot-fixes)
Phase 1 — engine + IPC + canvas-2d renderer (cycles 1–7):
terminal_engineRust module:TerminalEnginetrait,AlacrittyEnginewrappingTerm<MyListener>+vte::ProcessorGridSnapshot/GridDiffIPC types with serde round-trip + TS mirror; TauriChannel<TerminalChannelMessage>dispatchAlacrittyTerminalSurface.sveltewith canvas-2d renderer (16-color palette, SGR attrs, block cursor)terminalEnginefeature flag ingnar-term.json(default"xterm"during Phase 1)Phase 1 remediation (cycle-8 + 2 hot-fixes):
detach_alacritty_engineTauri command +kill_ptysafety-net cleanuptextBaselinesurvives canvas resize (HTML spec §4.12.5.1 fix); poisoned-mutex logging in pty reader threadSmoke-note hand-offs (cycles 9–11):
ATTR_ITALICrenderingMyListenerevent surfacing (Bell/Title/PtyWrite/etc. drained and logged)PtyBridge::viewport_dimscached as fields (no more per-feed full-grid snapshot)Alacritty alignment audit + resolution (cycle-12):
Event::PtyWriteandEvent::ColorRequestnow invoke the formatter againstterm.colors()and write the response — programs no longer hang on OSC color queriesCursorPos.shape(Block/Beam/Underline/HollowBlock/Hidden) viaRenderableCursor::new— neovim insert-mode beam cursor renders correctlyCell.chFinal deferrals closed (cycles 13–15):
tauri-plugin-clipboard-managervia aClipboardAccesstrait (testable independent of Tauri)ColorIndex::Namedwire-format variant preserves Foreground/Background/Cursor/Dim*/BrightForeground semantics — themes overriding via OSC 10/11/12 actually reach the rendererGrid::display_itersodisplay_offsetis honored (scrollback no longer shows the wrong rows)Phase 2 parity (cycles 16–20):
search-bridge.tswiresalacritty_terminal::regex_search_left/rightto the search-result API (cycle-16)mouse-handler.tsfor selection + middle-click paste + OSC 52 read response (cycle-17)link-overlay.ts+ OSC 7 cwd tap per-pane (cycle-18)paste-handler.tsbracketed paste mode + full key-corpus + scroll anchor (cycle-19)theme-bridge.tsruntime theme + font hot-reload (cycle-20)Phase 3 cutover (cycle-21):
@xterm/xterm,@xterm/addon-fit,@xterm/addon-search,@xterm/addon-webglremoved frompackage.jsonsrc/types/xterm.d.tsandsrc/lib/components/TerminalSurface.sveltedeletedterminal-service.tsxterm code paths removed (flushPtyBufferHWM backpressure,clearTextureAtlashooks, xterm-stylelinkHandler, xterm theme mapping) — ~660 lines deletedFindBar.svelterewired to alacritty's search bridgePaneView.svelterendersAlacrittyTerminalSurfaceunconditionally;terminalEnginefield + getter deletedcutover.test.tsintegration test asserts no xterm imports remain and the cutover surface is wirede2e-smoke-notes.mdexpanded with cutover QA planpackage-lock.jsonscrubbed of the four@xterm/*transitive entriesStats
src/net of xterm removalcargo clippy -- -D warningscleannpm testhook passesVerification sweep
Test plan
npm install— verify package-lock has no@xterm/*entriesnpm test— verify 2412 vitest tests pass (7 skipped expected)cargo test --manifest-path src-tauri/Cargo.toml --package gnar-term --lib terminal_engine— 83 tests passcargo clippy --manifest-path src-tauri/Cargo.toml --lib --tests -- -D warnings— cleannpm run build— full Tauri build succeedsls --color=autorenders 16-color ANSI + extended palette correctlygit log --color=always | head -50paginates and colors correctlynvim(or any program that queries OSC 10/11 colors): app does NOT hangcdsomewhere → check the pane title reflects the OSC 7 cwdCtrl+Fopens FindBar → search returns highlighted matches → next/prev cycles through themread_output— invoke against an alacritty pane: returns recent terminal text (whatever the bridge plumbing routed it to per cycle-21 prose)Files of interest
src-tauri/src/terminal_engine/— the entire engine modulesrc-tauri/src/terminal_engine/pty_bridge.rs— bridge between the PTY reader thread and the engine, plus the clipboard/PTY-write event routingsrc/lib/components/AlacrittyTerminalSurface.svelte— single terminal renderersrc/lib/components/alacritty/— five Phase-2 parity bridgessrc/lib/components/cutover.test.ts— proves no xterm imports remaindocs/implement/2026-05-16-alacritty-terminal-engine/— full audit trail (intent.md, plan.md, review.md, ship.md, journal.jsonl, per-cycle prose, e2e-smoke-notes.md) — kept locally per project policy (docs/implement/is gitignored)🤖 Generated with Claude Code