Merge pull request #191 from l0ng-ai/fix/option-as-meta-under-ime

fix(input): keep Option chords out of the IME when Option is Meta
This commit is contained in:
l0ng-ai
2026-07-26 20:35:43 +08:00
committed by GitHub
4 changed files with 156 additions and 27 deletions
+6
View File
@@ -3,3 +3,9 @@
# config/session/history. Extra args pass through, e.g. `cargo dev -- --foo`.
[alias]
dev = "run -- --config-dir .tty7-dev"
# The gpui fork lives in a fork of the Zed monorepo, and cargo's built-in libgit2
# fetch reliably times out partway through a clone that size. The git CLI handles
# it (resumable, packed transfer), and is present on every platform we build on.
[net]
git-fetch-with-cli = true
Generated
+25 -25
View File
@@ -1369,7 +1369,7 @@ dependencies = [
[[package]]
name = "collections"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"gpui_util",
"indexmap",
@@ -1927,7 +1927,7 @@ dependencies = [
[[package]]
name = "derive_refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"proc-macro2",
"quote",
@@ -3096,7 +3096,7 @@ dependencies = [
[[package]]
name = "gpui"
version = "0.2.2"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"accesskit",
"anyhow",
@@ -3287,7 +3287,7 @@ dependencies = [
[[package]]
name = "gpui_linux"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"accesskit",
"accesskit_unix",
@@ -3338,7 +3338,7 @@ dependencies = [
[[package]]
name = "gpui_macos"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"accesskit",
"accesskit_macos",
@@ -3385,7 +3385,7 @@ dependencies = [
[[package]]
name = "gpui_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -3396,7 +3396,7 @@ dependencies = [
[[package]]
name = "gpui_platform"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"console_error_panic_hook",
"gpui",
@@ -3409,7 +3409,7 @@ dependencies = [
[[package]]
name = "gpui_shared_string"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"schemars",
"serde",
@@ -3419,7 +3419,7 @@ dependencies = [
[[package]]
name = "gpui_util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"anyhow",
"log",
@@ -3428,7 +3428,7 @@ dependencies = [
[[package]]
name = "gpui_web"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"anyhow",
"console_error_panic_hook",
@@ -3452,7 +3452,7 @@ dependencies = [
[[package]]
name = "gpui_wgpu"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"anyhow",
"bytemuck",
@@ -3481,7 +3481,7 @@ dependencies = [
[[package]]
name = "gpui_windows"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"accesskit",
"accesskit_windows",
@@ -3800,7 +3800,7 @@ dependencies = [
[[package]]
name = "http_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"anyhow",
"async-compression",
@@ -3825,7 +3825,7 @@ dependencies = [
[[package]]
name = "http_client_tls"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"rustls",
"rustls-platform-verifier",
@@ -4946,7 +4946,7 @@ checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c"
[[package]]
name = "media"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"anyhow",
"bindgen",
@@ -6044,7 +6044,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "perf"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"collections",
"serde",
@@ -7021,7 +7021,7 @@ dependencies = [
[[package]]
name = "refineable"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"derive_refineable",
]
@@ -7064,7 +7064,7 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "reqwest_client"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"anyhow",
"bytes",
@@ -7589,7 +7589,7 @@ dependencies = [
[[package]]
name = "scheduler"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"async-task",
"backtrace",
@@ -8396,7 +8396,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "sum_tree"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"heapless",
"log",
@@ -9775,7 +9775,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"anyhow",
"async-fs",
@@ -9814,7 +9814,7 @@ dependencies = [
[[package]]
name = "util_macros"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"perf",
"quote",
@@ -11620,7 +11620,7 @@ dependencies = [
[[package]]
name = "zlog"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"anyhow",
"chrono",
@@ -11665,7 +11665,7 @@ dependencies = [
[[package]]
name = "ztracing"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
dependencies = [
"tracing",
"tracing-subscriber",
@@ -11676,7 +11676,7 @@ dependencies = [
[[package]]
name = "ztracing_macro"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed?rev=1d217ee39d381ac101b7cf49d3d22451ac1093fe#1d217ee39d381ac101b7cf49d3d22451ac1093fe"
source = "git+https://github.com/l0ng-ai/zed?branch=tty7#24ed55f4cf1c6f90c2fb4a1db0f33aca949a7f02"
[[package]]
name = "zune-core"
+23
View File
@@ -307,3 +307,26 @@ alacritty_terminal = { opt-level = 3 }
[profile.release]
lto = "thin"
codegen-units = 1
# ---- gpui fork ------------------------------------------------------------
# Our `tty7` branch (cut from the pinned upstream rev, one commit on top) carries
# a single patch: `prefers_ime_for_printable_keys` takes the keystroke, so an
# input handler can answer per key instead of per view. tty7 needs it for
# Option-as-Meta — macOS routes ⌥-chords to the IME whenever a CJK input source
# is active, and without the keystroke there is no way to decline just those
# chords (see `terminal::input::prefers_ime_for_printable_keys`, issue #177).
#
# Patching by source rather than editing the `gpui`/`gpui_platform` pins above is
# deliberate: `gpui-component` declares its own `gpui` from the upstream URL, and
# a plain pin swap would put two incompatible copies of gpui in the tree. `[patch]`
# rewrites the source for every dependent at once, so the fork's sibling crates
# (`gpui_macos`, `gpui_web`, …) come along through their in-repo path deps.
#
# When bumping the upstream rev: rebase the fork's `tty7` branch onto the new rev,
# push, then update the `rev` pins above — the branch here follows automatically.
[patch."https://github.com/zed-industries/zed"]
gpui = { git = "https://github.com/l0ng-ai/zed", branch = "tty7" }
gpui_platform = { git = "https://github.com/l0ng-ai/zed", branch = "tty7" }
gpui_macros = { git = "https://github.com/l0ng-ai/zed", branch = "tty7" }
gpui_web = { git = "https://github.com/l0ng-ai/zed", branch = "tty7" }
reqwest_client = { git = "https://github.com/l0ng-ai/zed", branch = "tty7" }
+102 -2
View File
@@ -6,6 +6,10 @@ use alacritty_terminal::term::TermMode;
use gpui::{App, Bounds, InputHandler, Pixels, UTF16Selection, Window};
use super::view::TerminalView;
// Only the macOS Option/Meta split reads config from this file; elsewhere the import
// would be dead.
#[cfg(target_os = "macos")]
use crate::core::config::Config;
/// The Kitty keyboard-protocol progressive-enhancement flags currently active in
/// the terminal, distilled from `TermMode`. We read them straight off the client's
@@ -150,6 +154,30 @@ pub(super) fn defer_to_ime(ks: &gpui::Keystroke, kitty: KittyFlags) -> bool {
.is_some_and(|ch| !ch.is_empty() && ch.chars().all(|c| c >= '\u{20}' && c != '\u{7f}'))
}
/// Whether an Option chord must be kept away from the IME so the Meta policy in
/// [`reshape_option_keystroke`] can claim it.
///
/// macOS counts ⌥-chords as printable text — ⌥B composes `∫` — so while a CJK input
/// source is active gpui routes them to the IME before the key handler ever runs. The
/// IME commits the composed character and swallows the event, and Option-as-Meta
/// silently does nothing (#177). This is the predicate
/// [`TerminalInputHandler::prefers_ime_for_printable_keys`] answers `false` on.
///
/// Only ⌥ alone (optionally with Shift) counts: ⌘ chords are app shortcuts and Ctrl
/// chords already bypass the IME upstream, and both keep their existing routing.
///
/// With the setting off the chord is text input and the IME is the right owner — it is
/// what makes dead keys (⌥E then E → `é`) compose at all — so this returns `false` and
/// nothing changes.
///
/// Compiled under `test` on every platform so CI covers the rule everywhere, not just
/// on the macOS runner.
#[cfg(any(target_os = "macos", test))]
pub(super) fn meta_chord_bypasses_ime(ks: &gpui::Keystroke, option_as_alt: bool) -> bool {
let m = &ks.modifiers;
option_as_alt && m.alt && !m.platform && !m.control
}
/// Translate a GPUI keystroke into the bytes a PTY expects.
///
/// When the app has enabled the Kitty keyboard protocol (`kitty.active()`) we try
@@ -577,7 +605,23 @@ impl InputHandler for TerminalInputHandler {
false
}
fn prefers_ime_for_printable_keys(&mut self, window: &mut Window, cx: &mut App) -> bool {
// `keystroke` only feeds the macOS Option/Meta split; elsewhere Alt already carries
// Meta and never reaches an IME.
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
fn prefers_ime_for_printable_keys(
&mut self,
keystroke: &gpui::Keystroke,
window: &mut Window,
cx: &mut App,
) -> bool {
// An Option chord under Option-as-Meta belongs to `reshape_option_keystroke`,
// not the IME — see `meta_chord_bypasses_ime`. Answering per keystroke is why
// tty7 carries a gpui patch: upstream asks this once per view, with no key in
// hand, so it cannot say "IME for text, but not for this chord".
#[cfg(target_os = "macos")]
if meta_chord_bypasses_ime(keystroke, cx.global::<Config>().macos_option_as_alt) {
return false;
}
// REPORT_ALL_KEYS_AS_ESC wants every key as `CSI <code>;<mods>[;<text>]u`,
// which only `keystroke_to_bytes` produces — the IME path commits raw
// UTF-8. Keep printable keys on the dispatch path so they get encoded,
@@ -619,7 +663,8 @@ impl InputHandler for TerminalInputHandler {
#[cfg(test)]
mod tests {
use super::{
KittyFlags, defer_to_ime, keystroke_to_bytes, reshape_option_keystroke, tab_bytes,
KittyFlags, defer_to_ime, keystroke_to_bytes, meta_chord_bypasses_ime,
reshape_option_keystroke, tab_bytes,
};
use gpui::{Keystroke, Modifiers};
@@ -1172,6 +1217,61 @@ mod tests {
);
}
/// The routing half of Option-as-Meta (#177): with a CJK input source active,
/// macOS hands ⌥-chords to the IME before the key handler runs, because ⌥B
/// composes printable text. The IME commits `∫` and eats the event, so the
/// reshape above never gets a say — unless the handler declines IME for exactly
/// these chords. Everything else keeps composing.
#[test]
fn meta_chords_skip_the_ime_only_when_option_is_meta() {
let alt = Modifiers {
alt: true,
..Default::default()
};
// The bug: ⌥B with the setting on must reach `on_key_down`, not the IME.
assert!(meta_chord_bypasses_ime(&option_b(), true));
// Shift rides along — ⌥⇧B is still a Meta chord.
let alt_shift = Modifiers {
alt: true,
shift: true,
..Default::default()
};
assert!(meta_chord_bypasses_ime(
&ks(alt_shift, "b", Some("ı")),
true
));
// Setting off: ⌥ is text input, and the IME owns it — this is what makes
// dead keys (⌥E then E → `é`) compose.
assert!(!meta_chord_bypasses_ime(&option_b(), false));
// Plain text is never claimed, on either setting: CJK composition is the
// whole reason the handler prefers the IME in the first place.
assert!(!meta_chord_bypasses_ime(
&ks(Modifiers::default(), "n", Some("n")),
true
));
// ⌘ chords are app shortcuts and ⌃ chords already bypass the IME upstream;
// both keep their existing routing rather than being claimed here.
let cmd_alt = Modifiers {
alt: true,
platform: true,
..Default::default()
};
assert!(!meta_chord_bypasses_ime(&ks(cmd_alt, "b", None), true));
let ctrl_alt = Modifiers {
alt: true,
control: true,
..Default::default()
};
assert!(!meta_chord_bypasses_ime(&ks(ctrl_alt, "b", None), true));
// Named keys carry the alt bit too and take the same route — Alt+Left must
// not be diverted into a composition either.
assert!(meta_chord_bypasses_ime(&ks(alt, "left", None), true));
}
#[test]
fn option_reshape_leaves_named_keys_and_ctrl_chords_alone() {
let alt = Modifiers {