Skip to content

refactor(input): make pane key forwarding protocol-complete - #2578

Draft
ogulcancelik wants to merge 19 commits into
masterfrom
refactor/input-key-encoding
Draft

refactor(input): make pane key forwarding protocol-complete#2578
ogulcancelik wants to merge 19 commits into
masterfrom
refactor/input-key-encoding

Conversation

@ogulcancelik

Copy link
Copy Markdown
Collaborator

Summary

  • make raw terminal decoding incremental and typed while preserving Kitty alternates, associated text, extended modifiers, key lifecycle, and source metadata
  • make the state-synchronized Ghostty encoder the single pane key encoder, with explicit suppression/error outcomes and no duplicate fallback encoder
  • preserve modifyOtherKeys across handoff and close legacy compatibility gaps for Alt UTF-8, Ctrl aliases, Ctrl-Tab, and extended function keys
  • add an end-to-end keyboard compatibility corpus covering framing, wire transport, pane state, and final encoded bytes
  • track every local libghostty-vt extension as a reversible vendor patch

Refs #2514

Validation

  • just check — 3312 Rust tests passed, Windows-target clippy passed, maintenance and integration suites passed
  • cd vendor/libghostty-vt && zig build test-lib-vt -Dsimd=true
  • full vendor patch reverse/forward replay
  • live disposable Herdr pane checks for legacy, Kitty, function-key, Ctrl, and modifyOtherKeys sequences
  • final read-only code review and simplification review: no remaining P1/P2 findings

The Windows VM was unreachable from this workstation (No route to host); Windows-target compilation and clippy passed locally.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 170af0b9-83c2-4c5d-a5e2-bf5d23b04178

📥 Commits

Reviewing files that changed from the base of the PR and between 0f363a1 and 0690d0a.

📒 Files selected for processing (2)
  • vendor/libghostty-vt/src/input/key_encode.zig
  • vendor/patches/libghostty-vt/0005-proxy-key-encoding.patch
🚧 Files skipped from review as they are similar to previous changes (2)
  • vendor/patches/libghostty-vt/0005-proxy-key-encoding.patch
  • vendor/libghostty-vt/src/input/key_encode.zig

📝 Walkthrough

Walkthrough

The PR unifies keyboard parsing and forwarding around state-aware Kitty and modifyOtherKeys handling. It preserves alternate codepoints, generated text, modifiers, repeats, releases, and extended function keys through client, protocol, pane, and handoff paths.

Changes

Unified keyboard forwarding

