From 6172275252f5f724d874163462f5394800f8d55b Mon Sep 17 00:00:00 2001 From: ChihGodlee <61595521+ChihGodlee@users.noreply.github.com> Date: Wed, 20 May 2026 20:05:38 +0800 Subject: feat(experimental): add reveal_hidden_cursor_for_cjk_ime opt-in for CJK IME tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the focused pane requests ?25l (cursor hidden) but paints its own cursor — common in TUIs like Claude Code, pi, and codex — macOS native input methods stop tracking the candidate-window position. CJK users (Chinese / Japanese / Korean) cannot see candidate windows follow as they type. Setting [experimental] reveal_hidden_cursor_for_cjk_ime = true exposes the focused pane's cursor anchor to the outer terminal regardless of the pane's hidden-cursor request, restoring IME tracking for CJK input. [experimental] cjk_ime_agents is an optional allow-list. When empty, the reveal applies to any focused pane. When non-empty (e.g. ["claude", "codex"]), the reveal only applies if the focused pane's detected agent matches one of the listed names. This lets users enable the workaround only for AI-agent TUIs that paint their own cursor, leaving plain shells and other programs untouched. Accepted names: pi, claude, codex, gemini, cursor, cline, opencode, copilot, kimi, kiro, droid, amp, grok, hermes. Unknown names are ignored. When the pane reports no cursor position (cursor_state returns None for some TUIs), the anchor falls back to the pane's top-left so a stable IME hint is always available. [experimental] cjk_ime_cursor_shape (default "steady_block") controls the DECSCUSR shape rendered for the revealed cursor; six variants are accepted: block, steady_block, underline, steady_underline, bar, steady_bar. Trade-off when enabled: a hardware cursor will be visible in the outer terminal for apps that hide the cursor without painting a replacement. Default false preserves the behavior set in 2c95071. Pair the reveal with cjk_ime_agents to scope it to specific TUIs. Manually verified on macOS 26.5 aarch64 with system Pinyin IME against Claude Code: candidate window tracks cursor with flag enabled, returns to anchored-at-start behavior with flag disabled. refs #149 --- src/config/model.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) (limited to 'src/config') diff --git a/src/config/model.rs b/src/config/model.rs index f0557072..ba52d579 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -209,6 +209,33 @@ pub struct UiConfig { pub sound: SoundConfig, } +/// Cursor shape (DECSCUSR) used for the forced IME anchor. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ImeCursorShape { + Block, + #[default] + SteadyBlock, + Underline, + SteadyUnderline, + Bar, + SteadyBar, +} + +impl ImeCursorShape { + /// Convert to DECSCUSR parameter (1–6). + pub fn to_decscusr(self) -> u8 { + match self { + Self::Block => 1, + Self::SteadyBlock => 2, + Self::Underline => 3, + Self::SteadyUnderline => 4, + Self::Bar => 5, + Self::SteadyBar => 6, + } + } +} + #[derive(Debug, Deserialize)] #[serde(default)] pub struct AdvancedConfig { @@ -224,6 +251,27 @@ pub struct ExperimentalConfig { pub allow_nested: bool, /// Experimental local Kitty graphics rendering for attached clients. Default: false. pub kitty_graphics: bool, + /// Expose the focused pane's cursor anchor to the outer terminal even when + /// the pane requested `?25l`, so macOS native input methods keep tracking + /// the candidate window when TUIs paint their own cursor (Claude Code, pi, + /// codex, etc.). Default: false. + /// + /// When the pane reports no cursor position, falls back to the pane's + /// top-left so a stable IME anchor is always available. + /// + /// Trade-off when enabled: an extra hardware cursor will be visible in the + /// outer terminal for apps that hide the cursor without painting a + /// replacement (vim normal mode, etc.). See #149. + pub reveal_hidden_cursor_for_cjk_ime: bool, + /// Restrict `reveal_hidden_cursor_for_cjk_ime` to focused panes whose + /// detected agent matches one of these names (case-insensitive). Empty + /// list means apply to any focused pane. Unknown agent names are ignored. + /// Accepted names: pi, claude, codex, gemini, cursor, cline, opencode, + /// copilot, kimi, kiro, droid, amp, grok, hermes. Default: empty. + pub cjk_ime_agents: Vec, + /// Cursor shape rendered for the IME anchor when + /// `reveal_hidden_cursor_for_cjk_ime` is enabled. Default: "steady_block". + pub cjk_ime_cursor_shape: ImeCursorShape, } impl Default for KeysConfig { @@ -380,6 +428,54 @@ prompt_new_tab_name = false assert!(!config.ui.prompt_new_tab_name); } + #[test] + fn reveal_hidden_cursor_for_cjk_ime_default_off_and_parse() { + let default_config = Config::default(); + assert!(!default_config.experimental.reveal_hidden_cursor_for_cjk_ime); + + let toml = r#" +[experimental] +reveal_hidden_cursor_for_cjk_ime = true +"#; + let config: Config = toml::from_str(toml).unwrap(); + assert!(config.experimental.reveal_hidden_cursor_for_cjk_ime); + } + + #[test] + fn cjk_ime_cursor_shape_default_steady_block_and_parse() { + let default_config = Config::default(); + assert_eq!( + default_config.experimental.cjk_ime_cursor_shape, + ImeCursorShape::SteadyBlock + ); + + let toml = r#" +[experimental] +cjk_ime_cursor_shape = "bar" +"#; + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!( + config.experimental.cjk_ime_cursor_shape, + ImeCursorShape::Bar + ); + } + + #[test] + fn cjk_ime_agents_default_empty_and_parse() { + let default_config = Config::default(); + assert!(default_config.experimental.cjk_ime_agents.is_empty()); + + let toml = r#" +[experimental] +cjk_ime_agents = ["claude", "codex"] +"#; + let config: Config = toml::from_str(toml).unwrap(); + assert_eq!( + config.experimental.cjk_ime_agents, + vec!["claude".to_string(), "codex".to_string()] + ); + } + #[test] fn sidebar_bounds_default_and_parse() { let default_config = Config::default(); -- cgit v1.2.3