aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/input
diff options
context:
space:
mode:
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,
+ }
+}