aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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"))]