Layer / File(s) Summary
Keyboard metadata and parsing
src/input/*, src/protocol/wire.rs, src/client/input.rs, src/client/input/windows_vti.rs
Keyboard events preserve shifted and base-layout codepoints, generated text, text-commit state, modifiers, repeats, and releases. Kitty parsing validates alternate fields and associated text.
Raw input framing
src/raw_input.rs
Framing distinguishes complete, incomplete, and unsupported input. Oversized CSI sequences, malformed escapes, invalid UTF-8, and repeated escape prefixes use bounded recovery.
Ghostty and libghostty-vt encoding
src/ghostty/*, vendor/libghostty-vt/*, vendor/patches/libghostty-vt/*
Proxy-event encoding preserves semantic modifiers and UTF-8 text. Hyper, Meta, F13–F35, alternate codepoints, repeat events, and related C APIs are supported.
Pane state and encoding
src/pane/input.rs, src/pane/kitty_keyboard.rs, src/pane/terminal.rs, src/pane.rs
Pane encoding uses live terminal state, tracks exact modifyOtherKeys modes, normalizes pane-specific keys, suppresses unavailable keys, and preserves handoff state.
Vendoring workflow
scripts/vendor_libghostty_vt.py, scripts/test_vendor_libghostty_vt.py, vendor/libghostty-vt.patches.md
The vendoring workflow validates ordered patches, applies them before installation, supports rollback and backup preservation, and tests reconstruction and failure paths.
Integration expectations
src/app/*, src/server/*, tests/live_handoff.rs, scripts/windows_conpty_enhanced_input_probe.ps1
Forwarded Kitty presses use compact encoding while repeat and release behavior remains explicit. Handoff tests cover exact mode 1 handling and legacy mode compatibility.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClientInput
  participant RawInputFramer
  participant ClientKeySource
  participant PaneTerminal
  participant GhosttyEncoder
  ClientInput->>RawInputFramer: frame and decode keyboard bytes
  RawInputFramer->>ClientKeySource: preserve VT metadata and text-commit state
  ClientKeySource->>PaneTerminal: restore terminal key state
  PaneTerminal->>GhosttyEncoder: encode using live protocol state
  GhosttyEncoder-->>PaneTerminal: return encoded, suppressed, or unavailable result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: a refactor that makes pane key forwarding protocol-complete.
Description check ✅ Passed The description directly explains the pane key forwarding refactor, compatibility work, testing, and vendor patch changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/input-key-encoding

Comment @coderabbitai help to get the list of available commands.

@kangal-bot kangal-bot added the ai-review Trigger automated AI reviews for pull requests admitted by the PR gate label Aug 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (6)
src/input/parse.rs (1)

1030-1036: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider reusing the shared fixture parsers for the variant corpora.

This test now uses crate::input::test_support. The local helpers decode_hex, parse_fixture_key_code, parse_fixture_modifiers, and parse_fixture_kind remain in this file and duplicate the shared versions. Only the column layout differs. Move the column-count handling into test_support and delete the local copies to keep one parser per concept.

src/protocol/wire.rs (1)

313-344: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider deriving the commit flag once instead of per branch.

All three branches apply the same with_text_commit() decision with a different predicate. Compute the predicate first, then apply the source and the commit flag once. This also makes the asymmetry explicit: the Vt branch trusts the transmitted text_commit, while the other two infer it from generated_text.

Note one coupling to keep in mind: with_text_commit() recomputes generated_text from code rather than keeping the transmitted text. No current producer sends a multi-codepoint commit with text_commit: true, so nothing breaks today. If that changes, the receiver would silently truncate the text.

vendor/libghostty-vt/src/input/key_mods.zig (1)

114-115: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a bit-position test for hyper and meta.

binding() now propagates the two new flags. The existing self-check test only pins bit 0 (shift). Add assertions for the new bit positions so a future field reorder cannot silently shift the ABI that the C header mirrors.

Run the targeted test with zig build test-lib-vt -Dtest-filter=<filter>.

As per coding guidelines: "For libghostty-vt changes, prefer zig build test-lib-vt -Dtest-filter=<filter> for targeted tests."

♻️ Proposed test addition
     test {
         const testing = std.testing;
         try testing.expectEqual(`@as`(Backing, `@bitCast`(Mods{})), `@as`(Backing, 0b0));
         try testing.expectEqual(
             `@as`(Backing, `@bitCast`(Mods{ .shift = true })),
             `@as`(Backing, 0b0000_0001),
         );
+        try testing.expectEqual(
+            `@as`(Backing, `@bitCast`(Mods{ .hyper = true })),
+            `@as`(Backing, 0b0000_0100_0000_0000),
+        );
+        try testing.expectEqual(
+            `@as`(Backing, `@bitCast`(Mods{ .meta = true })),
+            `@as`(Backing, 0b0000_1000_0000_0000),
+        );
     }

Source: Coding guidelines

vendor/libghostty-vt/src/input/function_keys.zig (1)

310-326: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Set an explicit eval branch quota in pcStyleWithImplicitMods.

pcStyle calls @setEvalBranchQuota(500_000) inside its own comptime block. pcStyleWithImplicitMods does not. The helper runs 13 times during the keys comptime block and relies on the quota that pcStyle happens to raise first. Set the quota explicitly so the helper does not depend on call ordering.

♻️ Proposed fix
 fn pcStyleWithImplicitMods(comptime fmt: []const u8, comptime implicit: key.Mods) []Entry {
     comptime {
+        `@setEvalBranchQuota`(500_000);
         var entries: [modifiers.len]Entry = undefined;
src/pane/input.rs (1)

77-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the consumed-Shift predicate for readability.

ghostty_consumed_mods packs four disjunctive conditions into a matches! guard nested inside a boolean expression. The rule is hard to read and hard to extend. The behavior is correct; only the shape is a concern.

Split the character-specific decision into a named helper.

♻️ Proposed refactor
 fn ghostty_consumed_mods(key: &crate::input::TerminalKey) -> u16 {
-    let only_shift = key.modifiers == crossterm::event::KeyModifiers::SHIFT;
-    let has_generated_text = key
-        .generated_text
-        .as_ref()
-        .is_some_and(|text| !text.is_empty());
-    let shift_generated_text = key
-        .modifiers
-        .contains(crossterm::event::KeyModifiers::SHIFT)
-        && matches!(key.code, crossterm::event::KeyCode::Char(c) if
-            has_generated_text
-                || key.shifted_codepoint.and_then(char::from_u32).is_some()
-                || c.is_ascii_uppercase()
-                || (only_shift
-                    && (c.is_ascii_alphabetic()
-                        || ghostty_unshifted_ascii_pair(c).is_some())));
-    if shift_generated_text {
+    if !key
+        .modifiers
+        .contains(crossterm::event::KeyModifiers::SHIFT)
+    {
+        return 0;
+    }
+    let crossterm::event::KeyCode::Char(c) = key.code else {
+        return 0;
+    };
+    if shift_produced_the_character(key, c) {
         crate::ghostty::MOD_SHIFT
     } else {
         0
     }
 }
+
+/// True when Shift was used to produce the character rather than acting as a
+/// separate binding modifier.
+fn shift_produced_the_character(key: &crate::input::TerminalKey, c: char) -> bool {
+    let has_generated_text = key
+        .generated_text
+        .as_ref()
+        .is_some_and(|text| !text.is_empty());
+    let only_shift = key.modifiers == crossterm::event::KeyModifiers::SHIFT;
+    has_generated_text
+        || key.shifted_codepoint.and_then(char::from_u32).is_some()
+        || c.is_ascii_uppercase()
+        || (only_shift && (c.is_ascii_alphabetic() || ghostty_unshifted_ascii_pair(c).is_some()))
+}
src/pane/terminal.rs (1)

2186-2207: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

The one-shot latches hide recurring encoder failures.

EVENT_ALLOCATION_LOGGED, ENCODER_LOCK_LOGGED, and ENCODER_ERROR_LOGGED are process-wide AtomicBool values that are never reset. After the first failure of each category, every later failure is silent for the lifetime of the process. A pane whose encoder lock is poisoned drops every key with no further signal.

Keep the noise suppression but preserve visibility. Count the suppressed failures and report the total periodically, or emit a metric alongside the first log.

♻️ Proposed refactor
 fn log_key_encoding_unavailable(reason: KeyEncodingUnavailable) {
-    use std::sync::atomic::{AtomicBool, Ordering};
+    use std::sync::atomic::{AtomicU64, Ordering};
 
-    static EVENT_ALLOCATION_LOGGED: AtomicBool = AtomicBool::new(false);
-    static ENCODER_LOCK_LOGGED: AtomicBool = AtomicBool::new(false);
-    static ENCODER_ERROR_LOGGED: AtomicBool = AtomicBool::new(false);
+    // Log the first failure, then every 1024th, so recurring faults stay visible.
+    const LOG_INTERVAL: u64 = 1024;
+    static EVENT_ALLOCATION_FAILURES: AtomicU64 = AtomicU64::new(0);
+    static ENCODER_LOCK_FAILURES: AtomicU64 = AtomicU64::new(0);
+    static ENCODER_ERROR_FAILURES: AtomicU64 = AtomicU64::new(0);
 
-    let first_failure = match reason {
+    let failures = match reason {
         KeyEncodingUnavailable::Adapter(GhosttyKeyEventAdapterError::UnsupportedKey) => {
             debug!(?reason, "Ghostty key encoding unavailable; suppressing key");
             return;
         }
         KeyEncodingUnavailable::Adapter(GhosttyKeyEventAdapterError::EventAllocation) => {
-            &EVENT_ALLOCATION_LOGGED
+            &EVENT_ALLOCATION_FAILURES
         }
-        KeyEncodingUnavailable::EncoderLockPoisoned => &ENCODER_LOCK_LOGGED,
-        KeyEncodingUnavailable::EncoderError => &ENCODER_ERROR_LOGGED,
+        KeyEncodingUnavailable::EncoderLockPoisoned => &ENCODER_LOCK_FAILURES,
+        KeyEncodingUnavailable::EncoderError => &ENCODER_ERROR_FAILURES,
     };
-    if !first_failure.swap(true, Ordering::Relaxed) {
-        error!(?reason, "Ghostty key encoding failed; suppressing key");
+    let count = failures.fetch_add(1, Ordering::Relaxed) + 1;
+    if count == 1 || count.is_multiple_of(LOG_INTERVAL) {
+        error!(
+            ?reason,
+            count, "Ghostty key encoding failed; suppressing key"
+        );
     }
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cc10e144-35ef-4225-89d5-33bd66710878

📥 Commits

Reviewing files that changed from the base of the PR and between e2aa86a and faefa14.

⛔ Files ignored due to path filters (1)
  • tests/fixtures/keyboard_protocol_corpus.tsv is excluded by !**/*.tsv
