1
0
Fork 0
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:
Christian Dürr 2018-10-05 11:45:43 +02:00
parent 4102fc994a
commit 06d3edabfb
7 changed files with 219 additions and 89 deletions

View file

@ -259,11 +259,18 @@ mouse:
double_click: { threshold: 300 }
triple_click: { threshold: 300 }
# URL Launcher
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.
url_launcher: xdg-open
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: ",│`|:\"' ()[]{}<>"

View file

@ -257,11 +257,18 @@ mouse:
double_click: { threshold: 300 }
triple_click: { threshold: 300 }
# URL Launcher
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.
url_launcher: open
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: ",│`|:\"' ()[]{}<>"

View file

@ -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,
}
}
}

View file

@ -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) {

View file

@ -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();

View file

@ -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

View file

@ -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"))]