use std::collections::HashMap; use std::hash::Hash; use super::{KeyIdentity, TerminalKey}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub(crate) struct InputLeaseKey { source: Source, identity: KeyIdentity, } impl InputLeaseKey { pub(crate) fn new(source: Source, key: &TerminalKey) -> Self { Self { source, identity: key.identity(), } } } #[derive(Debug, PartialEq, Eq)] pub(crate) struct ForwardedInputLease { pub(crate) target: Target, pub(crate) key: TerminalKey, } #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum ConsumedInputLease { ReprocessRepeats(Context), SuppressRepeats, } #[derive(Debug, PartialEq, Eq)] pub(crate) enum InputLease { Forwarded(ForwardedInputLease), Consumed(ConsumedInputLease), } pub(crate) enum RepeatPlan { Forwarded(Target), Reprocess { context: Context, repetitions: u16, tracked: bool, }, Ignore, } pub(crate) struct InputLeaseTable { leases: HashMap, InputLease>, } impl Default for InputLeaseTable { fn default() -> Self { Self { leases: HashMap::new(), } } } impl InputLeaseTable where Source: Copy + Eq + Hash, Context: Clone + Eq, Target: Clone + Eq, { pub(crate) fn normalize_press( &mut self, lease_key: &InputLeaseKey, key: TerminalKey, ) -> TerminalKey { 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) { key.with_kind(crossterm::event::KeyEventKind::Repeat) } else { self.leases.remove(lease_key); key } } pub(crate) fn complete_press( &mut self, lease_key: InputLeaseKey, key: &TerminalKey, initial_context: Option<&Context>, resulting_context: Option<&Context>, target: Option, ) -> RepeatPlan { if key.generated_text.is_some() && !key.has_physical_identity() { return RepeatPlan::Ignore; } if let Some(target) = target { self.insert_forwarded(lease_key, target, key.clone()); return RepeatPlan::Ignore; } if !self.leases.contains_key(&lease_key) { let disposition = match (initial_context, resulting_context) { (Some(initial), Some(resulting)) if initial == resulting => { ConsumedInputLease::ReprocessRepeats(initial.clone()) } _ => ConsumedInputLease::SuppressRepeats, }; self.insert_consumed(lease_key, disposition); } match self.leases.get(&lease_key) { Some(InputLease::Consumed(ConsumedInputLease::ReprocessRepeats(context))) if key.repeat_count > 1 => { RepeatPlan::Reprocess { context: context.clone(), repetitions: key.repeat_count - 1, tracked: true, } } _ => RepeatPlan::Ignore, } } pub(crate) fn plan_repeat( &mut self, lease_key: InputLeaseKey, key: &TerminalKey, current_context: Option<&Context>, ) -> RepeatPlan { match self.leases.get(&lease_key) { Some(InputLease::Forwarded(lease)) => { return RepeatPlan::Forwarded(lease.target.clone()); } Some(InputLease::Consumed(ConsumedInputLease::ReprocessRepeats(context))) if current_context == Some(context) => { return RepeatPlan::Reprocess { context: context.clone(), repetitions: key.repeat_count, tracked: true, }; } Some(InputLease::Consumed(ConsumedInputLease::ReprocessRepeats(_))) => { self.insert_consumed(lease_key, ConsumedInputLease::SuppressRepeats); return RepeatPlan::Ignore; } Some(InputLease::Consumed(ConsumedInputLease::SuppressRepeats)) => { return RepeatPlan::Ignore; } None => {} } match current_context { Some(context) => RepeatPlan::Reprocess { context: context.clone(), repetitions: key.repeat_count, tracked: false, }, None => RepeatPlan::Ignore, } } pub(crate) fn reprocess_allowed( &mut self, lease_key: InputLeaseKey, expected_context: &Context, current_context: Option<&Context>, tracked: bool, ) -> bool { let allowed = current_context == Some(expected_context); if tracked && !allowed { self.insert_consumed(lease_key, ConsumedInputLease::SuppressRepeats); } allowed } pub(crate) fn remove_forwarded( &mut self, key: &InputLeaseKey, ) -> Option> { match self.leases.remove(key) { Some(InputLease::Forwarded(lease)) => Some(lease), Some(InputLease::Consumed(_)) | None => None, } } #[cfg(test)] pub(crate) fn contains(&self, key: &InputLeaseKey) -> bool { self.leases.contains_key(key) } pub(crate) fn insert_forwarded( &mut self, key: InputLeaseKey, target: Target, original: TerminalKey, ) { self.leases.insert( key, InputLease::Forwarded(ForwardedInputLease { target, key: original, }), ); } pub(crate) fn insert_consumed( &mut self, key: InputLeaseKey, disposition: ConsumedInputLease, ) { self.leases.insert(key, InputLease::Consumed(disposition)); } pub(crate) fn remove( &mut self, key: &InputLeaseKey, ) -> Option> { self.leases.remove(key) } pub(crate) fn remove_source(&mut self, source: Source) -> Vec> { let keys = self .leases .keys() .filter(|key| key.source == source) .copied() .collect::>(); self.remove_keys(keys) } pub(crate) fn remove_target(&mut self, target: &Target) -> Vec> { let keys = self .leases .iter() .filter_map(|(key, lease)| match lease { InputLease::Forwarded(lease) if &lease.target == target => Some(*key), InputLease::Forwarded(_) | InputLease::Consumed(_) => None, }) .collect::>(); self.remove_keys(keys) } fn remove_keys( &mut self, keys: impl IntoIterator>, ) -> Vec> { keys.into_iter() .filter_map(|key| match self.leases.remove(&key) { Some(InputLease::Forwarded(lease)) => Some(lease), Some(InputLease::Consumed(_)) | None => None, }) .collect() } #[cfg(test)] pub(crate) fn is_empty(&self) -> bool { self.leases.is_empty() } #[cfg(test)] pub(crate) fn len(&self) -> usize { self.leases.len() } } #[cfg(test)] mod tests { use crossterm::event::{KeyCode, KeyModifiers}; use super::*; #[derive(Clone, Debug, PartialEq, Eq)] enum Context { Pane, } type Leases = InputLeaseTable; 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()); let forwarded = InputLeaseKey::new(7, &key); let consumed = InputLeaseKey::new( 7, &TerminalKey::new(KeyCode::Char('c'), KeyModifiers::CONTROL), ); let other_source = InputLeaseKey::new(8, &key); let mut leases = Leases::default(); leases.insert_forwarded(forwarded, 10, key.clone()); leases.insert_consumed(consumed, ConsumedInputLease::SuppressRepeats); leases.insert_forwarded(other_source, 11, key.clone()); assert_eq!( leases.remove_source(7), vec![ForwardedInputLease { target: 10, key }] ); assert_eq!(leases.len(), 1); assert!(leases.contains(&other_source)); } #[test] fn remove_target_closes_only_forwarded_leases_for_that_target() { let key = TerminalKey::new(KeyCode::Esc, KeyModifiers::empty()); let removed_key = InputLeaseKey::new(7, &key); let retained_key = InputLeaseKey::new(8, &key); let mut leases = Leases::default(); leases.insert_forwarded(removed_key, 10, key.clone()); leases.insert_forwarded(retained_key, 11, key.clone()); assert_eq!( leases.remove_target(&10), vec![ForwardedInputLease { target: 10, key }] ); assert_eq!(leases.len(), 1); assert!(leases.contains(&retained_key)); } #[test] fn duplicate_physical_press_normalizes_for_forwarded_and_consumed_leases() { let record = crate::input::WindowsKeyRecord { key_down: true, repeat_count: 1, virtual_key_code: 65, virtual_scan_code: 30, unicode: 97, control_key_state: 0, }; let physical = TerminalKey::new(KeyCode::Char('a'), KeyModifiers::empty()).with_windows_record(record); let lease_key = InputLeaseKey::new(7, &physical); let mut leases = Leases::default(); assert_eq!( leases.normalize_press(&lease_key, physical.clone()).kind, crossterm::event::KeyEventKind::Press ); leases.insert_consumed(lease_key, ConsumedInputLease::SuppressRepeats); assert_eq!( leases.normalize_press(&lease_key, physical.clone()).kind, crossterm::event::KeyEventKind::Repeat ); leases.insert_forwarded(lease_key, 10, physical.clone()); assert_eq!( leases.normalize_press(&lease_key, physical).kind, crossterm::event::KeyEventKind::Repeat ); } #[test] fn physical_generated_text_keeps_native_repeat_lifecycle() { let key = physical_generated_slash(3); let lease_key = InputLeaseKey::new(7, &key); let context = Context::Pane; let mut leases = Leases::default(); assert!(matches!( leases.complete_press(lease_key, &key, Some(&context), Some(&context), Some(10),), RepeatPlan::Ignore )); let repeated = leases.normalize_press(&lease_key, key.with_repeat_count(1)); assert_eq!(repeated.kind, crossterm::event::KeyEventKind::Repeat); assert!(matches!( leases.plan_repeat(lease_key, &repeated, Some(&context)), RepeatPlan::Forwarded(10) )); 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 = Context::Pane; let mut leases = Leases::default(); assert!(matches!( leases.complete_press(lease_key, &key, Some(&context), Some(&context), None), RepeatPlan::Reprocess { context: Context::Pane, repetitions: 2, tracked: true, } )); } #[test] fn forwarded_semantic_generated_text_has_no_release_lease() { 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 = Context::Pane; let mut leases = Leases::default(); assert!(matches!( leases.complete_press(lease_key, &key, Some(&context), Some(&context), Some(10)), RepeatPlan::Ignore )); assert_eq!(leases.remove_forwarded(&lease_key), None); } #[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); let context = Context::Pane; let mut leases = Leases::default(); leases.insert_consumed(lease_key, ConsumedInputLease::SuppressRepeats); let key = leases.normalize_press(&lease_key, key); assert!(matches!( leases.complete_press(lease_key, &key, Some(&context), Some(&context), None), RepeatPlan::Reprocess { context: Context::Pane, repetitions: 2, tracked: true, } )); } #[test] fn physical_and_semantic_identities_do_not_collide() { let record = crate::input::WindowsKeyRecord { key_down: true, repeat_count: 1, virtual_key_code: 65, virtual_scan_code: 30, unicode: 97, control_key_state: 0, }; let physical = TerminalKey::new(KeyCode::Char('a'), KeyModifiers::empty()).with_windows_record(record); let semantic = TerminalKey::new(KeyCode::Char('a'), KeyModifiers::empty()); assert_ne!( InputLeaseKey::new(7, &physical), InputLeaseKey::new(7, &semantic) ); } }