mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-18 13:55:23 -05:00
Add optional modifiers for url launching
It is not always desired for URLs to open automatically when clicking on them. To resolve this a new `modifiers` field has been introduced to the config, which allows specifying which keyboard modifiers need to be held down to launch URLs in the specified launcher. Since the config now has two different fields for the URL clicking feature, the new `url` field has been added to `mouse` and the `mouse.url_launcher` field has been moved to `mouse.url.launcher`. Some tests have been added to make sure that the edge-cases of the URL parsing are protected against future regressions. To make testing easier the parsing method has been moved into the `SemanticSearch` trait. The name of the trait has also been changed to just `Search` and it has been moved to `src/term/mod.rs` to fit the additional functionality. Some minor style improvements have also been made.
This commit is contained in:
parent
4102fc994a
commit
06d3edabfb
7 changed files with 219 additions and 89 deletions
|
@ -259,11 +259,18 @@ mouse:
|
|||
double_click: { threshold: 300 }
|
||||
triple_click: { threshold: 300 }
|
||||
|
||||
# URL Launcher
|
||||
#
|
||||
# This program is executed when clicking on a text which is recognized as a URL.
|
||||
# The URL is always added to the command as the last parameter.
|
||||
url_launcher: xdg-open
|
||||
url:
|
||||
# URL launcher
|
||||
#
|
||||
# This program is executed when clicking on a text which is recognized as a URL.
|
||||
# The URL is always added to the command as the last parameter.
|
||||
launcher: open
|
||||
|
||||
# URL modifiers
|
||||
#
|
||||
# These are the modifiers that need to be held down for opening URLs when clicking
|
||||
# on them. The available modifiers are documented in the key binding section
|
||||
#modifiers: Control|Shift
|
||||
|
||||
selection:
|
||||
semantic_escape_chars: ",│`|:\"' ()[]{}<>"
|
||||
|
|
|
@ -257,11 +257,18 @@ mouse:
|
|||
double_click: { threshold: 300 }
|
||||
triple_click: { threshold: 300 }
|
||||
|
||||
# URL Launcher
|
||||
#
|
||||
# This program is executed when clicking on a text which is recognized as a URL.
|
||||
# The URL is always added to the command as the last parameter.
|
||||
url_launcher: open
|
||||
url:
|
||||
# URL launcher
|
||||
#
|
||||
# This program is executed when clicking on a text which is recognized as a URL.
|
||||
# The URL is always added to the command as the last parameter.
|
||||
launcher: open
|
||||
|
||||
# URL modifiers
|
||||
#
|
||||
# These are the modifiers that need to be held down for opening URLs when clicking
|
||||
# on them. The available modifiers are documented in the key binding section
|
||||
#modifiers: Control|Shift
|
||||
|
||||
selection:
|
||||
semantic_escape_chars: ",│`|:\"' ()[]{}<>"
|
||||
|
|
|
@ -87,16 +87,31 @@ pub struct Mouse {
|
|||
pub double_click: ClickHandler,
|
||||
#[serde(default, deserialize_with = "failure_default")]
|
||||
pub triple_click: ClickHandler,
|
||||
|
||||
// Program for opening links
|
||||
#[serde(default, deserialize_with = "failure_default")]
|
||||
pub url_launcher: Option<CommandWrapper>,
|
||||
pub url: Url,
|
||||
|
||||
// TODO: DEPRECATED
|
||||
#[serde(default)]
|
||||
pub faux_scrollback_lines: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Deserialize)]
|
||||
pub struct Url {
|
||||
// Program for opening links
|
||||
#[serde(default, deserialize_with = "failure_default")]
|
||||
pub launcher: Option<CommandWrapper>,
|
||||
|
||||
// Modifier used to open links
|
||||
#[serde(default, deserialize_with = "deserialize_modifiers")]
|
||||
pub modifiers: ModifiersState,
|
||||
}
|
||||
|
||||
fn deserialize_modifiers<'a, D>(deserializer: D) -> ::std::result::Result<ModifiersState, D::Error>
|
||||
where D: de::Deserializer<'a>
|
||||
{
|
||||
ModsWrapper::deserialize(deserializer).map(|wrapper| wrapper.into_inner())
|
||||
}
|
||||
|
||||
impl Default for Mouse {
|
||||
fn default() -> Mouse {
|
||||
Mouse {
|
||||
|
@ -106,7 +121,7 @@ impl Default for Mouse {
|
|||
triple_click: ClickHandler {
|
||||
threshold: Duration::from_millis(300),
|
||||
},
|
||||
url_launcher: None,
|
||||
url: Url::default(),
|
||||
faux_scrollback_lines: None,
|
||||
}
|
||||
}
|
||||
|
@ -647,6 +662,7 @@ fn deserialize_scrolling_multiplier<'a, D>(deserializer: D) -> ::std::result::Re
|
|||
///
|
||||
/// Our deserialize impl wouldn't be covered by a derive(Deserialize); see the
|
||||
/// impl below.
|
||||
#[derive(Debug, Copy, Clone, Hash, Default, Eq, PartialEq)]
|
||||
struct ModsWrapper(ModifiersState);
|
||||
|
||||
impl ModsWrapper {
|
||||
|
@ -750,17 +766,17 @@ pub enum CommandWrapper {
|
|||
}
|
||||
|
||||
impl CommandWrapper {
|
||||
pub fn program(&self) -> &String {
|
||||
match *self {
|
||||
CommandWrapper::Just(ref program) => program,
|
||||
CommandWrapper::WithArgs { ref program, .. } => program,
|
||||
pub fn program(&self) -> &str {
|
||||
match self {
|
||||
CommandWrapper::Just(program) => program,
|
||||
CommandWrapper::WithArgs { program, .. } => program,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn args(&self) -> &[String] {
|
||||
match *self {
|
||||
match self {
|
||||
CommandWrapper::Just(_) => &[],
|
||||
CommandWrapper::WithArgs { ref args, .. } => &args,
|
||||
CommandWrapper::WithArgs { args, .. } => args,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
55
src/event.rs
55
src/event.rs
|
@ -4,16 +4,14 @@ use std::fs::File;
|
|||
use std::io::Write;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Instant};
|
||||
use std::cmp::min;
|
||||
|
||||
use serde_json as json;
|
||||
use parking_lot::MutexGuard;
|
||||
use glutin::{self, ModifiersState, Event, ElementState};
|
||||
use copypasta::{Clipboard, Load, Store, Buffer as ClipboardBuffer};
|
||||
use url::Url;
|
||||
|
||||
use ansi::{Handler, ClearMode};
|
||||
use grid::{Scroll, BidirectionalIterator};
|
||||
use grid::Scroll;
|
||||
use config::{self, Config};
|
||||
use cli::Options;
|
||||
use display::OnResize;
|
||||
|
@ -21,13 +19,11 @@ use index::{Line, Column, Side, Point};
|
|||
use input::{self, MouseBinding, KeyBinding};
|
||||
use selection::Selection;
|
||||
use sync::FairMutex;
|
||||
use term::{Term, SizeInfo, TermMode};
|
||||
use term::{Term, SizeInfo, TermMode, Search};
|
||||
use util::limit;
|
||||
use util::fmt::Red;
|
||||
use window::Window;
|
||||
|
||||
const URL_SEPARATOR_CHARS: [char; 3] = [' ', '"', '\''];
|
||||
|
||||
/// Byte sequences are sent to a `Notify` in response to some events
|
||||
pub trait Notify {
|
||||
/// Notify that an escape sequence should be written to the pty
|
||||
|
@ -108,51 +104,8 @@ impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> {
|
|||
self.terminal.dirty = true;
|
||||
}
|
||||
|
||||
fn url(&self, mut point: Point<usize>) -> Option<String> {
|
||||
let grid = self.terminal.grid();
|
||||
point.line = grid.num_lines().0 - point.line - 1;
|
||||
|
||||
// Limit the starting point to the last line in the history
|
||||
point.line = min(point.line, grid.len() - 1);
|
||||
|
||||
// Create forwards and backwards iterators
|
||||
let iterf = grid.iter_from(point);
|
||||
point.col += 1;
|
||||
let mut iterb = grid.iter_from(point);
|
||||
|
||||
// Put all characters until separators into a string
|
||||
let mut buf = String::new();
|
||||
while let Some(cell) = iterb.prev() {
|
||||
if URL_SEPARATOR_CHARS.contains(&cell.c) {
|
||||
break;
|
||||
}
|
||||
buf.insert(0, cell.c);
|
||||
}
|
||||
for cell in iterf {
|
||||
if URL_SEPARATOR_CHARS.contains(&cell.c) {
|
||||
break;
|
||||
}
|
||||
buf.push(cell.c);
|
||||
}
|
||||
|
||||
// Heuristic to remove all leading '('
|
||||
while buf.starts_with('(') {
|
||||
buf.remove(0);
|
||||
}
|
||||
|
||||
// Heuristic to remove all ')' from end of URLs without matching '('
|
||||
let str_count = |text: &str, c: char| {
|
||||
text.chars().filter(|tc| *tc == c).count()
|
||||
};
|
||||
while buf.ends_with(')') && str_count(&buf, '(') < str_count(&buf, ')') {
|
||||
buf.pop();
|
||||
}
|
||||
|
||||
// Check if string is valid url
|
||||
match Url::parse(&buf) {
|
||||
Ok(_) => Some(buf),
|
||||
Err(_) => None,
|
||||
}
|
||||
fn url(&self, point: Point<usize>) -> Option<String> {
|
||||
self.terminal.url_search(point)
|
||||
}
|
||||
|
||||
fn line_selection(&mut self, point: Point) {
|
||||
|
|
|
@ -488,8 +488,9 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
|
|||
} else {
|
||||
// Spawn URL launcher when clicking on URLs
|
||||
let moved = self.ctx.mouse().last_press_pos != (self.ctx.mouse().x, self.ctx.mouse().y);
|
||||
if let (Some(point), Some(launcher), false) =
|
||||
(self.ctx.mouse_coords(), &self.mouse_config.url_launcher, moved)
|
||||
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();
|
||||
|
|
|
@ -22,6 +22,7 @@ use std::cmp::{min, max};
|
|||
use std::ops::Range;
|
||||
|
||||
use index::{Point, Column, Side};
|
||||
use term::Search;
|
||||
|
||||
/// Describes a region of a 2-dimensional area
|
||||
///
|
||||
|
@ -71,17 +72,6 @@ impl Anchor {
|
|||
}
|
||||
}
|
||||
|
||||
/// A type that can expand a given point to a region
|
||||
///
|
||||
/// Usually this is implemented for some 2-D array type since
|
||||
/// points are two dimensional indices.
|
||||
pub trait SemanticSearch {
|
||||
/// Find the nearest semantic boundary _to the left_ of provided point.
|
||||
fn semantic_search_left(&self, _: Point<usize>) -> Point<usize>;
|
||||
/// Find the nearest semantic boundary _to the point_ of provided point.
|
||||
fn semantic_search_right(&self, _: Point<usize>) -> Point<usize>;
|
||||
}
|
||||
|
||||
/// A type that has 2-dimensional boundaries
|
||||
pub trait Dimensions {
|
||||
/// Get the size of the area
|
||||
|
@ -149,7 +139,7 @@ impl Selection {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_span<G: SemanticSearch + Dimensions>(&self, grid: &G) -> Option<Span> {
|
||||
pub fn to_span<G: Search + Dimensions>(&self, grid: &G) -> Option<Span> {
|
||||
match *self {
|
||||
Selection::Simple { ref region } => {
|
||||
Selection::span_simple(grid, region)
|
||||
|
@ -166,7 +156,7 @@ impl Selection {
|
|||
grid: &G,
|
||||
region: &Range<Point<usize>>,
|
||||
) -> Option<Span>
|
||||
where G: SemanticSearch + Dimensions
|
||||
where G: Search + Dimensions
|
||||
{
|
||||
// Normalize ordering of selected cells
|
||||
let (front, tail) = if region.start < region.end {
|
||||
|
@ -383,9 +373,10 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
impl super::SemanticSearch for Dimensions {
|
||||
impl super::Search for Dimensions {
|
||||
fn semantic_search_left(&self, _: Point<usize>) -> Point<usize> { unimplemented!(); }
|
||||
fn semantic_search_right(&self, _: Point<usize>) -> Point<usize> { unimplemented!(); }
|
||||
fn url_search(&self, _: Point<usize>) -> Option<String> { unimplemented!(); }
|
||||
}
|
||||
|
||||
/// Test case of single cell selection
|
||||
|
|
159
src/term/mod.rs
159
src/term/mod.rs
|
@ -20,6 +20,7 @@ use std::time::{Duration, Instant};
|
|||
|
||||
use arraydeque::ArrayDeque;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
use url::Url;
|
||||
|
||||
use font::{self, Size};
|
||||
use ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle};
|
||||
|
@ -36,7 +37,22 @@ pub mod color;
|
|||
pub use self::cell::Cell;
|
||||
use self::cell::LineLength;
|
||||
|
||||
impl selection::SemanticSearch for Term {
|
||||
const URL_SEPARATOR_CHARS: [char; 3] = [' ', '"', '\''];
|
||||
|
||||
/// A type that can expand a given point to a region
|
||||
///
|
||||
/// Usually this is implemented for some 2-D array type since
|
||||
/// points are two dimensional indices.
|
||||
pub trait Search {
|
||||
/// Find the nearest semantic boundary _to the left_ of provided point.
|
||||
fn semantic_search_left(&self, _: Point<usize>) -> Point<usize>;
|
||||
/// Find the nearest semantic boundary _to the point_ of provided point.
|
||||
fn semantic_search_right(&self, _: Point<usize>) -> Point<usize>;
|
||||
/// Find the nearest URL boundary in both directions.
|
||||
fn url_search(&self, _: Point<usize>) -> Option<String>;
|
||||
}
|
||||
|
||||
impl Search for Term {
|
||||
fn semantic_search_left(&self, mut point: Point<usize>) -> Point<usize> {
|
||||
// Limit the starting point to the last line in the history
|
||||
point.line = min(point.line, self.grid.len() - 1);
|
||||
|
@ -80,6 +96,52 @@ impl selection::SemanticSearch for Term {
|
|||
|
||||
point
|
||||
}
|
||||
|
||||
fn url_search(&self, mut point: Point<usize>) -> Option<String> {
|
||||
point.line = self.grid.num_lines().0 - point.line - 1;
|
||||
|
||||
// Limit the starting point to the last line in the history
|
||||
point.line = min(point.line, self.grid.len() - 1);
|
||||
|
||||
// Create forwards and backwards iterators
|
||||
let iterf = self.grid.iter_from(point);
|
||||
point.col += 1;
|
||||
let mut iterb = self.grid.iter_from(point);
|
||||
|
||||
// Put all characters until separators into a string
|
||||
let mut buf = String::new();
|
||||
while let Some(cell) = iterb.prev() {
|
||||
if URL_SEPARATOR_CHARS.contains(&cell.c) {
|
||||
break;
|
||||
}
|
||||
buf.insert(0, cell.c);
|
||||
}
|
||||
for cell in iterf {
|
||||
if URL_SEPARATOR_CHARS.contains(&cell.c) {
|
||||
break;
|
||||
}
|
||||
buf.push(cell.c);
|
||||
}
|
||||
|
||||
// Heuristic to remove all leading '('
|
||||
while buf.starts_with('(') {
|
||||
buf.remove(0);
|
||||
}
|
||||
|
||||
// Heuristic to remove all ')' from end of URLs without matching '('
|
||||
let str_count = |text: &str, c: char| {
|
||||
text.chars().filter(|tc| *tc == c).count()
|
||||
};
|
||||
while buf.ends_with(')') && str_count(&buf, '(') < str_count(&buf, ')') {
|
||||
buf.pop();
|
||||
}
|
||||
|
||||
// Check if string is valid url
|
||||
match Url::parse(&buf) {
|
||||
Ok(_) => Some(buf),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl selection::Dimensions for Term {
|
||||
|
@ -1996,7 +2058,7 @@ mod tests {
|
|||
extern crate serde_json;
|
||||
|
||||
use super::{Cell, Term, SizeInfo};
|
||||
use term::cell;
|
||||
use term::{cell, Search};
|
||||
|
||||
use grid::{Grid, Scroll};
|
||||
use index::{Point, Line, Column, Side};
|
||||
|
@ -2228,6 +2290,99 @@ mod tests {
|
|||
scrolled_grid.scroll_display(Scroll::Top);
|
||||
assert_eq!(term.grid, scrolled_grid);
|
||||
}
|
||||
|
||||
// `((ftp://a.de))` -> `Some("ftp://a.de")`
|
||||
#[test]
|
||||
fn url_trim_unmatched_parens() {
|
||||
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 term = Term::new(&Default::default(), size);
|
||||
let mut grid: Grid<Cell> = Grid::new(Line(1), Column(15), 0, Cell::default());
|
||||
grid[Line(0)][Column(0)].c = '(';
|
||||
grid[Line(0)][Column(1)].c = '(';
|
||||
grid[Line(0)][Column(2)].c = 'f';
|
||||
grid[Line(0)][Column(3)].c = 't';
|
||||
grid[Line(0)][Column(4)].c = 'p';
|
||||
grid[Line(0)][Column(5)].c = ':';
|
||||
grid[Line(0)][Column(6)].c = '/';
|
||||
grid[Line(0)][Column(7)].c = '/';
|
||||
grid[Line(0)][Column(8)].c = 'a';
|
||||
grid[Line(0)][Column(9)].c = '.';
|
||||
grid[Line(0)][Column(10)].c = 'd';
|
||||
grid[Line(0)][Column(11)].c = 'e';
|
||||
grid[Line(0)][Column(12)].c = ')';
|
||||
grid[Line(0)][Column(13)].c = ')';
|
||||
mem::swap(&mut term.grid, &mut grid);
|
||||
|
||||
// Search for URL in grid
|
||||
let url = term.url_search(Point::new(0, Column(4)));
|
||||
|
||||
assert_eq!(url, Some("ftp://a.de".into()));
|
||||
}
|
||||
|
||||
// `ftp://a.de/()` -> `Some("ftp://a.de/()")`
|
||||
#[test]
|
||||
fn url_allow_matching_parens() {
|
||||
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 term = Term::new(&Default::default(), size);
|
||||
let mut grid: Grid<Cell> = Grid::new(Line(1), Column(15), 0, Cell::default());
|
||||
grid[Line(0)][Column(0)].c = 'f';
|
||||
grid[Line(0)][Column(1)].c = 't';
|
||||
grid[Line(0)][Column(2)].c = 'p';
|
||||
grid[Line(0)][Column(3)].c = ':';
|
||||
grid[Line(0)][Column(4)].c = '/';
|
||||
grid[Line(0)][Column(5)].c = '/';
|
||||
grid[Line(0)][Column(6)].c = 'a';
|
||||
grid[Line(0)][Column(7)].c = '.';
|
||||
grid[Line(0)][Column(8)].c = 'd';
|
||||
grid[Line(0)][Column(9)].c = 'e';
|
||||
grid[Line(0)][Column(10)].c = '/';
|
||||
grid[Line(0)][Column(11)].c = '(';
|
||||
grid[Line(0)][Column(12)].c = ')';
|
||||
mem::swap(&mut term.grid, &mut grid);
|
||||
|
||||
// Search for URL in grid
|
||||
let url = term.url_search(Point::new(0, Column(4)));
|
||||
|
||||
assert_eq!(url, Some("ftp://a.de/()".into()));
|
||||
}
|
||||
|
||||
// `aze` -> `None`
|
||||
#[test]
|
||||
fn url_skip_invalid() {
|
||||
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 term = Term::new(&Default::default(), size);
|
||||
let mut grid: Grid<Cell> = Grid::new(Line(1), Column(15), 0, Cell::default());
|
||||
grid[Line(0)][Column(0)].c = 'a';
|
||||
grid[Line(0)][Column(1)].c = 'z';
|
||||
grid[Line(0)][Column(2)].c = 'e';
|
||||
mem::swap(&mut term.grid, &mut grid);
|
||||
|
||||
// Search for URL in grid
|
||||
let url = term.url_search(Point::new(0, Column(1)));
|
||||
|
||||
assert_eq!(url, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "bench"))]
|
||||
|
|
Loading…
Reference in a new issue