Skip to content

Match Ghostty keyboard input architecture - #8770

Closed
lawrencecchen wants to merge 116 commits into
mainfrom
feat-keyboard-input-architecture
Closed

lawrencecchen wants to merge 116 commits into
mainfrom
feat-keyboard-input-architecture

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Route native key events through AppKit text input after libghostty applies modifier translation, so Option-as-Alt matches Ghostty while dead keys and IMEs retain AppKit ownership.
  • Replace language, input-source, keycode, and timing branches with a pure input planner driven by composition state, AppKit consumption, committed text, and Ghostty key semantics.
  • Normalize committed C0 and DEL controls only at the universal raw-text boundary, without language or layout branches.
  • Keep exact configured cmux shortcuts ahead of unmatched Option input and preserve one physical owner across press, repeat, focus changes, and release.
  • Dispatch shortcuts outside the mutable lifecycle borrow, then reconcile with a generation token so modal actions and reentrant key-up events cannot violate Swift exclusivity or revive stale ownership.
  • Restore first-click terminal focus without bypassing the shared input path.

Testing

  • swift test --package-path Packages/macOS/CmuxTerminal: 174 Swift tests in 23 suites plus 11 XCTest cases passed.
  • Exhaustive planner coverage includes all 1,112,064 valid Unicode scalars, generated multi-scalar sequences, and every installed static macOS keyboard layout.
  • Regression proof for modal shortcut reentrancy: test-only commit failed in run 30275041274, then passed unchanged with the fix in run 30275772770.
  • Final-head hosted suites passed for Option shortcuts, modifier lifecycle on macOS 26 and macOS 15, Option-as-Alt modifiers, and physical-input focus reassertion.
  • git diff --check, scripts/check-pbxproj.sh, plutil, and scripts/lint-pbxproj-test-wiring.sh passed.
  • Production added-line audit found no language names, layout names, script ranges, locale checks, or input-source identifiers.
  • Tagged cloud build passed in run 30275917039.
  • Speculative merge CI passed in run 30276850977, including all four app-host groups, package tests, lag checks, and the universal release build.
  • Manual $autoreview, Greptile, CodeRabbit, and Socket Security found no unresolved actionable findings. GitHub reports zero unresolved review threads.
  • Fresh Computer Use preflight on the final tagged build is pending an unlocked macOS session.

Review Trigger

Review the AppKit/Ghostty ownership boundary, exact release ownership, and the prepare/dispatch/reconcile shortcut lifecycle.

Checklist

  • No production language, script, locale, layout, or input-source one-offs.
  • Regression tests precede fixes in separate commits.
  • Tagged macOS build completed.
  • Focused package and hosted tests passed.
  • Speculative merge CI passed.
  • Final tagged Computer Use preflight.

@cursor

cursor Bot commented Jul 23, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change introduces explicit terminal key-input planning, integrates AppKit text interpretation with IME state and physical-key replay, simplifies keyboard translation, and revises Option shortcut routing. It adds extensive planner, Unicode, keyboard-layout, IME, and shortcut regression coverage.

Changes

IME and Option Key Routing

