blob: 468e9ef3f4ed1574bf2b06fcf553455fbbe68e7b [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 copypasta::{Clipboard, Load, Buffer};
use glutin::{ElementState, VirtualKeyCode, MouseButton};
use glutin::{Mods, mods};
use glutin::{TouchPhase, MouseScrollDelta};
use config;
use event::{ClickState, Mouse};
use index::{Line, Column, Side, Point};
use term::SizeInfo;
use term::mode::{self, TermMode};
use util::fmt::Red;
/// 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 ctx: A,
}
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, Buffer);
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_coords(&self) -> Option<Point>;
}
/// 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: Mods,
/// 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: &Mods,
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)
}
#[inline]
fn mods_match(&self, mods: &Mods) -> bool {
self.mods.is_all() || *mods == self.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,
/// Run given command
Command(String, Vec<String>),
/// Quits Alacritty.
Quit,
}
impl Action {
#[inline]
fn execute<A: ActionContext>(&self, ctx: &mut A) {
match *self {
Action::Esc(ref s) => {
ctx.write_to_pty(s.clone().into_bytes())
},
Action::Copy => {
ctx.copy_selection(Buffer::Primary);
},
Action::Paste => {
Clipboard::new()
.and_then(|clipboard| clipboard.load_primary() )
.map(|contents| { self.paste(ctx, contents) })
.unwrap_or_else(|err| {
err_println!("Error loading data from clipboard. {}", Red(err));
});
},
Action::PasteSelection => {
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).spawn() {
Ok(child) => {
debug!("spawned new proc with pid: {}", child.id());
},
Err(err) => {
warn!("couldn't run command: {}", err);
},
}
},
Action::Quit => {
// FIXME should do a more graceful shutdown
::std::process::exit(0);
},
}
}
fn paste<A: ActionContext>(&self, ctx: &mut A, contents: String) {
if ctx.terminal_mode().contains(mode::BRACKETED_PASTE) {
ctx.write_to_pty(&b"\x1b[200~"[..]);
ctx.write_to_pty(contents.into_bytes());
ctx.write_to_pty(&b"\x1b[201~"[..]);
} else {
ctx.write_to_pty(contents.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: u32, y: u32) {
self.ctx.mouse_mut().x = x;
self.ctx.mouse_mut().y = y;
let size_info = self.ctx.size_info();
if let Some(point) = size_info.pixels_to_coords(x as usize, y as usize) {
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 cell_x = (x as usize - 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 cell_side = if cell_x > half_cell_width {
Side::Right
} else {
Side::Left
};
self.ctx.mouse_mut().cell_side = cell_side;
if self.ctx.mouse_mut().left_button_state == ElementState::Pressed {
let report_mode = mode::MOUSE_REPORT_CLICK | mode::MOUSE_MOTION;
if !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().contains(mode::MOUSE_MOTION)
// Only report motion when changing cells
&& (
prev_line != self.ctx.mouse_mut().line
|| prev_col != self.ctx.mouse_mut().column
) {
self.mouse_report(32);
}
}
}
}
pub fn normal_mouse_report(&mut self, button: u8) {
let (line, column) = (self.ctx.mouse_mut().line, self.ctx.mouse_mut().column);
if line < Line(223) && column < Column(223) {
let msg = vec![
'\x1b' as u8,
'[' as u8,
'M' as u8,
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, release: bool) {
let (line, column) = (self.ctx.mouse_mut().line, self.ctx.mouse_mut().column);
let c = if release { 'm' } else { '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) {
if self.ctx.terminal_mode().contains(mode::SGR_MOUSE) {
let release = self.ctx.mouse_mut().left_button_state != ElementState::Pressed;
self.sgr_mouse_report(button, release);
} else {
self.normal_mouse_report(button);
}
}
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) {
let now = Instant::now();
let elapsed = self.ctx.mouse_mut().last_click_timestamp.elapsed();
self.ctx.mouse_mut().last_click_timestamp = now;
self.ctx.mouse_mut().click_state = match self.ctx.mouse_mut().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
},
_ => {
let report_modes = mode::MOUSE_REPORT_CLICK | mode::MOUSE_MOTION;
if self.ctx.terminal_mode().intersects(report_modes) {
self.mouse_report(0);
return;
}
self.ctx.clear_selection();
ClickState::Click
}
};
}
pub fn on_mouse_release(&mut self) {
if self.ctx.terminal_mode().intersects(mode::MOUSE_REPORT_CLICK | mode::MOUSE_MOTION) {
self.mouse_report(3);
return;
}
self.ctx.copy_selection(Buffer::Selection);
}
pub fn on_mouse_wheel(&mut self, delta: MouseScrollDelta, phase: TouchPhase) {
let modes = mode::MOUSE_REPORT_CLICK | mode::MOUSE_MOTION | mode::SGR_MOUSE;
if !self.ctx.terminal_mode().intersects(modes) {
return;
}
match delta {
MouseScrollDelta::LineDelta(_columns, lines) => {
let code = if lines > 0.0 {
64
} else {
65
};
for _ in 0..(lines.abs() as usize) {
self.normal_mouse_report(code);
}
},
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_mut().scroll_px.abs() >= height {
let button = if self.ctx.mouse_mut().scroll_px > 0 {
self.ctx.mouse_mut().scroll_px -= height;
64
} else {
self.ctx.mouse_mut().scroll_px += height;
65
};
self.normal_mouse_report(button);
}
},
_ => (),
}
}
}
}
pub fn on_focus_change(&mut self, is_focused: bool) {
if self.ctx.terminal_mode().contains(mode::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) {
if let MouseButton::Left = button {
let state = mem::replace(&mut self.ctx.mouse_mut().left_button_state, state);
if self.ctx.mouse_mut().left_button_state != state {
match self.ctx.mouse_mut().left_button_state {
ElementState::Pressed => {
self.on_mouse_press();
},
ElementState::Released => {
self.on_mouse_release();
}
}
}
}
if let ElementState::Released = state {
return;
}
self.process_mouse_bindings(mods::NONE, button);
}
pub fn process_key(
&mut self,
state: ElementState,
key: Option<VirtualKeyCode>,
mods: Mods,
string: Option<String>,
) {
if let Some(key) = key {
// Ignore release events
if state == ElementState::Released {
return;
}
if self.process_key_bindings(mods, key) {
return;
}
}
// Didn't process a binding; print the provided character
if let Some(mut string) = string {
// from ST
if string.len() == 1 && mods.contains(mods::ALT) {
string.insert(0, '\x1b');
}
self.ctx.write_to_pty(string.into_bytes());
self.ctx.clear_selection();
}
}
/// 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: Mods, key: VirtualKeyCode) -> bool {
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);
return true;
}
}
false
}
/// 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: Mods, button: MouseButton) -> bool {
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);
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use std::time::Duration;
use glutin::{mods, VirtualKeyCode, Event, ElementState, MouseButton};
use term::{SizeInfo, Term, TermMode, mode};
use event::{Mouse, ClickState};
use config::{self, Config, ClickHandler};
use index::{Point, Side};
use selection::Selection;
use super::{Action, Binding, Processor};
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,
}
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: ::copypasta::Buffer) {
// 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 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
}
}
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,
};
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),
}
},
key_bindings: &config.key_bindings()[..],
mouse_bindings: &config.mouse_bindings()[..],
};
if let Event::MouseInput(state, input) = $input {
processor.mouse_input(state, input);
};
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::MouseInput(ElementState::Pressed, MouseButton::Left),
end_state: ClickState::Click,
last_action: MultiClick::None
}
test_clickstate! {
name: double_click,
initial_state: ClickState::Click,
input: Event::MouseInput(ElementState::Pressed, MouseButton::Left),
end_state: ClickState::DoubleClick,
last_action: MultiClick::DoubleClick
}
test_clickstate! {
name: triple_click,
initial_state: ClickState::DoubleClick,
input: Event::MouseInput(ElementState::Pressed, MouseButton::Left),
end_state: ClickState::TripleClick,
last_action: MultiClick::TripleClick
}
test_process_binding! {
name: process_binding_nomode_shiftmod_require_shift,
binding: Binding { trigger: KEY, mods: mods::SHIFT, action: Action::from("\x1b[1;2D"), mode: mode::NONE, notmode: mode::NONE },
triggers: true,
mode: mode::NONE,
mods: mods::SHIFT
}
test_process_binding! {
name: process_binding_nomode_nomod_require_shift,
binding: Binding { trigger: KEY, mods: mods::SHIFT, action: Action::from("\x1b[1;2D"), mode: mode::NONE, notmode: mode::NONE },
triggers: false,
mode: mode::NONE,
mods: mods::NONE
}
test_process_binding! {
name: process_binding_nomode_controlmod,
binding: Binding { trigger: KEY, mods: mods::CONTROL, action: Action::from("\x1b[1;5D"), mode: mode::NONE, notmode: mode::NONE },
triggers: true,
mode: mode::NONE,
mods: mods::CONTROL
}
test_process_binding! {
name: process_binding_nomode_nomod_require_not_appcursor,
binding: Binding { trigger: KEY, mods: mods::ANY, action: Action::from("\x1b[D"), mode: mode::NONE, notmode: mode::APP_CURSOR },
triggers: true,
mode: mode::NONE,
mods: mods::NONE
}
test_process_binding! {
name: process_binding_appcursormode_nomod_require_appcursor,
binding: Binding { trigger: KEY, mods: mods::ANY, action: Action::from("\x1bOD"), mode: mode::APP_CURSOR, notmode: mode::NONE },
triggers: true,
mode: mode::APP_CURSOR,
mods: mods::NONE
}
test_process_binding! {
name: process_binding_nomode_nomod_require_appcursor,
binding: Binding { trigger: KEY, mods: mods::ANY, action: Action::from("\x1bOD"), mode: mode::APP_CURSOR, notmode: mode::NONE },
triggers: false,
mode: mode::NONE,
mods: mods::NONE
}
test_process_binding! {
name: process_binding_appcursormode_appkeypadmode_nomod_require_appcursor,
binding: Binding { trigger: KEY, mods: mods::ANY, action: Action::from("\x1bOD"), mode: mode::APP_CURSOR, notmode: mode::NONE },
triggers: true,
mode: mode::APP_CURSOR | mode::APP_KEYPAD,
mods: mods::NONE
}
test_process_binding! {
name: process_binding_fail_with_extra_mods,
binding: Binding { trigger: KEY, mods: mods::SUPER, action: Action::from("arst"), mode: mode::NONE, notmode: mode::NONE },
triggers: false,
mode: mode::NONE,
mods: mods::SUPER | mods::ALT
}
}