📒 Files selected for processing (37)
  • docs/next/CHANGELOG.md
  • src/app/api/panes.rs
  • src/app/input/mouse.rs
  • src/app/mod.rs
  • src/client/input.rs
  • src/client/input/windows_vti.rs
  • src/ghostty/bindings.rs
  • src/ghostty/mod.rs
  • src/input/encode.rs
  • src/input/mod.rs
  • src/input/model.rs
  • src/input/parse.rs
  • src/input/test_support.rs
  • src/pane.rs
  • src/pane/input.rs
  • src/pane/kitty_keyboard.rs
  • src/pane/terminal.rs
  • src/protocol/wire.rs
  • src/raw_input.rs
  • src/server/client_transport.rs
  • src/server/headless.rs
  • tests/live_handoff.rs
  • vendor/libghostty-vt.patches.md
  • vendor/libghostty-vt/include/ghostty/vt/key/event.h
  • vendor/libghostty-vt/src/input/function_keys.zig
  • vendor/libghostty-vt/src/input/key.zig
  • vendor/libghostty-vt/src/input/key_encode.zig
  • vendor/libghostty-vt/src/input/key_mods.zig
  • vendor/libghostty-vt/src/lib_vt.zig
  • vendor/libghostty-vt/src/terminal/c/key_event.zig
  • vendor/libghostty-vt/src/terminal/c/main.zig
  • vendor/patches/libghostty-vt/0002-proxied-kitty-key-metadata.patch
  • vendor/patches/libghostty-vt/0003-report-kitty-repeat-events.patch
  • vendor/patches/libghostty-vt/0004-encode-extended-function-keys.patch
  • vendor/patches/libghostty-vt/0005-preserve-legacy-ctrl-tab.patch
  • vendor/patches/libghostty-vt/0006-honor-consumed-shift-in-legacy-control-keys.patch
  • vendor/patches/libghostty-vt/0007-preserve-proxy-key-compatibility.patch

