Skip to content

Honor macos-option-as-alt left/right: send sided modifier bits to libghostty#6007

Open
austinywang wants to merge 11 commits into
mainfrom
issue-5993-option-as-alt
Open

Honor macos-option-as-alt left/right: send sided modifier bits to libghostty#6007
austinywang wants to merge 11 commits into
mainfrom
issue-5993-option-as-alt

Conversation

@austinywang

@austinywang austinywang commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

cmux's NSEvent → libghostty modifier translation only ever set the generic GHOSTTY_MODS_ALT bit, so libghostty could never tell the left Option key from the right one. macos-option-as-alt = left|right — which works in standalone Ghostty with the same libghostty engine — therefore had no effect in cmux: with = left both Option keys were stripped before character translation (right Option could not compose , @, ą, /, …; the key encoder ESC-prefixed instead), and with = right neither Option was treated as Alt (Zellij/Helix Alt bindings dead).

The fix mirrors Ghostty.app's ghosttyMods: set GHOSTTY_MODS_{SHIFT,CTRL,ALT,SUPER}_RIGHT from the NSEvent device-dependent flags (NX_DEVICER*KEYMASK) so that:

  • ghostty_surface_key_translation_mods strips Alt only for the configured side — the other Option key stays available to AppKit for character composition, and
  • the key encoder's legacyAltPrefix / kitty associated-text rules (which read mods.sides.alt from the key event itself) apply per physical side.

