aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/input.rs
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2020-03-18 02:35:08 +0000
committerGitHub <noreply@github.com>2020-03-18 02:35:08 +0000
commit1a8cd172e520e493bacc9c6a2ae6f80de086eaa3 (patch)
tree0b837f1f52f72fe00e258afc34094d60b5d18f04 /alacritty/src/input.rs
parent64db7d3daaed4e06fb8292227622bbc4cdaa2cf0 (diff)
downloadr-alacritty-1a8cd172e520e493bacc9c6a2ae6f80de086eaa3.tar.gz
r-alacritty-1a8cd172e520e493bacc9c6a2ae6f80de086eaa3.tar.bz2
r-alacritty-1a8cd172e520e493bacc9c6a2ae6f80de086eaa3.zip
Add modal keyboard motion mode
This implements a basic mode for navigating inside of Alacritty's history with keyboard bindings. They're bound by default to vi's motion shortcuts but are fully customizable. Since this relies on key bindings only single key bindings are currently supported (so no `ge`, or repetition). Other than navigating the history and moving the viewport, this mode should enable making use of all available selection modes to copy content to the clipboard and launch URLs below the cursor. This also changes the rendering of the block cursor at the side of selections, since previously it could be inverted to be completely invisible. Since that would have caused some troubles with this keyboard selection mode, the block cursor now is no longer inverted when it is at the edges of a selection. Fixes #262.
Diffstat (limited to 'alacritty/src/input.rs')
-rw-r--r--alacritty/src/input.rs279
1 files changed, 198 insertions, 81 deletions
diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs
index 844710d7..937457c4 100644
--- a/alacritty/src/input.rs
+++ b/alacritty/src/input.rs
@@ -19,8 +19,7 @@
//! 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::min;
-use std::cmp::Ordering;
+use std::cmp::{min, Ordering};
use std::marker::PhantomData;
use std::time::Instant;
@@ -40,12 +39,13 @@ use alacritty_terminal::event::{Event, EventListener};
use alacritty_terminal::grid::Scroll;
use alacritty_terminal::index::{Column, Line, Point, Side};
use alacritty_terminal::message_bar::{self, Message};
-use alacritty_terminal::selection::Selection;
+use alacritty_terminal::selection::SelectionType;
use alacritty_terminal::term::mode::TermMode;
use alacritty_terminal::term::{SizeInfo, Term};
use alacritty_terminal::util::start_daemon;
+use alacritty_terminal::vi_mode::ViMotion;
-use crate::config::{Action, Binding, Config, Key};
+use crate::config::{Action, Binding, Config, Key, ViAction};
use crate::event::{ClickState, Mouse};
use crate::url::{Url, Urls};
use crate::window::Window;
@@ -59,21 +59,18 @@ pub const FONT_SIZE_STEP: f32 = 0.5;
/// are activated.
pub struct Processor<'a, T: EventListener, A: ActionContext<T>> {
pub ctx: A,
- pub urls: &'a Urls,
pub highlighted_url: &'a Option<Url>,
_phantom: PhantomData<T>,
}
pub trait ActionContext<T: EventListener> {
- fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, _: B);
+ fn write_to_pty<B: Into<Cow<'static, [u8]>>>(&mut self, data: B);
fn size_info(&self) -> SizeInfo;
- fn copy_selection(&mut self, _: ClipboardType);
- fn clear_selection(&mut self);
+ 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 simple_selection(&mut self, point: Point, side: Side);
- fn block_selection(&mut self, point: Point, side: Side);
- fn semantic_selection(&mut self, point: Point);
- fn line_selection(&mut self, point: Point);
+ fn clear_selection(&mut self);
fn selection_is_empty(&self) -> bool;
fn mouse_mut(&mut self) -> &mut Mouse;
fn mouse(&self) -> &Mouse;
@@ -93,6 +90,9 @@ pub trait ActionContext<T: EventListener> {
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 mouse_mode(&self) -> bool;
}
trait Execute<T: EventListener> {
@@ -107,6 +107,22 @@ impl<T, U: EventListener> Execute<U> for Binding<T> {
}
}
+impl Action {
+ fn toggle_selection<T, A>(ctx: &mut A, ty: SelectionType)
+ where
+ T: EventListener,
+ A: ActionContext<T>,
+ {
+ 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) = ctx.terminal_mut().selection_mut() {
+ selection.include_all();
+ }
+ }
+}
+
impl<T: EventListener> Execute<T> for Action {
#[inline]
fn execute<A: ActionContext<T>>(&self, ctx: &mut A) {
@@ -118,6 +134,11 @@ impl<T: EventListener> Execute<T> for Action {
},
Action::Copy => {
ctx.copy_selection(ClipboardType::Clipboard);
+
+ // Clear selection in vi mode for better user feedback
+ if ctx.terminal().mode().contains(TermMode::VI) {
+ ctx.clear_selection();
+ }
},
Action::Paste => {
let text = ctx.terminal_mut().clipboard().load(ClipboardType::Clipboard);
@@ -135,6 +156,27 @@ impl<T: EventListener> Execute<T> for Action {
Err(err) => warn!("Couldn't run command {}", err),
}
},
+ Action::ClearSelection => ctx.clear_selection(),
+ Action::ToggleViMode => ctx.terminal_mut().toggle_vi_mode(),
+ 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::ViMotion(motion) => ctx.terminal_mut().vi_motion(motion),
Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(),
#[cfg(target_os = "macos")]
Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(),
@@ -147,12 +189,74 @@ impl<T: EventListener> Execute<T> for Action {
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 => 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::ScrollPageUp => {
+ // Move vi mode cursor
+ let term = ctx.terminal_mut();
+ let scroll_lines = term.grid().num_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.grid().num_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.grid().num_lines().0 as isize / 2;
+ term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
+
+ ctx.scroll(Scroll::Lines(scroll_lines));
+ },
+ Action::ScrollHalfPageDown => {
+ // Move vi mode cursor
+ let term = ctx.terminal_mut();
+ let scroll_lines = -(term.grid().num_lines().0 as isize / 2);
+ term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, scroll_lines);
+
+ ctx.scroll(Scroll::Lines(scroll_lines));
+ },
+ Action::ScrollLineUp => {
+ // Move vi mode cursor
+ let term = ctx.terminal();
+ if term.grid().display_offset() != term.grid().history_size()
+ && term.vi_mode_cursor.point.line + 1 != term.grid().num_lines()
+ {
+ ctx.terminal_mut().vi_mode_cursor.point.line += 1;
+ }
+
+ ctx.scroll(Scroll::Lines(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::Lines(-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);
+ },
+ Action::ScrollToBottom => {
+ ctx.scroll(Scroll::Bottom);
+
+ // Move vi mode cursor
+ let term = ctx.terminal_mut();
+ term.vi_mode_cursor.point.line = term.grid().num_lines() - 1;
+ term.vi_motion(ViMotion::FirstOccupied);
+ },
Action::ClearHistory => ctx.terminal_mut().clear_screen(ClearMode::Saved),
Action::ClearLogNotice => ctx.pop_message(),
Action::SpawnNewInstance => ctx.spawn_new_instance(),
@@ -197,8 +301,8 @@ impl From<MouseState> for CursorIcon {
}
impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
- pub fn new(ctx: A, urls: &'a Urls, highlighted_url: &'a Option<Url>) -> Self {
- Self { ctx, urls, highlighted_url, _phantom: Default::default() }
+ pub fn new(ctx: A, highlighted_url: &'a Option<Url>) -> Self {
+ Self { ctx, highlighted_url, _phantom: Default::default() }
}
#[inline]
@@ -238,12 +342,16 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
let last_term_line = self.ctx.terminal().grid().num_lines() - 1;
if self.ctx.mouse().left_button_state == ElementState::Pressed
- && (self.ctx.modifiers().shift()
- || !self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE))
+ && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode())
{
// Treat motion over message bar like motion over the last line
let line = min(point.line, last_term_line);
+ // Move vi mode cursor to mouse cursor position
+ if self.ctx.terminal().mode().contains(TermMode::VI) {
+ self.ctx.terminal_mut().vi_mode_cursor.point = point;
+ }
+
self.ctx.update_selection(Point { line, col: point.col }, cell_side);
} else if inside_grid
&& cell_changed
@@ -354,13 +462,15 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
fn on_mouse_double_click(&mut self, button: MouseButton, point: Point) {
if button == MouseButton::Left {
- self.ctx.semantic_selection(point);
+ let side = self.ctx.mouse().cell_side;
+ self.ctx.start_selection(SelectionType::Semantic, point, side);
}
}
fn on_mouse_triple_click(&mut self, button: MouseButton, point: Point) {
if button == MouseButton::Left {
- self.ctx.line_selection(point);
+ let side = self.ctx.mouse().cell_side;
+ self.ctx.start_selection(SelectionType::Lines, point, side);
}
}
@@ -402,14 +512,18 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
// Start new empty selection
let side = self.ctx.mouse().cell_side;
if self.ctx.modifiers().ctrl() {
- self.ctx.block_selection(point, side);
+ self.ctx.start_selection(SelectionType::Block, point, side);
} else {
- self.ctx.simple_selection(point, side);
+ self.ctx.start_selection(SelectionType::Simple, point, side);
}
- if !self.ctx.modifiers().shift()
- && self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE)
- {
+ // Move vi mode cursor to mouse position
+ if self.ctx.terminal().mode().contains(TermMode::VI) {
+ // Update vi mode cursor position on click
+ self.ctx.terminal_mut().vi_mode_cursor.point = point;
+ }
+
+ if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() {
let code = match button {
MouseButton::Left => 0,
MouseButton::Middle => 1,
@@ -427,9 +541,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
}
fn on_mouse_release(&mut self, button: MouseButton) {
- if !self.ctx.modifiers().shift()
- && self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE)
- {
+ if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() {
let code = match button {
MouseButton::Left => 0,
MouseButton::Middle => 1,
@@ -440,31 +552,12 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
self.mouse_report(code, ElementState::Released);
return;
} else if let (MouseButton::Left, MouseState::Url(url)) = (button, self.mouse_state()) {
- self.launch_url(url);
+ self.ctx.launch_url(url);
}
self.copy_selection();
}
- /// Spawn URL launcher when clicking on URLs.
- fn launch_url(&self, url: Url) {
- if self.ctx.mouse().block_url_launcher {
- return;
- }
-
- if let Some(ref launcher) = self.ctx.config().ui_config.mouse.url.launcher {
- let mut args = launcher.args().to_vec();
- let start = self.ctx.terminal().visible_to_buffer(url.start());
- let end = self.ctx.terminal().visible_to_buffer(url.end());
- args.push(self.ctx.terminal().bounds_to_string(start, end));
-
- match start_daemon(launcher.program(), &args) {
- Ok(_) => debug!("Launched {} with args {:?}", launcher.program(), args),
- Err(_) => warn!("Unable to launch {} with args {:?}", launcher.program(), args),
- }
- }
- }
-
pub fn mouse_wheel_input(&mut self, delta: MouseScrollDelta, phase: TouchPhase) {
match delta {
MouseScrollDelta::LineDelta(_columns, lines) => {
@@ -489,7 +582,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
fn scroll_terminal(&mut self, new_scroll_px: f64) {
let height = f64::from(self.ctx.size_info().cell_height);
- if self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE) {
+ if self.ctx.mouse_mode() {
self.ctx.mouse_mut().scroll_px += new_scroll_px;
let code = if new_scroll_px > 0. { 64 } else { 65 };
@@ -530,7 +623,22 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
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::Lines(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.col = absolute.col;
+
+ // Update selection
+ 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;
@@ -560,7 +668,6 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
// Reset cursor when message bar height changed or all messages are gone
let size = self.ctx.size_info();
- let mouse_mode = self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE);
let current_lines = (size.lines() - self.ctx.terminal().grid().num_lines()).0;
let new_lines = self.ctx.message().map(|m| m.text(&size).len()).unwrap_or(0);
@@ -568,7 +675,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
Ordering::Less => CursorIcon::Default,
Ordering::Equal => CursorIcon::Hand,
Ordering::Greater => {
- if mouse_mode {
+ if self.ctx.mouse_mode() {
CursorIcon::Default
} else {
CursorIcon::Text
@@ -613,7 +720,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
/// Process a received character.
pub fn received_char(&mut self, c: char) {
- if *self.ctx.suppress_chars() {
+ if *self.ctx.suppress_chars() || self.ctx.terminal().mode().contains(TermMode::VI) {
return;
}
@@ -685,7 +792,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
fn process_mouse_bindings(&mut self, button: MouseButton) {
let mods = *self.ctx.modifiers();
let mode = *self.ctx.terminal().mode();
- let mouse_mode = mode.intersects(TermMode::MOUSE_MODE);
+ let mouse_mode = self.ctx.mouse_mode();
for i in 0..self.ctx.config().ui_config.mouse_bindings.len() {
let mut binding = self.ctx.config().ui_config.mouse_bindings[i].clone();
@@ -743,22 +850,24 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
return MouseState::MessageBar;
}
+ let mouse_mode = self.ctx.mouse_mode();
+
// Check for URL at mouse cursor
let mods = *self.ctx.modifiers();
- let selection =
- !self.ctx.terminal().selection().as_ref().map(Selection::is_empty).unwrap_or(true);
- let mouse_mode = self.ctx.terminal().mode().intersects(TermMode::MOUSE_MODE);
- let highlighted_url =
- self.urls.highlighted(self.ctx.config(), self.ctx.mouse(), mods, mouse_mode, selection);
+ 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.terminal().mode().intersects(TermMode::MOUSE_MODE)
- && !self.ctx.modifiers().shift()
- {
+ if !self.ctx.modifiers().shift() && mouse_mode {
MouseState::Mouse
} else {
MouseState::Text
@@ -781,12 +890,12 @@ mod tests {
use alacritty_terminal::grid::Scroll;
use alacritty_terminal::index::{Point, Side};
use alacritty_terminal::message_bar::{Message, MessageBuffer};
- use alacritty_terminal::selection::Selection;
+ use alacritty_terminal::selection::{Selection, SelectionType};
use alacritty_terminal::term::{SizeInfo, Term, TermMode};
use crate::config::{ClickHandler, Config};
use crate::event::{ClickState, Mouse};
- use crate::url::Urls;
+ use crate::url::{Url, Urls};
use crate::window::Window;
use super::{Action, Binding, Processor};
@@ -799,7 +908,7 @@ mod tests {
fn send_event(&self, _event: TerminalEvent) {}
}
- #[derive(PartialEq)]
+ #[derive(Debug, PartialEq)]
enum MultiClick {
DoubleClick,
TripleClick,
@@ -824,9 +933,15 @@ mod tests {
fn update_selection(&mut self, _point: Point, _side: Side) {}
- fn simple_selection(&mut self, _point: Point, _side: Side) {}
+ fn start_selection(&mut self, ty: SelectionType, _point: Point, _side: Side) {
+ match ty {
+ SelectionType::Semantic => self.last_action = MultiClick::DoubleClick,
+ SelectionType::Lines => self.last_action = MultiClick::TripleClick,
+ _ => (),
+ }
+ }
- fn block_selection(&mut self, _point: Point, _side: Side) {}
+ fn toggle_selection(&mut self, _ty: SelectionType, _point: Point, _side: Side) {}
fn copy_selection(&mut self, _: ClipboardType) {}
@@ -850,15 +965,6 @@ mod tests {
*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
}
@@ -878,6 +984,10 @@ mod tests {
}
}
+ fn mouse_mode(&self) -> bool {
+ false
+ }
+
#[inline]
fn mouse_mut(&mut self) -> &mut Mouse {
self.mouse
@@ -923,6 +1033,14 @@ mod tests {
fn event_loop(&self) -> &EventLoopWindowTarget<TerminalEvent> {
unimplemented!();
}
+
+ fn urls(&self) -> &Urls {
+ unimplemented!();
+ }
+
+ fn launch_url(&self, _: Url) {
+ unimplemented!();
+ }
}
macro_rules! test_clickstate {
@@ -981,8 +1099,7 @@ mod tests {
config: &cfg,
};
- let urls = Urls::new();
- let mut processor = Processor::new(context, &urls, &None);
+ let mut processor = Processor::new(context, &None);
let event: Event::<'_, TerminalEvent> = $input;
if let Event::WindowEvent {