blob: 2de73dfeb3ffc3cb1c901cb6afdf27f8dd60c35b [file] [log] [blame]
// 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::process::Command;
use std::time::Instant;
use std::os::unix::process::CommandExt;
use copypasta::{Clipboard, Load, Buffer as ClipboardBuffer};
use glutin::{ElementState, VirtualKeyCode, MouseButton, TouchPhase, MouseScrollDelta, ModifiersState};
use config;
use grid::Scroll;
use event::{ClickState, Mouse};
use index::{Line, Column, Side, Point};
use term::SizeInfo;
use term::mode::TermMode;
use util::fmt::Red;
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 trait ActionContext {
fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, B);
fn terminal_mode(&self) -> TermMode;
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 mouse_mut(&mut self) -> &mut Mouse;
fn mouse(&self) -> &Mouse;
fn mouse_coords(&self) -> Option<Point>;
fn received_count(&mut self) -> &mut usize;
fn suppress_chars(&mut self) -> &mut bool;
fn last_modifiers(&mut self) -> &mut ModifiersState;
fn change_font_size(&mut self, delta: f32);
fn reset_font_size(&mut self);
fn scroll(&mut self, scroll: Scroll);
fn clear_history(&mut self);
fn hide_window(&mut self);
fn url(&self, _: Point<usize>) -> Option<String>;
}
/// Describes a state and action to take in that state
///
/// This is the shared component of `MouseBinding` and `KeyBinding`
#[derive(Debug, Clone)]
pub struct Binding<T> {
/// 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<VirtualKeyCode>;
/// Bindings that are triggered by a mouse button
pub type MouseBinding = Binding<MouseButton>;
impl<T: Eq> Binding<T> {
#[inline]
fn is_triggered_by(
&self,
mode: TermMode,
mods: ModifiersState,
input: &T
) -> 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)
}
}
impl<T> Binding<T> {
/// Execute the action associate with this binding
#[inline]
fn execute<A: ActionContext>(&self, ctx: &mut A) {
self.action.execute(ctx)
}
#[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
///
/// Optimized to use single check instead of four (one per modifier)
#[inline]
fn mods_match(&self, mods: ModifiersState) -> bool {
assert_eq_size!(ModifiersState, u32);
unsafe {
mem::transmute_copy::<_, u32>(&self.mods) == mem::transmute_copy::<_, u32>(&mods)
}
}
}
#[derive(Debug, Clone)]
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 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<String>),
/// Hides the Alacritty window
Hide,
/// Quits Alacritty.
Quit,
}
impl Action {
#[inline]
fn execute<A: ActionContext>(&self, ctx: &mut A) {
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| {
eprintln!("Error loading data from clipboard. {}", Red(err));
});
},
Action::PasteSelection => {
// Only paste if mouse events are not captured by an application
let mouse_modes = TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION;
if !ctx.terminal_mode().intersects(mouse_modes) {
Clipboard::new()
.and_then(|clipboard| clipboard.load_selection() )
.map(|contents| { self.paste(ctx, &contents) })
.unwrap_or_else(|err| {
warn!("Error loading data from clipboard. {}", Red(err));
});
}
},
Action::Command(ref program, ref args) => {
trace!("running command: {} {:?}", program, args);
match Command::new(program)
.args(args)
.before_exec(|| {
// Detach forked process from Alacritty. This will cause
// init or whatever to clean up child processes for us.
unsafe { ::libc::daemon(1, 0); }
Ok(())
})
.spawn()
{
Ok(child) => {
debug!("spawned new proc with pid: {}", child.id());
},
Err(err) => {
warn!("couldn't run command: {}", err);
},
}
},
Action::Hide => {
ctx.hide_window();
},
Action::Quit => {
// FIXME should do a more graceful shutdown
::std::process::exit(0);
},
Action::IncreaseFontSize => {
ctx.change_font_size(FONT_SIZE_STEP);
},
Action::DecreaseFontSize => {
ctx.change_font_size(-FONT_SIZE_STEP);
}
Action::ResetFontSize => {
ctx.reset_font_size();
},
Action::ScrollPageUp => {
ctx.scroll(Scroll::PageUp);
},
Action::ScrollPageDown => {
ctx.scroll(Scroll::PageDown);
},
Action::ScrollToTop => {
ctx.scroll(Scroll::Top);
},
Action::ScrollToBottom => {
ctx.scroll(Scroll::Bottom);
},
Action::ClearHistory => {
ctx.clear_history();
},
}
}
fn paste<A: ActionContext>(&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());
}
}
}
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 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;
if self.ctx.mouse().left_button_state == ElementState::Pressed &&
( modifiers.shift || !self.ctx.terminal_mode().intersects(report_mode))
{
let cell_side = self.get_mouse_side();
self.ctx.update_selection(Point {
line: point.line,
col: point.col
}, cell_side);
} else if self.ctx.terminal_mode().intersects(motion_mode)
// Only report motion when changing cells
&& (prev_line != self.ctx.mouse().line || prev_col != self.ctx.mouse().column)
&& size_info.contains_point(x, y)
{
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);
}
}
}
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) {
if let Some(point) = self.ctx.mouse_coords() {
self.ctx.semantic_selection(point);
}
}
pub fn on_mouse_triple_click(&mut self) {
if let Some(point) = self.ctx.mouse_coords() {
self.ctx.line_selection(point);
}
}
pub fn on_mouse_press(&mut self, button: MouseButton, modifiers: ModifiersState) {
let now = Instant::now();
let elapsed = self.ctx.mouse().last_click_timestamp.elapsed();
self.ctx.mouse_mut().last_click_timestamp = now;
self.ctx.mouse_mut().last_press_pos = (self.ctx.mouse().x, self.ctx.mouse().y);
self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state {
ClickState::Click if elapsed < self.mouse_config.double_click.threshold => {
self.on_mouse_double_click();
ClickState::DoubleClick
},
ClickState::DoubleClick if elapsed < self.mouse_config.triple_click.threshold => {
self.on_mouse_triple_click();
ClickState::TripleClick
},
_ => {
self.ctx.clear_selection();
// Start new empty selection
if let Some(point) = self.ctx.mouse_coords() {
let side = self.get_mouse_side();
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) {
match button {
MouseButton::Left => self.mouse_report(0, ElementState::Pressed, modifiers),
MouseButton::Middle => self.mouse_report(1, ElementState::Pressed, modifiers),
MouseButton::Right => self.mouse_report(2, ElementState::Pressed, modifiers),
// Can't properly report more than three buttons.
MouseButton::Other(_) => (),
};
return;
}
ClickState::Click
}
};
}
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 on_mouse_release(&mut self, button: MouseButton, modifiers: ModifiersState) {
let report_modes = TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION;
if !modifiers.shift && self.ctx.terminal_mode().intersects(report_modes)
{
match button {
MouseButton::Left => self.mouse_report(0, ElementState::Released, modifiers),
MouseButton::Middle => self.mouse_report(1, ElementState::Released, modifiers),
MouseButton::Right => self.mouse_report(2, ElementState::Released, modifiers),
// Can't properly report more than three buttons.
MouseButton::Other(_) => (),
};
return;
} else {
// Spawn URL launcher when clicking on URLs
let moved = self.ctx.mouse().last_press_pos != (self.ctx.mouse().x, self.ctx.mouse().y);
let modifiers_match = modifiers == self.mouse_config.url.modifiers;
if let (Some(point), Some(launcher), true, true) =
(self.ctx.mouse_coords(), &self.mouse_config.url.launcher, !moved, modifiers_match)
{
if let Some(text) = self.ctx.url(Point::new(point.line.0, point.col)) {
let mut args = launcher.args().to_vec();
args.push(text);
match Command::new(launcher.program()).args(&args).spawn() {
Ok(_) => debug!("Launching: {} {:?}", launcher.program(), args),
Err(_) => warn!("Unable to launch: {} {:?}", launcher.program(), args),
}
}
}
}
if self.save_to_clipboard {
self.ctx.copy_selection(ClipboardBuffer::Primary);
}
self.ctx.copy_selection(ClipboardBuffer::Selection);
}
pub fn on_mouse_wheel(&mut self, delta: MouseScrollDelta, phase: TouchPhase, modifiers: ModifiersState) {
match delta {
MouseScrollDelta::LineDelta(_columns, lines) => {
let to_scroll = self.ctx.mouse().lines_scrolled + lines;
let code = if to_scroll > 0.0 {
64
} else {
65
};
let scrolling_multiplier = self.scrolling_config.multiplier;
for _ in 0..(to_scroll.abs() as usize) {
self.scroll_terminal(code, modifiers, scrolling_multiplier)
}
self.ctx.mouse_mut().lines_scrolled = to_scroll % 1.0;
},
MouseScrollDelta::PixelDelta(_x, y) => {
match phase {
TouchPhase::Started => {
// Reset offset to zero
self.ctx.mouse_mut().scroll_px = 0;
},
TouchPhase::Moved => {
self.ctx.mouse_mut().scroll_px += y as i32;
let height = self.ctx.size_info().cell_height as i32;
while self.ctx.mouse().scroll_px.abs() >= height {
let code = if self.ctx.mouse().scroll_px > 0 {
self.ctx.mouse_mut().scroll_px -= height;
64
} else {
self.ctx.mouse_mut().scroll_px += height;
65
};
self.scroll_terminal(code, modifiers, 1)
}
},
_ => (),
}
}
}
}
fn scroll_terminal(&mut self, code: u8, modifiers: ModifiersState, scroll_multiplier: u8) {
debug_assert!(code == 64 || code == 65);
let mouse_modes = TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION;
// Make sure the new and deprecated setting are both allowed
let faux_scrolling_lines = self.mouse_config
.faux_scrollback_lines
.unwrap_or(self.scrolling_config.faux_multiplier as usize);
if self.ctx.terminal_mode().intersects(mouse_modes) {
self.mouse_report(code, ElementState::Pressed, modifiers);
} else if self.ctx.terminal_mode().contains(TermMode::ALT_SCREEN)
&& faux_scrolling_lines > 0 && !modifiers.shift
{
// Faux scrolling
let cmd = code + 1; // 64 + 1 = A, 65 + 1 = B
let mut content = Vec::with_capacity(faux_scrolling_lines as usize * 3);
for _ in 0..faux_scrolling_lines {
content.push(0x1b);
content.push(b'O');
content.push(cmd);
}
self.ctx.write_to_pty(content);
} else {
for _ in 0..scroll_multiplier {
// Transform the reported button codes 64 and 65 into 1 and -1 lines to scroll
self.ctx.scroll(Scroll::Lines(-(code as isize * 2 - 129)));
}
}
}
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) {
let prev_state = match button {
MouseButton::Left => Some(mem::replace(&mut self.ctx.mouse_mut().left_button_state, state)),
MouseButton::Middle => Some(mem::replace(&mut self.ctx.mouse_mut().middle_button_state, state)),
MouseButton::Right => Some(mem::replace(&mut self.ctx.mouse_mut().right_button_state, state)),
// Can't properly report more than three buttons.
MouseButton::Other(_) => None,
};
if let Some(prev_state) = prev_state {
if prev_state != state {
match state {
ElementState::Pressed => self.on_mouse_press(button, modifiers),
ElementState::Released => self.on_mouse_release(button, modifiers),
};
}
}
if let ElementState::Released = state {
return;
}
self.process_mouse_bindings(modifiers, button);
}
/// Process key input
///
/// If a keybinding was run, returns true. Otherwise returns false.
pub fn process_key(
&mut self,
state: ElementState,
key: Option<VirtualKeyCode>,
mods: ModifiersState,
) {
match (key, state) {
(Some(key), ElementState::Pressed) => {
*self.ctx.last_modifiers() = mods;
*self.ctx.received_count() = 0;
*self.ctx.suppress_chars() = false;
if self.process_key_bindings(mods, key) {
*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() {
self.ctx.scroll(Scroll::Bottom);
self.ctx.clear_selection();
let utf8_len = c.len_utf8();
if *self.ctx.received_count() == 0 && self.ctx.last_modifiers().alt && utf8_len == 1 {
self.ctx.write_to_pty(b"\x1b".to_vec());
}
let mut bytes = Vec::with_capacity(utf8_len);
unsafe {
bytes.set_len(utf8_len);
c.encode_utf8(&mut bytes[..]);
}
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, mods: ModifiersState, key: VirtualKeyCode) -> bool {
let mut has_binding = false;
for binding in self.key_bindings {
if binding.is_triggered_by(self.ctx.terminal_mode(), mods, &key) {
// binding was triggered; run the action
binding.execute(&mut self.ctx);
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) {
// binding was triggered; run the action
binding.execute(&mut self.ctx);
has_binding = true;
}
}
has_binding
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use std::time::Duration;
use glutin::{VirtualKeyCode, Event, WindowEvent, ElementState, MouseButton, ModifiersState};
use term::{SizeInfo, Term, TermMode};
use event::{Mouse, ClickState, WindowChanges};
use config::{self, Config, ClickHandler};
use index::{Point, Side};
use selection::Selection;
use grid::Scroll;
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<Selection>,
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<B: Into<Cow<'static, [u8]>>>(&mut self, _val: B) {
// STUBBED
}
fn terminal_mode(&self) -> TermMode {
*self.terminal.mode()
}
fn size_info(&self) -> SizeInfo {
*self.size_info
}
fn copy_selection(&self, _buffer: ClipboardBuffer) {
// STUBBED
}
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) {
// 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 scroll(&mut self, scroll: Scroll) {
self.terminal.scroll_display(scroll);
}
fn mouse_coords(&self) -> Option<Point> {
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 url(&self, _: Point<usize>) -> Option<String> {
None
}
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
}
fn change_font_size(&mut self, _delta: f32) {
}
fn reset_font_size(&mut self) {
}
fn clear_history(&mut self) {
}
fn hide_window(&mut self) {
}
}
macro_rules! test_clickstate {
{
name: $name:ident,
initial_state: $initial_state: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,
};
let mut terminal = Term::new(&config, size);
let mut mouse = Mouse::default();
mouse.click_state = $initial_state;
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),
},
faux_scrollback_lines: None,
url_launcher: None,
},
scrolling_config: &config::Scrolling::default(),
key_bindings: &config.key_bindings()[..],
mouse_bindings: &config.mouse_bindings()[..],
save_to_clipboard: config.selection().save_to_clipboard,
};
if let Event::WindowEvent { event: WindowEvent::MouseInput { state, button, modifiers, .. }, .. } = $input {
processor.mouse_input(state, button, modifiers);
};
assert!(match 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));
} else {
assert!(!$binding.is_triggered_by($mode, $mods, &KEY));
}
}
}
}
test_clickstate! {
name: single_click,
initial_state: ClickState::None,
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,
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,
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_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 }
}
}