diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/config.rs | 41 | ||||
-rw-r--r-- | src/event.rs | 12 | ||||
-rw-r--r-- | src/index.rs | 6 | ||||
-rw-r--r-- | src/input.rs | 94 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/selection.rs | 31 | ||||
-rw-r--r-- | src/term/mod.rs | 159 |
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 { @@ -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"))] |