aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-23 03:47:34 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-23 03:47:44 +0300
commite7fc85bfdb51f89488430adbfe5bbced3be79c2f (patch)
treec1c0324f28040c50d0075d6407bdf6ab14d1b8c0 /src/client
parent2a20e90a026936d0d5b96823d74e2e4fe13a166f (diff)
fix: preserve kitty printable key releases
refs #1746
Diffstat (limited to 'src/client')
-rw-r--r--src/client/input.rs48
-rw-r--r--src/client/input/windows_vti.rs31
-rw-r--r--src/client/mod.rs13
3 files changed, 81 insertions, 11 deletions
diff --git a/src/client/input.rs b/src/client/input.rs
index 1d557543..c37a15c7 100644
--- a/src/client/input.rs
+++ b/src/client/input.rs
@@ -210,7 +210,7 @@ fn windows_crossterm_reader_loop(
tracing::debug!(event = ?event, "windows control key forwarded as semantic input");
}
- let Some(event) = crate::protocol::ClientInputEvent::from_crossterm(event) else {
+ let Some(event) = windows_crossterm_input_event(event) else {
continue;
};
if event_tx
@@ -226,6 +226,21 @@ fn windows_crossterm_reader_loop(
}
}
+#[cfg(any(windows, test))]
+fn windows_crossterm_input_event(
+ event: crossterm::event::Event,
+) -> Option<crate::protocol::ClientInputEvent> {
+ let event = crate::protocol::ClientInputEvent::from_crossterm(event)?;
+ match event {
+ crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Char(codepoint),
+ modifiers: 0,
+ kind: crate::protocol::ClientKeyKind::Press,
+ } => Some(crate::protocol::ClientInputEvent::Text { codepoint }),
+ event => Some(event),
+ }
+}
+
#[cfg(windows)]
fn windows_event_is_control_key(event: &crossterm::event::Event) -> bool {
use crossterm::event::{Event, KeyModifiers};
@@ -308,11 +323,22 @@ fn windows_client_input_event_from_raw(
event: crate::raw_input::RawInputEvent,
) -> Option<crate::protocol::ClientInputEvent> {
match event {
- crate::raw_input::RawInputEvent::Key(key) => Some(crate::protocol::ClientInputEvent::Key {
- code: crate::protocol::ClientKeyCode::from_crossterm(key.code)?,
- modifiers: key.modifiers.bits(),
- kind: crate::protocol::ClientKeyKind::from_crossterm(key.kind),
- }),
+ crate::raw_input::RawInputEvent::Key(key) if key.is_text_commit => {
+ let crossterm::event::KeyCode::Char(codepoint) = key.code else {
+ return None;
+ };
+ Some(crate::protocol::ClientInputEvent::Text { codepoint })
+ }
+ crate::raw_input::RawInputEvent::Key(key) => {
+ let code = crate::protocol::ClientKeyCode::from_crossterm(key.code)?;
+ let modifiers = key.modifiers.bits();
+ let kind = crate::protocol::ClientKeyKind::from_crossterm(key.kind);
+ Some(crate::protocol::ClientInputEvent::Key {
+ code,
+ modifiers,
+ kind,
+ })
+ }
crate::raw_input::RawInputEvent::Mouse(mouse) => {
Some(crate::protocol::ClientInputEvent::Mouse {
kind: crate::protocol::ClientMouseKind::from_crossterm(mouse.kind)?,
@@ -474,6 +500,16 @@ mod windows_tests {
}
#[test]
+ fn windows_crossterm_printable_press_is_text() {
+ let event = Event::Key(KeyEvent::new(KeyCode::Char('你'), KeyModifiers::empty()));
+
+ assert_eq!(
+ windows_crossterm_input_event(event),
+ Some(crate::protocol::ClientInputEvent::Text { codepoint: '你' })
+ );
+ }
+
+ #[test]
fn windows_ctrl_bracket_starts_raw_escape_sequence() {
let ctrl_bracket = Event::Key(KeyEvent::new(KeyCode::Char('['), KeyModifiers::CONTROL));
assert_eq!(
diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs
index 5742b825..8966112e 100644
--- a/src/client/input/windows_vti.rs
+++ b/src/client/input/windows_vti.rs
@@ -622,6 +622,12 @@ impl WindowsInputMapper {
kind: crate::protocol::ClientKeyKind,
) -> Option<crate::protocol::ClientInputEvent> {
let modifiers = windows_key_modifiers(key.control_key_state);
+ if key.virtual_key_code == 0 {
+ let codepoint = self.utf16_unit_to_char(key.unicode)?;
+ if !codepoint.is_control() {
+ return Some(crate::protocol::ClientInputEvent::Text { codepoint });
+ }
+ }
if modifiers.contains(crossterm::event::KeyModifiers::CONTROL)
&& key.unicode == 0x000a
&& (key.virtual_key_code == 0x4a || key.virtual_scan_code == 0x24)
@@ -1220,6 +1226,25 @@ mod tests {
}
#[test]
+ fn vti_win32_input_mode_marks_ime_commit_as_text() {
+ for control_key_state in [0, 0x0010, 0x0008] {
+ let records = win32_input_mode_encoded_record(WindowsKeyRecord {
+ key_down: true,
+ repeat_count: 1,
+ virtual_key_code: 0,
+ virtual_scan_code: 0,
+ unicode: '你' as u16,
+ control_key_state,
+ });
+
+ assert_eq!(
+ translate(records),
+ vec![crate::protocol::ClientInputEvent::Text { codepoint: '你' }]
+ );
+ }
+ }
+
+ #[test]
fn vti_win32_input_mode_decoded_paste_flag_clears_after_raw_completion() {
let mut records = win32_input_mode_encoded_key_bytes(b"\x1b[200~");
records.extend("one\x1b[201~".chars().map(key_char));
@@ -1694,11 +1719,7 @@ mod tests {
assert_eq!(
translate(records),
vec![
- crate::protocol::ClientInputEvent::Key {
- code: crate::protocol::ClientKeyCode::Char('a'),
- modifiers: 0,
- kind: crate::protocol::ClientKeyKind::Press,
- },
+ crate::protocol::ClientInputEvent::Text { codepoint: 'a' },
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Char('a'),
modifiers: 0,
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 011de937..a22fd227 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -69,6 +69,8 @@ struct ClientState {
blit_encoder: render_ansi::BlitEncoder,
/// Whether host mouse capture is currently active.
mouse_capture_active: bool,
+ /// Whether the host terminal currently reports all keys as Kitty sequences.
+ keyboard_report_all_active: bool,
/// The terminal size we reported to the server in our last Hello/Resize.
reported_size: (u16, u16),
/// Client-local sound playback config, refreshed on server request.
@@ -1290,6 +1292,7 @@ async fn run_client_loop(
let mut state = ClientState {
blit_encoder: render_ansi::BlitEncoder::new(),
mouse_capture_active: config.mouse_capture_active,
+ keyboard_report_all_active: false,
reported_size: (cols, rows),
sound_config: config.sound_config,
kitty_graphics_enabled: config.kitty_graphics_enabled,
@@ -1585,6 +1588,16 @@ async fn run_client_loop(
host_mouse_capture_active.store(desired, Ordering::Release);
}
}
+ ServerMessage::KittyKeyboardReportAll { enabled } => {
+ if enabled != state.keyboard_report_all_active {
+ crate::terminal_modes::set_host_kitty_keyboard_report_all(
+ &mut io::stdout(),
+ enabled,
+ )
+ .map_err(ClientError::ConnectionFailed)?;
+ state.keyboard_report_all_active = enabled;
+ }
+ }
ServerMessage::PrefixInputSource { active } => {
if active {
prefix_input_source.switch_to_ascii();