aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-23 00:01:57 +0300
committerGitHub <noreply@github.com>2026-08-23 00:01:57 +0300
commit1d604329b0ded93d18fd6b27f72c7473d1fee20f (patch)
tree2ac419b930b57c232b3e7bc8921beebfa1b997bb /src/app/input
parentc714e4c20e3272c21722a6f083fc2d592c244385 (diff)
fix(windows): preserve non-us shifted text (#3055)
* fix(windows): preserve non-us shifted text refs #3045 * fix(windows): retain shifted key repeat lifecycle refs #3045
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/lease.rs94
1 files changed, 92 insertions, 2 deletions
diff --git a/src/app/input/lease.rs b/src/app/input/lease.rs
index de118334..6724b804 100644
--- a/src/app/input/lease.rs
+++ b/src/app/input/lease.rs
@@ -57,7 +57,11 @@ impl InputLeaseTable {
lease_key: &InputLeaseKey,
key: TerminalKey,
) -> TerminalKey {
- if key.kind != crossterm::event::KeyEventKind::Press || key.generated_text.is_some() {
+ // Generated text is normally stateless, but a native physical key still owns
+ // press/repeat/release lifecycle even when it carries a layout result.
+ if key.kind != crossterm::event::KeyEventKind::Press
+ || (key.generated_text.is_some() && !key.has_physical_identity())
+ {
return key;
}
if key.has_physical_identity() && self.leases.contains_key(lease_key) {
@@ -76,7 +80,7 @@ impl InputLeaseTable {
resulting_context: Option<&TerminalInputContext>,
target: Option<TerminalInputTarget>,
) -> RepeatPlan {
- if key.generated_text.is_some() {
+ if key.generated_text.is_some() && !key.has_physical_identity() {
return RepeatPlan::Ignore;
}
if let Some(target) = target {
@@ -253,6 +257,19 @@ mod tests {
}
}
+ fn physical_generated_slash(repeat_count: u16) -> TerminalKey {
+ TerminalKey::new(KeyCode::Char('/'), KeyModifiers::SHIFT)
+ .with_generated_text(Some("/".to_owned()))
+ .with_windows_record(crate::input::WindowsKeyRecord {
+ key_down: true,
+ repeat_count,
+ virtual_key_code: 0x37,
+ virtual_scan_code: 0x08,
+ unicode: u16::from(b'/'),
+ control_key_state: 0x0010,
+ })
+ }
+
#[test]
fn remove_source_returns_forwarded_and_discards_consumed_leases() {
let key = TerminalKey::new(KeyCode::Esc, KeyModifiers::empty());
@@ -333,6 +350,79 @@ mod tests {
}
#[test]
+ fn physical_generated_text_keeps_native_repeat_lifecycle() {
+ let key = physical_generated_slash(3);
+ let lease_key = InputLeaseKey::new(7, &key);
+ let context = TerminalInputContext::Pane;
+ let forwarded_target = target();
+ let mut leases = InputLeaseTable::default();
+
+ assert_eq!(
+ leases.normalize_press(&lease_key, key.clone()).kind,
+ crossterm::event::KeyEventKind::Press
+ );
+ assert!(matches!(
+ leases.complete_press(
+ lease_key,
+ &key,
+ Some(&context),
+ Some(&context),
+ Some(forwarded_target.clone()),
+ ),
+ RepeatPlan::Ignore
+ ));
+ assert!(leases.contains(&lease_key));
+
+ let repeated = key.with_repeat_count(1);
+ let repeated = leases.normalize_press(&lease_key, repeated);
+ assert_eq!(repeated.kind, crossterm::event::KeyEventKind::Repeat);
+ assert!(matches!(
+ leases.plan_repeat(lease_key, &repeated, Some(&context)),
+ RepeatPlan::Forwarded(target) if target == forwarded_target
+ ));
+ assert!(leases.remove_forwarded(&lease_key).is_some());
+ }
+
+ #[test]
+ fn consumed_grouped_physical_generated_text_reprocesses_repeats() {
+ let key = physical_generated_slash(3);
+ let lease_key = InputLeaseKey::new(7, &key);
+ let context = TerminalInputContext::Pane;
+ let mut leases = InputLeaseTable::default();
+
+ assert!(matches!(
+ leases.complete_press(lease_key, &key, Some(&context), Some(&context), None),
+ RepeatPlan::Reprocess {
+ context: TerminalInputContext::Pane,
+ repetitions: 2,
+ tracked: true,
+ }
+ ));
+ }
+
+ #[test]
+ fn semantic_generated_text_remains_untracked() {
+ let key = TerminalKey::new(KeyCode::Char('/'), KeyModifiers::SHIFT)
+ .with_generated_text(Some("/".to_owned()))
+ .with_repeat_count(3);
+ let lease_key = InputLeaseKey::new(7, &key);
+ let context = TerminalInputContext::Pane;
+ let mut leases = InputLeaseTable::default();
+
+ assert!(matches!(
+ leases.complete_press(
+ lease_key,
+ &key,
+ Some(&context),
+ Some(&context),
+ Some(target())
+ ),
+ RepeatPlan::Ignore
+ ));
+ assert!(leases.is_empty());
+ }
+
+ #[test]
fn new_semantic_press_recomputes_consumed_repeat_disposition() {
let key = TerminalKey::new(KeyCode::Esc, KeyModifiers::empty()).with_repeat_count(3);
let lease_key = InputLeaseKey::new(7, &key);