Commits (two-commit regression policy, plus a seam commit)

  1. Seam (green): extract the NSEvent-flags → ghostty mods mapping and the duplicated translation-mods loop into testable free functions (cmuxGhosttyModsFromFlags, cmuxTranslationModifierFlags); add a KeyboardLayout.textInputCharacter(forKeyCode:modifierFlags:inputSourceID:) overload that translates against a specific installed input source. No behavior change.
  2. Failing tests (red): regression tests for the side bits and Option composition, plus a focused -only-testing:cmuxTests/GhosttyOptionAsAltModsTests CI gate step (same pattern as the Browser pane routes localhost/LAN requests through the macOS system proxy — local dev servers unreachable under a global proxy (no implicit loopback bypass) #5888 browser-proxy gate). The gate is required because the full "Run unit tests" step tolerates app-host crashes by parsing only the last test summary: on the first push of the tests-only commit, the app-host run crashed and timed out before this suite executed and the job still reported green ("All failures are expected, treating as pass") — the suite compiled but never ran. The four right-side-bit tests fail at this commit by design.
  3. Fix (green): set the four *_RIGHT bits from the device-dependent flags.

Acceptance criteria from #5993

  • = left → left Option sends Alt/Meta, right Option composes (, @, ą, /) — side bits now reach libghostty's Mods.translation.
  • = right → mirror image.
  • = true / = false → unchanged (side-independent in libghostty), now consistent with sided encoding rules.
  • Regression coverage: US (Opt+; → ), German (Opt+L → @), Polish Pro (Opt+A → ą), Canadian-CSA (Opt → /, base é), all via the real UCKeyTranslate text-input path against the named Apple layouts, headless-safe (no live Metal surface needed), enforced by the focused CI gate.

Related

Localization audit

No user-facing strings, settings rows, menus, shortcuts, schema/config text, or docs changed — Swift key-event plumbing, tests, and a CI step only, so there is nothing to localize for en/ja.

Closes #5993, closes #691, closes #1349, closes #1397, closes #2369, closes #647, closes #725, closes #1469, closes #5025

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Input-source aware keyboard layout support for more accurate text composition and improved mouse modifier handling.
  • Bug Fixes

    • Corrected Option/Alt sided-bit behavior across layouts; ensured mouse/link modifier paths never carry side bits.
    • Translation now preserves unrelated modifier flags correctly.
  • Refactor

    • Centralized modifier-translation logic for consistent mapping.
  • Tests

    • Added regression and layout-specific composition tests (US, German, Polish, Canadian).
  • Chores

    • CI adds a focused unit-test gate for the Option/Alt regression; updated pinned submodule checksum.

Move GhosttyNSView's NSEvent-flags -> ghostty mods mapping and the
duplicated libghostty translation-mods -> NSEvent.ModifierFlags loop into
free functions (cmuxGhosttyModsFromFlags, cmuxTranslationModifierFlags),
and add a KeyboardLayout.textInputCharacter overload that translates
against a specific input source ID. No behavior change; this provides the
runtime seams for Option/Alt regression coverage (#5993).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 12, 2026 11:05pm
cmux-staging Building Building Preview, Comment Jun 12, 2026 11:05pm

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e7367f0a-35e5-4008-a671-b48840acf398

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds centralized NSEvent-to-libghostty modifier helpers (including right-side bits), applies them across key and mouse event paths, introduces a DEBUG keyboard-layout lookup, adds a focused Option/Alt regression test suite, wires sources into the Xcode project and CI, and updates the ghostty submodule and checksum.

Changes

Option key side-bit and composition handling

Layer / File(s) Summary
Ghostty modifier translation helpers
Sources/GhosttyKeyModifiers.swift
cmuxGhosttyModsFromFlags(modifierFlagsRawValue:) maps NSEvent modifier flags to ghostty_input_mods_e with right-side detection; cmuxGhosttyMouseModsFromFlags(modifierFlagsRawValue:) maps only generic mouse modifiers; cmuxTranslationModifierFlags(original:ghosttyTranslationMods:) reapplies ghostty translation mods to AppKit flags while preserving unsupported flags.
Terminal view key-event refactoring
Sources/GhosttyTerminalView.swift
keyDown(with:), modsFromFlags(_:), ghosttyKeyEvent(for:surface:), and mouse forwarding paths now use the centralized helpers; mouse calls use the mouse-specific mapping to avoid propagating side-bits.
Regression tests and project/CI wiring
cmuxTests/GhosttyOptionAsAltModsTests.swift, cmux.xcodeproj/project.pbxproj, .github/workflows/ci.yml
Adds @Suite GhosttyOptionAsAltModsTests covering sided modifier mapping, mouse regression, translation-flag behavior, and per-layout composition checks; registers new sources/tests in the Xcode project and adds a CI xcodebuild gate step that runs the suite.
Keyboard layout input source support (DEBUG)
Sources/KeyboardLayout.swift
Adds a #if DEBUG overload textInputCharacter(forKeyCode:modifierFlags:inputSourceID:) and installedInputSource(forID:) helper to resolve a specific TIS input source for deterministic composition tests.
Submodule, checksum, and docs note
ghostty, scripts/ghosttykit-checksums.txt, docs/ghostty-fork.md
Bumps the ghostty submodule SHA, pins its xcframework checksum, and documents a merge-note to normalize binding vs stored mouse modifiers when gating link-highlight re-dirtying.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jesstelford

Poem

🐰 I hopped through flags and bits tonight,
split Option left from Option right.
Mouse kept simple, chars can bloom —
tests now guard the typing room.
A tiny hop, a giant byte.

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: implementing sided modifier bits for the Option/Alt key to honor the macos-option-as-alt configuration.
Description check ✅ Passed The PR description comprehensively covers the problem, solution, commit structure, acceptance criteria, and related issues. It follows the template structure with Summary and Testing sections filled in with substantial detail.
Linked Issues check ✅ Passed The PR implements all objectives from the linked issues: distinguishes left/right Option keys by setting GHOSTTY_MODS_*_RIGHT bits from device-dependent flags, preserves non-configured-side Option for character composition, applies side-specific Alt stripping, and provides regression coverage across US, German, Polish Pro, and Canadian-CSA layouts.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue objectives: three NSEvent-to-libghostty modifier translation helper functions, a DEBUG-only KeyboardLayout overload for testing, comprehensive regression tests, CI gating, and necessary project/config file updates. No unrelated changes detected.
Cmux Swift Actor Isolation ✅ Passed Inspected new/modified Swift files: added nonisolated top-level modifier helpers and updated key/mouse translation synchronously in NSView code; no new MainActor/service or unsafe shared Sendable/b...
Cmux Swift Blocking Runtime ✅ Passed Scanned modified Swift code (GhosttyKeyModifiers, GhosttyTerminalView translation/mouse sections, KeyboardLayout, new tests) for blocking/timing primitives (Task.sleep, DispatchSemaphore.wait, main...
Cmux Expensive Synchronous Load ✅ Passed Checked Sources/GhosttyKeyModifiers.swift, Sources/GhosttyTerminalView.swift, and Sources/KeyboardLayout.swift for RestorableAgentSessionIndex.load/sysctl; none found. Added logic is modifier trans...
Cmux Cache Substitution Correctness ✅ Passed Inspected PR-added Swift code paths (modifier translation helpers, event translation in GhosttyTerminalView, DEBUG KeyboardLayout overload); no cache/opportunistic substitution in persistence/histo...
Cmux No Hacky Sleeps ✅ Passed The added CI gate step (Run Option/Alt sided-modifier regression) has no sleep/wait/timeout commands; rule file marks GitHub Actions workflow YAML out of scope.
Cmux Algorithmic Complexity ✅ Passed New mapping/translation helpers only do constant-time bitmask checks and a fixed loop over 4 modifier flags; DEBUG input-source resolution scans once via TISCreateInputSourceList; no scalable colle...
Cmux Swift Concurrency ✅ Passed Scanned PR’s new/modified Swift files and the key translation/mouse sections in GhosttyTerminalView.swift for legacy patterns (DispatchQueue/Task/completion handlers/Combine/async); none found.
Cmux Swift @Concurrent ✅ Passed Rule file requires @concurrent only for nonisolated async cases. New GhosttyKeyModifiers helpers are synchronous nonisolated (no async/@Concurrent), and GhosttyTerminalView keyDown translation chan...
Cmux Swift File And Package Boundaries ✅ Passed Production Swift changes only add 74-line new file plus +35/-66 edits to oversized GhosttyTerminalView; no >400-line files or >250-line growth, and refactor keeps logic coherent.
Cmux Swift Logging ✅ Passed Scanned Sources/GhosttyKeyModifiers.swift, Sources/GhosttyTerminalView.swift, and Sources/KeyboardLayout.swift for print/debugPrint/dump/NSLog and MainActor file-scoped Logger constants; only an #i...
Cmux User-Facing Error Privacy ✅ Passed Reviewed user-facing error privacy rules and scanned the PR’s modified files; no new user-facing alerts/errors/recovery text leaked vendor/provider/env-var details (only generic 'Missing surface' i...
Cmux Full Internationalization ✅ Passed Touched Swift code is modifier-translation logic + DEBUG helpers and tests; no new user-facing Swift UI strings, xcstrings, Info.plist, or web i18n/messages updates detected.
Cmux Swiftui State Layout ✅ Passed Inspected PR-touched Swift files: GhosttyTerminalView changes are NSView keyDown translation (no GeometryReader/Lazy/List or @State/@published additions found); new Swift files and tests contain no...
Cmux Architecture Rethink ✅ Passed Inspected new/changed Swift areas (modifier translation + mouse/KeyboardLayout helpers): no Task.sleep/asyncAfter polling locks or observer/side-channel patterns introduced near new logic; CI gate...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains no NSWindow/NSPanel/NSWindowController/SwiftUI Window/WindowGroup code. Changes are modifier translation utilities, event handler refactoring, keyboard helpers, and test fixtures (expli...
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test/CI/docs/checksum assets; no evidence of local/generated artifacts, DerivedData, screenshots/recordings, temp folders, or dependency checkouts added as...

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5993-option-as-alt

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes macos-option-as-alt = left|right in cmux by forwarding per-side modifier bits from NSEvent's device-dependent flags (NX_DEVICER*KEYMASK) to libghostty as GHOSTTY_MODS_*_RIGHT, which libghostty uses to determine which physical Option key should act as Alt and which should remain available for character composition.

  • Extracts modifier translation into three nonisolated free functions (cmuxGhosttyModsFromFlags, cmuxGhosttyMouseModsFromFlags, cmuxTranslationModifierFlags) replacing duplicated loops; key event paths now carry side bits while mouse/hover paths intentionally strip them to avoid false modifier-change events.
  • Adds a #if DEBUG-guarded textInputCharacter(forKeyCode:modifierFlags:inputSourceID:) seam in KeyboardLayout and a comprehensive GhosttyOptionAsAltModsTests suite covering US, German, Polish Pro, and Canadian-CSA layouts via UCKeyTranslate, gated by a dedicated CI step that runs before the crash-tolerant full test step.

Confidence Score: 5/5

Safe to merge. The change is a narrow NSEvent→libghostty modifier-bit translation fix with no new concurrency, no blocking primitives, no user-facing strings, and a #if DEBUG guard on the test seam. The previous two concerns flagged by review were both addressed in subsequent commits on this branch.

The fix is self-contained: three nonisolated free functions replace duplicated modifier-translation loops, the key/mouse path split is complete and verified, and the #if DEBUG guard on installedInputSource(forID:) matches the file's existing pattern. The regression suite covers side-bit mapping, mouse-path exclusion, translation-flag round-tripping, and four real keyboard layout compositions via UCKeyTranslate. The dedicated CI gate closes the crash-tolerance gap in the full test run.

No files require special attention.

Important Files Changed

Filename Overview
Sources/GhosttyKeyModifiers.swift New 74-line file with three nonisolated free functions that centralize modifier translation; correctly sets GHOSTTY_MODS__RIGHT from NX_DEVICERKEYMASK, and separately provides a mouse path that strips side bits to avoid false modifier-change events.
Sources/GhosttyTerminalView.swift All mouse/hover/link-update paths migrated from modsFromEvent/modsFromFlags to mouseModsFromEvent/mouseModsFromFlags (no side bits); key event and flagsChanged paths retain modsFromEvent (with side bits); translation-mods loop extracted to cmuxTranslationModifierFlags. Migration appears complete.
Sources/KeyboardLayout.swift Adds #if DEBUG-guarded textInputCharacter(forKeyCode:modifierFlags:inputSourceID:) overload and installedInputSource(forID:) for test-only UCKeyTranslate against non-current layouts; matches the file's existing debugInputSourceIdOverride guard pattern.
cmuxTests/GhosttyOptionAsAltModsTests.swift Regression suite with 12 tests: 5 side-bit unit tests, 2 translation-flag tests, 1 mouse-path test, and 4 Option-composition acceptance tests (US/German/Polish/Canadian-CSA via real UCKeyTranslate). Correctly #if DEBUG-gated on the layout-specific overload.
.github/workflows/ci.yml Adds focused -only-testing:cmuxTests/GhosttyOptionAsAltModsTests gate before the crash-tolerant full unit test step, consistent with the existing BrowserSystemProxyMirrorTests gate pattern.
docs/ghostty-fork.md Adds a maintenance note: modsChanged and link-highlight gate in src/Surface.zig must compare binding-normalized mods to avoid re-dirtying the screen when sided modifier bits are held.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[NSEvent keypress / flagsChanged] --> B[modsFromEvent]
    B --> C[cmuxGhosttyModsFromFlags
device-independent + NX_DEVICER*KEYMASK
sets GHOSTTY_MODS_*_RIGHT]
    C --> D[ghostty_surface_key_translation_mods
libghostty checks macos-option-as-alt
left/right via side bits]
    D --> E[cmuxTranslationModifierFlags
map ghostty translation mods
back to NSEvent.ModifierFlags]
    E --> F{Option in translation mods?}
    F -->|Yes - composing side| G[Keep .option → AppKit composes character]
    F -->|No - Alt/Meta side| H[Strip .option → libghostty encodes as Alt]
    A2[NSEvent mouse / hover / flagsChanged mouse_pos] --> B2[mouseModsFromEvent]
    B2 --> C2[cmuxGhosttyMouseModsFromFlags
device-independent only, no side bits]
    C2 --> D2[ghostty_surface_mouse_pos / mouse_button
no false re-dirty on right-side held]
Loading

Reviews (7): Last reviewed commit: "Refresh Swift file length budget for mai..." | Re-trigger Greptile

Comment thread Sources/GhosttyKeyModifiers.swift
Comment thread Sources/KeyboardLayout.swift
austinywang and others added 2 commits June 12, 2026 14:50
Covers the consolidated Option/Alt issue: NSEvent device side bits must map
to GHOSTTY_MODS_*_RIGHT so libghostty can apply macos-option-as-alt =
left|right per physical key, translation flags must keep Option on the
composing side, and Option composition must resolve on US (Opt+; -> ...),
German (Opt+L -> @), Polish Pro (Opt+A -> a-ogonek), and Canadian-CSA
(Opt -> /) layouts.

The four right-side-bit tests fail at this commit by design (two-commit
regression policy): cmux currently maps both physical Option keys to the
generic GHOSTTY_MODS_ALT bit. The follow-up commit adds the side bits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ghostty (#5993)

Set GHOSTTY_MODS_{SHIFT,CTRL,ALT,SUPER}_RIGHT from the NSEvent
device-dependent flags when translating modifiers for libghostty,
mirroring Ghostty.app's ghosttyMods. With the side bits present:

- ghostty_surface_key_translation_mods strips Alt only for the configured
  side, so with macos-option-as-alt = left the right Option key keeps
  composing characters (Opt+; -> ..., right-Opt+L -> @ on German,
  right-Opt+A -> a-ogonek on Polish Pro, AltGr -> / on Canadian-CSA)
  while the left Option sends Alt/Meta - and mirrored for = right.
- The key encoder's legacyAltPrefix and kitty associated-text rules apply
  per physical side instead of treating every Option as the left key.

Closes the gap where cmux captured both Option keys regardless of the
macos-option-as-alt setting while standalone Ghostty honored it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@austinywang austinywang force-pushed the issue-5993-option-as-alt branch from f50f5a5 to 9b2bb43 Compare June 12, 2026 21:51
austinywang and others added 2 commits June 12, 2026 14:59
The inputSourceID-parameterized textInputCharacter overload and its
TISCreateInputSourceList helper exist for the Option-composition
regression suite; keep them out of the release binary (Greptile P2,
matches the existing debugInputSourceIdOverride precedent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang and others added 6 commits June 12, 2026 15:11
With sided modifier mapping, subtracting only the generic .command flag
left NX_DEVICE*CMDKEYMASK bits behind, producing a SUPER_RIGHT-only mod
during command-hover suppression. libghostty stores binding-only mouse
mods, so the side-only value compared unequal on every hover event and
re-dirtied the screen while suppression was active.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
libghostty stores only binding modifiers for mouse state but compares
incoming mods against that stored value, so sided mods on the mouse path
made every event with a held right-side modifier register as a modifier
change and re-dirty the screen (full-row rebuild per mouse move). Split
the boundary: key events keep the side bits macos-option-as-alt needs
(cmuxGhosttyModsFromFlags); mouse, hover, and link updates send
normalized binding bits (cmuxGhosttyMouseModsFromFlags), restoring
cmux's pre-fix mouse behavior. Supersedes the narrower hover-suppression
device-bit fix and locks the boundary with a regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pulls manaflow-ai/ghostty 05c3e2908 (feat-renderer-realized-offscreen):
modsChanged and the key callback's link-highlight gate now compare
binding mods against binding mods, so the sided modifier bits cmux
sends for macos-option-as-alt = left|right no longer register as a
modifier change on every key/cursor event (full-screen re-dirty + link
refresh per keystroke while a right-side modifier is held).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
main commit 11f1f6c (#5651) grew TerminalNotificationStore.swift to
2623 lines and Feed/FeedCoordinator.swift to 1255 without refreshing
the budget, leaving workflow-guard-tests red on main and on any branch
that merges it. Accept that growth here so the merge is buildable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment