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, + pub url: Url, // TODO: DEPRECATED #[serde(default)] pub faux_scrollback_lines: Option, } +#[derive(Default, Clone, Debug, Deserialize)] +pub struct Url { + // Program for opening links + #[serde(default, deserialize_with = "failure_default")] + pub launcher: Option, + + // Modifier used to open links + #[serde(default, deserialize_with = "deserialize_modifiers")] + pub modifiers: ModifiersState, +} + +fn deserialize_modifiers<'a, D>(deserializer: D) -> ::std::result::Result + 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) -> Option { - 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) -> Option { + 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) -> Point; - /// Find the nearest semantic boundary _to the point_ of provided point. - fn semantic_search_right(&self, _: Point) -> Point; -} - /// 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(&self, grid: &G) -> Option { + pub fn to_span(&self, grid: &G) -> Option { match *self { Selection::Simple { ref region } => { Selection::span_simple(grid, region) @@ -166,7 +156,7 @@ impl Selection { grid: &G, region: &Range>, ) -> Option - 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) -> Point { unimplemented!(); } fn semantic_search_right(&self, _: Point) -> Point { unimplemented!(); } + fn url_search(&self, _: Point) -> Option { 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) -> Point; + /// Find the nearest semantic boundary _to the point_ of provided point. + fn semantic_search_right(&self, _: Point) -> Point; + /// Find the nearest URL boundary in both directions. + fn url_search(&self, _: Point) -> Option; +} + +impl Search for Term { fn semantic_search_left(&self, mut point: Point) -> Point { // 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) -> Option { + 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 = 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 = 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 = 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"))]