alacritty/alacritty/src/input.rs

1485 lines
54 KiB
Rust

//! 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::cmp::{max, min, Ordering};
use std::marker::PhantomData;
use std::time::{Duration, Instant};
use glutin::dpi::PhysicalPosition;
use glutin::event::{
ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase,
};
use glutin::event_loop::EventLoopWindowTarget;
#[cfg(target_os = "macos")]
use glutin::platform::macos::EventLoopWindowTargetExtMacOS;
use glutin::window::CursorIcon;
use alacritty_terminal::ansi::{ClearMode, Handler};
use alacritty_terminal::event::EventListener;
use alacritty_terminal::grid::{Dimensions, Scroll};
use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side};
use alacritty_terminal::selection::SelectionType;
use alacritty_terminal::term::search::Match;
use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode};
use alacritty_terminal::vi_mode::ViMotion;
use crate::clipboard::Clipboard;
use crate::config::{Action, BindingMode, Config, Key, SearchAction, ViAction};
use crate::daemon::start_daemon;
use crate::display::hint::HintState;
use crate::display::window::Window;
use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY};
use crate::message_bar::{self, Message};
use crate::scheduler::{Scheduler, TimerId};
use crate::url::{Url, Urls};
/// 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.;
/// Processes input from glutin.
///
/// 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]>>>(&mut 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 mouse_coords(&self) -> Option<Point>;
fn received_count(&mut self) -> &mut usize;
fn suppress_chars(&mut self) -> &mut bool;
fn modifiers(&mut self) -> &mut ModifiersState;
fn scroll(&mut self, _scroll: Scroll) {}
fn window(&self) -> &Window;
fn window_mut(&mut self) -> &mut Window;
fn terminal(&self) -> &Term<T>;
fn terminal_mut(&mut self) -> &mut Term<T>;
fn spawn_new_instance(&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) -> &Config;
fn event_loop(&self) -> &EventLoopWindowTarget<Event>;
fn urls(&self) -> &Urls;
fn launch_url(&self, _url: Url) {}
fn highlighted_url(&self) -> Option<&Url>;
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<usize>,
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 hint_state(&mut self) -> &mut HintState;
fn hint_input(&mut self, _character: char) {}
}
impl Action {
fn toggle_selection<T, A>(ctx: &mut A, ty: SelectionType)
where
A: ActionContext<T>,
T: EventListener,
{
let cursor_point = ctx.terminal().vi_mode_cursor.point;
ctx.toggle_selection(ty, 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.on_typing_start();
ctx.clear_selection();
ctx.scroll(Scroll::Bottom);
ctx.write_to_pty(s.clone().into_bytes())
},
Action::Command(program) => start_daemon(program.program(), program.args()),
Action::Hint(hint) => {
ctx.hint_state().start(hint.clone());
ctx.mark_dirty();
},
Action::ToggleViMode => ctx.toggle_vi_mode(),
Action::ViMotion(motion) => {
ctx.on_typing_start();
ctx.terminal_mut().vi_motion(*motion);
ctx.mark_dirty();
},
Action::ViAction(ViAction::ToggleNormalSelection) => {
Self::toggle_selection(ctx, SelectionType::Simple);
},
Action::ViAction(ViAction::ToggleLineSelection) => {
Self::toggle_selection(ctx, SelectionType::Lines);
},
Action::ViAction(ViAction::ToggleBlockSelection) => {
Self::toggle_selection(ctx, SelectionType::Block);
},
Action::ViAction(ViAction::ToggleSemanticSelection) => {
Self::toggle_selection(ctx, SelectionType::Semantic);
},
Action::ViAction(ViAction::Open) => {
ctx.mouse_mut().block_url_launcher = false;
if let Some(url) = ctx.urls().find_at(ctx.terminal().vi_mode_cursor.point) {
ctx.launch_url(url);
}
},
Action::ViAction(ViAction::SearchNext) => {
let terminal = ctx.terminal();
let direction = ctx.search_direction();
let vi_point = terminal.visible_to_buffer(terminal.vi_mode_cursor.point);
let origin = match direction {
Direction::Right => vi_point.add_absolute(terminal, Boundary::Wrap, 1),
Direction::Left => vi_point.sub_absolute(terminal, Boundary::Wrap, 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::ViAction(ViAction::SearchPrevious) => {
let terminal = ctx.terminal();
let direction = ctx.search_direction().opposite();
let vi_point = terminal.visible_to_buffer(terminal.vi_mode_cursor.point);
let origin = match direction {
Direction::Right => vi_point.add_absolute(terminal, Boundary::Wrap, 1),
Direction::Left => vi_point.sub_absolute(terminal, Boundary::Wrap, 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::ViAction(ViAction::SearchStart) => {
let terminal = ctx.terminal();
let origin = terminal
.visible_to_buffer(terminal.vi_mode_cursor.point)
.sub_absolute(terminal, Boundary::Wrap, 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::ViAction(ViAction::SearchEnd) => {
let terminal = ctx.terminal();
let origin = terminal
.visible_to_buffer(terminal.vi_mode_cursor.point)
.add_absolute(terminal, Boundary::Wrap, 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::SearchAction(SearchAction::SearchFocusNext) => {
ctx.advance_search_origin(ctx.search_direction());
},
Action::SearchAction(SearchAction::SearchFocusPrevious) => {
let direction = ctx.search_direction().opposite();
ctx.advance_search_origin(direction);
},
Action::SearchAction(SearchAction::SearchConfirm) => ctx.confirm_search(),
Action::SearchAction(SearchAction::SearchCancel) => ctx.cancel_search(),
Action::SearchAction(SearchAction::SearchClear) => {
let direction = ctx.search_direction();
ctx.cancel_search();
ctx.start_search(direction);
},
Action::SearchAction(SearchAction::SearchDeleteWord) => ctx.search_pop_word(),
Action::SearchAction(SearchAction::SearchHistoryPrevious) => {
ctx.search_history_previous()
},
Action::SearchAction(SearchAction::SearchHistoryNext) => ctx.search_history_next(),
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);
paste(ctx, &text);
},
Action::PasteSelection => {
let text = ctx.clipboard_mut().load(ClipboardType::Selection);
paste(ctx, &text);
},
Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(),
#[cfg(target_os = "macos")]
Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(),
#[cfg(target_os = "macos")]
Action::Hide => ctx.event_loop().hide_application(),
#[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 => {
// Move vi mode cursor.
let term = ctx.terminal_mut();
let scroll_lines = term.screen_lines().0 as isize;
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::PageUp);
},
Action::ScrollPageDown => {
// Move vi mode cursor.
let term = ctx.terminal_mut();
let scroll_lines = -(term.screen_lines().0 as isize);
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::PageDown);
},
Action::ScrollHalfPageUp => {
// Move vi mode cursor.
let term = ctx.terminal_mut();
let scroll_lines = term.screen_lines().0 as isize / 2;
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::Delta(scroll_lines));
},
Action::ScrollHalfPageDown => {
// Move vi mode cursor.
let term = ctx.terminal_mut();
let scroll_lines = -(term.screen_lines().0 as isize / 2);
term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
ctx.scroll(Scroll::Delta(scroll_lines));
},
Action::ScrollLineUp => {
// Move vi mode cursor.
let term = ctx.terminal();
if term.grid().display_offset() != term.history_size()
&& term.vi_mode_cursor.point.line + 1 != term.screen_lines()
{
ctx.terminal_mut().vi_mode_cursor.point.line += 1;
}
ctx.scroll(Scroll::Delta(1));
},
Action::ScrollLineDown => {
// Move vi mode cursor.
if ctx.terminal().grid().display_offset() != 0
&& ctx.terminal().vi_mode_cursor.point.line.0 != 0
{
ctx.terminal_mut().vi_mode_cursor.point.line -= 1;
}
ctx.scroll(Scroll::Delta(-1));
},
Action::ScrollToTop => {
ctx.scroll(Scroll::Top);
// Move vi mode cursor.
ctx.terminal_mut().vi_mode_cursor.point.line = Line(0);
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.screen_lines() - 1;
// 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(),
Action::SpawnNewInstance => ctx.spawn_new_instance(),
Action::ReceiveChar | Action::None => (),
}
}
}
fn paste<T: EventListener, A: ActionContext<T>>(ctx: &mut A, contents: &str) {
if ctx.search_active() {
for c in contents.chars() {
ctx.search_input(c);
}
} else 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 single carriage return (\r, which is what the Enter key produces).
ctx.write_to_pty(contents.replace("\r\n", "\r").replace("\n", "\r").into_bytes());
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MouseState {
Url(Url),
MessageBar,
MessageBarButton,
Mouse,
Text,
}
impl From<MouseState> for CursorIcon {
fn from(mouse_state: MouseState) -> CursorIcon {
match mouse_state {
MouseState::Url(_) | MouseState::MessageBarButton => CursorIcon::Hand,
MouseState::Text => CursorIcon::Text,
_ => CursorIcon::Default,
}
}
}
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 x = min(max(x, 0), size_info.width() as i32 - 1) as usize;
let y = min(max(y, 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 point = size_info.pixels_to_coords(x, y);
let cell_side = self.get_mouse_side();
let cell_changed =
point.line != self.ctx.mouse().line || point.column != self.ctx.mouse().column;
// Update mouse state and check for URL change.
let mouse_state = self.mouse_state();
self.update_url_state(&mouse_state);
self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
// 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;
self.ctx.mouse_mut().line = point.line;
self.ctx.mouse_mut().column = point.column;
// Don't launch URLs if mouse has moved.
self.ctx.mouse_mut().block_url_launcher = true;
if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode())
{
self.ctx.update_selection(point, cell_side);
} else if cell_changed
&& point.line < self.ctx.terminal().screen_lines()
&& 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);
}
}
}
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
}
}
fn normal_mouse_report(&mut self, button: u8) {
let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column);
let utf8 = self.ctx.terminal().mode().contains(TermMode::UTF8_MOUSE);
let max_point = if utf8 { 2015 } else { 223 };
if line >= Line(max_point) || column >= 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 >= Line(95) {
msg.append(&mut mouse_pos_encode(line.0));
} else {
msg.push(32 + 1 + line.0 as u8);
}
self.ctx.write_to_pty(msg);
}
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());
}
fn mouse_report(&mut self, button: u8, state: ElementState) {
// Calculate modifiers value.
let mut mods = 0;
let modifiers = self.ctx.modifiers();
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);
}
}
fn on_mouse_press(&mut self, button: MouseButton) {
// Handle mouse mode.
if !self.ctx.modifiers().shift() && 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::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.
let mouse_config = &self.ctx.config().ui_config.mouse;
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 < mouse_config.double_click.threshold() => {
ClickState::DoubleClick
},
ClickState::DoubleClick if elapsed < mouse_config.triple_click.threshold() => {
ClickState::TripleClick
},
_ => ClickState::Click,
};
// Load mouse point, treating message bar and padding as the closest cell.
let mouse = self.ctx.mouse();
let mut point = self.ctx.size_info().pixels_to_coords(mouse.x, mouse.y);
point.line = min(point.line, self.ctx.terminal().screen_lines() - 1);
match button {
MouseButton::Left => self.on_left_click(point),
MouseButton::Right => self.on_right_click(point),
// Do nothing when using buttons other than LMB.
_ => self.ctx.mouse_mut().click_state = ClickState::None,
}
}
}
/// Handle selection expansion on right click.
fn on_right_click(&mut self, point: Point) {
match self.ctx.mouse().click_state {
ClickState::Click => {
let selection_type = if self.ctx.modifiers().ctrl() {
SelectionType::Block
} else {
SelectionType::Simple
};
self.expand_selection(point, selection_type);
},
ClickState::DoubleClick => self.expand_selection(point, SelectionType::Semantic),
ClickState::TripleClick => self.expand_selection(point, SelectionType::Lines),
ClickState::None => (),
}
}
/// Expand existing selection.
fn expand_selection(&mut self, point: Point, selection_type: SelectionType) {
let cell_side = self.ctx.mouse().cell_side;
let selection = match &mut self.ctx.terminal_mut().selection {
Some(selection) => selection,
None => return,
};
selection.ty = selection_type;
self.ctx.update_selection(point, cell_side);
// 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;
}
}
/// 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_url_launcher = !self.ctx.selection_is_empty();
self.ctx.clear_selection();
// Start new empty selection.
if self.ctx.modifiers().ctrl() {
self.ctx.start_selection(SelectionType::Block, point, side);
} else {
self.ctx.start_selection(SelectionType::Simple, point, side);
}
},
ClickState::DoubleClick => {
self.ctx.mouse_mut().block_url_launcher = true;
self.ctx.start_selection(SelectionType::Semantic, point, side);
},
ClickState::TripleClick => {
self.ctx.mouse_mut().block_url_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;
}
}
fn on_mouse_release(&mut self, button: MouseButton) {
if !self.ctx.modifiers().shift() && 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::Other(_) => return,
};
self.mouse_report(code, ElementState::Released);
return;
} else if let (MouseButton::Left, MouseState::Url(url)) = (button, self.mouse_state()) {
self.ctx.launch_url(url);
}
self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling);
self.copy_selection();
}
pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) {
match delta {
MouseScrollDelta::LineDelta(_columns, lines) => {
let new_scroll_px = lines * self.ctx.size_info().cell_height();
self.scroll_terminal(f64::from(new_scroll_px));
},
MouseScrollDelta::PixelDelta(lpos) => {
match phase {
TouchPhase::Started => {
// Reset offset to zero.
self.ctx.mouse_mut().scroll_px = 0.;
},
TouchPhase::Moved => {
self.scroll_terminal(lpos.y);
},
_ => (),
}
},
}
}
fn scroll_terminal(&mut self, new_scroll_px: f64) {
let height = f64::from(self.ctx.size_info().cell_height());
if self.ctx.mouse_mode() {
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() as i32;
for _ in 0..lines {
self.mouse_report(code, ElementState::Pressed);
}
} else if self
.ctx
.terminal()
.mode()
.contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
&& !self.ctx.modifiers().shift()
{
let multiplier = f64::from(self.ctx.config().scrolling.multiplier);
self.ctx.mouse_mut().scroll_px += new_scroll_px * multiplier;
let cmd = if new_scroll_px > 0. { b'A' } else { b'B' };
let lines = (self.ctx.mouse().scroll_px / height).abs() as i32;
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 = f64::from(self.ctx.config().scrolling.multiplier);
self.ctx.mouse_mut().scroll_px += new_scroll_px * multiplier;
let lines = self.ctx.mouse().scroll_px / height;
// Store absolute position of vi mode cursor.
let term = self.ctx.terminal();
let absolute = term.visible_to_buffer(term.vi_mode_cursor.point);
self.ctx.scroll(Scroll::Delta(lines as isize));
// Try to restore vi mode cursor position, to keep it above its previous content.
let term = self.ctx.terminal_mut();
term.vi_mode_cursor.point = term.grid().clamp_buffer_to_visible(absolute);
term.vi_mode_cursor.point.column = absolute.column;
// Update selection.
if term.mode().contains(TermMode::VI) {
let point = term.vi_mode_cursor.point;
if !self.ctx.selection_is_empty() {
self.ctx.update_selection(point, Side::Right);
}
}
}
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) {
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_mouse_state() == Some(MouseState::MessageBarButton)
&& state == ElementState::Pressed
{
let size = self.ctx.size_info();
let current_lines = self.ctx.message().map(|m| m.text(&size).len()).unwrap_or(0);
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(|m| m.text(&size).len()).unwrap_or(0);
let new_icon = match current_lines.cmp(&new_lines) {
Ordering::Less => CursorIcon::Default,
Ordering::Equal => CursorIcon::Hand,
Ordering::Greater => {
if self.ctx.mouse_mode() {
CursorIcon::Default
} else {
CursorIcon::Text
}
},
};
self.ctx.window_mut().set_mouse_cursor(new_icon);
} else {
match state {
ElementState::Pressed => {
self.process_mouse_bindings(button);
self.on_mouse_press(button);
},
ElementState::Released => self.on_mouse_release(button),
}
}
}
/// Process key input.
pub fn key_input(&mut self, input: KeyboardInput) {
// All key bindings are disabled while a hint is being selected.
if self.ctx.hint_state().active() {
*self.ctx.suppress_chars() = false;
return;
}
// Reset search delay when the user is still typing.
if self.ctx.search_active() {
if let Some(timer) = self.ctx.scheduler_mut().get_mut(TimerId::DelayedSearch) {
timer.deadline = Instant::now() + TYPING_SEARCH_DELAY;
}
}
match input.state {
ElementState::Pressed => {
*self.ctx.received_count() = 0;
self.process_key_bindings(input);
},
ElementState::Released => *self.ctx.suppress_chars() = false,
}
}
/// Modifier state change.
pub fn modifiers_input(&mut self, modifiers: ModifiersState) {
*self.ctx.modifiers() = modifiers;
// Update mouse state and check for URL change.
let mouse_state = self.mouse_state();
self.update_url_state(&mouse_state);
self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
}
/// Reset mouse cursor based on modifier and terminal state.
#[inline]
pub fn reset_mouse_cursor(&mut self) {
let mouse_state = self.mouse_state();
self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
}
/// Process a received character.
pub fn received_char(&mut self, c: char) {
let suppress_chars = *self.ctx.suppress_chars();
// Handle hint selection over anything else.
if self.ctx.hint_state().active() && !suppress_chars {
self.ctx.hint_input(c);
return;
}
// Pass keys to search and ignore them during `suppress_chars`.
let search_active = self.ctx.search_active();
if suppress_chars || search_active || self.ctx.terminal().mode().contains(TermMode::VI) {
if search_active && !suppress_chars {
self.ctx.search_input(c);
}
return;
}
self.ctx.on_typing_start();
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.ctx.config().ui_config.alt_send_esc
&& *self.ctx.received_count() == 0
&& self.ctx.modifiers().alt()
&& utf8_len == 1
{
bytes.insert(0, b'\x1b');
}
self.ctx.write_to_pty(bytes);
*self.ctx.received_count() += 1;
}
/// 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, input: KeyboardInput) {
let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active());
let mods = *self.ctx.modifiers();
let mut suppress_chars = None;
for i in 0..self.ctx.config().ui_config.key_bindings().len() {
let binding = &self.ctx.config().ui_config.key_bindings()[i];
let key = match (binding.trigger, input.virtual_keycode) {
(Key::Scancode(_), _) => Key::Scancode(input.scancode),
(_, Some(key)) => Key::Keycode(key),
_ => continue,
};
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);
}
}
// Don't suppress char if no bindings were triggered.
*self.ctx.suppress_chars() = suppress_chars.unwrap_or(false);
}
/// 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();
for i in 0..self.ctx.config().ui_config.mouse_bindings().len() {
let mut binding = self.ctx.config().ui_config.mouse_bindings()[i].clone();
// Require shift for all modifiers when mouse mode is active.
if mouse_mode {
binding.mods |= ModifiersState::SHIFT;
}
if binding.is_triggered_by(mode, mods, &button) {
binding.action.execute(&mut self.ctx);
}
}
}
/// Check mouse state in relation to the message bar.
fn message_bar_mouse_state(&self) -> Option<MouseState> {
// Since search is above the message bar, the button is offset by search's height.
let search_height = if self.ctx.search_active() { 1 } else { 0 };
// 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().0 + search_height);
let mouse = self.ctx.mouse();
if self.ctx.message().is_none() || (mouse.y <= terminal_end) {
None
} else if mouse.y <= terminal_end + size.cell_height() as usize
&& mouse.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.cols()
{
Some(MouseState::MessageBarButton)
} else {
Some(MouseState::MessageBar)
}
}
/// Copy text selection.
fn copy_selection(&mut self) {
if self.ctx.config().selection.save_to_clipboard {
self.ctx.copy_selection(ClipboardType::Clipboard);
}
self.ctx.copy_selection(ClipboardType::Selection);
}
/// Trigger redraw when URL highlight changed.
#[inline]
fn update_url_state(&mut self, mouse_state: &MouseState) {
let highlighted_url = self.ctx.highlighted_url();
if let MouseState::Url(url) = mouse_state {
if Some(url) != highlighted_url {
self.ctx.mark_dirty();
}
} else if highlighted_url.is_some() {
self.ctx.mark_dirty();
}
}
/// Location of the mouse cursor.
fn mouse_state(&mut self) -> MouseState {
// Check message bar before URL to ignore URLs in the message bar.
if let Some(mouse_state) = self.message_bar_mouse_state() {
return mouse_state;
}
let mouse_mode = self.ctx.mouse_mode();
// Check for URL at mouse cursor.
let mods = *self.ctx.modifiers();
let highlighted_url = self.ctx.urls().highlighted(
self.ctx.config(),
self.ctx.mouse(),
mods,
mouse_mode,
!self.ctx.selection_is_empty(),
);
if let Some(url) = highlighted_url {
return MouseState::Url(url);
}
// Check mouse mode if location is not special.
if !self.ctx.modifiers().shift() && mouse_mode {
MouseState::Mouse
} else {
MouseState::Text
}
}
/// Handle automatic scrolling when selecting above/below the window.
fn update_selection_scrolling(&mut self, mouse_y: i32) {
let dpr = self.ctx.window().dpr;
let size = self.ctx.size_info();
let scheduler = self.ctx.scheduler_mut();
// Scale constants by DPI.
let min_height = (MIN_SELECTION_SCROLLING_HEIGHT * dpr) as i32;
let step = (SELECTION_SCROLLING_STEP * dpr) 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().0 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::SelectionScrolling);
return;
};
// Scale number of lines scrolled based on distance to boundary.
let delta = delta as isize / step as isize;
let event = Event::Scroll(Scroll::Delta(delta));
// Schedule event.
match scheduler.get_mut(TimerId::SelectionScrolling) {
Some(timer) => timer.event = event.into(),
None => {
scheduler.schedule(
event.into(),
SELECTION_SCROLLING_INTERVAL,
true,
TimerId::SelectionScrolling,
);
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use glutin::event::{Event as GlutinEvent, VirtualKeyCode, WindowEvent};
use alacritty_terminal::event::Event as TerminalEvent;
use alacritty_terminal::selection::Selection;
use crate::config::Binding;
use crate::message_bar::MessageBuffer;
const KEY: VirtualKeyCode = VirtualKeyCode::Key0;
struct MockEventProxy;
impl EventListener for MockEventProxy {}
struct ActionContext<'a, T> {
pub terminal: &'a mut Term<T>,
pub selection: &'a mut Option<Selection>,
pub size_info: &'a SizeInfo,
pub mouse: &'a mut Mouse,
pub clipboard: &'a mut Clipboard,
pub message_buffer: &'a mut MessageBuffer,
pub received_count: usize,
pub suppress_chars: bool,
pub modifiers: ModifiersState,
config: &'a Config,
}
impl<'a, T: EventListener> super::ActionContext<T> for ActionContext<'a, T> {
fn search_next(
&mut self,
_origin: Point<usize>,
_direction: Direction,
_side: Side,
) -> Option<Match> {
None
}
fn search_direction(&self) -> Direction {
Direction::Right
}
fn search_active(&self) -> bool {
false
}
fn terminal(&self) -> &Term<T> {
&self.terminal
}
fn terminal_mut(&mut self) -> &mut Term<T> {
&mut 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_coords(&self) -> Option<Point> {
let x = self.mouse.x as usize;
let y = self.mouse.y as usize;
if self.size_info.contains_point(x, y) {
Some(self.size_info.pixels_to_coords(x, y))
} else {
None
}
}
fn mouse_mode(&self) -> bool {
false
}
#[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 modifiers(&mut self) -> &mut ModifiersState {
&mut self.modifiers
}
fn window(&self) -> &Window {
unimplemented!();
}
fn window_mut(&mut self) -> &mut Window {
unimplemented!();
}
fn pop_message(&mut self) {
self.message_buffer.pop();
}
fn message(&self) -> Option<&Message> {
self.message_buffer.message()
}
fn config(&self) -> &Config {
self.config
}
fn clipboard_mut(&mut self) -> &mut Clipboard {
self.clipboard
}
fn event_loop(&self) -> &EventLoopWindowTarget<Event> {
unimplemented!();
}
fn urls(&self) -> &Urls {
unimplemented!();
}
fn highlighted_url(&self) -> Option<&Url> {
unimplemented!();
}
fn scheduler_mut(&mut self) -> &mut Scheduler {
unimplemented!();
}
fn hint_state(&mut self) -> &mut HintState {
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,
} => {
#[test]
fn $name() {
let mut clipboard = Clipboard::new_nop();
let cfg = Config::default();
let size = SizeInfo::new(
21.0,
51.0,
3.0,
3.0,
0.,
0.,
false,
);
let mut terminal = Term::new(&cfg, size, MockEventProxy);
let mut mouse = Mouse {
click_state: $initial_state,
..Mouse::default()
};
let mut selection = None;
let mut message_buffer = MessageBuffer::new();
let context = ActionContext {
terminal: &mut terminal,
selection: &mut selection,
mouse: &mut mouse,
size_info: &size,
clipboard: &mut clipboard,
received_count: 0,
suppress_chars: false,
modifiers: Default::default(),
message_buffer: &mut message_buffer,
config: &cfg,
};
let mut processor = Processor::new(context);
let event: GlutinEvent::<'_, TerminalEvent> = $input;
if let GlutinEvent::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: GlutinEvent::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,
}
test_clickstate! {
name: single_right_click,
initial_state: ClickState::None,
initial_button: MouseButton::Other(0),
input: GlutinEvent::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,
}
test_clickstate! {
name: single_middle_click,
initial_state: ClickState::None,
initial_button: MouseButton::Other(0),
input: GlutinEvent::WindowEvent {
event: WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Middle,
device_id: unsafe { std::mem::transmute_copy(&0) },
modifiers: ModifiersState::default(),
},
window_id: unsafe { std::mem::transmute_copy(&0) },
},
end_state: ClickState::None,
}
test_clickstate! {
name: double_click,
initial_state: ClickState::Click,
initial_button: MouseButton::Left,
input: GlutinEvent::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,
}
test_clickstate! {
name: triple_click,
initial_state: ClickState::DoubleClick,
initial_button: MouseButton::Left,
input: GlutinEvent::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,
}
test_clickstate! {
name: multi_click_separate_buttons,
initial_state: ClickState::DoubleClick,
initial_button: MouseButton::Left,
input: GlutinEvent::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,
}
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::CTRL, action: Action::from("\x1b[1;5D"), mode: BindingMode::empty(), notmode: BindingMode::empty() },
triggers: true,
mode: BindingMode::empty(),
mods: ModifiersState::CTRL,
}
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::LOGO, action: Action::from("arst"), mode: BindingMode::empty(), notmode: BindingMode::empty() },
triggers: false,
mode: BindingMode::empty(),
mods: ModifiersState::ALT | ModifiersState::LOGO,
}
}