aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs41
-rw-r--r--src/event.rs12
-rw-r--r--src/index.rs6
-rw-r--r--src/input.rs94
-rw-r--r--src/lib.rs1
-rw-r--r--src/selection.rs31
-rw-r--r--src/term/mod.rs159
7 files changed, 302 insertions, 42 deletions
diff --git a/src/config.rs b/src/config.rs
index 069eef0e..2e746623 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -87,12 +87,31 @@ pub struct Mouse {
pub double_click: ClickHandler,
#[serde(default, deserialize_with = "failure_default")]
pub triple_click: ClickHandler,
+ #[serde(default, deserialize_with = "failure_default")]
+ 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 {
@@ -102,6 +121,7 @@ impl Default for Mouse {
triple_click: ClickHandler {
threshold: Duration::from_millis(300),
},
+ url: Url::default(),
faux_scrollback_lines: None,
}
}
@@ -644,6 +664,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 {
@@ -735,9 +756,9 @@ impl<'a> de::Deserialize<'a> for ActionWrapper {
}
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
-enum CommandWrapper {
+pub enum CommandWrapper {
Just(String),
WithArgs {
program: String,
@@ -746,6 +767,22 @@ enum CommandWrapper {
},
}
+impl CommandWrapper {
+ pub fn program(&self) -> &str {
+ match self {
+ CommandWrapper::Just(program) => program,
+ CommandWrapper::WithArgs { program, .. } => program,
+ }
+ }
+
+ pub fn args(&self) -> &[String] {
+ match self {
+ CommandWrapper::Just(_) => &[],
+ CommandWrapper::WithArgs { args, .. } => args,
+ }
+ }
+}
+
use ::term::{mode, TermMode};
struct ModeWrapper {
diff --git a/src/event.rs b/src/event.rs
index eef04a8c..185a7d70 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -19,7 +19,7 @@ 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;
@@ -76,6 +76,10 @@ impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> {
}
}
+ fn selection_is_empty(&self) -> bool {
+ self.terminal.selection().as_ref().map(|s| s.is_empty()).unwrap_or(true)
+ }
+
fn clear_selection(&mut self) {
*self.terminal.selection_mut() = None;
self.terminal.dirty = true;
@@ -104,6 +108,10 @@ impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> {
self.terminal.dirty = true;
}
+ fn url(&self, point: Point<usize>) -> Option<String> {
+ self.terminal.url_search(point)
+ }
+
fn line_selection(&mut self, point: Point) {
let point = self.terminal.visible_to_buffer(point);
*self.terminal.selection_mut() = Some(Selection::lines(point));
@@ -196,6 +204,7 @@ pub struct Mouse {
pub column: Column,
pub cell_side: Side,
pub lines_scrolled: f32,
+ pub block_url_launcher: bool,
}
impl Default for Mouse {
@@ -213,6 +222,7 @@ impl Default for Mouse {
column: Column(0),
cell_side: Side::Left,
lines_scrolled: 0.0,
+ block_url_launcher: false,
}
}
}
diff --git a/src/index.rs b/src/index.rs
index 149fd458..8ac1ab3a 100644
--- a/src/index.rs
+++ b/src/index.rs
@@ -64,6 +64,12 @@ impl From<Point<isize>> for Point<usize> {
}
}
+impl From<Point> for Point<usize> {
+ fn from(point: Point) -> Self {
+ Point::new(point.line.0, point.col)
+ }
+}
+
/// A line
///
/// Newtype to avoid passing values incorrectly
diff --git a/src/input.rs b/src/input.rs
index ed9aa7fc..49a9101e 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -63,6 +63,7 @@ pub trait ActionContext {
fn simple_selection(&mut self, point: Point, side: Side);
fn semantic_selection(&mut self, point: Point);
fn line_selection(&mut self, point: Point);
+ fn selection_is_empty(&self) -> bool;
fn mouse_mut(&mut self) -> &mut Mouse;
fn mouse(&self) -> &Mouse;
fn mouse_coords(&self) -> Option<Point>;
@@ -74,6 +75,7 @@ pub trait ActionContext {
fn scroll(&mut self, scroll: Scroll);
fn clear_history(&mut self);
fn hide_window(&mut self);
+ fn url(&self, _: Point<usize>) -> Option<String>;
}
/// Describes a state and action to take in that state
@@ -326,16 +328,25 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
let size_info = self.ctx.size_info();
let point = size_info.pixels_to_coords(x, y);
+ let cell_side = self.get_mouse_side();
+ let prev_side = mem::replace(&mut self.ctx.mouse_mut().cell_side, cell_side);
let prev_line = mem::replace(&mut self.ctx.mouse_mut().line, point.line);
let prev_col = mem::replace(&mut self.ctx.mouse_mut().column, point.col);
let motion_mode = TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG;
let report_mode = TermMode::MOUSE_REPORT_CLICK | motion_mode;
+ // Don't launch URLs if mouse has moved
+ if prev_line != self.ctx.mouse().line
+ || prev_col != self.ctx.mouse().column
+ || prev_side != cell_side
+ {
+ self.ctx.mouse_mut().block_url_launcher = true;
+ }
+
if self.ctx.mouse().left_button_state == ElementState::Pressed &&
( modifiers.shift || !self.ctx.terminal_mode().intersects(report_mode))
{
- let cell_side = self.get_mouse_side();
self.ctx.update_selection(Point {
line: point.line,
col: point.col
@@ -357,6 +368,26 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
}
}
+ fn get_mouse_side(&self) -> Side {
+ let size_info = self.ctx.size_info();
+ let x = self.ctx.mouse().x;
+
+ let cell_x = x.saturating_sub(size_info.padding_x as usize) % size_info.cell_width as usize;
+ let half_cell_width = (size_info.cell_width / 2.0) as usize;
+
+ let additional_padding = (size_info.width - size_info.padding_x * 2.) % size_info.cell_width;
+ let end_of_grid = size_info.width - size_info.padding_x - additional_padding;
+
+ if cell_x > half_cell_width
+ // Edge case when mouse leaves the window
+ || x as f32 >= end_of_grid
+ {
+ Side::Right
+ } else {
+ Side::Left
+ }
+ }
+
pub fn normal_mouse_report(&mut self, button: u8) {
let (line, column) = (self.ctx.mouse().line, self.ctx.mouse().column);
@@ -427,19 +458,24 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state {
ClickState::Click if elapsed < self.mouse_config.double_click.threshold => {
+ self.ctx.mouse_mut().block_url_launcher = true;
self.on_mouse_double_click();
ClickState::DoubleClick
},
ClickState::DoubleClick if elapsed < self.mouse_config.triple_click.threshold => {
+ self.ctx.mouse_mut().block_url_launcher = true;
self.on_mouse_triple_click();
ClickState::TripleClick
},
_ => {
+ // Don't launch URLs if this click cleared the selection
+ self.ctx.mouse_mut().block_url_launcher = !self.ctx.selection_is_empty();
+
self.ctx.clear_selection();
// Start new empty selection
if let Some(point) = self.ctx.mouse_coords() {
- let side = self.get_mouse_side();
+ let side = self.ctx.mouse().cell_side;
self.ctx.simple_selection(point, side);
}
@@ -460,26 +496,6 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
};
}
- fn get_mouse_side(&self) -> Side {
- let size_info = self.ctx.size_info();
- let x = self.ctx.mouse().x;
-
- let cell_x = x.saturating_sub(size_info.padding_x as usize) % size_info.cell_width as usize;
- let half_cell_width = (size_info.cell_width / 2.0) as usize;
-
- let additional_padding = (size_info.width - size_info.padding_x * 2.) % size_info.cell_width;
- let end_of_grid = size_info.width - size_info.padding_x - additional_padding;
-
- if cell_x > half_cell_width
- // Edge case when mouse leaves the window
- || x as f32 >= end_of_grid
- {
- Side::Right
- } else {
- Side::Left
- }
- }
-
pub fn on_mouse_release(&mut self, button: MouseButton, modifiers: ModifiersState) {
let report_modes = TermMode::MOUSE_REPORT_CLICK | TermMode::MOUSE_DRAG | TermMode::MOUSE_MOTION;
if !modifiers.shift && self.ctx.terminal_mode().intersects(report_modes)
@@ -492,6 +508,8 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
MouseButton::Other(_) => (),
};
return;
+ } else {
+ self.launch_url(modifiers);
}
if self.save_to_clipboard {
@@ -500,6 +518,27 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
self.ctx.copy_selection(ClipboardBuffer::Selection);
}
+ // Spawn URL launcher when clicking on URLs
+ fn launch_url(&self, modifiers: ModifiersState) -> Option<()> {
+ if modifiers != self.mouse_config.url.modifiers || self.ctx.mouse().block_url_launcher {
+ return None;
+ }
+
+ let point = self.ctx.mouse_coords()?;
+ let text = self.ctx.url(point.into())?;
+
+ let launcher = self.mouse_config.url.launcher.as_ref()?;
+ let mut args = launcher.args().to_vec();
+ args.push(text);
+
+ match Command::new(launcher.program()).args(&args).spawn() {
+ Ok(_) => debug!("Launched: {} {:?}", launcher.program(), args),
+ Err(_) => warn!("Unable to launch: {} {:?}", launcher.program(), args),
+ }
+
+ Some(())
+ }
+
pub fn on_mouse_wheel(&mut self, delta: MouseScrollDelta, phase: TouchPhase, modifiers: ModifiersState) {
match delta {
MouseScrollDelta::LineDelta(_columns, lines) => {
@@ -777,6 +816,10 @@ mod tests {
self.last_action = MultiClick::TripleClick;
}
+ fn selection_is_empty(&self) -> bool {
+ true
+ }
+
fn scroll(&mut self, scroll: Scroll) {
self.terminal.scroll_display(scroll);
}
@@ -795,6 +838,10 @@ mod tests {
self.mouse
}
+ fn url(&self, _: Point<usize>) -> Option<String> {
+ None
+ }
+
fn received_count(&mut self) -> &mut usize {
&mut self.received_count
}
@@ -863,11 +910,12 @@ mod tests {
threshold: Duration::from_millis(1000),
},
faux_scrollback_lines: None,
+ url: Default::default(),
},
scrolling_config: &config::Scrolling::default(),
key_bindings: &config.key_bindings()[..],
mouse_bindings: &config.mouse_bindings()[..],
- save_to_clipboard: config.selection().save_to_clipboard
+ save_to_clipboard: config.selection().save_to_clipboard,
};
if let Event::WindowEvent { event: WindowEvent::MouseInput { state, button, modifiers, .. }, .. } = $input {
diff --git a/src/lib.rs b/src/lib.rs
index d0e48c3d..810e9a2e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -65,6 +65,7 @@ extern crate vte;
extern crate xdg;
extern crate base64;
extern crate terminfo;
+extern crate url;
#[macro_use]
pub mod macros;
diff --git a/src/selection.rs b/src/selection.rs
index 932a61ee..8eb08d70 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
@@ -151,7 +141,7 @@ impl Selection {
pub fn to_span<G>(&self, grid: &G, alt_screen: bool) -> Option<Span>
where
- G: SemanticSearch + Dimensions,
+ G: Search + Dimensions,
{
match *self {
Selection::Simple { ref region } => {
@@ -166,12 +156,24 @@ impl Selection {
}
}
+ pub fn is_empty(&self) -> bool
+ {
+ match *self {
+ Selection::Simple { ref region } => {
+ region.start == region.end && region.start.side == region.end.side
+ },
+ Selection::Semantic { .. } | Selection::Lines { .. } => {
+ false
+ },
+ }
+ }
+
fn span_semantic<G>(
grid: &G,
region: &Range<Point<isize>>,
alt_screen: bool,
) -> Option<Span>
- where G: SemanticSearch + Dimensions
+ where G: Search + Dimensions
{
let cols = grid.dimensions().col;
let lines = grid.dimensions().line.0 as isize;
@@ -448,9 +450,10 @@ mod test {
}
}
- impl super::SemanticSearch for Dimensions {
+ impl super::Search for Dimensions {
fn semantic_search_left(&self, point: Point<usize>) -> Point<usize> { point }
fn semantic_search_right(&self, point: Point<usize>) -> Point<usize> { point }
+ fn url_search(&self, _: Point<usize>) -> Option<String> { None }
}
/// Test case of single cell selection
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"))]