aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/input.rs54
-rw-r--r--src/client/input/windows_vti.rs503
-rw-r--r--src/client/mod.rs2
3 files changed, 475 insertions, 84 deletions
diff --git a/src/client/input.rs b/src/client/input.rs
index 28d38bda..09e0eb92 100644
--- a/src/client/input.rs
+++ b/src/client/input.rs
@@ -281,7 +281,16 @@ fn windows_crossterm_input_event(
code: crate::protocol::ClientKeyCode::Char(codepoint),
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
- } => Some(crate::protocol::ClientInputEvent::Text { codepoint }),
+ source,
+ ..
+ } => Some(crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Char(codepoint),
+ modifiers: 0,
+ kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: Some(codepoint.to_string()),
+ source,
+ }),
event => Some(event),
}
}
@@ -368,20 +377,29 @@ fn windows_client_input_event_from_raw(
event: crate::raw_input::RawInputEvent,
) -> Option<crate::protocol::ClientInputEvent> {
match event {
- 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::Text(text) => Some(
+ crate::protocol::ClientInputEvent::TextCommit(text.into_string()),
+ ),
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);
+ let source = if let Some(bytes) = key.vt_bytes() {
+ crate::protocol::ClientKeySource::Vt {
+ bytes: bytes.to_vec(),
+ }
+ } else if let Some(record) = key.windows_record() {
+ crate::protocol::ClientKeySource::WindowsConsole { record }
+ } else {
+ crate::protocol::ClientKeySource::Synthesized
+ };
Some(crate::protocol::ClientInputEvent::Key {
code,
modifiers,
kind,
+ repeat_count: key.repeat_count,
+ generated_text: key.generated_text.clone(),
+ source,
})
}
crate::raw_input::RawInputEvent::Mouse(mouse) => {
@@ -573,12 +591,19 @@ mod windows_tests {
}
#[test]
- fn windows_crossterm_printable_press_is_text() {
+ fn windows_crossterm_printable_press_keeps_key_semantics_and_text() {
let event = Event::Key(KeyEvent::new(KeyCode::Char('你'), KeyModifiers::empty()));
assert_eq!(
windows_crossterm_input_event(event),
- Some(crate::protocol::ClientInputEvent::Text { codepoint: '你' })
+ Some(crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Char('你'),
+ modifiers: 0,
+ kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: Some("你".to_string()),
+ source: crate::protocol::ClientKeySource::Synthesized,
+ })
);
}
@@ -673,6 +698,9 @@ mod windows_tests {
code: crate::protocol::ClientKeyCode::Char('d'),
modifiers: KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Vt { bytes: vec![4] },
}
);
}
@@ -693,6 +721,11 @@ mod windows_tests {
code: crate::protocol::ClientKeyCode::Up,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Vt {
+ bytes: b"\x1b[A".to_vec()
+ },
}
);
}
@@ -712,6 +745,9 @@ mod windows_tests {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] },
}
);
}
diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs
index 8966112e..c95cbc43 100644
--- a/src/client/input/windows_vti.rs
+++ b/src/client/input/windows_vti.rs
@@ -9,6 +9,7 @@ use tokio::sync::mpsc;
use super::windows_client_input_event_from_raw;
#[cfg(windows)]
use super::ClientLoopEvent;
+use crate::input::WindowsKeyRecord;
#[cfg(windows)]
pub(super) fn raw_console_reader_loop(
@@ -165,16 +166,6 @@ enum WindowsInputRecord {
}
#[derive(Clone, Copy, Debug)]
-struct WindowsKeyRecord {
- key_down: bool,
- repeat_count: u16,
- virtual_key_code: u16,
- virtual_scan_code: u16,
- unicode: u16,
- control_key_state: u32,
-}
-
-#[derive(Clone, Copy, Debug)]
struct WindowsMouseRecord {
x: u16,
y: u16,
@@ -502,6 +493,10 @@ impl WindowsInputMapper {
return Some(vec![0x1b]);
}
+ if record.virtual_scan_code != 0 {
+ return None;
+ }
+
if !record.key_down
|| record.repeat_count.max(1) != 1
|| windows_key_modifiers(record.control_key_state).bits() != 0
@@ -550,6 +545,10 @@ impl WindowsInputMapper {
code,
modifiers: modifiers.bits(),
kind,
+ repeat_count: 1,
+
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
))
}
@@ -574,6 +573,35 @@ impl WindowsInputMapper {
}
let is_alt_code = Self::is_alt_code(key);
+ let carries_native_record = key.repeat_count != 0
+ && key.virtual_key_code != 0
+ && key.virtual_key_code != 0x03
+ && !matches!(key.virtual_key_code, 0xe5 | 0xe7)
+ && !is_alt_code
+ && !(0xd800..=0xdfff).contains(&key.unicode);
+ if carries_native_record {
+ return self
+ .translate_semantic_key_event(key, Self::semantic_key_kind(key, is_alt_code, 0))
+ .map(|event| match event {
+ crate::protocol::ClientInputEvent::Key {
+ code,
+ modifiers,
+ kind,
+ ..
+ } => crate::protocol::ClientInputEvent::Key {
+ code,
+ modifiers,
+ kind,
+ repeat_count: key.repeat_count.max(1),
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::WindowsConsole { record: key },
+ },
+ event => event,
+ })
+ .into_iter()
+ .collect();
+ }
+
(0..key.repeat_count.max(1))
.filter_map(|repeat_idx| {
self.translate_semantic_key_event(
@@ -625,7 +653,9 @@ impl WindowsInputMapper {
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 });
+ return Some(crate::protocol::ClientInputEvent::TextCommit(
+ codepoint.to_string(),
+ ));
}
}
if modifiers.contains(crossterm::event::KeyModifiers::CONTROL)
@@ -637,6 +667,10 @@ impl WindowsInputMapper {
code: crate::protocol::ClientKeyCode::Char('j'),
modifiers: modifiers.bits(),
kind,
+ repeat_count: 1,
+
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
});
}
@@ -656,6 +690,10 @@ impl WindowsInputMapper {
code,
modifiers: modifiers.bits(),
kind,
+ repeat_count: 1,
+
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
});
}
}
@@ -667,10 +705,20 @@ impl WindowsInputMapper {
})
};
- code.map(|code| crate::protocol::ClientInputEvent::Key {
- code,
- modifiers: modifiers.bits(),
- kind,
+ code.map(|code| {
+ let generated_text = matches!(code, crate::protocol::ClientKeyCode::Char(_))
+ .then(|| char::from_u32(key.unicode as u32))
+ .flatten()
+ .filter(|ch| !ch.is_control())
+ .map(|ch| ch.to_string());
+ crate::protocol::ClientInputEvent::Key {
+ code,
+ modifiers: modifiers.bits(),
+ kind,
+ repeat_count: 1,
+ generated_text,
+ source: crate::protocol::ClientKeySource::Synthesized,
+ }
})
}
@@ -1063,7 +1111,7 @@ mod tests {
key_down: true,
repeat_count,
virtual_key_code: vk,
- virtual_scan_code: 0,
+ virtual_scan_code: 1,
unicode: 0,
control_key_state: 0,
})
@@ -1115,6 +1163,38 @@ mod tests {
fn translate(
records: impl IntoIterator<Item = WindowsInputRecord>,
) -> Vec<crate::protocol::ClientInputEvent> {
+ semantic_only(translate_with_provenance(records))
+ }
+
+ fn semantic_only(
+ events: impl IntoIterator<Item = crate::protocol::ClientInputEvent>,
+ ) -> Vec<crate::protocol::ClientInputEvent> {
+ events
+ .into_iter()
+ .map(|event| match event {
+ crate::protocol::ClientInputEvent::Key {
+ code,
+ modifiers,
+ kind,
+ repeat_count,
+ generated_text,
+ ..
+ } => crate::protocol::ClientInputEvent::Key {
+ code,
+ modifiers,
+ kind,
+ repeat_count,
+ generated_text,
+ source: crate::protocol::ClientKeySource::Synthesized,
+ },
+ event => event,
+ })
+ .collect()
+ }
+
+ fn translate_with_provenance(
+ records: impl IntoIterator<Item = WindowsInputRecord>,
+ ) -> Vec<crate::protocol::ClientInputEvent> {
let mut translator = WindowsInputTranslator::default();
records
.into_iter()
@@ -1237,10 +1317,20 @@ mod tests {
control_key_state,
});
- assert_eq!(
- translate(records),
- vec![crate::protocol::ClientInputEvent::Text { codepoint: '你' }]
- );
+ let events = translate(records);
+ match events.as_slice() {
+ [crate::protocol::ClientInputEvent::TextCommit(text)] => {
+ assert_eq!(text, "你");
+ }
+ [crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Char('你'),
+ repeat_count: 1,
+ generated_text: Some(text),
+ source: crate::protocol::ClientKeySource::Synthesized,
+ ..
+ }] => assert_eq!(text, "你"),
+ other => panic!("unexpected VK=0 result: {other:?}"),
+ }
}
}
@@ -1292,6 +1382,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char('c'),
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1304,6 +1398,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char('j'),
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1344,6 +1442,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char(expected),
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}],
"vk={vk:#x} unicode={unicode:#x}"
);
@@ -1362,6 +1464,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] },
}]
);
}
@@ -1378,48 +1484,87 @@ mod tests {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] },
}]
);
}
#[test]
fn vti_physical_escape_key_record_is_immediately_semantic() {
- let mut translator = WindowsInputTranslator::default();
+ let record = WindowsKeyRecord {
+ key_down: true,
+ repeat_count: 1,
+ virtual_key_code: 0x1b,
+ virtual_scan_code: 0x01,
+ unicode: 0x1b,
+ control_key_state: 0,
+ };
assert_eq!(
- translator.translate(key_vk_with_scan_unicode(0x1b, 0x01, '\x1b', 0)),
+ translate_with_provenance([WindowsInputRecord::Key(record)]),
vec![crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::WindowsConsole { record },
}]
);
}
#[test]
- fn vti_repeated_escape_record_stays_semantic() {
+ fn vti_grouped_escape_down_and_up_keep_native_ownership_record() {
+ use crate::protocol::ClientKeyKind::{Press, Release};
+
+ let events = translate_with_provenance([
+ key_vk_with_repeat(0x1b, 3),
+ key_vk_up_with_scan_unicode(0x1b, 1, '\0', 0),
+ ]);
+
assert_eq!(
- translate([key_vk_with_repeat(0x1b, 3)]),
- vec![
- crate::protocol::ClientInputEvent::Key {
- code: crate::protocol::ClientKeyCode::Esc,
- modifiers: 0,
- kind: crate::protocol::ClientKeyKind::Press,
- },
- crate::protocol::ClientInputEvent::Key {
- code: crate::protocol::ClientKeyCode::Esc,
- modifiers: 0,
- kind: crate::protocol::ClientKeyKind::Repeat,
- },
- crate::protocol::ClientInputEvent::Key {
- code: crate::protocol::ClientKeyCode::Esc,
- modifiers: 0,
- kind: crate::protocol::ClientKeyKind::Repeat,
- },
- ]
+ events
+ .iter()
+ .map(|event| match event {
+ crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Esc,
+ kind,
+ repeat_count,
+ source: crate::protocol::ClientKeySource::WindowsConsole { record },
+ ..
+ } => (*kind, *repeat_count, record.key_down, record.repeat_count),
+ event => panic!("unexpected grouped event: {event:?}"),
+ })
+ .collect::<Vec<_>>(),
+ [(Press, 3, true, 3), (Release, 1, false, 1)]
);
}
#[test]
+ fn vti_ctrl_break_record_keeps_semantic_path() {
+ assert!(matches!(
+ translate_with_provenance([key_vk(0x03, 0x0008)]).as_slice(),
+ [crate::protocol::ClientInputEvent::Key { .. }]
+ ));
+ }
+
+ #[test]
+ fn vti_ime_virtual_keys_keep_semantic_path() {
+ for vk in [0xe5, 0xe7] {
+ assert!(matches!(
+ translate_with_provenance([key_vk_with_unicode(vk, 'é', 0)]).as_slice(),
+ [crate::protocol::ClientInputEvent::Key {
+ generated_text: Some(text),
+ source: crate::protocol::ClientKeySource::Synthesized,
+ ..
+ }] if text == "é"
+ ));
+ }
+ }
+
+ #[test]
fn vti_modified_escape_remains_semantic() {
assert_eq!(
translate([key_vk_with_utf16_mods(0x1b, 0, 0x0010)]),
@@ -1427,6 +1572,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: crossterm::event::KeyModifiers::SHIFT.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1445,6 +1594,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1453,23 +1606,14 @@ mod tests {
fn vti_repeated_virtual_key_records_emit_repeats() {
assert_eq!(
translate([key_vk_with_repeat(0x08, 3)]),
- vec![
- crate::protocol::ClientInputEvent::Key {
- code: crate::protocol::ClientKeyCode::Backspace,
- modifiers: 0,
- kind: crate::protocol::ClientKeyKind::Press,
- },
- crate::protocol::ClientInputEvent::Key {
- code: crate::protocol::ClientKeyCode::Backspace,
- modifiers: 0,
- kind: crate::protocol::ClientKeyKind::Repeat,
- },
- crate::protocol::ClientInputEvent::Key {
- code: crate::protocol::ClientKeyCode::Backspace,
- modifiers: 0,
- kind: crate::protocol::ClientKeyKind::Repeat,
- },
- ]
+ vec![crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Backspace,
+ modifiers: 0,
+ kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 3,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
+ }]
);
}
@@ -1481,6 +1625,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::SHIFT.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1493,6 +1641,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char('j'),
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Release,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1505,6 +1657,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::SHIFT.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1518,16 +1674,28 @@ mod tests {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::SHIFT.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::SHIFT.bits(),
kind: crate::protocol::ClientKeyKind::Repeat,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::SHIFT.bits(),
kind: crate::protocol::ClientKeyKind::Repeat,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
]
);
@@ -1589,11 +1757,19 @@ mod tests {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::SHIFT.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::SHIFT.bits(),
kind: crate::protocol::ClientKeyKind::Release,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
]
);
@@ -1612,11 +1788,19 @@ mod tests {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Release,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
]
);
@@ -1633,11 +1817,19 @@ mod tests {
code: crate::protocol::ClientKeyCode::Backspace,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Backspace,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Release,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
]
);
@@ -1656,11 +1848,19 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char('j'),
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Char('j'),
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Release,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
]
);
@@ -1678,11 +1878,19 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char('j'),
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Char('j'),
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Release,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
]
);
@@ -1700,33 +1908,121 @@ mod tests {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Release,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
]
);
}
#[test]
- fn vti_win32_input_mode_printable_key_becomes_char() {
- let records = "\x1b[65;30;97;1;0;1_\x1b[65;30;97;0;0;1_"
- .chars()
- .map(key_char);
+ fn vti_special_key_does_not_emit_incidental_printable_text() {
+ assert_eq!(
+ translate_with_provenance([WindowsInputRecord::Key(WindowsKeyRecord {
+ key_down: true,
+ repeat_count: 0,
+ virtual_key_code: 0x0d,
+ virtual_scan_code: 0,
+ unicode: b'a'.into(),
+ control_key_state: 0,
+ })]),
+ vec![crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Enter,
+ modifiers: 0,
+ kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
+ }]
+ );
+ }
+ #[test]
+ fn vti_win32_input_mode_printable_keys_preserve_physical_records() {
assert_eq!(
- translate(records),
- vec![
- crate::protocol::ClientInputEvent::Text { codepoint: 'a' },
- crate::protocol::ClientInputEvent::Key {
- code: crate::protocol::ClientKeyCode::Char('a'),
- modifiers: 0,
- kind: crate::protocol::ClientKeyKind::Release,
- },
- ]
+ translate(win32_input_mode_encoded_record(WindowsKeyRecord {
+ key_down: true,
+ repeat_count: 1,
+ virtual_key_code: 0,
+ virtual_scan_code: 0,
+ unicode: b'a'.into(),
+ control_key_state: 0,
+ })),
+ vec![crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Char('a'),
+ modifiers: 0,
+ kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: Some("a".into()),
+ source: crate::protocol::ClientKeySource::Synthesized,
+ }]
);
+
+ use crate::protocol::ClientKeyKind::{Press, Release};
+ for repeat_count in [1, 3] {
+ let mut records = win32_input_mode_encoded_record(WindowsKeyRecord {
+ key_down: true,
+ repeat_count,
+ virtual_key_code: 0x41,
+ virtual_scan_code: 30,
+ unicode: b'a'.into(),
+ control_key_state: 0,
+ });
+ records.extend(win32_input_mode_encoded_record(WindowsKeyRecord {
+ key_down: false,
+ repeat_count: 1,
+ virtual_key_code: 0x41,
+ virtual_scan_code: 30,
+ unicode: b'a'.into(),
+ control_key_state: 0,
+ }));
+
+ assert_eq!(
+ translate_with_provenance(records)
+ .iter()
+ .map(|event| match event {
+ crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Char('a'),
+ modifiers: 0,
+ kind,
+ repeat_count: event_repeat_count,
+ generated_text: None,
+ source:
+ crate::protocol::ClientKeySource::WindowsConsole {
+ record:
+ WindowsKeyRecord {
+ repeat_count: record_repeat_count,
+ virtual_key_code: 0x41,
+ virtual_scan_code: 30,
+ unicode,
+ control_key_state: 0,
+ key_down,
+ },
+ },
+ } if *unicode == u16::from(b'a') => {
+ (*kind, *event_repeat_count, *key_down, *record_repeat_count)
+ }
+ event => panic!("unexpected printable key event: {event:?}"),
+ })
+ .collect::<Vec<_>>(),
+ [
+ (Press, repeat_count, true, repeat_count),
+ (Release, 1, false, 1)
+ ],
+ "repeat_count={repeat_count}"
+ );
+ }
}
#[test]
@@ -1817,20 +2113,33 @@ mod tests {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::WindowsConsole {
+ record: WindowsKeyRecord {
+ key_down: true,
+ repeat_count: 1,
+ virtual_key_code: 0x1b,
+ virtual_scan_code: 0x02,
+ unicode: 0,
+ control_key_state: 0,
+ }
+ },
}]
);
}
#[test]
fn vti_win32_input_mode_physical_escape_is_immediately_semantic() {
- let records = win32_input_mode_encoded_record(WindowsKeyRecord {
+ let record = WindowsKeyRecord {
key_down: true,
repeat_count: 1,
virtual_key_code: 0x1b,
virtual_scan_code: 0x01,
unicode: 0x1b,
control_key_state: 0,
- });
+ };
+ let records = win32_input_mode_encoded_record(record);
let mut translator = WindowsInputTranslator::default();
assert_eq!(
@@ -1842,6 +2151,9 @@ mod tests {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::WindowsConsole { record },
}]
);
}
@@ -1868,6 +2180,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Up,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1882,6 +2198,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] },
}]
);
}
@@ -1897,11 +2217,19 @@ mod tests {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Vt { bytes: vec![0x1b] },
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Enter,
modifiers: crossterm::event::KeyModifiers::SHIFT.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
]
);
@@ -1912,17 +2240,25 @@ mod tests {
let mut translator = WindowsInputTranslator::default();
assert!(translator.translate(key_char('\x1b')).is_empty());
assert_eq!(
- translator.translate(key_vk(0x26, 0)),
+ semantic_only(translator.translate(key_vk(0x26, 0))),
vec![
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Esc,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
crate::protocol::ClientInputEvent::Key {
code: crate::protocol::ClientKeyCode::Up,
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
},
]
);
@@ -1936,6 +2272,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char('c'),
modifiers: crossterm::event::KeyModifiers::CONTROL.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1948,6 +2288,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Up,
modifiers: crossterm::event::KeyModifiers::ALT.bits(),
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1960,6 +2304,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char('@'),
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1972,6 +2320,9 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char('é'),
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+ repeat_count: 1,
+ generated_text: Some("é".into()),
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
@@ -1989,6 +2340,10 @@ mod tests {
code: crate::protocol::ClientKeyCode::Char('🙂'),
modifiers: 0,
kind: crate::protocol::ClientKeyKind::Press,
+
+ repeat_count: 1,
+ generated_text: None,
+ source: crate::protocol::ClientKeySource::Synthesized,
}]
);
}
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 52fd6dd6..d7fcdf71 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -1841,7 +1841,7 @@ fn should_bridge_clipboard_image_paste(
events.as_slice(),
[crate::raw_input::RawInputEvent::Key(key)]
if key.kind == crossterm::event::KeyEventKind::Press
- && crate::config::terminal_key_matches_combo(*key, remote_image_paste_key)
+ && crate::config::terminal_key_matches_combo(key, remote_image_paste_key)
)
}