diff options
author | Christian Duerr <chrisduerr@users.noreply.github.com> | 2018-10-22 19:39:26 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-10-22 19:39:26 +0000 |
commit | 8ee0d2b5b26faacb5f2663bae1fc056ac5ee26bf (patch) | |
tree | 093022ebdb474b891c8946ae42972dc23f73fefe /src/term/mod.rs | |
parent | 4380d0864b1098909bdcfec132b866c34924517e (diff) | |
download | alacritty-8ee0d2b5b26faacb5f2663bae1fc056ac5ee26bf.tar.gz alacritty-8ee0d2b5b26faacb5f2663bae1fc056ac5ee26bf.zip |
Add option to open URLs on click
This adds the option to automatically launch URLs with a specified
program when clicking on them.
The config option `mouse.url_launcher` has been added to specify which
program should be used to open the URL. The URL is always passed as the
last parameter to the specified command.
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.
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.
This fixes #113.
Diffstat (limited to 'src/term/mod.rs')
-rw-r--r-- | src/term/mod.rs | 159 |
1 files changed, 157 insertions, 2 deletions
diff --git a/src/term/mod.rs b/src/term/mod.rs index f4563922..3b238a41 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 { @@ -1999,7 +2061,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}; @@ -2231,6 +2293,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"))] |