// Copyright 2016 Joe Wilm, The Alacritty Project Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //! Handle input from glutin //! //! 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::mem; use std::ops::RangeInclusive; use std::time::Instant; use copypasta::{Buffer as ClipboardBuffer, Clipboard, Load}; use glutin::{ ElementState, KeyboardInput, ModifiersState, MouseButton, MouseCursor, MouseScrollDelta, TouchPhase, }; use unicode_width::UnicodeWidthStr; use crate::ansi::{ClearMode, Handler}; use crate::config::{self, Key}; use crate::event::{ClickState, Mouse}; use crate::grid::Scroll; use crate::index::{Column, Line, Linear, Point, Side}; use crate::message_bar::{self, Message}; use crate::term::mode::TermMode; use crate::term::{Search, SizeInfo, Term}; use crate::url::Url; use crate::util::fmt::Red; use crate::util::start_daemon; pub const FONT_SIZE_STEP: f32 = 0.5; /// Processes input from glutin. /// /// An escape sequence may be emitted in case specific keys or key combinations /// are activated. /// /// TODO also need terminal state when processing input pub struct Processor<'a, A: 'a> { pub key_bindings: &'a [KeyBinding], pub mouse_bindings: &'a [MouseBinding], pub mouse_config: &'a config::Mouse, pub scrolling_config: &'a config::Scrolling, pub ctx: A, pub save_to_clipboard: bool, pub alt_send_esc: bool, } pub trait ActionContext { fn write_to_pty>>(&mut self, _: B); fn size_info(&self) -> SizeInfo; fn copy_selection(&self, _: ClipboardBuffer); fn clear_selection(&mut self); fn update_selection(&mut self, point: Point, side: Side); fn simple_selection(&mut self, point: Point, side: Side); fn semantic_selection(&mut self, point: Point); fn line_selection(&mut self, point: Point); fn selection_is_empty(&self) -> bool; fn mouse_mut(&mut self) -> &mut Mouse; fn mouse(&self) -> &Mouse; fn mouse_coords(&self) -> Option; fn received_count(&mut self) -> &mut usize; fn suppress_chars(&mut self) -> &mut bool; fn last_modifiers(&mut self) -> &mut ModifiersState; fn scroll(&mut self, scroll: Scroll); fn hide_window(&mut self); fn terminal(&self) -> &Term; fn terminal_mut(&mut self) -> &mut Term; fn spawn_new_instance(&mut self); } /// Describes a state and action to take in that state /// /// This is the shared component of `MouseBinding` and `KeyBinding` #[derive(Debug, Clone, PartialEq, Eq)] pub struct Binding { /// Modifier keys required to activate binding pub mods: ModifiersState, /// String to send to pty if mods and mode match pub action: Action, /// Terminal mode required to activate binding pub mode: TermMode, /// excluded terminal modes where the binding won't be activated pub notmode: TermMode, /// This property is used as part of the trigger detection code. /// /// For example, this might be a key like "G", or a mouse button. pub trigger: T, } /// Bindings that are triggered by a keyboard key pub type KeyBinding = Binding; /// Bindings that are triggered by a mouse button pub type MouseBinding = Binding; impl Default for KeyBinding { fn default() -> KeyBinding { KeyBinding { mods: Default::default(), action: Action::Esc(String::new()), mode: TermMode::NONE, notmode: TermMode::NONE, trigger: Key::A, } } } impl Default for MouseBinding { fn default() -> MouseBinding { MouseBinding { mods: Default::default(), action: Action::Esc(String::new()), mode: TermMode::NONE, notmode: TermMode::NONE, trigger: MouseButton::Left, } } } impl Binding { #[inline] fn is_triggered_by( &self, mode: TermMode, mods: ModifiersState, input: &T, relaxed: bool, ) -> bool { // Check input first since bindings are stored in one big list. This is // the most likely item to fail so prioritizing it here allows more // checks to be short circuited. self.trigger == *input && self.mode_matches(mode) && self.not_mode_matches(mode) && self.mods_match(mods, relaxed) } #[inline] pub fn triggers_match(&self, binding: &Binding) -> bool { self.trigger == binding.trigger && self.mode == binding.mode && self.notmode == binding.notmode && self.mods == binding.mods } } impl Binding { /// Execute the action associate with this binding #[inline] fn execute(&self, ctx: &mut A, mouse_mode: bool) { self.action.execute(ctx, mouse_mode) } #[inline] fn mode_matches(&self, mode: TermMode) -> bool { self.mode.is_empty() || mode.intersects(self.mode) } #[inline] fn not_mode_matches(&self, mode: TermMode) -> bool { self.notmode.is_empty() || !mode.intersects(self.notmode) } /// Check that two mods descriptions for equivalence #[inline] fn mods_match(&self, mods: ModifiersState, relaxed: bool) -> bool { if relaxed { self.mods.relaxed_eq(mods) } else { self.mods == mods } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Action { /// Write an escape sequence Esc(String), /// Paste contents of system clipboard Paste, // Store current selection into clipboard Copy, /// Paste contents of selection buffer PasteSelection, /// Increase font size IncreaseFontSize, /// Decrease font size DecreaseFontSize, /// Reset font size to the config value ResetFontSize, /// Scroll exactly one page up ScrollPageUp, /// Scroll exactly one page down ScrollPageDown, /// Scroll one line up ScrollLineUp, /// Scroll one line down ScrollLineDown, /// Scroll all the way to the top ScrollToTop, /// Scroll all the way to the bottom ScrollToBottom, /// Clear the display buffer(s) to remove history ClearHistory, /// Run given command Command(String, Vec), /// Hides the Alacritty window Hide, /// Quits Alacritty. Quit, /// Clears warning and error notices. ClearLogNotice, /// Spawn a new instance of Alacritty. SpawnNewInstance, /// No action. None, } impl Default for Action { fn default() -> Action { Action::None } } impl Action { #[inline] fn execute(&self, ctx: &mut A, mouse_mode: bool) { match *self { Action::Esc(ref s) => { ctx.scroll(Scroll::Bottom); ctx.write_to_pty(s.clone().into_bytes()) }, Action::Copy => { ctx.copy_selection(ClipboardBuffer::Primary); }, Action::Paste => { Clipboard::new() .and_then(|clipboard| clipboard.load_primary()) .map(|contents| self.paste(ctx, &contents)) .unwrap_or_else(|err| { error!("Error loading data from clipboard: {}", Red(err)); }); }, Action::PasteSelection => { // Only paste if mouse events are not captured by an application if !mouse_mode { Clipboard::new() .and_then(|clipboard| clipboard.load_selection()) .map(|contents| self.paste(ctx, &contents)) .unwrap_or_else(|err| { error!("Error loading data from clipboard: {}", Red(err)); }); } }, Action::Command(ref program, ref args) => { trace!("Running command {} with args {:?}", program, args); match start_daemon(program, args) { Ok(_) => { debug!("Spawned new proc"); }, Err(err) => { warn!("Couldn't run command {}", err); }, } }, Action::Hide => { ctx.hide_window(); }, Action::Quit => { ctx.terminal_mut().exit(); }, Action::IncreaseFontSize => { ctx.terminal_mut().change_font_size(FONT_SIZE_STEP); }, Action::DecreaseFontSize => { ctx.terminal_mut().change_font_size(-FONT_SIZE_STEP); }, Action::ResetFontSize => { ctx.terminal_mut().reset_font_size(); }, Action::ScrollPageUp => { ctx.scroll(Scroll::PageUp); }, Action::ScrollPageDown => { ctx.scroll(Scroll::PageDown); }, Action::ScrollLineUp => { ctx.scroll(Scroll::Lines(1)); }, Action::ScrollLineDown => { ctx.scroll(Scroll::Lines(-1)); }, Action::ScrollToTop => { ctx.scroll(Scroll::Top); }, Action::ScrollToBottom => { ctx.scroll(Scroll::Bottom); }, Action::ClearHistory => { ctx.terminal_mut().clear_screen(ClearMode::Saved); }, Action::ClearLogNotice => { ctx.terminal_mut().message_buffer_mut().pop(); }, Action::SpawnNewInstance => { ctx.spawn_new_instance(); }, Action::None => (), } } fn paste(&self, ctx: &mut A, contents: &str) { if ctx.terminal().mode().contains(TermMode::BRACKETED_PASTE) { ctx.write_to_pty(&b"\x1b[200~"[..]); ctx.write_to_pty(contents.replace("\x1b", "").into_bytes()); ctx.write_to_pty(&b"\x1b[201~"[..]); } else { // In non-bracketed (ie: normal) mode, terminal applications cannot distinguish // pasted data from keystrokes. // In theory, we should construct the keystrokes needed to produce the data we are // pasting... since that's neither practical nor sensible (and probably an impossible // task to solve in a general way), we'll just replace line breaks (windows and unix // style) with a singe carriage return (\r, which is what the Enter key produces). ctx.write_to_pty(contents.replace("\r\n", "\r").replace("\n", "\r").into_bytes()); } } } trait RelaxedEq { fn relaxed_eq(&self, other: T) -> bool; } impl RelaxedEq for ModifiersState { // Make sure that modifiers in the config are always present, // but ignore surplus modifiers. fn relaxed_eq(&self, other: Self) -> bool { (!self.logo || other.logo) && (!self.alt || other.alt) && (!self.ctrl || other.ctrl) && (!self.shift || other.shift) } } impl From<&'static str> for Action { fn from(s: &'static str) -> Action { Action::Esc(s.into()) } } impl<'a, A: ActionContext + 'a> Processor<'a, A> { #[inline] pub fn mouse_moved(&mut self, x: usize, y: usize, modifiers: ModifiersState) { self.ctx.mouse_mut().x = x; self.ctx.mouse_mut().y = y; let size_info = self.ctx.size_info(); let point = size_info.pixels_to_coords(x, y); let cell_side = self.get_mouse_side(); let prev_side = mem::replace(&mut self.ctx.mouse_mut().cell_side, cell_side); let prev_line = mem::replace(&mut self.ctx.mouse_mut().line, point.line); let prev_col = mem::replace(&mut self.ctx.mouse_mut().column, point.col); let motion_mode = TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG; let report_mode = TermMode::MOUSE_REPORT_CLICK | motion_mode; let mouse_moved = prev_line != self.ctx.mouse().line || prev_col != self.ctx.mouse().column || prev_side != cell_side; // Don't launch URLs if mouse has moved if mouse_moved { self.ctx.mouse_mut().block_url_launcher = true; } // Only report motions when cell changed and mouse is not over the message bar if self.message_at_point(Some(point)).is_some() || !mouse_moved { return; } // Underline URLs and change cursor on hover self.update_url_highlight(point, modifiers); if self.ctx.mouse().left_button_state == ElementState::Pressed && (modifiers.shift || !self.ctx.terminal().mode().intersects(report_mode)) { self.ctx.update_selection(Point { line: point.line, col: point.col }, cell_side); } else if self.ctx.terminal().mode().intersects(motion_mode) && size_info.contains_point(x, y, false) { if self.ctx.mouse().left_button_state == ElementState::Pressed { self.mouse_report(32, ElementState::Pressed, modifiers); } else if self.ctx.mouse().middle_button_state == ElementState::Pressed { self.mouse_report(33, ElementState::Pressed, modifiers); } else if self.ctx.mouse().right_button_state == ElementState::Pressed { self.mouse_report(34, ElementState::Pressed, modifiers); } else if self.ctx.terminal().mode().contains(TermMode::MOUSE_MOTION) { self.mouse_report(35, ElementState::Pressed, modifiers); } } } /// Underline URLs and change the mouse cursor when URL hover state changes. fn update_url_highlight(&mut self, point: Point, modifiers: ModifiersState) { let mouse_mode = TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG | TermMode::MOUSE_REPORT_CLICK; // Only show URLs as launchable when all required modifiers are pressed let url = if self.mouse_config.url.modifiers.relaxed_eq(modifiers) && (!self.ctx.terminal().mode().intersects(mouse_mode) || modifiers.shift) && self.mouse_config.url.launcher.is_some() { self.ctx.terminal().url_search(point.into()) } else { None }; if let Some(Url { origin, text }) = url { let cols = self.ctx.size_info().cols().0; // Calculate the URL's start position let lines_before = (origin + cols - point.col.0 - 1) / cols; let (start_col, start_line) = if lines_before > point.line.0 { (0, 0) } else { let start_col = (cols + point.col.0 - origin % cols) % cols; let start_line = point.line.0 - lines_before; (start_col, start_line) }; let start = Point::new(start_line, Column(start_col)); // Calculate the URL's end position let len = text.width(); let end_col = (point.col.0 + len - origin) % cols - 1; let end_line = point.line.0 + (point.col.0 + len - origin) / cols; let end = Point::new(end_line, Column(end_col)); let start = Linear::from_point(Column(cols), start); let end = Linear::from_point(Column(cols), end); self.ctx.terminal_mut().set_url_highlight(RangeInclusive::new(start, end)); self.ctx.terminal_mut().set_mouse_cursor(MouseCursor::Hand); self.ctx.terminal_mut().dirty = true; } else { self.ctx.terminal_mut().reset_url_highlight(); } } fn get_mouse_side(&self) -> Side { let size_info = self.ctx.size_info(); let x = self.ctx.mouse().x; 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 } } pub fn normal_mouse_report(&mut self, button: u8) { let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column); if line < Line(223) && column < Column(223) { let msg = vec![ b'\x1b', b'[', b'M', 32 + button, 32 + 1 + column.0 as u8, 32 + 1 + line.0 as u8, ]; self.ctx.write_to_pty(msg); } } pub fn sgr_mouse_report(&mut self, button: u8, state: ElementState) { let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column); let c = match state { ElementState::Pressed => 'M', ElementState::Released => 'm', }; let msg = format!("\x1b[<{};{};{}{}", button, column + 1, line + 1, c); self.ctx.write_to_pty(msg.into_bytes()); } pub fn mouse_report(&mut self, button: u8, state: ElementState, modifiers: ModifiersState) { // Calculate modifiers value let mut mods = 0; if modifiers.shift { mods += 4; } if modifiers.alt { mods += 8; } if modifiers.ctrl { mods += 16; } // Report mouse events if self.ctx.terminal().mode().contains(TermMode::SGR_MOUSE) { self.sgr_mouse_report(button + mods, state); } else if let ElementState::Released = state { self.normal_mouse_report(3 + mods); } else { self.normal_mouse_report(button + mods); } } pub fn on_mouse_double_click(&mut self, button: MouseButton, point: Option) { if let (Some(point), true) = (point, button == MouseButton::Left) { self.ctx.semantic_selection(point); } } pub fn on_mouse_triple_click(&mut self, button: MouseButton, point: Option) { if let (Some(point), true) = (point, button == MouseButton::Left) { self.ctx.line_selection(point); } } pub fn on_mouse_press( &mut self, button: MouseButton, modifiers: ModifiersState, point: Option, ) { let now = Instant::now(); let elapsed = self.ctx.mouse().last_click_timestamp.elapsed(); self.ctx.mouse_mut().last_click_timestamp = now; let button_changed = self.ctx.mouse().last_button != button; self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state { ClickState::Click if !button_changed && elapsed < self.mouse_config.double_click.threshold => { self.ctx.mouse_mut().block_url_launcher = true; self.on_mouse_double_click(button, point); ClickState::DoubleClick } ClickState::DoubleClick if !button_changed && elapsed < self.mouse_config.triple_click.threshold => { self.ctx.mouse_mut().block_url_launcher = true; self.on_mouse_triple_click(button, point); ClickState::TripleClick } _ => { // Don't launch URLs if this click cleared the selection self.ctx.mouse_mut().block_url_launcher = !self.ctx.selection_is_empty(); self.ctx.clear_selection(); // Start new empty selection let side = self.ctx.mouse().cell_side; if let Some(point) = point { self.ctx.simple_selection(point, side); } let report_modes = TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION; if !modifiers.shift && self.ctx.terminal().mode().intersects(report_modes) { let code = match button { MouseButton::Left => 0, MouseButton::Middle => 1, MouseButton::Right => 2, // Can't properly report more than three buttons. MouseButton::Other(_) => return, }; self.mouse_report(code, ElementState::Pressed, modifiers); return; } ClickState::Click }, }; } pub fn on_mouse_release( &mut self, button: MouseButton, modifiers: ModifiersState, point: Option, ) { let report_modes = TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION; if !modifiers.shift && self.ctx.terminal().mode().intersects(report_modes) { let code = match button { MouseButton::Left => 0, MouseButton::Middle => 1, MouseButton::Right => 2, // Can't properly report more than three buttons. MouseButton::Other(_) => return, }; self.mouse_report(code, ElementState::Released, modifiers); return; } else if let (Some(point), true) = (point, button == MouseButton::Left) { self.launch_url(modifiers, point); } self.copy_selection(); } // Spawn URL launcher when clicking on URLs fn launch_url(&self, modifiers: ModifiersState, point: Point) -> Option<()> { if !self.mouse_config.url.modifiers.relaxed_eq(modifiers) || self.ctx.mouse().block_url_launcher { return None; } let text = self.ctx.terminal().url_search(point.into())?.text; let launcher = self.mouse_config.url.launcher.as_ref()?; let mut args = launcher.args().to_vec(); args.push(text); match start_daemon(launcher.program(), &args) { Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args), Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args), } Some(()) } pub fn on_mouse_wheel( &mut self, delta: MouseScrollDelta, phase: TouchPhase, modifiers: ModifiersState, ) { match delta { MouseScrollDelta::LineDelta(_columns, lines) => { let new_scroll_px = lines * self.ctx.size_info().cell_height; self.scroll_terminal(modifiers, new_scroll_px as i32); }, MouseScrollDelta::PixelDelta(lpos) => { match phase { TouchPhase::Started => { // Reset offset to zero self.ctx.mouse_mut().scroll_px = 0; }, TouchPhase::Moved => { self.scroll_terminal(modifiers, lpos.y as i32); }, _ => (), } }, } } fn scroll_terminal(&mut self, modifiers: ModifiersState, new_scroll_px: i32) { let mouse_modes = TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION; let height = self.ctx.size_info().cell_height as i32; // Make sure the new and deprecated setting are both allowed let faux_multiplier = self .mouse_config .faux_scrollback_lines .unwrap_or(self.scrolling_config.faux_multiplier as usize); if self.ctx.terminal().mode().intersects(mouse_modes) { self.ctx.mouse_mut().scroll_px += new_scroll_px; let code = if new_scroll_px > 0 { 64 } else { 65 }; let lines = (self.ctx.mouse().scroll_px / height).abs(); for _ in 0..lines { self.mouse_report(code, ElementState::Pressed, modifiers); } } else if self.ctx.terminal().mode().contains(TermMode::ALT_SCREEN) && faux_multiplier > 0 && !modifiers.shift { self.ctx.mouse_mut().scroll_px += new_scroll_px * faux_multiplier as i32; let cmd = if new_scroll_px > 0 { b'A' } else { b'B' }; let lines = (self.ctx.mouse().scroll_px / height).abs(); let mut content = Vec::with_capacity(lines as usize * 3); for _ in 0..lines { content.push(0x1b); content.push(b'O'); content.push(cmd); } self.ctx.write_to_pty(content); } else { let multiplier = i32::from(self.scrolling_config.multiplier); self.ctx.mouse_mut().scroll_px += new_scroll_px * multiplier; let lines = self.ctx.mouse().scroll_px / height; self.ctx.scroll(Scroll::Lines(lines as isize)); } self.ctx.mouse_mut().scroll_px %= 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()); } } pub fn mouse_input( &mut self, state: ElementState, button: MouseButton, modifiers: ModifiersState, ) { 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, _ => (), } let point = self.ctx.mouse_coords(); // Skip normal mouse events if the message bar has been clicked if let Some(message) = self.message_at_point(point) { // Message should never be `Some` if point is `None` debug_assert!(point.is_some()); self.on_message_bar_click(state, point.unwrap(), message); } else { match state { ElementState::Pressed => { self.process_mouse_bindings(modifiers, button); self.on_mouse_press(button, modifiers, point); }, ElementState::Released => self.on_mouse_release(button, modifiers, point), } } self.ctx.mouse_mut().last_button = button; } /// Process key input pub fn process_key(&mut self, input: KeyboardInput) { match input.state { ElementState::Pressed => { *self.ctx.last_modifiers() = input.modifiers; *self.ctx.received_count() = 0; *self.ctx.suppress_chars() = false; if self.process_key_bindings(input) { *self.ctx.suppress_chars() = true; } }, ElementState::Released => *self.ctx.suppress_chars() = false, } } /// Process a received character pub fn received_char(&mut self, c: char) { if *self.ctx.suppress_chars() { return; } self.ctx.scroll(Scroll::Bottom); self.ctx.clear_selection(); let utf8_len = c.len_utf8(); let mut bytes = Vec::with_capacity(utf8_len); unsafe { bytes.set_len(utf8_len); c.encode_utf8(&mut bytes[..]); } if self.alt_send_esc && *self.ctx.received_count() == 0 && self.ctx.last_modifiers().alt && utf8_len == 1 { bytes.insert(0, b'\x1b'); } self.ctx.write_to_pty(bytes); *self.ctx.received_count() += 1; } /// Attempts 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. /// /// Returns true if an action is executed. fn process_key_bindings(&mut self, input: KeyboardInput) -> bool { let mut has_binding = false; for binding in self.key_bindings { let is_triggered = match binding.trigger { Key::Scancode(_) => binding.is_triggered_by( *self.ctx.terminal().mode(), input.modifiers, &Key::Scancode(input.scancode), false, ), _ => { if let Some(key) = input.virtual_keycode { let key = Key::from_glutin_input(key); binding.is_triggered_by( *self.ctx.terminal().mode(), input.modifiers, &key, false, ) } else { false } }, }; if is_triggered { // binding was triggered; run the action binding.execute(&mut self.ctx, false); has_binding = true; } } has_binding } /// Attempts 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. /// /// Returns true if an action is executed. fn process_mouse_bindings(&mut self, mods: ModifiersState, button: MouseButton) -> bool { let mut has_binding = false; for binding in self.mouse_bindings { if binding.is_triggered_by(*self.ctx.terminal().mode(), mods, &button, true) { // binding was triggered; run the action let mouse_mode = !mods.shift && self.ctx.terminal().mode().intersects( TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION, ); binding.execute(&mut self.ctx, mouse_mode); has_binding = true; } } has_binding } /// Return the message bar's message if there is some at the specified point fn message_at_point(&mut self, point: Option) -> Option { if let (Some(point), Some(message)) = (point, self.ctx.terminal_mut().message_buffer_mut().message()) { let size = self.ctx.size_info(); if point.line.0 >= size.lines().saturating_sub(message.text(&size).len()) { return Some(message); } } None } /// Handle clicks on the message bar. fn on_message_bar_click(&mut self, button_state: ElementState, point: Point, message: Message) { match button_state { ElementState::Released => self.copy_selection(), ElementState::Pressed => { let size = self.ctx.size_info(); if point.col + message_bar::CLOSE_BUTTON_TEXT.len() >= size.cols() && point.line == size.lines() - message.text(&size).len() { self.ctx.terminal_mut().message_buffer_mut().pop(); } self.ctx.clear_selection(); }, } } /// Copy text selection. fn copy_selection(&mut self) { if self.save_to_clipboard { self.ctx.copy_selection(ClipboardBuffer::Primary); } self.ctx.copy_selection(ClipboardBuffer::Selection); } } #[cfg(test)] mod tests { use std::borrow::Cow; use std::time::Duration; use glutin::{ElementState, Event, ModifiersState, MouseButton, VirtualKeyCode, WindowEvent}; use crate::config::{self, ClickHandler, Config}; use crate::event::{ClickState, Mouse, WindowChanges}; use crate::grid::Scroll; use crate::index::{Point, Side}; use crate::message_bar::MessageBuffer; use crate::selection::Selection; use crate::term::{SizeInfo, Term, TermMode}; use super::{Action, Binding, Processor}; use copypasta::Buffer as ClipboardBuffer; const KEY: VirtualKeyCode = VirtualKeyCode::Key0; #[derive(PartialEq)] enum MultiClick { DoubleClick, TripleClick, None, } struct ActionContext<'a> { pub terminal: &'a mut Term, pub selection: &'a mut Option, pub size_info: &'a SizeInfo, pub mouse: &'a mut Mouse, pub last_action: MultiClick, pub received_count: usize, pub suppress_chars: bool, pub last_modifiers: ModifiersState, pub window_changes: &'a mut WindowChanges, } impl<'a> super::ActionContext for ActionContext<'a> { fn write_to_pty>>(&mut self, _val: B) {} fn update_selection(&mut self, _point: Point, _side: Side) {} fn simple_selection(&mut self, _point: Point, _side: Side) {} fn copy_selection(&self, _buffer: ClipboardBuffer) {} fn clear_selection(&mut self) {} fn hide_window(&mut self) {} fn spawn_new_instance(&mut self) {} fn terminal(&self) -> &Term { &self.terminal } fn terminal_mut(&mut self) -> &mut Term { &mut self.terminal } fn size_info(&self) -> SizeInfo { *self.size_info } fn semantic_selection(&mut self, _point: Point) { // set something that we can check for here self.last_action = MultiClick::DoubleClick; } fn line_selection(&mut self, _point: Point) { self.last_action = MultiClick::TripleClick; } fn selection_is_empty(&self) -> bool { true } fn scroll(&mut self, scroll: Scroll) { self.terminal.scroll_display(scroll); } fn mouse_coords(&self) -> Option { self.terminal.pixels_to_coords(self.mouse.x as usize, self.mouse.y as usize) } #[inline] fn mouse_mut(&mut self) -> &mut Mouse { self.mouse } #[inline] fn mouse(&self) -> &Mouse { self.mouse } fn received_count(&mut self) -> &mut usize { &mut self.received_count } fn suppress_chars(&mut self) -> &mut bool { &mut self.suppress_chars } fn last_modifiers(&mut self) -> &mut ModifiersState { &mut self.last_modifiers } } macro_rules! test_clickstate { { name: $name:ident, initial_state: $initial_state:expr, initial_button: $initial_button:expr, input: $input:expr, end_state: $end_state:pat, last_action: $last_action:expr } => { #[test] fn $name() { let config = Config::default(); let size = SizeInfo { width: 21.0, height: 51.0, cell_width: 3.0, cell_height: 3.0, padding_x: 0.0, padding_y: 0.0, dpr: 1.0, }; let mut terminal = Term::new(&config, size, MessageBuffer::new()); let mut mouse = Mouse::default(); mouse.click_state = $initial_state; mouse.last_button = $initial_button; let mut selection = None; let context = ActionContext { terminal: &mut terminal, selection: &mut selection, mouse: &mut mouse, size_info: &size, last_action: MultiClick::None, received_count: 0, suppress_chars: false, last_modifiers: ModifiersState::default(), window_changes: &mut WindowChanges::default(), }; let mut processor = Processor { ctx: context, mouse_config: &config::Mouse { double_click: ClickHandler { threshold: Duration::from_millis(1000), }, triple_click: ClickHandler { threshold: Duration::from_millis(1000), }, hide_when_typing: false, faux_scrollback_lines: None, url: Default::default(), }, scrolling_config: &config::Scrolling::default(), key_bindings: &config.key_bindings()[..], mouse_bindings: &config.mouse_bindings()[..], save_to_clipboard: config.selection().save_to_clipboard, alt_send_esc: config.alt_send_esc(), }; if let Event::WindowEvent { event: WindowEvent::MouseInput { state, button, modifiers, .. }, .. } = $input { processor.mouse_input(state, button, modifiers); }; assert!(match processor.ctx.mouse.click_state { $end_state => processor.ctx.last_action == $last_action, _ => false }); } } } 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, false)); } else { assert!(!$binding.is_triggered_by($mode, $mods, &KEY, false)); } } } } test_clickstate! { name: single_click, initial_state: ClickState::None, initial_button: MouseButton::Other(0), input: Event::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, device_id: unsafe { ::std::mem::transmute_copy(&0) }, modifiers: ModifiersState::default(), }, window_id: unsafe { ::std::mem::transmute_copy(&0) }, }, end_state: ClickState::Click, last_action: MultiClick::None } test_clickstate! { name: double_click, initial_state: ClickState::Click, initial_button: MouseButton::Left, input: Event::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, device_id: unsafe { ::std::mem::transmute_copy(&0) }, modifiers: ModifiersState::default(), }, window_id: unsafe { ::std::mem::transmute_copy(&0) }, }, end_state: ClickState::DoubleClick, last_action: MultiClick::DoubleClick } test_clickstate! { name: triple_click, initial_state: ClickState::DoubleClick, initial_button: MouseButton::Left, input: Event::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, device_id: unsafe { ::std::mem::transmute_copy(&0) }, modifiers: ModifiersState::default(), }, window_id: unsafe { ::std::mem::transmute_copy(&0) }, }, end_state: ClickState::TripleClick, last_action: MultiClick::TripleClick } test_clickstate! { name: multi_click_separate_buttons, initial_state: ClickState::DoubleClick, initial_button: MouseButton::Left, input: Event::WindowEvent { event: WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Right, device_id: unsafe { ::std::mem::transmute_copy(&0) }, modifiers: ModifiersState::default(), }, window_id: unsafe { ::std::mem::transmute_copy(&0) }, }, end_state: ClickState::Click, last_action: MultiClick::None } test_process_binding! { name: process_binding_nomode_shiftmod_require_shift, binding: Binding { trigger: KEY, mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[1;2D"), mode: TermMode::NONE, notmode: TermMode::NONE }, triggers: true, mode: TermMode::NONE, mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false } } test_process_binding! { name: process_binding_nomode_nomod_require_shift, binding: Binding { trigger: KEY, mods: ModifiersState { shift: true, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[1;2D"), mode: TermMode::NONE, notmode: TermMode::NONE }, triggers: false, mode: TermMode::NONE, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } } test_process_binding! { name: process_binding_nomode_controlmod, binding: Binding { trigger: KEY, mods: ModifiersState { ctrl: true, shift: false, alt: false, logo: false }, action: Action::from("\x1b[1;5D"), mode: TermMode::NONE, notmode: TermMode::NONE }, triggers: true, mode: TermMode::NONE, mods: ModifiersState { ctrl: true, shift: false, alt: false, logo: false } } test_process_binding! { name: process_binding_nomode_nomod_require_not_appcursor, binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1b[D"), mode: TermMode::NONE, notmode: TermMode::APP_CURSOR }, triggers: true, mode: TermMode::NONE, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } } test_process_binding! { name: process_binding_appcursormode_nomod_require_appcursor, binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE }, triggers: true, mode: TermMode::APP_CURSOR, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } } test_process_binding! { name: process_binding_nomode_nomod_require_appcursor, binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE }, triggers: false, mode: TermMode::NONE, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } } test_process_binding! { name: process_binding_appcursormode_appkeypadmode_nomod_require_appcursor, binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false }, action: Action::from("\x1bOD"), mode: TermMode::APP_CURSOR, notmode: TermMode::NONE }, triggers: true, mode: TermMode::APP_CURSOR | TermMode::APP_KEYPAD, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: false } } test_process_binding! { name: process_binding_fail_with_extra_mods, binding: Binding { trigger: KEY, mods: ModifiersState { shift: false, ctrl: false, alt: false, logo: true }, action: Action::from("arst"), mode: TermMode::NONE, notmode: TermMode::NONE }, triggers: false, mode: TermMode::NONE, mods: ModifiersState { shift: false, ctrl: false, alt: true, logo: true } } }