diff --git a/CHANGELOG.md b/CHANGELOG.md index b6283e41..d405d9ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `save_to_clipboard` configuration option for copying selected text to the system clipboard - New terminfo entry, `alacritty-direct`, that advertises 24-bit color support - Add support for CSI sequences Cursor Next Line (`\e[nE`) and Cursor Previous Line (`\e[nF`) +- When `mouse.url_launcher` is set, clicking on URLs will now open them with the specified program ### Changed diff --git a/Cargo.lock b/Cargo.lock index 7a524fca..9ac0cd83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,7 @@ dependencies = [ "static_assertions 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "terminfo 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "vte 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "x11-dl 2.18.3 (registry+https://github.com/rust-lang/crates.io-index)", "xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -479,6 +480,16 @@ dependencies = [ "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "inotify" version = "0.6.1" @@ -610,6 +621,11 @@ dependencies = [ "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "memchr" version = "2.1.0" @@ -1236,6 +1252,19 @@ name = "ucd-util" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-width" version = "0.1.5" @@ -1254,6 +1283,16 @@ dependencies = [ "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "url" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "utf8-ranges" version = "1.0.1" @@ -1511,6 +1550,7 @@ dependencies = [ "checksum gleam 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0d41e7ac812597988fdae31c9baec3c6d35cadb8ad9ab88a9bf9c0f119ed66c2" "checksum glutin 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "42fb2de780307bd2bedbe013bc585659a683e7c6307d0baa878aec3da9250fc1" "checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e" +"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum inotify 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40b54539f3910d6f84fbf9a643efd6e3aa6e4f001426c0329576128255994718" "checksum inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" @@ -1528,6 +1568,7 @@ dependencies = [ "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fcce5fa49cc693c312001daf1d13411c4a5283796bac1084299ea3e567113f" "checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum memchr 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4b3629fe9fdbff6daa6c33b90f7c08355c1aca05a3d01fa8063b822fcf185f3b" "checksum memmap 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e2ffa2c986de11a9df78620c01eeaaf27d94d3ff02bf81bfcca953102dd0c6ff" "checksum mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)" = "71646331f2619b1026cc302f87a2b8b648d5c6dd6937846a16cc8ce0f347f432" @@ -1600,9 +1641,12 @@ dependencies = [ "checksum tokio-io 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8d6cc2de7725863c86ac71b0b9068476fec50834f055a243558ef1655bbd34cb" "checksum tokio-reactor 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4bfbaf9f260635649ec26b6fb4aded03887295ffcd999f6e43fd2c4758f758ea" "checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6a0180bc61fc5a987082bfa111f4cc95c4caff7f9799f3e46df09163a937aa25" "checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +"checksum url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a321979c09843d272956e73700d12c4e7d3d92b2ee112b31548aef0d4efc5a6" "checksum utf8-ranges 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd70f467df6810094968e2fce0ee1bd0e87157aceb026a8c083bcf5e25b9efe4" "checksum utf8parse 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a15ea87f3194a3a454c78d79082b4f5e85f6956ddb6cb86bbfbe4892aa3c0323" "checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d" diff --git a/Cargo.toml b/Cargo.toml index 6c54c0ac..82f805f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ env_logger = "0.5" base64 = "0.9.0" static_assertions = "0.2.5" terminfo = "0.6.1" +url = "1.7.1" [target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os="dragonfly", target_os="openbsd"))'.dependencies] x11-dl = "2" diff --git a/alacritty.yml b/alacritty.yml index 91bff074..a0765b12 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -259,6 +259,15 @@ 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: + # program: /usr/bin/firefox + # args: + # - --new-tab + selection: semantic_escape_chars: ",│`|:\"' ()[]{}<>" diff --git a/alacritty_macos.yml b/alacritty_macos.yml index 8ce0181a..cf102764 100644 --- a/alacritty_macos.yml +++ b/alacritty_macos.yml @@ -257,6 +257,15 @@ 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: + # program: /usr/bin/firefox + # args: + # - --new-tab + selection: semantic_escape_chars: ",│`|:\"' ()[]{}<>" diff --git a/src/config.rs b/src/config.rs index 0f250522..58bb977c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -88,6 +88,10 @@ pub struct Mouse { #[serde(default, deserialize_with = "failure_default")] pub triple_click: ClickHandler, + // Program for opening links + #[serde(default, deserialize_with = "failure_default")] + pub url_launcher: Option, + // TODO: DEPRECATED #[serde(default)] pub faux_scrollback_lines: Option, @@ -102,6 +106,7 @@ impl Default for Mouse { triple_click: ClickHandler { threshold: Duration::from_millis(300), }, + url_launcher: None, faux_scrollback_lines: None, } } @@ -733,9 +738,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, @@ -744,6 +749,22 @@ enum CommandWrapper { }, } +impl CommandWrapper { + pub fn program(&self) -> &String { + match *self { + CommandWrapper::Just(ref program) => program, + CommandWrapper::WithArgs { ref program, .. } => program, + } + } + + pub fn args(&self) -> &[String] { + match *self { + CommandWrapper::Just(_) => &[], + CommandWrapper::WithArgs { ref args, .. } => &args, + } + } +} + use ::term::{mode, TermMode}; struct ModeWrapper { diff --git a/src/event.rs b/src/event.rs index 9db0680d..9d29f2ed 100644 --- a/src/event.rs +++ b/src/event.rs @@ -4,11 +4,13 @@ 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; @@ -19,7 +21,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, Cell}; use util::limit; use util::fmt::Red; use window::Window; @@ -104,6 +106,36 @@ 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 iterb = grid.iter_from(point); + + // Put all characters until separators into a String + let url_char = |cell: &&Cell| { + cell.c != ' ' && cell.c != '\'' && cell.c != '"' + }; + + let mut buf = String::new(); + + iterb.rev().take_while(url_char).for_each(|cell| buf.push(cell.c)); + buf = buf.chars().rev().collect(); + iterf.take_while(url_char).for_each(|cell| buf.push(cell.c)); + + // Check if string is valid url + match Url::parse(&buf) { + Ok(_) => Some(buf), + Err(_) => None, + } + } + 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 +228,7 @@ pub struct Mouse { pub column: Column, pub cell_side: Side, pub lines_scrolled: f32, + pub last_press_pos: (usize, usize), } impl Default for Mouse { @@ -213,6 +246,7 @@ impl Default for Mouse { column: Column(0), cell_side: Side::Left, lines_scrolled: 0.0, + last_press_pos: (0, 0), } } } diff --git a/src/grid/mod.rs b/src/grid/mod.rs index 9e15bd02..3d39f0ac 100644 --- a/src/grid/mod.rs +++ b/src/grid/mod.rs @@ -31,11 +31,6 @@ use self::storage::Storage; const MIN_INIT_SIZE: usize = 1_000; -/// Bidirection iterator -pub trait BidirectionalIterator: Iterator { - fn prev(&mut self) -> Option; -} - /// An item in the grid along with its Line and Column. pub struct Indexed { pub inner: T, @@ -474,8 +469,8 @@ impl<'a, T> Iterator for GridIterator<'a, T> { } } -impl<'a, T> BidirectionalIterator for GridIterator<'a, T> { - fn prev(&mut self) -> Option { +impl<'a, T> DoubleEndedIterator for GridIterator<'a, T> { + fn next_back(&mut self) -> Option { let num_cols = self.grid.num_cols(); match self.cur { diff --git a/src/input.rs b/src/input.rs index cc3f13df..28681341 100644 --- a/src/input.rs +++ b/src/input.rs @@ -73,6 +73,7 @@ pub trait ActionContext { fn scroll(&mut self, scroll: Scroll); fn clear_history(&mut self); fn hide_window(&mut self); + fn url(&self, _: Point) -> Option; } /// Describes a state and action to take in that state @@ -415,6 +416,8 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> { let elapsed = self.ctx.mouse().last_click_timestamp.elapsed(); self.ctx.mouse_mut().last_click_timestamp = now; + self.ctx.mouse_mut().last_press_pos = (self.ctx.mouse().x, self.ctx.mouse().y); + self.ctx.mouse_mut().click_state = match self.ctx.mouse().click_state { ClickState::Click if elapsed < self.mouse_config.double_click.threshold => { self.on_mouse_double_click(); @@ -482,6 +485,24 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> { MouseButton::Other(_) => (), }; return; + } else { + // Spawn URL launcher when clicking on URLs + let moved = self.ctx.mouse().last_press_pos != (self.ctx.mouse().x, self.ctx.mouse().y); + let ref url_launcher = self.mouse_config.url_launcher; + match (self.ctx.mouse_coords(), url_launcher, moved) { + (Some(point), Some(launcher), false) => { + if let Some(text) = self.ctx.url(Point::new(point.line.0, point.col)) { + let mut args = launcher.args().to_vec(); + args.push(text); + debug!("Launching: {} {:?}", launcher.program(), args); + Command::new(launcher.program()) + .args(&args) + .spawn() + .expect("url launcher error"); + } + } + _ => (), + } } if self.save_to_clipboard { diff --git a/src/lib.rs b/src/lib.rs index fcc55799..d7c437a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,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/term/mod.rs b/src/term/mod.rs index 9be8b96b..ce1d3a5b 100644 --- a/src/term/mod.rs +++ b/src/term/mod.rs @@ -23,7 +23,7 @@ use unicode_width::UnicodeWidthChar; use font::{self, Size}; use ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle}; -use grid::{BidirectionalIterator, Grid, Indexed, IndexRegion, DisplayIter, Scroll, ViewportPosition}; +use grid::{Grid, Indexed, IndexRegion, DisplayIter, Scroll, ViewportPosition}; use index::{self, Point, Column, Line, IndexRange, Contains, RangeInclusive, Linear}; use selection::{self, Selection, Locations}; use config::{Config, VisualBellAnimation}; @@ -44,7 +44,7 @@ impl selection::SemanticSearch for Term { let mut iter = self.grid.iter_from(point); let last_col = self.grid.num_cols() - Column(1); - while let Some(cell) = iter.prev() { + while let Some(cell) = iter.next_back() { if self.semantic_escape_chars.contains(cell.c) { break; }