aboutsummaryrefslogtreecommitdiff
path: root/src/config
diff options
context:
space:
mode:
authorChihGodlee <61595521+ChihGodlee@users.noreply.github.com>2026-05-20 20:05:38 +0800
committerCan Celik <ogulcancelik@gmail.com>2026-05-21 03:10:52 +0300
commit6172275252f5f724d874163462f5394800f8d55b (patch)
tree86ff28c99faf9e20b74a75ee0abcd503e44e06db /src/config
parentd1576ade07a7236996869d78f37a4aee00367b42 (diff)
feat(experimental): add reveal_hidden_cursor_for_cjk_ime opt-in for CJK IME tracking
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
Diffstat (limited to 'src/config')
-rw-r--r--src/config/model.rs96
1 files changed, 96 insertions, 0 deletions
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<String>,
+ /// 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 {
@@ -381,6 +429,54 @@ prompt_new_tab_name = false
}
#[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();
assert_eq!(default_config.ui.sidebar_min_width, 18);