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/input.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/input.rs')
-rw-r--r-- | src/input.rs | 94 |
1 files changed, 71 insertions, 23 deletions
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 { |