Comment thread src/input/mod.rs
Comment thread src/input/parse.rs Outdated
Comment thread src/input/test_support.rs Outdated
Comment thread src/raw_input.rs
Comment thread vendor/libghostty-vt/src/input/function_keys.zig
Comment thread vendor/libghostty-vt/src/input/key_encode.zig Outdated
Comment thread vendor/libghostty-vt/src/input/key_encode.zig Outdated
@greptile-apps

greptile-apps Bot commented Aug 9, 2026

Copy link
Copy Markdown

Greptile Summary

The PR replaces duplicate pane-key encoding with a state-synchronized Ghostty encoder and expands typed input metadata, incremental framing, keyboard lifecycle handling, and handoff state preservation.

  • Preserves Kitty alternate keys, associated text, extended modifiers, repeats, releases, and extended function keys.
  • Carries richer VT key metadata through the client/server protocol and preserves exact modifyOtherKeys state during handoff.
  • Adds a keyboard compatibility corpus and reproducible patch-series management for local libghostty-vt extensions.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains in the eligible follow-up review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
src/raw_input.rs Reworks raw input decoding into an incremental state machine covering fragmented UTF-8, CSI, control strings, malformed input, and timeout recovery.
src/pane/input.rs Adapts normalized TerminalKey values and source metadata into semantic Ghostty key events.
src/pane/terminal.rs Makes the pane’s state-synchronized Ghostty encoder authoritative and restores exact keyboard state during handoff.
src/protocol/wire.rs Extends structured VT key-source metadata and updates protocol-version and frozen-wire coverage.
src/ghostty/mod.rs Extends the Ghostty bindings wrapper with retained UTF-8 storage, alternate codepoints, consumed modifiers, and proxy-event encoding.
src/pane/kitty_keyboard.rs Tracks exact modifyOtherKeys modes alongside Kitty keyboard protocol state.
vendor/libghostty-vt/src/input/key_encode.zig Adds proxy-event behavior and compatibility handling for lifecycle, modifiers, Alt text, and extended function keys.
tests/fixtures/keyboard_protocol_corpus.tsv Expands end-to-end compatibility cases across framing, transport, pane state, and final PTY bytes.

