From f76d341501a0ec9f6c1b5c1972e6d081595716e4 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Thu, 18 Jun 2026 16:09:45 +0800 Subject: [PATCH] feat(terminal): Chinese / IME input (P3) Implement IME composition support and migrate regular character input to a hidden textarea, matching the xterm.js approach. Key changes: - Add a hidden, absolutely-positioned ` @@ -1402,8 +1517,9 @@ fn point_to_cell( ) } -fn focus_terminal(terminal_ref: NodeRef) { - if let Some(element) = terminal_ref.get_untracked() { +/// Focus the hidden IME textarea so keyboard input and composition go there. +fn focus_ime_textarea(ime_ref: &NodeRef) { + if let Some(element) = ime_ref.get_untracked() { let _ = element.focus(); } } @@ -1430,6 +1546,42 @@ fn copy_current_selection(selection: RwSignal>, core: RwSignal } } +/// Send a string as terminal input, scrolling to the bottom if needed. +fn send_input_data( + data: String, + socket_signal: RwSignal>, + mode_signal: RwSignal, + terminal_ref: NodeRef, + prog_scroll: RwSignal, +) { + if data.is_empty() { + return; + } + let Some(socket) = socket_signal.get_untracked() else { + return; + }; + if !mode_signal.get_untracked().is_live() { + mode_signal.set(ViewMode::Live); + sync_scroll_to_bottom(terminal_ref, prog_scroll); + } + send_terminal_message( + &socket, + &ClientTerminalMessage::Input { data }, + ); +} + +/// Read the current value of the hidden IME textarea. +fn ime_ref_text(ime_ref: &NodeRef) -> Option { + ime_ref.get_untracked().map(|t| t.value()) +} + +/// Clear the hidden IME textarea after sending composed/typed text. +fn clear_ime_textarea(ime_ref: &NodeRef) { + if let Some(t) = ime_ref.get_untracked() { + t.set_value(""); + } +} + // --------------------------------------------------------------------------- // Key mapping // --------------------------------------------------------------------------- @@ -1668,6 +1820,14 @@ fn map_key_to_terminal_input( ) } +/// Whether a keydown event represents a regular printable character that +/// should be handled by the IME textarea's `input` event, rather than sent +/// directly from `keydown`. This lets the browser manage IME composition, +/// dead keys, and upper/lower case before we see the final text. +fn is_regular_text_input(key: &str, ctrl: bool, alt: bool, meta: bool) -> bool { + key.chars().count() == 1 && !ctrl && !alt && !meta +} + #[cfg(test)] mod tests { use super::*; @@ -1854,6 +2014,19 @@ mod tests { assert!(selection_rects(None, 0, 30, cols, cw).is_empty()); } + #[test] + fn is_regular_text_input_detects_printable_keys() { + assert!(is_regular_text_input("a", false, false, false)); + assert!(is_regular_text_input("A", false, false, false)); // Shift handled by browser + assert!(is_regular_text_input(" ", false, false, false)); + assert!(!is_regular_text_input("Enter", false, false, false)); + assert!(!is_regular_text_input("ArrowUp", false, false, false)); + assert!(!is_regular_text_input("a", true, false, false)); // Ctrl+a + assert!(!is_regular_text_input("a", false, true, false)); // Alt+a + assert!(!is_regular_text_input("a", false, false, true)); // Meta+a + assert!(!is_regular_text_input("Dead", false, false, false)); + } + #[test] fn key_to_bytes_table() { // --- basic printable / control keys --- diff --git a/style/tailwind.css b/style/tailwind.css index 29b2dfc..6232064 100644 --- a/style/tailwind.css +++ b/style/tailwind.css @@ -610,13 +610,32 @@ scrollbar-color: rgb(148 163 184 / 60%) transparent; } - .terminal-screen:focus { + .terminal-screen:focus-within { border-color: #38bdf8; box-shadow: inset 0 1px 0 rgb(255 255 255 / 4%), 0 0 0 3px rgb(56 189 248 / 15%); } + .terminal-ime { + position: absolute; + z-index: 100; + opacity: 0; + background: transparent; + color: transparent; + caret-color: transparent; + border: none; + outline: none; + resize: none; + padding: 0; + margin: 0; + overflow: hidden; + pointer-events: auto; + font-family: inherit; + font-size: 1px; + line-height: 1px; + } + .terminal-text { margin: 0; min-height: 100%;