aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/input
diff options
context:
space:
mode:
authorKirill Chibisov <contact@kchibisov.com>2023-12-06 09:26:07 +0400
committerGitHub <noreply@github.com>2023-12-06 09:26:07 +0400
commitcb03806e2ab85674c45e87e1bb24dfe2fd1a918c (patch)
tree3561fc6785281fb3a963c199fe9a12df4007bed7 /alacritty/src/input
parent7c9d9f3b166f2aade76d35408b5acb5d3ccd1c94 (diff)
downloadr-alacritty-cb03806e2ab85674c45e87e1bb24dfe2fd1a918c.tar.gz
r-alacritty-cb03806e2ab85674c45e87e1bb24dfe2fd1a918c.tar.bz2
r-alacritty-cb03806e2ab85674c45e87e1bb24dfe2fd1a918c.zip
Implement kitty's keyboard protocol
The protocol enables robust key reporting for the applications, so they could bind more keys and the user won't have collisions with the normal control keys. Links: https://sw.kovidgoyal.net/kitty/keyboard-protocol Fixes #6378.
Diffstat (limited to 'alacritty/src/input')
-rw-r--r--alacritty/src/input/keyboard.rs584
-rw-r--r--alacritty/src/input/mod.rs1503
2 files changed, 2087 insertions, 0 deletions
diff --git a/alacritty/src/input/keyboard.rs b/alacritty/src/input/keyboard.rs
new file mode 100644
index 00000000..94633cb1
--- /dev/null
+++ b/alacritty/src/input/keyboard.rs
@@ -0,0 +1,584 @@
+use std::borrow::Cow;
+use std::mem;
+
+use winit::event::{ElementState, KeyEvent};
+#[cfg(target_os = "macos")]
+use winit::keyboard::ModifiersKeyState;
+use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey};
+#[cfg(target_os = "macos")]
+use winit::platform::macos::OptionAsAlt;
+
+use alacritty_terminal::event::EventListener;
+use alacritty_terminal::term::TermMode;
+use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
+
+use crate::config::{Action, BindingKey, BindingMode};
+use crate::event::TYPING_SEARCH_DELAY;
+use crate::input::{ActionContext, Execute, Processor};
+use crate::scheduler::{TimerId, Topic};
+
+impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
+ /// Process key input.
+ pub fn key_input(&mut self, key: KeyEvent) {
+ // IME input will be applied on commit and shouldn't trigger key bindings.
+ if self.ctx.display().ime.preedit().is_some() {
+ return;
+ }
+
+ let mode = *self.ctx.terminal().mode();
+ let mods = self.ctx.modifiers().state();
+
+ if key.state == ElementState::Released {
+ self.key_release(key, mode, mods);
+ return;
+ }
+
+ let text = key.text_with_all_modifiers().unwrap_or_default();
+
+ // All key bindings are disabled while a hint is being selected.
+ if self.ctx.display().hint_state.active() {
+ for character in text.chars() {
+ self.ctx.hint_input(character);
+ }
+ return;
+ }
+
+ // First key after inline search is captured.
+ let inline_state = self.ctx.inline_search_state();
+ if mem::take(&mut inline_state.char_pending) {
+ if let Some(c) = text.chars().next() {
+ inline_state.character = Some(c);
+
+ // Immediately move to the captured character.
+ self.ctx.inline_search_next();
+ }
+
+ // Ignore all other characters in `text`.
+ return;
+ }
+
+ // Reset search delay when the user is still typing.
+ self.reset_search_delay();
+
+ // Key bindings suppress the character input.
+ if self.process_key_bindings(&key) {
+ return;
+ }
+
+ if self.ctx.search_active() {
+ for character in text.chars() {
+ self.ctx.search_input(character);
+ }
+
+ return;
+ }
+
+ // Vi mode on its own doesn't have any input, the search input was done before.
+ if mode.contains(TermMode::VI) {
+ return;
+ }
+
+ let build_key_sequence = Self::should_build_sequence(&key, text, mode, mods);
+
+ let bytes = if build_key_sequence {
+ build_sequence(key, mods, mode)
+ } else {
+ let mut bytes = Vec::with_capacity(text.len() + 1);
+ if self.alt_send_esc() && text.len() == 1 {
+ bytes.push(b'\x1b');
+ }
+
+ bytes.extend_from_slice(text.as_bytes());
+ bytes
+ };
+
+ // Write only if we have something to write.
+ if !bytes.is_empty() {
+ self.ctx.on_terminal_input_start();
+ self.ctx.write_to_pty(bytes);
+ }
+ }
+
+ /// Check whether we should try to build escape sequence for the [`KeyEvent`].
+ fn should_build_sequence(
+ key: &KeyEvent,
+ text: &str,
+ mode: TermMode,
+ mods: ModifiersState,
+ ) -> bool {
+ if mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) {
+ true
+ } else if mode.contains(TermMode::DISAMBIGUATE_ESC_CODES) {
+ let on_numpad = key.location == KeyLocation::Numpad;
+ let is_escape = key.logical_key == Key::Named(NamedKey::Escape);
+ is_escape || (!mods.is_empty() && mods != ModifiersState::SHIFT) || on_numpad
+ } else {
+ // `Delete` key always has text attached to it, but it's a named key, thus needs to be
+ // excluded here as well.
+ text.is_empty() || key.logical_key == Key::Named(NamedKey::Delete)
+ }
+ }
+
+ /// Whether we should send `ESC` due to `Alt` being pressed.
+ #[cfg(not(target_os = "macos"))]
+ fn alt_send_esc(&mut self) -> bool {
+ self.ctx.modifiers().state().alt_key()
+ }
+
+ #[cfg(target_os = "macos")]
+ fn alt_send_esc(&mut self) -> bool {
+ let option_as_alt = self.ctx.config().window.option_as_alt();
+ self.ctx.modifiers().state().alt_key()
+ && (option_as_alt == OptionAsAlt::Both
+ || (option_as_alt == OptionAsAlt::OnlyLeft
+ && self.ctx.modifiers().lalt_state() == ModifiersKeyState::Pressed)
+ || (option_as_alt == OptionAsAlt::OnlyRight
+ && self.ctx.modifiers().ralt_state() == ModifiersKeyState::Pressed))
+ }
+
+ /// Attempt to find a binding and execute its action.
+ ///
+ /// The provided mode, mods, and key must match what is allowed by a binding
+ /// for its action to be executed.
+ fn process_key_bindings(&mut self, key: &KeyEvent) -> bool {
+ let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active());
+ let mods = self.ctx.modifiers().state();
+
+ // Don't suppress char if no bindings were triggered.
+ let mut suppress_chars = None;
+
+ for i in 0..self.ctx.config().key_bindings().len() {
+ let binding = &self.ctx.config().key_bindings()[i];
+
+ // We don't want the key without modifier, because it means something else most of
+ // the time. However what we want is to manually lowercase the character to account
+ // for both small and capital letters on regular characters at the same time.
+ let logical_key = if let Key::Character(ch) = key.logical_key.as_ref() {
+ Key::Character(ch.to_lowercase().into())
+ } else {
+ key.logical_key.clone()
+ };
+
+ let key = match (&binding.trigger, logical_key) {
+ (BindingKey::Scancode(_), _) => BindingKey::Scancode(key.physical_key),
+ (_, code) => BindingKey::Keycode { key: code, location: key.location.into() },
+ };
+
+ if binding.is_triggered_by(mode, mods, &key) {
+ // Pass through the key if any of the bindings has the `ReceiveChar` action.
+ *suppress_chars.get_or_insert(true) &= binding.action != Action::ReceiveChar;
+
+ // Binding was triggered; run the action.
+ binding.action.clone().execute(&mut self.ctx);
+ }
+ }
+
+ suppress_chars.unwrap_or(false)
+ }
+
+ /// Handle key release.
+ fn key_release(&mut self, key: KeyEvent, mode: TermMode, mods: ModifiersState) {
+ if !mode.contains(TermMode::REPORT_EVENT_TYPES)
+ || mode.contains(TermMode::VI)
+ || self.ctx.search_active()
+ || self.ctx.display().hint_state.active()
+ {
+ return;
+ }
+
+ let bytes: Cow<'static, [u8]> = match key.logical_key.as_ref() {
+ // NOTE: Echo the key back on release to follow kitty/foot behavior. When
+ // KEYBOARD_REPORT_ALL_KEYS_AS_ESC is used, we build proper escapes for
+ // the keys below.
+ _ if mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) => {
+ build_sequence(key, mods, mode).into()
+ },
+ // Winit uses different keys for `Backspace` so we expliictly specify the
+ // values, instead of using what was passed to us from it.
+ Key::Named(NamedKey::Tab) => [b'\t'].as_slice().into(),
+ Key::Named(NamedKey::Enter) => [b'\r'].as_slice().into(),
+ Key::Named(NamedKey::Backspace) => [b'\x7f'].as_slice().into(),
+ Key::Named(NamedKey::Escape) => [b'\x1b'].as_slice().into(),
+ _ => build_sequence(key, mods, mode).into(),
+ };
+
+ self.ctx.write_to_pty(bytes);
+ }
+
+ /// Reset search delay.
+ fn reset_search_delay(&mut self) {
+ if self.ctx.search_active() {
+ let timer_id = TimerId::new(Topic::DelayedSearch, self.ctx.window().id());
+ let scheduler = self.ctx.scheduler_mut();
+ if let Some(timer) = scheduler.unschedule(timer_id) {
+ scheduler.schedule(timer.event, TYPING_SEARCH_DELAY, false, timer.id);
+ }
+ }
+ }
+}
+
+/// Build a key's keyboard escape sequence based on the given `key`, `mods`, and `mode`.
+///
+/// The key sequences for `APP_KEYPAD` and alike are handled inside the bindings.
+#[inline(never)]
+fn build_sequence(key: KeyEvent, mods: ModifiersState, mode: TermMode) -> Vec<u8> {
+ let modifiers = mods.into();
+
+ let kitty_seq = mode.intersects(
+ TermMode::REPORT_ALL_KEYS_AS_ESC
+ | TermMode::DISAMBIGUATE_ESC_CODES
+ | TermMode::REPORT_EVENT_TYPES,
+ );
+
+ let kitty_encode_all = mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC);
+ // The default parameter is 1, so we can omit it.
+ let kitty_event_type = mode.contains(TermMode::REPORT_EVENT_TYPES)
+ && (key.repeat || key.state == ElementState::Released);
+
+ let context =
+ SequenceBuilder { mode, modifiers, kitty_seq, kitty_encode_all, kitty_event_type };
+
+ let sequence_base = context
+ .try_build_numpad(&key)
+ .or_else(|| context.try_build_named(&key))
+ .or_else(|| context.try_build_control_char_or_mod(&key))
+ .or_else(|| context.try_build_textual(&key));
+
+ let (payload, terminator) = match sequence_base {
+ Some(SequenceBase { payload, terminator }) => (payload, terminator),
+ _ => return Vec::new(),
+ };
+
+ let mut payload = format!("\x1b[{}", payload);
+
+ // Add modifiers information.
+ if kitty_event_type
+ || !modifiers.is_empty()
+ || (mode.contains(TermMode::REPORT_ASSOCIATED_TEXT) && key.text.is_some())
+ {
+ payload.push_str(&format!(";{}", modifiers.encode_esc_sequence()));
+ }
+
+ // Push event type.
+ if kitty_event_type {
+ payload.push(':');
+ let event_type = match key.state {
+ _ if key.repeat => '2',
+ ElementState::Pressed => '1',
+ ElementState::Released => '3',
+ };
+ payload.push(event_type);
+ }
+
+ // Associated text is not reported when the control/alt/logo is pressesed.
+ if mode.contains(TermMode::REPORT_ASSOCIATED_TEXT)
+ && key.state != ElementState::Released
+ && (modifiers.is_empty() || modifiers == SequenceModifiers::SHIFT)
+ {
+ if let Some(text) = key.text {
+ let mut codepoints = text.chars().map(u32::from);
+ if let Some(codepoint) = codepoints.next() {
+ payload.push_str(&format!(";{codepoint}"));
+ }
+ for codepoint in codepoints {
+ payload.push_str(&format!(":{codepoint}"));
+ }
+ }
+ }
+
+ payload.push(terminator.encode_esc_sequence());
+
+ payload.into_bytes()
+}
+
+/// Helper to build escape sequence payloads from [`KeyEvent`].
+pub struct SequenceBuilder {
+ mode: TermMode,
+ /// The emitted sequence should follow the kitty keyboard protocol.
+ kitty_seq: bool,
+ /// Encode all the keys according to the protocol.
+ kitty_encode_all: bool,
+ /// Report event types.
+ kitty_event_type: bool,
+ modifiers: SequenceModifiers,
+}
+
+impl SequenceBuilder {
+ /// Try building sequence from the event's emitting text.
+ fn try_build_textual(&self, key: &KeyEvent) -> Option<SequenceBase> {
+ let character = match key.logical_key.as_ref() {
+ Key::Character(character) => character,
+ _ => return None,
+ };
+
+ if character.chars().count() == 1 {
+ let character = character.chars().next().unwrap();
+ let base_character = character.to_lowercase().next().unwrap();
+
+ let codepoint = u32::from(character);
+ let base_codepoint = u32::from(base_character);
+
+ // NOTE: Base layouts are ignored, since winit doesn't expose this information
+ // yet.
+ let payload = if self.mode.contains(TermMode::REPORT_ALTERNATE_KEYS)
+ && codepoint != base_codepoint
+ {
+ format!("{codepoint}:{base_codepoint}")
+ } else {
+ codepoint.to_string()
+ };
+
+ Some(SequenceBase::new(payload.into(), SequenceTerminator::Kitty))
+ } else if self.kitty_encode_all
+ && self.mode.contains(TermMode::REPORT_ASSOCIATED_TEXT)
+ && key.text.is_some()
+ {
+ // Fallback when need to report text, but we don't have any key associated with this
+ // text.
+ Some(SequenceBase::new("0".into(), SequenceTerminator::Kitty))
+ } else {
+ None
+ }
+ }
+
+ /// Try building from numpad key.
+ ///
+ /// `None` is returned when the key is neither known nor numpad.
+ fn try_build_numpad(&self, key: &KeyEvent) -> Option<SequenceBase> {
+ if !self.kitty_seq || key.location != KeyLocation::Numpad {
+ return None;
+ }
+
+ let base = match key.logical_key.as_ref() {
+ Key::Character("0") => "57399",
+ Key::Character("1") => "57400",
+ Key::Character("2") => "57401",
+ Key::Character("3") => "57402",
+ Key::Character("4") => "57403",
+ Key::Character("5") => "57404",
+ Key::Character("6") => "57405",
+ Key::Character("7") => "57406",
+ Key::Character("8") => "57407",
+ Key::Character("9") => "57408",
+ Key::Character(".") => "57409",
+ Key::Character("/") => "57410",
+ Key::Character("*") => "57411",
+ Key::Character("-") => "57412",
+ Key::Character("+") => "57413",
+ Key::Character("=") => "57415",
+ Key::Named(named) => match named {
+ NamedKey::Enter => "57414",
+ NamedKey::ArrowLeft => "57417",
+ NamedKey::ArrowRight => "57418",
+ NamedKey::ArrowUp => "57419",
+ NamedKey::ArrowDown => "57420",
+ NamedKey::PageUp => "57421",
+ NamedKey::PageDown => "57422",
+ NamedKey::Home => "57423",
+ NamedKey::End => "57424",
+ NamedKey::Insert => "57425",
+ NamedKey::Delete => "57426",
+ _ => return None,
+ },
+ _ => return None,
+ };
+
+ Some(SequenceBase::new(base.into(), SequenceTerminator::Kitty))
+ }
+
+ /// Try building from [`NamedKey`].
+ fn try_build_named(&self, key: &KeyEvent) -> Option<SequenceBase> {
+ let named = match key.logical_key {
+ Key::Named(named) => named,
+ _ => return None,
+ };
+
+ // The default parameter is 1, so we can omit it.
+ let one_based = if self.modifiers.is_empty() && !self.kitty_event_type { "" } else { "1" };
+ let (base, terminator) = match named {
+ NamedKey::PageUp => ("5", SequenceTerminator::Normal('~')),
+ NamedKey::PageDown => ("6", SequenceTerminator::Normal('~')),
+ NamedKey::Insert => ("2", SequenceTerminator::Normal('~')),
+ NamedKey::Delete => ("3", SequenceTerminator::Normal('~')),
+ NamedKey::Home => (one_based, SequenceTerminator::Normal('H')),
+ NamedKey::End => (one_based, SequenceTerminator::Normal('F')),
+ NamedKey::ArrowLeft => (one_based, SequenceTerminator::Normal('D')),
+ NamedKey::ArrowRight => (one_based, SequenceTerminator::Normal('C')),
+ NamedKey::ArrowUp => (one_based, SequenceTerminator::Normal('A')),
+ NamedKey::ArrowDown => (one_based, SequenceTerminator::Normal('B')),
+ NamedKey::F1 => (one_based, SequenceTerminator::Normal('P')),
+ NamedKey::F2 => (one_based, SequenceTerminator::Normal('Q')),
+ NamedKey::F3 => {
+ // F3 in kitty protocol diverges from alacritty's terminfo.
+ if self.kitty_seq {
+ ("13", SequenceTerminator::Normal('~'))
+ } else {
+ (one_based, SequenceTerminator::Normal('R'))
+ }
+ },
+ NamedKey::F4 => (one_based, SequenceTerminator::Normal('S')),
+ NamedKey::F5 => ("15", SequenceTerminator::Normal('~')),
+ NamedKey::F6 => ("17", SequenceTerminator::Normal('~')),
+ NamedKey::F7 => ("18", SequenceTerminator::Normal('~')),
+ NamedKey::F8 => ("19", SequenceTerminator::Normal('~')),
+ NamedKey::F9 => ("20", SequenceTerminator::Normal('~')),
+ NamedKey::F10 => ("21", SequenceTerminator::Normal('~')),
+ NamedKey::F11 => ("23", SequenceTerminator::Normal('~')),
+ NamedKey::F12 => ("24", SequenceTerminator::Normal('~')),
+ NamedKey::F13 => ("57376", SequenceTerminator::Kitty),
+ NamedKey::F14 => ("57377", SequenceTerminator::Kitty),
+ NamedKey::F15 => ("57378", SequenceTerminator::Kitty),
+ NamedKey::F16 => ("57379", SequenceTerminator::Kitty),
+ NamedKey::F17 => ("57380", SequenceTerminator::Kitty),
+ NamedKey::F18 => ("57381", SequenceTerminator::Kitty),
+ NamedKey::F19 => ("57382", SequenceTerminator::Kitty),
+ NamedKey::F20 => ("57383", SequenceTerminator::Kitty),
+ NamedKey::F21 => ("57384", SequenceTerminator::Kitty),
+ NamedKey::F22 => ("57385", SequenceTerminator::Kitty),
+ NamedKey::F23 => ("57386", SequenceTerminator::Kitty),
+ NamedKey::F24 => ("57387", SequenceTerminator::Kitty),
+ NamedKey::F25 => ("57388", SequenceTerminator::Kitty),
+ NamedKey::F26 => ("57389", SequenceTerminator::Kitty),
+ NamedKey::F27 => ("57390", SequenceTerminator::Kitty),
+ NamedKey::F28 => ("57391", SequenceTerminator::Kitty),
+ NamedKey::F29 => ("57392", SequenceTerminator::Kitty),
+ NamedKey::F30 => ("57393", SequenceTerminator::Kitty),
+ NamedKey::F31 => ("57394", SequenceTerminator::Kitty),
+ NamedKey::F32 => ("57395", SequenceTerminator::Kitty),
+ NamedKey::F33 => ("57396", SequenceTerminator::Kitty),
+ NamedKey::F34 => ("57397", SequenceTerminator::Kitty),
+ NamedKey::F35 => ("57398", SequenceTerminator::Kitty),
+ NamedKey::ScrollLock => ("57359", SequenceTerminator::Kitty),
+ NamedKey::PrintScreen => ("57361", SequenceTerminator::Kitty),
+ NamedKey::Pause => ("57362", SequenceTerminator::Kitty),
+ NamedKey::ContextMenu => ("57363", SequenceTerminator::Kitty),
+ NamedKey::MediaPlay => ("57428", SequenceTerminator::Kitty),
+ NamedKey::MediaPause => ("57429", SequenceTerminator::Kitty),
+ NamedKey::MediaPlayPause => ("57430", SequenceTerminator::Kitty),
+ NamedKey::MediaStop => ("57432", SequenceTerminator::Kitty),
+ NamedKey::MediaFastForward => ("57433", SequenceTerminator::Kitty),
+ NamedKey::MediaRewind => ("57434", SequenceTerminator::Kitty),
+ NamedKey::MediaTrackNext => ("57435", SequenceTerminator::Kitty),
+ NamedKey::MediaTrackPrevious => ("57436", SequenceTerminator::Kitty),
+ NamedKey::MediaRecord => ("57437", SequenceTerminator::Kitty),
+ NamedKey::AudioVolumeDown => ("57438", SequenceTerminator::Kitty),
+ NamedKey::AudioVolumeUp => ("57439", SequenceTerminator::Kitty),
+ NamedKey::AudioVolumeMute => ("57440", SequenceTerminator::Kitty),
+ _ => return None,
+ };
+
+ Some(SequenceBase::new(base.into(), terminator))
+ }
+
+ /// Try building escape from control characters (e.g. Enter) and modifiers.
+ fn try_build_control_char_or_mod(&self, key: &KeyEvent) -> Option<SequenceBase> {
+ if !self.kitty_encode_all && !self.kitty_seq {
+ return None;
+ }
+
+ let named = match key.logical_key {
+ Key::Named(named) => named,
+ _ => return None,
+ };
+
+ let base = match named {
+ NamedKey::Tab => "9",
+ NamedKey::Enter => "13",
+ NamedKey::Escape => "27",
+ NamedKey::Space => "32",
+ NamedKey::Backspace => "127",
+ _ => "",
+ };
+
+ // Fail when the key is not a named control character and the active mode prohibits us
+ // from encoding modifier keys.
+ if !self.kitty_encode_all && base.is_empty() {
+ return None;
+ }
+
+ let base = match (named, key.location) {
+ (NamedKey::Shift, KeyLocation::Left) => "57441",
+ (NamedKey::Control, KeyLocation::Left) => "57442",
+ (NamedKey::Alt, KeyLocation::Left) => "57443",
+ (NamedKey::Super, KeyLocation::Left) => "57444",
+ (NamedKey::Hyper, KeyLocation::Left) => "57445",
+ (NamedKey::Meta, KeyLocation::Left) => "57446",
+ (NamedKey::Shift, _) => "57447",
+ (NamedKey::Control, _) => "57448",
+ (NamedKey::Alt, _) => "57449",
+ (NamedKey::Super, _) => "57450",
+ (NamedKey::Hyper, _) => "57451",
+ (NamedKey::Meta, _) => "57452",
+ (NamedKey::CapsLock, _) => "57358",
+ (NamedKey::NumLock, _) => "57360",
+ _ => base,
+ };
+
+ if base.is_empty() {
+ None
+ } else {
+ Some(SequenceBase::new(base.into(), SequenceTerminator::Kitty))
+ }
+ }
+}
+
+pub struct SequenceBase {
+ /// The base of the payload, which is the `number` and optionally an alt base from the kitty
+ /// spec.
+ payload: Cow<'static, str>,
+ terminator: SequenceTerminator,
+}
+
+impl SequenceBase {
+ fn new(payload: Cow<'static, str>, terminator: SequenceTerminator) -> Self {
+ Self { payload, terminator }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum SequenceTerminator {
+ /// The normal key esc sequence terminator defined by xterm/dec.
+ Normal(char),
+ /// The terminator is for kitty escape sequence.
+ Kitty,
+}
+
+impl SequenceTerminator {
+ fn encode_esc_sequence(self) -> char {
+ match self {
+ SequenceTerminator::Normal(char) => char,
+ SequenceTerminator::Kitty => 'u',
+ }
+ }
+}
+
+bitflags::bitflags! {
+ /// The modifiers encoding for escape sequence.
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ struct SequenceModifiers : u8 {
+ const SHIFT = 0b0000_0001;
+ const ALT = 0b0000_0010;
+ const CONTROL = 0b0000_0100;
+ const SUPER = 0b0000_1000;
+ // NOTE: Kitty protocol defines additional modifiers to what is present here, like
+ // Capslock, but it's not a modifier as per winit.
+ }
+}
+
+impl SequenceModifiers {
+ /// Get the value which should be passed to escape sequence.
+ pub fn encode_esc_sequence(self) -> u8 {
+ self.bits() + 1
+ }
+}
+
+impl From<ModifiersState> for SequenceModifiers {
+ fn from(mods: ModifiersState) -> Self {
+ let mut modifiers = Self::empty();
+ modifiers.set(Self::SHIFT, mods.shift_key());
+ modifiers.set(Self::ALT, mods.alt_key());
+ modifiers.set(Self::CONTROL, mods.control_key());
+ modifiers.set(Self::SUPER, mods.super_key());
+ modifiers
+ }
+}
diff --git a/alacritty/src/input/mod.rs b/alacritty/src/input/mod.rs
new file mode 100644
index 00000000..584b8240
--- /dev/null
+++ b/alacritty/src/input/mod.rs
@@ -0,0 +1,1503 @@
+//! Handle input from winit.
+//!
+//! Certain key combinations should send some escape sequence back to the PTY.
+//! In order to figure that out, state about which modifier keys are pressed
+//! needs to be tracked. Additionally, we need a bit of a state machine to
+//! determine what to do when a non-modifier key is pressed.
+
+use std::borrow::Cow;
+use std::cmp::{max, min, Ordering};
+use std::collections::HashSet;
+use std::ffi::OsStr;
+use std::fmt::Debug;
+use std::marker::PhantomData;
+use std::mem;
+use std::time::{Duration, Instant};
+
+use log::debug;
+use winit::dpi::PhysicalPosition;
+use winit::event::{
+ ElementState, Modifiers, MouseButton, MouseScrollDelta, Touch as TouchEvent, TouchPhase,
+};
+use winit::event_loop::EventLoopWindowTarget;
+use winit::keyboard::ModifiersState;
+#[cfg(target_os = "macos")]
+use winit::platform::macos::EventLoopWindowTargetExtMacOS;
+use winit::window::CursorIcon;
+
+use alacritty_terminal::event::EventListener;
+use alacritty_terminal::grid::{Dimensions, Scroll};
+use alacritty_terminal::index::{Boundary, Column, Direction, Point, Side};
+use alacritty_terminal::selection::SelectionType;
+use alacritty_terminal::term::search::Match;
+use alacritty_terminal::term::{ClipboardType, Term, TermMode};
+use alacritty_terminal::vi_mode::ViMotion;
+use alacritty_terminal::vte::ansi::{ClearMode, Handler};
+
+use crate::clipboard::Clipboard;
+use crate::config::{Action, BindingMode, MouseAction, SearchAction, UiConfig, ViAction};
+use crate::display::hint::HintMatch;
+use crate::display::window::Window;
+use crate::display::{Display, SizeInfo};
+use crate::event::{
+ ClickState, Event, EventType, InlineSearchState, Mouse, TouchPurpose, TouchZoom,
+};
+use crate::message_bar::{self, Message};
+use crate::scheduler::{Scheduler, TimerId, Topic};
+
+pub mod keyboard;
+
+/// Font size change interval.
+pub const FONT_SIZE_STEP: f32 = 0.5;
+
+/// Interval for mouse scrolling during selection outside of the boundaries.
+const SELECTION_SCROLLING_INTERVAL: Duration = Duration::from_millis(15);
+
+/// Minimum number of pixels at the bottom/top where selection scrolling is performed.
+const MIN_SELECTION_SCROLLING_HEIGHT: f64 = 5.;
+
+/// Number of pixels for increasing the selection scrolling speed factor by one.
+const SELECTION_SCROLLING_STEP: f64 = 20.;
+
+/// Distance before a touch input is considered a drag.
+const MAX_TAP_DISTANCE: f64 = 20.;
+
+/// Threshold used for double_click/triple_click.
+const CLICK_THRESHOLD: Duration = Duration::from_millis(400);
+
+/// Processes input from winit.
+///
+/// An escape sequence may be emitted in case specific keys or key combinations
+/// are activated.
+pub struct Processor<T: EventListener, A: ActionContext<T>> {
+ pub ctx: A,
+ _phantom: PhantomData<T>,
+}
+
+pub trait ActionContext<T: EventListener> {
+ fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&self, _data: B) {}
+ fn mark_dirty(&mut self) {}
+ fn size_info(&self) -> SizeInfo;
+ fn copy_selection(&mut self, _ty: ClipboardType) {}
+ fn start_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {}
+ fn toggle_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {}
+ fn update_selection(&mut self, _point: Point, _side: Side) {}
+ fn clear_selection(&mut self) {}
+ fn selection_is_empty(&self) -> bool;
+ fn mouse_mut(&mut self) -> &mut Mouse;
+ fn mouse(&self) -> &Mouse;
+ fn touch_purpose(&mut self) -> &mut TouchPurpose;
+ fn modifiers(&mut self) -> &mut Modifiers;
+ fn scroll(&mut self, _scroll: Scroll) {}
+ fn window(&mut self) -> &mut Window;
+ fn display(&mut self) -> &mut Display;
+ fn terminal(&self) -> &Term<T>;
+ fn terminal_mut(&mut self) -> &mut Term<T>;
+ fn spawn_new_instance(&mut self) {}
+ #[cfg(target_os = "macos")]
+ fn create_new_window(&mut self, _tabbing_id: Option<String>) {}
+ #[cfg(not(target_os = "macos"))]
+ fn create_new_window(&mut self) {}
+ fn change_font_size(&mut self, _delta: f32) {}
+ fn reset_font_size(&mut self) {}
+ fn pop_message(&mut self) {}
+ fn message(&self) -> Option<&Message>;
+ fn config(&self) -> &UiConfig;
+ fn event_loop(&self) -> &EventLoopWindowTarget<Event>;
+ fn mouse_mode(&self) -> bool;
+ fn clipboard_mut(&mut self) -> &mut Clipboard;
+ fn scheduler_mut(&mut self) -> &mut Scheduler;
+ fn start_search(&mut self, _direction: Direction) {}
+ fn confirm_search(&mut self) {}
+ fn cancel_search(&mut self) {}
+ fn search_input(&mut self, _c: char) {}
+ fn search_pop_word(&mut self) {}
+ fn search_history_previous(&mut self) {}
+ fn search_history_next(&mut self) {}
+ fn search_next(&mut self, origin: Point, direction: Direction, side: Side) -> Option<Match>;
+ fn advance_search_origin(&mut self, _direction: Direction) {}
+ fn search_direction(&self) -> Direction;
+ fn search_active(&self) -> bool;
+ fn on_typing_start(&mut self) {}
+ fn toggle_vi_mode(&mut self) {}
+ fn inline_search_state(&mut self) -> &mut InlineSearchState;
+ fn start_inline_search(&mut self, _direction: Direction, _stop_short: bool) {}
+ fn inline_search_next(&mut self) {}
+ fn inline_search_previous(&mut self) {}
+ fn hint_input(&mut self, _character: char) {}
+ fn trigger_hint(&mut self, _hint: &HintMatch) {}
+ fn expand_selection(&mut self) {}
+ fn on_terminal_input_start(&mut self) {}
+ fn paste(&mut self, _text: &str, _bracketed: bool) {}
+ fn spawn_daemon<I, S>(&self, _program: &str, _args: I)
+ where
+ I: IntoIterator<Item = S> + Debug + Copy,
+ S: AsRef<OsStr>,
+ {
+ }
+}
+
+impl Action {
+ fn toggle_selection<T, A>(ctx: &mut A, ty: SelectionType)
+ where
+ A: ActionContext<T>,
+ T: EventListener,
+ {
+ ctx.toggle_selection(ty, ctx.terminal().vi_mode_cursor.point, Side::Left);
+
+ // Make sure initial selection is not empty.
+ if let Some(selection) = &mut ctx.terminal_mut().selection {
+ selection.include_all();
+ }
+ }
+}
+
+trait Execute<T: EventListener> {
+ fn execute<A: ActionContext<T>>(&self, ctx: &mut A);
+}
+
+impl<T: EventListener> Execute<T> for Action {
+ #[inline]
+ fn execute<A: ActionContext<T>>(&self, ctx: &mut A) {
+ match self {
+ Action::Esc(s) => ctx.paste(s, false),
+ Action::Command(program) => ctx.spawn_daemon(program.program(), program.args()),
+ Action::Hint(hint) => {
+ ctx.display().hint_state.start(hint.clone());
+ ctx.mark_dirty();
+ },
+ Action::ToggleViMode => {
+ ctx.on_typing_start();
+ ctx.toggle_vi_mode()
+ },
+ action @ (Action::ViMotion(_) | Action::Vi(_))
+ if !ctx.terminal().mode().contains(TermMode::VI) =>
+ {
+ debug!("Ignoring {action:?}: Vi mode inactive");
+ },
+ Action::ViMotion(motion) => {
+ ctx.on_typing_start();
+ ctx.terminal_mut().vi_motion(*motion);
+ ctx.mark_dirty();
+ },
+ Action::Vi(ViAction::ToggleNormalSelection) => {
+ Self::toggle_selection(ctx, SelectionType::Simple);
+ },
+ Action::Vi(ViAction::ToggleLineSelection) => {
+ Self::toggle_selection(ctx, SelectionType::Lines);
+ },
+ Action::Vi(ViAction::ToggleBlockSelection) => {
+ Self::toggle_selection(ctx, SelectionType::Block);
+ },
+ Action::Vi(ViAction::ToggleSemanticSelection) => {
+ Self::toggle_selection(ctx, SelectionType::Semantic);
+ },
+ Action::Vi(ViAction::Open) => {
+ let hint = ctx.display().vi_highlighted_hint.take();
+ if let Some(hint) = &hint {
+ ctx.mouse_mut().block_hint_launcher = false;
+ ctx.trigger_hint(hint);
+ }
+ ctx.display().vi_highlighted_hint = hint;
+ },
+ Action::Vi(ViAction::SearchNext) => {
+ ctx.on_typing_start();
+
+ let terminal = ctx.terminal();
+ let direction = ctx.search_direction();
+ let vi_point = terminal.vi_mode_cursor.point;
+ let origin = match direction {
+ Direction::Right => vi_point.add(terminal, Boundary::None, 1),
+ Direction::Left => vi_point.sub(terminal, Boundary::None, 1),
+ };
+
+ if let Some(regex_match) = ctx.search_next(origin, direction, Side::Left) {
+ ctx.terminal_mut().vi_goto_point(*regex_match.start());
+ ctx.mark_dirty();
+ }
+ },
+ Action::Vi(ViAction::SearchPrevious) => {
+ ctx.on_typing_start();
+
+ let terminal = ctx.terminal();
+ let direction = ctx.search_direction().opposite();
+ let vi_point = terminal.vi_mode_cursor.point;
+ let origin = match direction {
+ Direction::Right => vi_point.add(terminal, Boundary::None, 1),
+ Direction::Left => vi_point.sub(terminal, Boundary::None, 1),
+ };
+
+ if let Some(regex_match) = ctx.search_next(origin, direction, Side::Left) {
+ ctx.terminal_mut().vi_goto_point(*regex_match.start());
+ ctx.mark_dirty();
+ }
+ },
+ Action::Vi(ViAction::SearchStart) => {
+ let terminal = ctx.terminal();
+ let origin = terminal.vi_mode_cursor.point.sub(terminal, Boundary::None, 1);
+
+ if let Some(regex_match) = ctx.search_next(origin, Direction::Left, Side::Left) {
+ ctx.terminal_mut().vi_goto_point(*regex_match.start());
+ ctx.mark_dirty();
+ }
+ },
+ Action::Vi(ViAction::SearchEnd) => {
+ let terminal = ctx.terminal();
+ let origin = terminal.vi_mode_cursor.point.add(terminal, Boundary::None, 1);
+
+ if let Some(regex_match) = ctx.search_next(origin, Direction::Right, Side::Right) {
+ ctx.terminal_mut().vi_goto_point(*regex_match.end());
+ ctx.mark_dirty();
+ }
+ },
+ Action::Vi(ViAction::CenterAroundViCursor) => {
+ let term = ctx.terminal();
+ let display_offset = term.grid().display_offset() as i32;
+ let target = -display_offset + term.screen_lines() as i32 / 2 - 1;
+ let line = term.vi_mode_cursor.point.line;
+ let scroll_lines = target - line.0;
+
+ ctx.scroll(Scroll::Delta(scroll_lines));
+ },
+ Action::Vi(ViAction::InlineSearchForward) => {
+ ctx.start_inline_search(Direction::Right, false)
+ },
+ Action::Vi(ViAction::InlineSearchBackward) => {
+ ctx.start_inline_search(Direction::Left, false)
+ },
+ Action::Vi(ViAction::InlineSearchForwardShort) => {
+ ctx.start_inline_search(Direction::Right, true)
+ },
+ Action::Vi(ViAction::InlineSearchBackwardShort) => {
+ ctx.start_inline_search(Direction::Left, true)
+ },
+ Action::Vi(ViAction::InlineSearchNext) => ctx.inline_search_next(),
+ Action::Vi(ViAction::InlineSearchPrevious) => ctx.inline_search_previous(),
+ action @ Action::Search(_) if !ctx.search_active() => {
+ debug!("Ignoring {action:?}: Search mode inactive");
+ },
+ Action::Search(SearchAction::SearchFocusNext) => {
+ ctx.advance_search_origin(ctx.search_direction());
+ },
+ Action::Search(SearchAction::SearchFocusPrevious) => {
+ let direction = ctx.search_direction().opposite();
+ ctx.advance_search_origin(direction);
+ },
+ Action::Search(SearchAction::SearchConfirm) => ctx.confirm_search(),
+ Action::Search(SearchAction::SearchCancel) => ctx.cancel_search(),
+ Action::Search(SearchAction::SearchClear) => {
+ let direction = ctx.search_direction();
+ ctx.cancel_search();
+ ctx.start_search(direction);
+ },
+ Action::Search(SearchAction::SearchDeleteWord) => ctx.search_pop_word(),
+ Action::Search(SearchAction::SearchHistoryPrevious) => ctx.search_history_previous(),
+ Action::Search(SearchAction::SearchHistoryNext) => ctx.search_history_next(),
+ Action::Mouse(MouseAction::ExpandSelection) => ctx.expand_selection(),
+ Action::SearchForward => ctx.start_search(Direction::Right),
+ Action::SearchBackward => ctx.start_search(Direction::Left),
+ Action::Copy => ctx.copy_selection(ClipboardType::Clipboard),
+ #[cfg(not(any(target_os = "macos", windows)))]
+ Action::CopySelection => ctx.copy_selection(ClipboardType::Selection),
+ Action::ClearSelection => ctx.clear_selection(),
+ Action::Paste => {
+ let text = ctx.clipboard_mut().load(ClipboardType::Clipboard);
+ ctx.paste(&text, true);
+ },
+ Action::PasteSelection => {
+ let text = ctx.clipboard_mut().load(ClipboardType::Selection);
+ ctx.paste(&text, true);
+ },
+ Action::ToggleFullscreen => ctx.window().toggle_fullscreen(),
+ Action::ToggleMaximized => ctx.window().toggle_maximized(),
+ #[cfg(target_os = "macos")]
+ Action::ToggleSimpleFullscreen => ctx.window().toggle_simple_fullscreen(),
+ #[cfg(target_os = "macos")]
+ Action::Hide => ctx.event_loop().hide_application(),
+ #[cfg(target_os = "macos")]
+ Action::HideOtherApplications => ctx.event_loop().hide_other_applications(),
+ #[cfg(not(target_os = "macos"))]
+ Action::Hide => ctx.window().set_visible(false),
+ Action::Minimize => ctx.window().set_minimized(true),
+ Action::Quit => ctx.terminal_mut().exit(),
+ Action::IncreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP),
+ Action::DecreaseFontSize => ctx.change_font_size(FONT_SIZE_STEP * -1.),
+ Action::ResetFontSize => ctx.reset_font_size(),
+ Action::ScrollPageUp
+ | Action::ScrollPageDown
+ | Action::ScrollHalfPageUp
+ | Action::ScrollHalfPageDown => {
+ // Move vi mode cursor.
+ let term = ctx.terminal_mut();
+ let (scroll, amount) = match self {
+ Action::ScrollPageUp => (Scroll::PageUp, term.screen_lines() as i32),
+ Action::ScrollPageDown => (Scroll::PageDown, -(term.screen_lines() as i32)),
+ Action::ScrollHalfPageUp => {
+ let amount = term.screen_lines() as i32 / 2;
+ (Scroll::Delta(amount), amount)
+ },
+ Action::ScrollHalfPageDown => {
+ let amount = -(term.screen_lines() as i32 / 2);
+ (Scroll::Delta(amount), amount)
+ },
+ _ => unreachable!(),
+ };
+
+ let old_vi_cursor = term.vi_mode_cursor;
+ term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, amount);
+ if old_vi_cursor != term.vi_mode_cursor {
+ ctx.mark_dirty();
+ }
+
+ ctx.scroll(scroll);
+ },
+ Action::ScrollLineUp => ctx.scroll(Scroll::Delta(1)),
+ Action::ScrollLineDown => ctx.scroll(Scroll::Delta(-1)),
+ Action::ScrollToTop => {
+ ctx.scroll(Scroll::Top);
+
+ // Move vi mode cursor.
+ let topmost_line = ctx.terminal().topmost_line();
+ ctx.terminal_mut().vi_mode_cursor.point.line = topmost_line;
+ ctx.terminal_mut().vi_motion(ViMotion::FirstOccupied);
+ ctx.mark_dirty();
+ },
+ Action::ScrollToBottom => {
+ ctx.scroll(Scroll::Bottom);
+
+ // Move vi mode cursor.
+ let term = ctx.terminal_mut();
+ term.vi_mode_cursor.point.line = term.bottommost_line();
+
+ // Move to beginning twice, to always jump across linewraps.
+ term.vi_motion(ViMotion::FirstOccupied);
+ term.vi_motion(ViMotion::FirstOccupied);
+ ctx.mark_dirty();
+ },
+ Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved),
+ Action::ClearLogNotice => ctx.pop_message(),
+ #[cfg(not(target_os = "macos"))]
+ Action::CreateNewWindow => ctx.create_new_window(),
+ Action::SpawnNewInstance => ctx.spawn_new_instance(),
+ #[cfg(target_os = "macos")]
+ Action::CreateNewWindow => ctx.create_new_window(None),
+ #[cfg(target_os = "macos")]
+ Action::CreateNewTab => {
+ let tabbing_id = Some(ctx.window().tabbing_id());
+ ctx.create_new_window(tabbing_id);
+ },
+ #[cfg(target_os = "macos")]
+ Action::SelectNextTab => ctx.window().select_next_tab(),
+ #[cfg(target_os = "macos")]
+ Action::SelectPreviousTab => ctx.window().select_previous_tab(),
+ #[cfg(target_os = "macos")]
+ Action::SelectTab1 => ctx.window().select_tab_at_index(0),
+ #[cfg(target_os = "macos")]
+ Action::SelectTab2 => ctx.window().select_tab_at_index(1),
+ #[cfg(target_os = "macos")]
+ Action::SelectTab3 => ctx.window().select_tab_at_index(2),
+ #[cfg(target_os = "macos")]
+ Action::SelectTab4 => ctx.window().select_tab_at_index(3),
+ #[cfg(target_os = "macos")]
+ Action::SelectTab5 => ctx.window().select_tab_at_index(4),
+ #[cfg(target_os = "macos")]
+ Action::SelectTab6 => ctx.window().select_tab_at_index(5),
+ #[cfg(target_os = "macos")]
+ Action::SelectTab7 => ctx.window().select_tab_at_index(6),
+ #[cfg(target_os = "macos")]
+ Action::SelectTab8 => ctx.window().select_tab_at_index(7),
+ #[cfg(target_os = "macos")]
+ Action::SelectTab9 => ctx.window().select_tab_at_index(8),
+ #[cfg(target_os = "macos")]
+ Action::SelectLastTab => ctx.window().select_last_tab(),
+ Action::ReceiveChar | Action::None => (),
+ }
+ }
+}
+
+impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
+ pub fn new(ctx: A) -> Self {
+ Self { ctx, _phantom: Default::default() }
+ }
+
+ #[inline]
+ pub fn mouse_moved(&mut self, position: PhysicalPosition<f64>) {
+ let size_info = self.ctx.size_info();
+
+ let (x, y) = position.into();
+
+ let lmb_pressed = self.ctx.mouse().left_button_state == ElementState::Pressed;
+ let rmb_pressed = self.ctx.mouse().right_button_state == ElementState::Pressed;
+ if !self.ctx.selection_is_empty() && (lmb_pressed || rmb_pressed) {
+ self.update_selection_scrolling(y);
+ }
+
+ let display_offset = self.ctx.terminal().grid().display_offset();
+ let old_point = self.ctx.mouse().point(&size_info, display_offset);
+
+ let x = x.clamp(0, size_info.width() as i32 - 1) as usize;
+ let y = y.clamp(0, size_info.height() as i32 - 1) as usize;
+ self.ctx.mouse_mut().x = x;
+ self.ctx.mouse_mut().y = y;
+
+ let inside_text_area = size_info.contains_point(x, y);
+ let cell_side = self.cell_side(x);
+
+ let point = self.ctx.mouse().point(&size_info, display_offset);
+ let cell_changed = old_point != point;
+
+ // If the mouse hasn't changed cells, do nothing.
+ if !cell_changed
+ && self.ctx.mouse().cell_side == cell_side
+ && self.ctx.mouse().inside_text_area == inside_text_area
+ {
+ return;
+ }
+
+ self.ctx.mouse_mut().inside_text_area = inside_text_area;
+ self.ctx.mouse_mut().cell_side = cell_side;
+
+ // Update mouse state and check for URL change.
+ let mouse_state = self.cursor_state();
+ self.ctx.window().set_mouse_cursor(mouse_state);
+
+ // Prompt hint highlight update.
+ self.ctx.mouse_mut().hint_highlight_dirty = true;
+
+ // Don't launch URLs if mouse has moved.
+ self.ctx.mouse_mut().block_hint_launcher = true;
+
+ if (lmb_pressed || rmb_pressed)
+ && (self.ctx.modifiers().state().shift_key() || !self.ctx.mouse_mode())
+ {
+ self.ctx.update_selection(point, cell_side);
+ } else if cell_changed
+ && self.ctx.terminal().mode().intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG)
+ {
+ if lmb_pressed {
+ self.mouse_report(32, ElementState::Pressed);
+ } else if self.ctx.mouse().middle_button_state == ElementState::Pressed {
+ self.mouse_report(33, ElementState::Pressed);
+ } else if self.ctx.mouse().right_button_state == ElementState::Pressed {
+ self.mouse_report(34, ElementState::Pressed);
+ } else if self.ctx.terminal().mode().contains(TermMode::MOUSE_MOTION) {
+ self.mouse_report(35, ElementState::Pressed);
+ }
+ }
+ }
+
+ /// Check which side of a cell an X coordinate lies on.
+ fn cell_side(&self, x: usize) -> Side {
+ let size_info = self.ctx.size_info();
+
+ let cell_x =
+ x.saturating_sub(size_info.padding_x() as usize) % size_info.cell_width() as usize;
+ let half_cell_width = (size_info.cell_width() / 2.0) as usize;
+
+ let additional_padding =
+ (size_info.width() - size_info.padding_x() * 2.) % size_info.cell_width();
+ let end_of_grid = size_info.width() - size_info.padding_x() - additional_padding;
+
+ if cell_x > half_cell_width
+ // Edge case when mouse leaves the window.
+ || x as f32 >= end_of_grid
+ {
+ Side::Right
+ } else {
+ Side::Left
+ }
+ }
+
+ fn mouse_report(&mut self, button: u8, state: ElementState) {
+ let display_offset = self.ctx.terminal().grid().display_offset();
+ let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset);
+
+ // Assure the mouse point is not in the scrollback.
+ if point.line < 0 {
+ return;
+ }
+
+ // Calculate modifiers value.
+ let mut mods = 0;
+ let modifiers = self.ctx.modifiers().state();
+ if modifiers.shift_key() {
+ mods += 4;
+ }
+ if modifiers.alt_key() {
+ mods += 8;
+ }
+ if modifiers.control_key() {
+ mods += 16;
+ }
+
+ // Report mouse events.
+ if self.ctx.terminal().mode().contains(TermMode::SGR_MOUSE) {
+ self.sgr_mouse_report(point, button + mods, state);
+ } else if let ElementState::Released = state {
+ self.normal_mouse_report(point, 3 + mods);
+ } else {
+ self.normal_mouse_report(point, button + mods);
+ }
+ }
+
+ fn normal_mouse_report(&mut self, point: Point, button: u8) {
+ let Point { line, column } = point;
+ let utf8 = self.ctx.terminal().mode().contains(TermMode::UTF8_MOUSE);
+
+ let max_point = if utf8 { 2015 } else { 223 };
+
+ if line >= max_point || column >= max_point {
+ return;
+ }
+
+ let mut msg = vec![b'\x1b', b'[', b'M', 32 + button];
+
+ let mouse_pos_encode = |pos: usize| -> Vec<u8> {
+ let pos = 32 + 1 + pos;
+ let first = 0xC0 + pos / 64;
+ let second = 0x80 + (pos & 63);
+ vec![first as u8, second as u8]
+ };
+
+ if utf8 && column >= Column(95) {
+ msg.append(&mut mouse_pos_encode(column.0));
+ } else {
+ msg.push(32 + 1 + column.0 as u8);
+ }
+
+ if utf8 && line >= 95 {
+ msg.append(&mut mouse_pos_encode(line.0 as usize));
+ } else {
+ msg.push(32 + 1 + line.0 as u8);
+ }
+
+ self.ctx.write_to_pty(msg);
+ }
+
+ fn sgr_mouse_report(&mut self, point: Point, button: u8, state: ElementState) {
+ let c = match state {
+ ElementState::Pressed => 'M',
+ ElementState::Released => 'm',
+ };
+
+ let msg = format!("\x1b[<{};{};{}{}", button, point.column + 1, point.line + 1, c);
+ self.ctx.write_to_pty(msg.into_bytes());
+ }
+
+ fn on_mouse_press(&mut self, button: MouseButton) {
+ // Handle mouse mode.
+ if !self.ctx.modifiers().state().shift_key() && self.ctx.mouse_mode() {
+ self.ctx.mouse_mut().click_state = ClickState::None;
+
+ let code = match button {
+ MouseButton::Left => 0,
+ MouseButton::Middle => 1,
+ MouseButton::Right => 2,
+ // Can't properly report more than three buttons..
+ MouseButton::Back | MouseButton::Forward | MouseButton::Other(_) => return,
+ };
+
+ self.mouse_report(code, ElementState::Pressed);
+ } else {
+ // Calculate time since the last click to handle double/triple clicks.
+ let now = Instant::now();
+ let elapsed = now - self.ctx.mouse().last_click_timestamp;
+ self.ctx.mouse_mut().last_click_timestamp = now;
+
+ // Update multi-click state.
+ self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state {
+ // Reset click state if button has changed.
+ _ if button != self.ctx.mouse().last_click_button => {
+ self.ctx.mouse_mut().last_click_button = button;
+ ClickState::Click
+ },
+ ClickState::Click if elapsed < CLICK_THRESHOLD => ClickState::DoubleClick,
+ ClickState::DoubleClick if elapsed < CLICK_THRESHOLD => ClickState::TripleClick,
+ _ => ClickState::Click,
+ };
+
+ // Load mouse point, treating message bar and padding as the closest cell.
+ let display_offset = self.ctx.terminal().grid().display_offset();
+ let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset);
+
+ if let MouseButton::Left = button {
+ self.on_left_click(point)
+ }
+ }
+ }
+
+ /// Handle left click selection and vi mode cursor movement.
+ fn on_left_click(&mut self, point: Point) {
+ let side = self.ctx.mouse().cell_side;
+
+ match self.ctx.mouse().click_state {
+ ClickState::Click => {
+ // Don't launch URLs if this click cleared the selection.
+ self.ctx.mouse_mut().block_hint_launcher = !self.ctx.selection_is_empty();
+
+ self.ctx.clear_selection();
+
+ // Start new empty selection.
+ if self.ctx.modifiers().state().control_key() {
+ self.ctx.start_selection(SelectionType::Block, point, side);
+ } else {
+ self.ctx.start_selection(SelectionType::Simple, point, side);
+ }
+ },
+ ClickState::DoubleClick => {
+ self.ctx.mouse_mut().block_hint_launcher = true;
+ self.ctx.start_selection(SelectionType::Semantic, point, side);
+ },
+ ClickState::TripleClick => {
+ self.ctx.mouse_mut().block_hint_launcher = true;
+ self.ctx.start_selection(SelectionType::Lines, point, side);
+ },
+ ClickState::None => (),
+ };
+
+ // Move vi mode cursor to mouse click position.
+ if self.ctx.terminal().mode().contains(TermMode::VI) && !self.ctx.search_active() {
+ self.ctx.terminal_mut().vi_mode_cursor.point = point;
+ self.ctx.mark_dirty();
+ }
+ }
+
+ fn on_mouse_release(&mut self, button: MouseButton) {
+ if !self.ctx.modifiers().state().shift_key() && self.ctx.mouse_mode() {
+ let code = match button {
+ MouseButton::Left => 0,
+ MouseButton::Middle => 1,
+ MouseButton::Right => 2,
+ // Can't properly report more than three buttons.
+ MouseButton::Back | MouseButton::Forward | MouseButton::Other(_) => return,
+ };
+ self.mouse_report(code, ElementState::Released);
+ return;
+ }
+
+ // Trigger hints highlighted by the mouse.
+ let hint = self.ctx.display().highlighted_hint.take();
+ if let Some(hint) = hint.as_ref().filter(|_| button == MouseButton::Left) {
+ self.ctx.trigger_hint(hint);
+ }
+ self.ctx.display().highlighted_hint = hint;
+
+ let timer_id = TimerId::new(Topic::SelectionScrolling, self.ctx.window().id());
+ self.ctx.scheduler_mut().unschedule(timer_id);
+
+ if let MouseButton::Left | MouseButton::Right = button {
+ // Copy selection on release, to prevent flooding the display server.
+ self.ctx.copy_selection(ClipboardType::Selection);
+ }
+ }
+
+ pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) {
+ let multiplier = self.ctx.config().scrolling.multiplier;
+ match delta {
+ MouseScrollDelta::LineDelta(columns, lines) => {
+ let new_scroll_px_x = columns * self.ctx.size_info().cell_width();
+ let new_scroll_px_y = lines * self.ctx.size_info().cell_height();
+ self.scroll_terminal(
+ new_scroll_px_x as f64,
+ new_scroll_px_y as f64,
+ multiplier as f64,
+ );
+ },
+ MouseScrollDelta::PixelDelta(mut lpos) => {
+ match phase {
+ TouchPhase::Started => {
+ // Reset offset to zero.
+ self.ctx.mouse_mut().accumulated_scroll = Default::default();
+ },
+ TouchPhase::Moved => {
+ // When the angle between (x, 0) and (x, y) is lower than ~25 degrees
+ // (cosine is larger that 0.9) we consider this scrolling as horizontal.
+ if lpos.x.abs() / lpos.x.hypot(lpos.y) > 0.9 {
+ lpos.y = 0.;
+ } else {
+ lpos.x = 0.;
+ }
+
+ self.scroll_terminal(lpos.x, lpos.y, multiplier as f64);
+ },
+ _ => (),
+ }
+ },
+ }
+ }
+
+ fn scroll_terminal(&mut self, new_scroll_x_px: f64, new_scroll_y_px: f64, multiplier: f64) {
+ const MOUSE_WHEEL_UP: u8 = 64;
+ const MOUSE_WHEEL_DOWN: u8 = 65;
+ const MOUSE_WHEEL_LEFT: u8 = 66;
+ const MOUSE_WHEEL_RIGHT: u8 = 67;
+
+ let width = f64::from(self.ctx.size_info().cell_width());
+ let height = f64::from(self.ctx.size_info().cell_height());
+
+ if self.ctx.mouse_mode() {
+ self.ctx.mouse_mut().accumulated_scroll.x += new_scroll_x_px;
+ self.ctx.mouse_mut().accumulated_scroll.y += new_scroll_y_px;
+
+ let code = if new_scroll_y_px > 0. { MOUSE_WHEEL_UP } else { MOUSE_WHEEL_DOWN };
+ let lines = (self.ctx.mouse().accumulated_scroll.y / height).abs() as i32;
+
+ for _ in 0..lines {
+ self.mouse_report(code, ElementState::Pressed);
+ }
+
+ let code = if new_scroll_x_px > 0. { MOUSE_WHEEL_LEFT } else { MOUSE_WHEEL_RIGHT };
+ let columns = (self.ctx.mouse().accumulated_scroll.x / width).abs() as i32;
+
+ for _ in 0..columns {
+ self.mouse_report(code, ElementState::Pressed);
+ }
+ } else if self
+ .ctx
+ .terminal()
+ .mode()
+ .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
+ && !self.ctx.modifiers().state().shift_key()
+ {
+ self.ctx.mouse_mut().accumulated_scroll.x += new_scroll_x_px * multiplier;
+ self.ctx.mouse_mut().accumulated_scroll.y += new_scroll_y_px * multiplier;
+
+ // The chars here are the same as for the respective arrow keys.
+ let line_cmd = if new_scroll_y_px > 0. { b'A' } else { b'B' };
+ let column_cmd = if new_scroll_x_px > 0. { b'D' } else { b'C' };
+
+ let lines = (self.ctx.mouse().accumulated_scroll.y / height).abs() as usize;
+ let columns = (self.ctx.mouse().accumulated_scroll.x / width).abs() as usize;
+
+ let mut content = Vec::with_capacity(3 * (lines + columns));
+
+ for _ in 0..lines {
+ content.push(0x1b);
+ content.push(b'O');
+ content.push(line_cmd);
+ }
+
+ for _ in 0..columns {
+ content.push(0x1b);
+ content.push(b'O');
+ content.push(column_cmd);
+ }
+
+ self.ctx.write_to_pty(content);
+ } else {
+ self.ctx.mouse_mut().accumulated_scroll.y += new_scroll_y_px * multiplier;
+
+ let lines = (self.ctx.mouse().accumulated_scroll.y / height) as i32;
+
+ if lines != 0 {
+ self.ctx.scroll(Scroll::Delta(lines));
+ }
+ }
+
+ self.ctx.mouse_mut().accumulated_scroll.x %= width;
+ self.ctx.mouse_mut().accumulated_scroll.y %= height;
+ }
+
+ pub fn on_focus_change(&mut self, is_focused: bool) {
+ if self.ctx.terminal().mode().contains(TermMode::FOCUS_IN_OUT) {
+ let chr = if is_focused { "I" } else { "O" };
+
+ let msg = format!("\x1b[{}", chr);
+ self.ctx.write_to_pty(msg.into_bytes());
+ }
+ }
+
+ /// Handle touch input.
+ pub fn touch(&mut self, touch: TouchEvent) {
+ match touch.phase {
+ TouchPhase::Started => self.on_touch_start(touch),
+ TouchPhase::Moved => self.on_touch_motion(touch),
+ TouchPhase::Ended | TouchPhase::Cancelled => self.on_touch_end(touch),
+ }
+ }
+
+ /// Handle beginning of touch input.
+ pub fn on_touch_start(&mut self, touch: TouchEvent) {
+ let touch_purpose = self.ctx.touch_purpose();
+ *touch_purpose = match mem::take(touch_purpose) {
+ TouchPurpose::None => TouchPurpose::Tap(touch),
+ TouchPurpose::Tap(start) => TouchPurpose::Zoom(TouchZoom::new((start, touch))),
+ TouchPurpose::Zoom(zoom) => TouchPurpose::Invalid(zoom.slots()),
+ TouchPurpose::Scroll(event) | TouchPurpose::Select(event) => {
+ let mut set = HashSet::default();
+ set.insert(event.id);
+ TouchPurpose::Invalid(set)
+ },
+ TouchPurpose::Invalid(mut slots) => {
+ slots.insert(touch.id);
+ TouchPurpose::Invalid(slots)
+ },
+ };
+ }
+
+ /// Handle touch input movement.
+ pub fn on_touch_motion(&mut self, touch: TouchEvent) {
+ let touch_purpose = self.ctx.touch_purpose();
+ match touch_purpose {
+ TouchPurpose::None => (),
+ // Handle transition from tap to scroll/select.
+ TouchPurpose::Tap(start) => {
+ let delta_x = touch.location.x - start.location.x;
+ let delta_y = touch.location.y - start.location.y;
+ if delta_x.abs() > MAX_TAP_DISTANCE {
+ // Update gesture state.
+ let start_location = start.location;
+ *touch_purpose = TouchPurpose::Select(*start);
+
+ // Start simulated mouse input.
+ self.mouse_moved(start_location);
+ self.mouse_input(ElementState::Pressed, MouseButton::Left);
+
+ // Apply motion since touch start.
+ self.on_touch_motion(touch);
+ } else if delta_y.abs() > MAX_TAP_DISTANCE {
+ // Update gesture state.
+ *touch_purpose = TouchPurpose::Scroll(*start);
+
+ // Apply motion since touch start.
+ self.on_touch_motion(touch);
+ }
+ },
+ TouchPurpose::Zoom(zoom) => {
+ let font_delta = zoom.font_delta(touch);
+ self.ctx.change_font_size(font_delta);
+ },
+ TouchPurpose::Scroll(last_touch) => {
+ // Calculate delta and update last touch position.
+ let delta_y = touch.location.y - last_touch.location.y;
+ *touch_purpose = TouchPurpose::Scroll(touch);
+
+ // Use a fixed scroll factor for touchscreens, to accurately track finger motion.
+ self.scroll_terminal(0., delta_y, 1.0);
+ },
+ TouchPurpose::Select(_) => self.mouse_moved(touch.location),
+ TouchPurpose::Invalid(_) => (),
+ }
+ }
+
+ /// Handle end of touch input.
+ pub fn on_touch_end(&mut self, touch: TouchEvent) {
+ // Finalize the touch motion up to the release point.
+ self.on_touch_motion(touch);
+
+ let touch_purpose = self.ctx.touch_purpose();
+ match touch_purpose {
+ // Simulate LMB clicks.
+ TouchPurpose::Tap(start) => {
+ let start_location = start.location;
+ *touch_purpose = Default::default();
+
+ self.mouse_moved(start_location);
+ self.mouse_input(ElementState::Pressed, MouseButton::Left);
+ self.mouse_input(ElementState::Released, MouseButton::Left);
+ },
+ // Invalidate zoom once a finger was released.
+ TouchPurpose::Zoom(zoom) => {
+ let mut slots = zoom.slots();
+ slots.remove(&touch.id);
+ *touch_purpose = TouchPurpose::Invalid(slots);
+ },
+ // Reset touch state once all slots were released.
+ TouchPurpose::Invalid(slots) => {
+ slots.remove(&touch.id);
+ if slots.is_empty() {
+ *touch_purpose = Default::default();
+ }
+ },
+ // Release simulated LMB.
+ TouchPurpose::Select(_) => {
+ *touch_purpose = Default::default();
+ self.mouse_input(ElementState::Released, MouseButton::Left);
+ },
+ // Reset touch state on scroll finish.
+ TouchPurpose::Scroll(_) => *touch_purpose = Default::default(),
+ TouchPurpose::None => (),
+ }
+ }
+
+ /// Reset mouse cursor based on modifier and terminal state.
+ #[inline]
+ pub fn reset_mouse_cursor(&mut self) {
+ let mouse_state = self.cursor_state();
+ self.ctx.window().set_mouse_cursor(mouse_state);
+ }
+
+ /// Modifier state change.
+ pub fn modifiers_input(&mut self, modifiers: Modifiers) {
+ *self.ctx.modifiers() = modifiers;
+
+ // Prompt hint highlight update.
+ self.ctx.mouse_mut().hint_highlight_dirty = true;
+
+ // Update mouse state and check for URL change.
+ let mouse_state = self.cursor_state();
+ self.ctx.window().set_mouse_cursor(mouse_state);
+ }
+
+ pub fn mouse_input(&mut self, state: ElementState, button: MouseButton) {
+ match button {
+ MouseButton::Left => self.ctx.mouse_mut().left_button_state = state,
+ MouseButton::Middle => self.ctx.mouse_mut().middle_button_state = state,
+ MouseButton::Right => self.ctx.mouse_mut().right_button_state = state,
+ _ => (),
+ }
+
+ // Skip normal mouse events if the message bar has been clicked.
+ if self.message_bar_cursor_state() == Some(CursorIcon::Pointer)
+ && state == ElementState::Pressed
+ {
+ let size = self.ctx.size_info();
+
+ let current_lines = self.ctx.message().map_or(0, |m| m.text(&size).len());
+
+ self.ctx.clear_selection();
+ self.ctx.pop_message();
+
+ // Reset cursor when message bar height changed or all messages are gone.
+ let new_lines = self.ctx.message().map_or(0, |m| m.text(&size).len());
+
+ let new_icon = match current_lines.cmp(&new_lines) {
+ Ordering::Less => CursorIcon::Default,
+ Ordering::Equal => CursorIcon::Pointer,
+ Ordering::Greater => {
+ if self.ctx.mouse_mode() {
+ CursorIcon::Default
+ } else {
+ CursorIcon::Text
+ }
+ },
+ };
+
+ self.ctx.window().set_mouse_cursor(new_icon);
+ } else {
+ match state {
+ ElementState::Pressed => {
+ // Process mouse press before bindings to update the `click_state`.
+ self.on_mouse_press(button);
+ self.process_mouse_bindings(button);
+ },
+ ElementState::Released => self.on_mouse_release(button),
+ }
+ }
+ }
+
+ /// Attempt to find a binding and execute its action.
+ ///
+ /// The provided mode, mods, and key must match what is allowed by a binding
+ /// for its action to be executed.
+ fn process_mouse_bindings(&mut self, button: MouseButton) {
+ let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active());
+ let mouse_mode = self.ctx.mouse_mode();
+ let mods = self.ctx.modifiers().state();
+ let mouse_bindings = self.ctx.config().mouse_bindings().to_owned();
+
+ // If mouse mode is active, also look for bindings without shift.
+ let mut check_fallback = mouse_mode && mods.contains(ModifiersState::SHIFT);
+
+ for binding in &mouse_bindings {
+ if binding.is_triggered_by(mode, mods, &button) {
+ binding.action.execute(&mut self.ctx);
+ check_fallback = false;
+ }
+ }
+
+ if check_fallback {
+ let fallback_mods = mods & !ModifiersState::SHIFT;
+ for binding in &mouse_bindings {
+ if binding.is_triggered_by(mode, fallback_mods, &button) {
+ binding.action.execute(&mut self.ctx);
+ }
+ }
+ }
+ }
+
+ /// Check mouse icon state in relation to the message bar.
+ fn message_bar_cursor_state(&self) -> Option<CursorIcon> {
+ // Since search is above the message bar, the button is offset by search's height.
+ let search_height = usize::from(self.ctx.search_active());
+
+ // Calculate Y position of the end of the last terminal line.
+ let size = self.ctx.size_info();
+ let terminal_end = size.padding_y() as usize
+ + size.cell_height() as usize * (size.screen_lines() + search_height);
+
+ let mouse = self.ctx.mouse();
+ let display_offset = self.ctx.terminal().grid().display_offset();
+ let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset);
+
+ if self.ctx.message().is_none() || (mouse.y <= terminal_end) {
+ None
+ } else if mouse.y <= terminal_end + size.cell_height() as usize
+ && point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns()
+ {
+ Some(CursorIcon::Pointer)
+ } else {
+ Some(CursorIcon::Default)
+ }
+ }
+
+ /// Icon state of the cursor.
+ fn cursor_state(&mut self) -> CursorIcon {
+ let display_offset = self.ctx.terminal().grid().display_offset();
+ let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset);
+ let hyperlink = self.ctx.terminal().grid()[point].hyperlink();
+
+ // Function to check if mouse is on top of a hint.
+ let hint_highlighted = |hint: &HintMatch| hint.should_highlight(point, hyperlink.as_ref());
+
+ if let Some(mouse_state) = self.message_bar_cursor_state() {
+ mouse_state
+ } else if self.ctx.display().highlighted_hint.as_ref().map_or(false, hint_highlighted) {
+ CursorIcon::Pointer
+ } else if !self.ctx.modifiers().state().shift_key() && self.ctx.mouse_mode() {
+ CursorIcon::Default
+ } else {
+ CursorIcon::Text
+ }
+ }
+
+ /// Handle automatic scrolling when selecting above/below the window.
+ fn update_selection_scrolling(&mut self, mouse_y: i32) {
+ let scale_factor = self.ctx.window().scale_factor;
+ let size = self.ctx.size_info();
+ let window_id = self.ctx.window().id();
+ let scheduler = self.ctx.scheduler_mut();
+
+ // Scale constants by DPI.
+ let min_height = (MIN_SELECTION_SCROLLING_HEIGHT * scale_factor) as i32;
+ let step = (SELECTION_SCROLLING_STEP * scale_factor) as i32;
+
+ // Compute the height of the scrolling areas.
+ let end_top = max(min_height, size.padding_y() as i32);
+ let text_area_bottom = size.padding_y() + size.screen_lines() as f32 * size.cell_height();
+ let start_bottom = min(size.height() as i32 - min_height, text_area_bottom as i32);
+
+ // Get distance from closest window boundary.
+ let delta = if mouse_y < end_top {
+ end_top - mouse_y + step
+ } else if mouse_y >= start_bottom {
+ start_bottom - mouse_y - step
+ } else {
+ scheduler.unschedule(TimerId::new(Topic::SelectionScrolling, window_id));
+ return;
+ };
+
+ // Scale number of lines scrolled based on distance to boundary.
+ let event = Event::new(EventType::Scroll(Scroll::Delta(delta / step)), Some(window_id));
+
+ // Schedule event.
+ let timer_id = TimerId::new(Topic::SelectionScrolling, window_id);
+ scheduler.unschedule(timer_id);
+ scheduler.schedule(event, SELECTION_SCROLLING_INTERVAL, true, timer_id);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use winit::event::{DeviceId, Event as WinitEvent, WindowEvent};
+ use winit::keyboard::Key;
+ use winit::window::WindowId;
+
+ use alacritty_terminal::event::Event as TerminalEvent;
+
+ use crate::config::Binding;
+ use crate::message_bar::MessageBuffer;
+
+ const KEY: Key<&'static str> = Key::Character("0");
+
+ struct MockEventProxy;
+ impl EventListener for MockEventProxy {}
+
+ struct ActionContext<'a, T> {
+ pub terminal: &'a mut Term<T>,
+ pub size_info: &'a SizeInfo,
+ pub mouse: &'a mut Mouse,
+ pub clipboard: &'a mut Clipboard,
+ pub message_buffer: &'a mut MessageBuffer,
+ pub modifiers: Modifiers,
+ config: &'a UiConfig,
+ inline_search_state: &'a mut InlineSearchState,
+ }
+
+ impl<'a, T: EventListener> super::ActionContext<T> for ActionContext<'a, T> {
+ fn search_next(
+ &mut self,
+ _origin: Point,
+ _direction: Direction,
+ _side: Side,
+ ) -> Option<Match> {
+ None
+ }
+
+ fn search_direction(&self) -> Direction {
+ Direction::Right
+ }
+
+ fn inline_search_state(&mut self) -> &mut InlineSearchState {
+ self.inline_search_state
+ }
+
+ fn search_active(&self) -> bool {
+ false
+ }
+
+ fn terminal(&self) -> &Term<T> {
+ self.terminal
+ }
+
+ fn terminal_mut(&mut self) -> &mut Term<T> {
+ self.terminal
+ }
+
+ fn size_info(&self) -> SizeInfo {
+ *self.size_info
+ }
+
+ fn selection_is_empty(&self) -> bool {
+ true
+ }
+
+ fn scroll(&mut self, scroll: Scroll) {
+ self.terminal.scroll_display(scroll);
+ }
+
+ fn mouse_mode(&self) -> bool {
+ false
+ }
+
+ #[inline]
+ fn mouse_mut(&mut self) -> &mut Mouse {
+ self.mouse
+ }
+
+ #[inline]
+ fn mouse(&self) -> &Mouse {
+ self.mouse
+ }
+
+ #[inline]
+ fn touch_purpose(&mut self) -> &mut TouchPurpose {
+ unimplemented!();
+ }
+
+ fn modifiers(&mut self) -> &mut Modifiers {
+ &mut self.modifiers
+ }
+
+ fn window(&mut self) -> &mut Window {
+ unimplemented!();
+ }
+
+ fn display(&mut self) -> &mut Display {
+ unimplemented!();
+ }
+
+ fn pop_message(&mut self) {
+ self.message_buffer.pop();
+ }
+
+ fn message(&self) -> Option<&Message> {
+ self.message_buffer.message()
+ }
+
+ fn config(&self) -> &UiConfig {
+ self.config
+ }
+
+ fn clipboard_mut(&mut self) -> &mut Clipboard {
+ self.clipboard
+ }
+
+ fn event_loop(&self) -> &EventLoopWindowTarget<Event> {
+ unimplemented!();
+ }
+
+ fn scheduler_mut(&mut self) -> &mut Scheduler {
+ unimplemented!();
+ }
+ }
+
+ macro_rules! test_clickstate {
+ {
+ name: $name:ident,
+ initial_state: $initial_state:expr,
+ initial_button: $initial_button:expr,
+ input: $input:expr,
+ end_state: $end_state:expr,
+ input_delay: $input_delay:expr,
+ } => {
+ #[test]
+ fn $name() {
+ let mut clipboard = Clipboard::new_nop();
+ let cfg = UiConfig::default();
+ let size = SizeInfo::new(
+ 21.0,
+ 51.0,
+ 3.0,
+ 3.0,
+ 0.,
+ 0.,
+ false,
+ );
+
+ let mut terminal = Term::new(cfg.term_options(), &size, MockEventProxy);
+
+ let mut mouse = Mouse {
+ click_state: $initial_state,
+ last_click_button: $initial_button,
+ last_click_timestamp: Instant::now() - $input_delay,
+ ..Mouse::default()
+ };
+
+ let mut inline_search_state = InlineSearchState::default();
+ let mut message_buffer = MessageBuffer::default();
+
+ let context = ActionContext {
+ terminal: &mut terminal,
+ mouse: &mut mouse,
+ size_info: &size,
+ clipboard: &mut clipboard,
+ modifiers: Default::default(),
+ message_buffer: &mut message_buffer,
+ inline_search_state: &mut inline_search_state,
+ config: &cfg,
+ };
+
+ let mut processor = Processor::new(context);
+
+ let event: WinitEvent::<TerminalEvent> = $input;
+ if let WinitEvent::WindowEvent {
+ event: WindowEvent::MouseInput {
+ state,
+ button,
+ ..
+ },
+ ..
+ } = event
+ {
+ processor.mouse_input(state, button);
+ };
+
+ assert_eq!(processor.ctx.mouse.click_state, $end_state);
+ }
+ }
+ }
+
+ macro_rules! test_process_binding {
+ {
+ name: $name:ident,
+ binding: $binding:expr,
+ triggers: $triggers:expr,
+ mode: $mode:expr,
+ mods: $mods:expr,
+ } => {
+ #[test]
+ fn $name() {
+ if $triggers {
+ assert!($binding.is_triggered_by($mode, $mods, &KEY));
+ } else {
+ assert!(!$binding.is_triggered_by($mode, $mods, &KEY));
+ }
+ }
+ }
+ }
+
+ test_clickstate! {
+ name: single_click,
+ initial_state: ClickState::None,
+ initial_button: MouseButton::Other(0),
+ input: WinitEvent::WindowEvent {
+ event: WindowEvent::MouseInput {
+ state: ElementState::Pressed,
+ button: MouseButton::Left,
+ device_id: unsafe { DeviceId::dummy() },
+ },
+ window_id: unsafe { WindowId::dummy() },
+ },
+ end_state: ClickState::Click,
+ input_delay: Duration::ZERO,
+ }
+
+ test_clickstate! {
+ name: single_right_click,
+ initial_state: ClickState::None,
+ initial_button: MouseButton::Other(0),
+ input: WinitEvent::WindowEvent {
+ event: WindowEvent::MouseInput {
+ state: ElementState::Pressed,
+ button: MouseButton::Right,
+ device_id: unsafe { DeviceId::dummy() },
+ },
+ window_id: unsafe { WindowId::dummy() },
+ },
+ end_state: ClickState::Click,
+ input_delay: Duration::ZERO,
+ }
+
+ test_clickstate! {
+ name: single_middle_click,
+ initial_state: ClickState::None,
+ initial_button: MouseButton::Other(0),
+ input: WinitEvent::WindowEvent {
+ event: WindowEvent::MouseInput {
+ state: ElementState::Pressed,
+ button: MouseButton::Middle,
+ device_id: unsafe { DeviceId::dummy() },
+ },
+ window_id: unsafe { WindowId::dummy() },
+ },
+ end_state: ClickState::Click,
+ input_delay: Duration::ZERO,
+ }
+
+ test_clickstate! {
+ name: double_click,
+ initial_state: ClickState::Click,
+ initial_button: MouseButton::Left,
+ input: WinitEvent::WindowEvent {
+ event: WindowEvent::MouseInput {
+ state: ElementState::Pressed,
+ button: MouseButton::Left,
+ device_id: unsafe { DeviceId::dummy() },
+ },
+ window_id: unsafe { WindowId::dummy() },
+ },
+ end_state: ClickState::DoubleClick,
+ input_delay: Duration::ZERO,
+ }
+
+ test_clickstate! {
+ name: double_click_failed,
+ initial_state: ClickState::Click,
+ initial_button: MouseButton::Left,
+ input: WinitEvent::WindowEvent {
+ event: WindowEvent::MouseInput {
+ state: ElementState::Pressed,
+ button: MouseButton::Left,
+ device_id: unsafe { DeviceId::dummy() },
+ },
+ window_id: unsafe { WindowId::dummy() },
+ },
+ end_state: ClickState::Click,
+ input_delay: CLICK_THRESHOLD,
+ }
+
+ test_clickstate! {
+ name: triple_click,
+ initial_state: ClickState::DoubleClick,
+ initial_button: MouseButton::Left,
+ input: WinitEvent::WindowEvent {
+ event: WindowEvent::MouseInput {
+ state: ElementState::Pressed,
+ button: MouseButton::Left,
+ device_id: unsafe { DeviceId::dummy() },
+ },
+ window_id: unsafe { WindowId::dummy() },
+ },
+ end_state: ClickState::TripleClick,
+ input_delay: Duration::ZERO,
+ }
+
+ test_clickstate! {
+ name: triple_click_failed,
+ initial_state: ClickState::DoubleClick,
+ initial_button: MouseButton::Left,
+ input: WinitEvent::WindowEvent {
+ event: WindowEvent::MouseInput {
+ state: ElementState::Pressed,
+ button: MouseButton::Left,
+ device_id: unsafe { DeviceId::dummy() },
+ },
+ window_id: unsafe { WindowId::dummy() },
+ },
+ end_state: ClickState::Click,
+ input_delay: CLICK_THRESHOLD,
+ }
+
+ test_clickstate! {
+ name: multi_click_separate_buttons,
+ initial_state: ClickState::DoubleClick,
+ initial_button: MouseButton::Left,
+ input: WinitEvent::WindowEvent {
+ event: WindowEvent::MouseInput {
+ state: ElementState::Pressed,
+ button: MouseButton::Right,
+ device_id: unsafe { DeviceId::dummy() },
+ },
+ window_id: unsafe { WindowId::dummy() },
+ },
+ end_state: ClickState::Click,
+ input_delay: Duration::ZERO,
+ }
+
+ test_process_binding! {
+ name: process_binding_nomode_shiftmod_require_shift,
+ binding: Binding { trigger: KEY, mods: ModifiersState::SHIFT, action: Action::from("\x1b[1;2D"), mode: BindingMode::empty(), notmode: BindingMode::empty() },
+ triggers: true,
+ mode: BindingMode::empty(),
+ mods: ModifiersState::SHIFT,
+ }
+
+ test_process_binding! {
+ name: process_binding_nomode_nomod_require_shift,
+ binding: Binding { trigger: KEY, mods: ModifiersState::SHIFT, action: Action::from("\x1b[1;2D"), mode: BindingMode::empty(), notmode: BindingMode::empty() },
+ triggers: false,
+ mode: BindingMode::empty(),
+ mods: ModifiersState::empty(),
+ }
+
+ test_process_binding! {
+ name: process_binding_nomode_controlmod,
+ binding: Binding { trigger: KEY, mods: ModifiersState::CONTROL, action: Action::from("\x1b[1;5D"), mode: BindingMode::empty(), notmode: BindingMode::empty() },
+ triggers: true,
+ mode: BindingMode::empty(),
+ mods: ModifiersState::CONTROL,
+ }
+
+ test_process_binding! {
+ name: process_binding_nomode_nomod_require_not_appcursor,
+ binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1b[D"), mode: BindingMode::empty(), notmode: BindingMode::APP_CURSOR },
+ triggers: true,
+ mode: BindingMode::empty(),
+ mods: ModifiersState::empty(),
+ }
+
+ test_process_binding! {
+ name: process_binding_appcursormode_nomod_require_appcursor,
+ binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1bOD"), mode: BindingMode::APP_CURSOR, notmode: BindingMode::empty() },
+ triggers: true,
+ mode: BindingMode::APP_CURSOR,
+ mods: ModifiersState::empty(),
+ }
+
+ test_process_binding! {
+ name: process_binding_nomode_nomod_require_appcursor,
+ binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1bOD"), mode: BindingMode::APP_CURSOR, notmode: BindingMode::empty() },
+ triggers: false,
+ mode: BindingMode::empty(),
+ mods: ModifiersState::empty(),
+ }
+
+ test_process_binding! {
+ name: process_binding_appcursormode_appkeypadmode_nomod_require_appcursor,
+ binding: Binding { trigger: KEY, mods: ModifiersState::empty(), action: Action::from("\x1bOD"), mode: BindingMode::APP_CURSOR, notmode: BindingMode::empty() },
+ triggers: true,
+ mode: BindingMode::APP_CURSOR | BindingMode::APP_KEYPAD,
+ mods: ModifiersState::empty(),
+ }
+
+ test_process_binding! {
+ name: process_binding_fail_with_extra_mods,
+ binding: Binding { trigger: KEY, mods: ModifiersState::SUPER, action: Action::from("arst"), mode: BindingMode::empty(), notmode: BindingMode::empty() },
+ triggers: false,
+ mode: BindingMode::empty(),
+ mods: ModifiersState::ALT | ModifiersState::SUPER,
+ }
+}