Sequence Diagram

sequenceDiagram
    participant T as Host terminal
    participant F as Incremental input framer
    participant W as Typed wire protocol
    participant P as Pane input adapter
    participant G as State-synchronized Ghostty encoder
    participant C as PTY child
    T->>F: Raw keyboard bytes/events
    F->>W: Typed key + lifecycle/source metadata
    W->>P: Reconstructed TerminalKey
    P->>G: Semantic proxy key event
    G->>G: Apply pane Kitty/modifyOtherKeys state
    G->>C: Encoded terminal bytes
Loading

Reviews (3): Last reviewed commit: "fix(input): encode no-text proxy alt eve..." | Re-trigger Greptile

@ogulcancelik
ogulcancelik marked this pull request as draft August 9, 2026 18:52
@ogulcancelik
ogulcancelik marked this pull request as ready for review August 9, 2026 21:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vendor/libghostty-vt/src/input/key_encode.zig (1)

426-431: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Complete proxy Alt handling for events without generated text.

Proxy events must not use host-specific macOS Option behavior. The current no-text path still does so, and the vendor patch does not correct it.

  • vendor/libghostty-vt/src/input/key_encode.zig#L426-L431: add a proxy-specific ESC-prefix path for ASCII unshifted_codepoint values before calling legacyAltPrefix.
  • vendor/patches/libghostty-vt/0005-proxy-key-encoding.patch#L83-L93: add the matching no-text-path hunk and regression test so patch replay preserves the correction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bf1a040c-420f-4325-8a83-51dcbf5b053b

📥 Commits

Reviewing files that changed from the base of the PR and between 6a15ddd and 0f363a1.

⛔ Files ignored due to path filters (1)
  • tests/fixtures/keyboard_protocol_corpus.tsv is excluded by !**/*.tsv
📒 Files selected for processing (18)
  • scripts/test_vendor_libghostty_vt.py
  • scripts/vendor_libghostty_vt.py
  • src/ghostty/bindings.rs
  • src/ghostty/mod.rs
  • src/input/model.rs
  • src/pane/input.rs
  • src/pane/terminal.rs
  • vendor/libghostty-vt.patches.md
  • vendor/libghostty-vt/include/ghostty/vt/key/encoder.h
  • vendor/libghostty-vt/include/ghostty/vt/key/event.h
  • vendor/libghostty-vt/src/input/function_keys.zig
  • vendor/libghostty-vt/src/input/key.zig
  • vendor/libghostty-vt/src/input/key_encode.zig
  • vendor/libghostty-vt/src/input/kitty.zig
  • vendor/libghostty-vt/src/terminal/c/key_encode.zig
  • vendor/patches/libghostty-vt/0005-proxy-key-encoding.patch
  • vendor/patches/libghostty-vt/0006-extended-function-keys-f35.patch
  • vendor/patches/libghostty-vt/series
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/ghostty/mod.rs
  • src/input/model.rs
  • src/pane/terminal.rs

@ogulcancelik

Copy link
Copy Markdown
Collaborator Author

Addressed CodeRabbit’s no-text proxy Alt finding in 0690d0a. Proxy mode now prefixes ASCII unshifted codepoints before any host-specific macOS Option policy, with a vendored Zig regression test and refreshed replayable patch.

@ogulcancelik

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ogulcancelik
ogulcancelik marked this pull request as draft August 9, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Trigger automated AI reviews for pull requests admitted by the PR gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants