aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Dürr <contact@christianduerr.com>2018-10-05 11:45:43 +0200
committerChristian Dürr <contact@christianduerr.com>2018-10-05 11:45:43 +0200
commit06d3edabfb524fc345a3935ec2e60627b97a277a (patch)
tree7dd924811c0ae0ca407fecd4a04b02e3687cd0e7
parent4102fc994a390cd3aef6c06a1ee3f8117d14ca63 (diff)
downloadalacritty-click-links.tar.gz
alacritty-click-links.zip
Add optional modifiers for url launchingclick-links
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.
-rw-r--r--alacritty.yml17
-rw-r--r--alacritty_macos.yml17
-rw-r--r--src/config.rs36
-rw-r--r--src/event.rs55
-rw-r--r--src/input.rs5
-rw-r--r--src/selection.rs19
-rw-r--r--src/term/mod.rs159
7 files changed, 219 insertions, 89 deletions
diff --git a/alacritty.yml b/alacritty.yml
index 5a841263..bcc8e9da 100644
--- a/alacritty.yml
+++ b/alacritty.yml
@@ -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: ",│`|:\"' ()[]{}<>"
diff --git a/alacritty_macos.yml b/alacritty_macos.yml
index c8ca3a0f..116f34f5 100644
--- a/alacritty_macos.yml
+++ b/alacritty_macos.yml
@@ -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: ",│`|:\"' ()[]{}<>"
diff --git a/src/config.rs b/src/config.rs
index 58bb977c..0252918c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -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,
}
}
}
diff --git a/src/event.rs b/src/event.rs
index 77cd9dff..9119cd2f 100644
--- a/src/event.rs
+++ b/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) {
diff --git a/src/input.rs b/src/input.rs
index 56ae36eb..2de73dfe 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -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();
diff --git a/src/selection.rs b/src/selection.rs
index 702599e3..fbdba3ca 100644
--- a/src/selection.rs
+++ b/src/selection.rs
@@ -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
diff --git a/src/term/mod.rs b/src/term/mod.rs
index 9be8b96b..5883579e 100644
--- a/src/term/mod.rs
+++ b/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"))]