Layer / File(s) Summary
Terminal input contracts and planning
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/*, Packages/macOS/CmuxTerminal/Tests/CmuxTerminalTests/Input/*
Defines key-input snapshots, events, actions, and plans; routes composition, committed text, control suppression, command handling, and physical-key replay through TerminalKeyInputPlanner.
IME keydown integration
Sources/GhosttyTerminalView.swift, Sources/GhosttyNSView+IMEComposition.swift, Sources/GhosttyTextInputSupport.swift, cmuxTests/*IME*, cmuxTests/*Numpad*
Routes AppKit text input through planner actions, updates key ownership and insertion behavior, removes specialized IME paths, and migrates or adjusts IME tests.
Keyboard translation and key identity
Sources/KeyboardLayout.swift, Sources/KeyboardLayoutKeyIdentityTracker.swift, cmux.xcodeproj/project.pbxproj
Adds control-character recovery, unshifted-codepoint extraction, modifier-specific Carbon translation, and repeat-safe key identity tracking.
Option shortcut routing and coverage
Sources/AppDelegate.swift, Sources/App/ShortcutRoutingSupport.swift, Sources/KeyboardShortcutSettings.swift, cmuxTests/*Option*, cmuxTests/WindowKeyDownReplayGuardTests.swift
Reclassifies Option candidates, tracks shortcut monitor ownership, routes configured equivalents before unmatched Option input, and updates routing regressions.

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

Sequence Diagram(s)

sequenceDiagram
  participant AppKit
  participant AppDelegate
  participant GhosttyNSView
  participant TerminalKeyInputPlanner
  participant libghostty
  AppKit->>AppDelegate: keyDown Option candidate
  AppDelegate->>AppDelegate: match configured shortcut
  AppDelegate->>GhosttyNSView: dispatch unmatched Option input
  GhosttyNSView->>TerminalKeyInputPlanner: plan interpreted snapshot
  TerminalKeyInputPlanner->>libghostty: send committed text or physical key
Loading

Possibly related PRs

Suggested reviewers: austinywang


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error New reusable input logic still lives under Sources/: KeyboardLayoutKeyIdentityTracker is pure and used in prod/tests, and KeyboardLayout adds testable text-recovery helpers instead of package code. Move reusable keyboard/input helpers and the key-identity tracker into the CmuxTerminal SwiftPM target; keep the app target to AppKit/Ghostty glue.
Cmux No Test Or Debug Seam In Production Source ❌ Error FAIL: Sources/GhosttyTerminalView.swift adds a #if DEBUG debugTextInputEventHandler hook used only by tests, with no production caller. Move the hook into the test target or a dedicated debug-only file/folder, or expose the needed state as internal and access it via @testable import.
Docstring Coverage ⚠️ Warning Docstring coverage is 21.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux No Ambient Global State ❓ Inconclusive Need code evidence to verify against the no-ambient-global-state rule. Inspect the changed production Swift files and the repo rule doc, then decide whether any new top-level API, namespace type, or singleton/global runtime state was introduced.
✅ Passed checks (21 passed)
Check name Status Explanation
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.
Cmux Swift Actor Isolation ✅ Passed PASS: New planner/snapshot/action types are pure value models, and the UI/state changes remain on @MainActor or nonisolated pure helpers.
Cmux Swift Blocking Runtime ✅ Passed Production Swift changes add no waits, sleeps, semaphores, main.sync, or new locks; the only timing heuristic removed is the old numpad deduplicator.
Cmux Browser Automation Off-Main ✅ Passed Diff only adds TerminalKeyInputPlanner files/tests; no browser.* socket commands, WebKit waits, router, or mainActor routing changes are touched.
Cmux Expensive Synchronous Load ✅ Passed Full base..HEAD diff only touches keyboard-input/planner code; no agent-history/session-store loads or JSON/transcript parsing were added or moved.
Cmux Cache Substitution Correctness ✅ Passed No persistence/history snapshot cache substitution found; the new snapshot/planner reads live AppKit state, and the only cache (key identity tracker) is event-driven and reset on focus changes.
Cmux No Hacky Sleeps ✅ Passed Diff against origin/main changes only Swift sources/tests and one pbxproj; no TS/JS/shell/runtime script sleeps or timers are introduced.
Cmux Algorithmic Complexity ✅ Passed Touched production paths are per-keystroke or fixed-size; planner/shortcut helpers only scan tiny event-local arrays or modifier flags, not scalable user collections.
Cmux Swift Concurrency ✅ Passed The new cmux input planner and touched routing code are synchronous; I found no added background queues, Combine state, completion-handler APIs, or unstructured Tasks in the changed logic.
Cmux Swift @Concurrent ✅ Passed Touched files add only synchronous pure helpers; no @concurrent or nonisolated async additions were found, and diff hunks with concurrency markers were empty.
Cmux Swiftpm Lockfiles ✅ Passed Only Swift source/test files changed in HEAD; no .gitignore, Package.swift, Package.resolved, workflow, or xcodeproj package-reference edits, so the lockfile rule isn't triggered.
Cmux Swift Logging ✅ Passed PASS: The changed Swift lines only adjust planner logic and tests; no added or changed print/debugPrint/dump/NSLog or Logger usage appears in the diff.
Cmux User-Facing Error Privacy ✅ Passed Only planner logic changed in production; no user-facing errors, alerts, or sensitive identifiers were added.
Cmux Full Internationalization ✅ Passed Only internal/debug literals changed; no new String(localized:) calls or .xcstrings/plist updates were added in the production diff.
Cmux Swiftui State Layout ✅ Passed Touched SwiftUI code is only an NSViewRepresentable bridge; no new ObservableObject/@published, GeometryReader, or lazy-list store patterns were added.
Cmux Architecture Rethink ✅ Passed The diff centralizes key routing in a pure planner/snapshot and uses only required platform observers plus local cache resets, not timing hacks or split ownership.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No standalone cmux window/controller/panel/WindowGroup changes are present; the PR is input-routing and test code only, and the existing lint script covers window identifier ownership.
Cmux Source Artifacts ✅ Passed All changed paths are intentional Swift source/test files; no logs, caches, build outputs, or other source artifacts appear in the diff.
Title check ✅ Passed The title is concise and accurately reflects the main theme of the change.
Description check ✅ Passed The description is detailed and covers summary, testing, review trigger, and checklist; the demo video section is the only notable omission.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-keyboard-input-architecture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces cmux's ad-hoc keyboard-input dispatch with a two-layer architecture borrowed from Ghostty: native key events are first routed through NSTextInputContext.handleEvent (instead of raw interpretKeyEvents) so the text input system's consumed/not-consumed verdict is structurally recorded, then a pure TerminalKeyInputPlanner converts the resulting snapshot into deterministic libghostty actions. Shortcut and physical-key lifecycle tracking (ShortcutKeyPressLifecycleTracker / TerminalKeyInputLifecycleTracker) preserve ownership across AppKit's multiple dispatch entrypoints and across keyboard-layout changes while a key is held.

  • Input planner: replaces all input-source, language, keycode, and timing branches in keyDown with composition state + AppKit consumption verdict + committed text + Ghostty key semantics.
  • Shortcut lifecycle: ensures each physical press dispatches exactly once across the local monitor, performKeyEquivalent, and key-equivalent fallback, and routes Command-modified key-ups to the terminal view that owned the press.
  • Physical key identity: stabilises unshifted_codepoint across press, repeats, and release even when the active keyboard layout changes mid-hold; uses TISCopyCurrentKeyboardLayoutInputSource (layout only) instead of TISCopyCurrentKeyboardInputSource (IME) so shortcut matching works independently of the writing system.

Confidence Score: 5/5

The change replaces ad-hoc IME and input-source branches with a well-bounded planner and two lifecycle trackers; the invariants are explicit and the test coverage is thorough. No correctness defects were found.

Every dispatch path through keyDown, performKeyEquivalent, and the local monitor now flows through the lifecycle trackers, eliminating the prior race between multiple AppKit entrypoints. The adoption of NSTextInputContext.handleEvent's native consumed/not-consumed verdict is structurally sound. No ambient global state, blocking primitives, test/debug seams, or actor-isolation mistakes were introduced.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/TerminalKeyInputPlanner.swift New pure-value planner; composition state, AppKit consumption, committed text, and Ghostty semantics drive actions cleanly. Logic for preedit-commit replay, control-text normalisation, and command-callback deduplication looks correct.
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/TerminalKeyInputLifecycleTracker.swift Physical-key ownership preserved across repeats, layout changes, and Ghostty menu-binding consumption. Unresolved-owner fallback for probe-only press and orphan-release forwarding (no prior lifecycle -> forwardsPhysicalKey = true) both handle edge cases correctly.
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/ShortcutKeyPressLifecycleTracker.swift Correctly deduplicates shortcut dispatch across local-monitor, performKeyEquivalent, and key-equivalent fallback. Generation counter prevents stale-lifecycle races.
Sources/GhosttyTerminalView.swift Core keyDown rewrite: NSTextInputContext.handleEvent replaces interpretKeyEvents for native events, planner + lifecycle tracker replace ad-hoc IME branches. windowFocusObserver lifecycle and resetTerminalKeyInputLifecycle teardown are wired correctly.
Sources/AppDelegate.swift shortcutConsumesKeyDown/keyUp wrap every shortcut entrypoint; terminalKeyReleaseOwners guarantees Command-modified key-ups reach the correct terminal view. WeakTerminalKeyReleaseOwner is a private file-scope helper, not ambient global state.
Sources/KeyboardLayout.swift Switch from TISCopyCurrentKeyboardInputSource to TISCopyCurrentKeyboardLayoutInputSource isolates shortcut character resolution from the active IME. carbonModifierKeyState adds .capsLock for text-input mode, matching Ghostty's uniform translation path.
Sources/GhosttyNSView+IMEComposition.swift Removed Korean-layout, Bopomofo, and numpad-deduplication branches replaced by the planner. replaysPhysicalKeyAfterPreeditCommit mirrors Ghostty's locale-independent navigation policy.
Sources/GhosttyTextInputSupport.swift Removed free-standing shouldSendText/isControlCharacterScalar and NumpadIMECommitDeduplicator; equivalents are now instance methods on GhosttyNSView or handled by the planner.
Sources/App/ShortcutRoutingSupport.swift shortcutRoutingShouldBypassForPrintableOptionText replaced by cmuxIsOptionTextInputCandidate NSEvent extension; caller sites still gate behind hasMarkedText() or explicit context checks.
cmuxTests/TraditionalChineseIMENumpadRegressionTests.swift Deleted alongside the NumpadIMECommitDeduplicator timing heuristic it asserted; covered by the planner's textInputConsumed path via handleEvent.

Sequence Diagram

sequenceDiagram
    participant Mon as Local Key Monitor
    participant AppKit as AppKit Responder Chain
    participant GNV as GhosttyNSView
    participant TIC as NSTextInputContext
    participant Plan as TerminalKeyInputPlanner
    participant LT as TerminalKeyInputLifecycleTracker
    participant ST as ShortcutKeyPressLifecycleTracker
    participant Ghost as libghostty

    Mon->>ST: prepareKeyDown(keyCode, identity, isRepeat)
    alt shortcut claimed
        ST-->>Mon: .dispatch / .consume
        Mon-->>AppKit: event consumed
    else pass through
        Mon-->>AppKit: forward event
        AppKit->>GNV: keyDown(event)
        GNV->>GNV: withGhosttyBindingKeyEvent (probe)
        GNV->>LT: physicalIdentityForBindingProbe
        GNV->>TIC: handleTextInputEvent(event)
        TIC-->>GNV: textInputConsumed (Bool)
        Note over GNV: insertText / doCommand callbacks populate committedText
        GNV->>Plan: plan(for: snapshot)
        Plan-->>GNV: TerminalKeyInputPlan(actions)
        GNV->>LT: actions(for: plan, isRepeat, eventIdentity)
        LT-->>GNV: filtered [TerminalKeyInputAction]
        loop each action
            GNV->>Ghost: sendCommittedText / sendGhosttyKey
        end
    end
    AppKit->>GNV: keyUp(event)
    GNV->>LT: release(forKeyUp: keyCode)
    LT-->>GNV: TerminalKeyInputRelease(forwardsPhysical, identity)
    alt forwardsPhysical
        GNV->>Ghost: ghostty_surface_key (RELEASE)
    end
Loading

Reviews (48): Last reviewed commit: "fix: dispatch shortcuts outside lifecycl..." | Re-trigger Greptile

Comment thread Sources/GhosttyTextInputSupport.swift

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/TerminalKeyInputPlanner.swift`:
- Around line 62-69: Update shouldSuppressControlText(_ text:composing:) so the
single-scalar control-character check also treats U+007F (DEL) as suppressible
during marked-text composition, while preserving existing behavior for other
text. Add a planner test covering forward-delete during composition and verify
it remains in AppKit’s preedit flow rather than emitting .sendKey.

In `@Sources/GhosttyTextInputSupport.swift`:
- Around line 5-7: Update isControlCharacterScalar so Unicode scalar value 0x7F
is classified as a control character alongside values below 0x20. Preserve the
existing keycode-side encoding in textForKeyEvent and sendGhosttyKey so
Backspace and Forward-Delete do not send literal DELETE text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 98cb55c4-4ee7-4684-9104-cdffd90ca38d

📥 Commits

Reviewing files that changed from the base of the PR and between d2d0ec4 and c0c2495.

📒 Files selected for processing (20)
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/TerminalKeyInputAction.swift
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/TerminalKeyInputEvent.swift
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/TerminalKeyInputKey.swift
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/TerminalKeyInputPlanner.swift
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Input/TerminalKeyInputSnapshot.swift
  • Packages/macOS/CmuxTerminal/Tests/CmuxTerminalTests/Input/TerminalKeyInputPlannerTests.swift
  • Sources/App/ShortcutRoutingSupport.swift
  • Sources/AppDelegate.swift
  • Sources/GhosttyNSView+IMEComposition.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/GhosttyTextInputSupport.swift
  • Sources/KeyboardLayout.swift
  • Sources/KeyboardShortcutSettings.swift
  • cmuxTests/AppDelegateOptionDigitShortcutRoutingTests.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • cmuxTests/CJKIMEInputTests.swift
  • cmuxTests/CJKIMEMarkedSelectionTests.swift
  • cmuxTests/GhosttyOptionAsAltModsTests.swift
  • cmuxTests/TraditionalChineseIMENumpadRegressionTests.swift
  • cmuxTests/WindowKeyDownReplayGuardTests.swift
💤 Files with no reviewable changes (1)
  • Sources/KeyboardShortcutSettings.swift

Comment thread Sources/GhosttyTextInputSupport.swift
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Expanded keyboard coverage in 9175039.

  • Ported Ghostty’s 10 active AppKit composition-control cases into the locale-independent planner tests.
  • Ran Ghostty’s unchanged Zig encoder cases for composition, Russian, Hungarian, dead keys, and Option-as-Alt with pinned Zig 0.15.2. The Linux-compatible cases pass; Ghostty’s macOS-only encoder case is covered by the tagged Option+N check.
  • Passed every installed static macOS keyboard layout on this Mac: 251 layouts and 155,516 translated key/modifier combinations, including Option, Shift, Command, and Dvorak Command-state mappings.
  • Expanded committed-text coverage across Korean, Simplified and Traditional Chinese, Japanese, Russian, Arabic, Hebrew, Devanagari, Thai, decomposed Vietnamese, and emoji.

The repository blocks local app-host test execution, so the pushed head leaves the expanded GhosttyNSView suite to the remote macOS test job.

Dictionary: An app-host test loads the test bundle inside a real cmux application process.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/macOS/CmuxTerminal/Tests/CmuxTerminalTests/Input/TerminalKeyInputPlannerTests.swift`:
- Around line 144-148: Replace the force cast in the input-source iteration with
a safe cast to TISInputSource, and skip or otherwise safely handle elements that
cannot be cast. Preserve processing for valid sources while removing the
SwiftLint force_cast violation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1e8e27a4-1dbe-470f-ba84-178b2776b8c3

📥 Commits

Reviewing files that changed from the base of the PR and between c0c2495 and 9175039.

📒 Files selected for processing (2)
  • Packages/macOS/CmuxTerminal/Tests/CmuxTerminalTests/Input/TerminalKeyInputPlannerTests.swift
  • cmuxTests/CJKIMEMarkedSelectionTests.swift

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants