mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-25 14:05:41 -05:00
Add URL hover highlighting
This changes the cursor whenever it moves to a cell which contains part of a URL. When a URL is hovered over, all characters that are recognized as part of the URL will be underlined and the mouse cursor shape will be changed. After the cursor leaves the URL, the previous hover state is restored. This also changes the behavior when clicking an illegal character right in front of a URL. Previously this would still launch the URL, but strip the illegal character. Now these clicks are ignored to make sure there's no mismatch between underline and legal URL click positions
This commit is contained in:
parent
eb7a1ea803
commit
a672f7d553
12 changed files with 312 additions and 146 deletions
|
@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- New configuration field `window.position` allows specifying the starting position
|
||||
- Added the ability to change the selection color
|
||||
- Text will reflow instead of truncating when resizing Alacritty
|
||||
- Underline text and change cursor when hovering over URLs with required modifiers pressed
|
||||
|
||||
### Changed
|
||||
|
||||
- Clicking on non-alphabetical characters in front of URLs will no longer open them
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -19,9 +19,9 @@ use std::str;
|
|||
|
||||
use vte;
|
||||
use base64;
|
||||
use glutin::MouseCursor;
|
||||
use crate::index::{Column, Line, Contains};
|
||||
|
||||
use crate::MouseCursor;
|
||||
use crate::term::color::Rgb;
|
||||
|
||||
// Parse color arguments
|
||||
|
|
11
src/event.rs
11
src/event.rs
|
@ -10,7 +10,7 @@ use std::env;
|
|||
|
||||
use serde_json as json;
|
||||
use parking_lot::MutexGuard;
|
||||
use glutin::{self, ModifiersState, Event, ElementState, MouseButton};
|
||||
use glutin::{self, ModifiersState, Event, ElementState, MouseButton, MouseCursor};
|
||||
use copypasta::{Clipboard, Load, Store, Buffer as ClipboardBuffer};
|
||||
use glutin::dpi::PhysicalSize;
|
||||
|
||||
|
@ -222,6 +222,13 @@ pub enum ClickState {
|
|||
TripleClick,
|
||||
}
|
||||
|
||||
/// Temporary save state for restoring mouse cursor and underline after unhovering a URL.
|
||||
pub struct UrlHoverSaveState {
|
||||
pub mouse_cursor: MouseCursor,
|
||||
pub underlined: Vec<bool>,
|
||||
pub start: Point<usize>,
|
||||
}
|
||||
|
||||
/// State of the mouse
|
||||
pub struct Mouse {
|
||||
pub x: usize,
|
||||
|
@ -238,6 +245,7 @@ pub struct Mouse {
|
|||
pub lines_scrolled: f32,
|
||||
pub block_url_launcher: bool,
|
||||
pub last_button: MouseButton,
|
||||
pub url_hover_save: Option<UrlHoverSaveState>,
|
||||
}
|
||||
|
||||
impl Default for Mouse {
|
||||
|
@ -257,6 +265,7 @@ impl Default for Mouse {
|
|||
lines_scrolled: 0.0,
|
||||
block_url_launcher: false,
|
||||
last_button: MouseButton::Other(0),
|
||||
url_hover_save: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
use std::cmp::{min, max, Ordering};
|
||||
use std::ops::{Deref, Range, Index, IndexMut, RangeTo, RangeFrom, RangeFull};
|
||||
|
||||
use crate::index::{self, Point, Line, Column, IndexRange};
|
||||
use crate::index::{self, Point, Line, Column, IndexRange, PointIterator};
|
||||
use crate::selection::Selection;
|
||||
|
||||
mod row;
|
||||
|
@ -105,14 +105,6 @@ pub struct Grid<T> {
|
|||
max_scroll_limit: usize,
|
||||
}
|
||||
|
||||
pub struct GridIterator<'a, T> {
|
||||
/// Immutable grid reference
|
||||
grid: &'a Grid<T>,
|
||||
|
||||
/// Current position of the iterator within the grid.
|
||||
pub cur: Point<usize>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Scroll {
|
||||
Lines(isize),
|
||||
|
@ -587,7 +579,7 @@ impl<T> Grid<T> {
|
|||
pub fn iter_from(&self, point: Point<usize>) -> GridIterator<'_, T> {
|
||||
GridIterator {
|
||||
grid: self,
|
||||
cur: point,
|
||||
point_iter: point.iter(self.num_cols() - 1, self.len() - 1),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -602,43 +594,28 @@ impl<T> Grid<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct GridIterator<'a, T> {
|
||||
point_iter: PointIterator<usize>,
|
||||
grid: &'a Grid<T>,
|
||||
}
|
||||
|
||||
impl<'a, T> GridIterator<'a, T> {
|
||||
pub fn cur(&self) -> Point<usize> {
|
||||
self.point_iter.cur
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for GridIterator<'a, T> {
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let last_col = self.grid.num_cols() - Column(1);
|
||||
match self.cur {
|
||||
Point { line, col } if line == 0 && col == last_col => None,
|
||||
Point { col, .. } if
|
||||
(col == last_col) => {
|
||||
self.cur.line -= 1;
|
||||
self.cur.col = Column(0);
|
||||
Some(&self.grid[self.cur.line][self.cur.col])
|
||||
},
|
||||
_ => {
|
||||
self.cur.col += Column(1);
|
||||
Some(&self.grid[self.cur.line][self.cur.col])
|
||||
}
|
||||
}
|
||||
self.point_iter.next().map(|p| &self.grid[p.line][p.col])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> BidirectionalIterator for GridIterator<'a, T> {
|
||||
fn prev(&mut self) -> Option<Self::Item> {
|
||||
let num_cols = self.grid.num_cols();
|
||||
|
||||
match self.cur {
|
||||
Point { line, col: Column(0) } if line == self.grid.len() - 1 => None,
|
||||
Point { col: Column(0), .. } => {
|
||||
self.cur.line += 1;
|
||||
self.cur.col = num_cols - Column(1);
|
||||
Some(&self.grid[self.cur.line][self.cur.col])
|
||||
},
|
||||
_ => {
|
||||
self.cur.col -= Column(1);
|
||||
Some(&self.grid[self.cur.line][self.cur.col])
|
||||
}
|
||||
}
|
||||
self.point_iter.prev().map(|p| &self.grid[p.line][p.col])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -669,6 +646,13 @@ impl<T> IndexMut<index::Line> for Grid<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> IndexMut<usize> for Grid<T> {
|
||||
#[inline]
|
||||
fn index_mut(&mut self, index: usize) -> &mut Row<T> {
|
||||
&mut self.raw[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'point, T> Index<&'point Point> for Grid<T> {
|
||||
type Output = T;
|
||||
|
||||
|
|
|
@ -112,8 +112,8 @@ fn test_iter() {
|
|||
|
||||
assert_eq!(None, iter.prev());
|
||||
assert_eq!(Some(&1), iter.next());
|
||||
assert_eq!(Column(1), iter.cur.col);
|
||||
assert_eq!(4, iter.cur.line);
|
||||
assert_eq!(Column(1), iter.cur().col);
|
||||
assert_eq!(4, iter.cur().line);
|
||||
|
||||
assert_eq!(Some(&2), iter.next());
|
||||
assert_eq!(Some(&3), iter.next());
|
||||
|
@ -121,12 +121,12 @@ fn test_iter() {
|
|||
|
||||
// test linewrapping
|
||||
assert_eq!(Some(&5), iter.next());
|
||||
assert_eq!(Column(0), iter.cur.col);
|
||||
assert_eq!(3, iter.cur.line);
|
||||
assert_eq!(Column(0), iter.cur().col);
|
||||
assert_eq!(3, iter.cur().line);
|
||||
|
||||
assert_eq!(Some(&4), iter.prev());
|
||||
assert_eq!(Column(4), iter.cur.col);
|
||||
assert_eq!(4, iter.cur.line);
|
||||
assert_eq!(Column(4), iter.cur().col);
|
||||
assert_eq!(4, iter.cur().line);
|
||||
|
||||
|
||||
// test that iter ends at end of grid
|
||||
|
|
77
src/index.rs
77
src/index.rs
|
@ -17,7 +17,9 @@
|
|||
/// Indexing types and implementations for Grid and Line
|
||||
use std::cmp::{Ord, Ordering};
|
||||
use std::fmt;
|
||||
use std::ops::{self, Deref, Add, Range, RangeInclusive};
|
||||
use std::ops::{self, Deref, Range, RangeInclusive, Add, Sub, AddAssign, SubAssign};
|
||||
|
||||
use crate::grid::BidirectionalIterator;
|
||||
|
||||
/// The side of a cell
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
|
@ -70,6 +72,67 @@ impl From<Point> for Point<usize> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Point<T>
|
||||
where
|
||||
T: Copy + Default + SubAssign<usize> + PartialEq,
|
||||
{
|
||||
pub fn iter(&self, last_col: Column, last_line: T) -> PointIterator<T> {
|
||||
PointIterator {
|
||||
cur: *self,
|
||||
last_col,
|
||||
last_line,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PointIterator<T> {
|
||||
pub cur: Point<T>,
|
||||
last_col: Column,
|
||||
last_line: T,
|
||||
}
|
||||
|
||||
impl<T> Iterator for PointIterator<T>
|
||||
where
|
||||
T: Copy + Default + SubAssign<usize> + PartialEq,
|
||||
{
|
||||
type Item = Point<T>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.cur {
|
||||
Point { line, col } if line == Default::default() && col == self.last_col => None,
|
||||
Point { col, .. } if col == self.last_col => {
|
||||
self.cur.line -= 1;
|
||||
self.cur.col = Column(0);
|
||||
Some(self.cur)
|
||||
},
|
||||
_ => {
|
||||
self.cur.col += Column(1);
|
||||
Some(self.cur)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BidirectionalIterator for PointIterator<T>
|
||||
where
|
||||
T: Copy + Default + AddAssign<usize> + SubAssign<usize> + PartialEq,
|
||||
{
|
||||
fn prev(&mut self) -> Option<Self::Item> {
|
||||
match self.cur {
|
||||
Point { line, col: Column(0) } if line == self.last_line => None,
|
||||
Point { col: Column(0), .. } => {
|
||||
self.cur.line += 1;
|
||||
self.cur.col = self.last_col;
|
||||
Some(self.cur)
|
||||
},
|
||||
_ => {
|
||||
self.cur.col -= Column(1);
|
||||
Some(self.cur)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A line
|
||||
///
|
||||
/// Newtype to avoid passing values incorrectly
|
||||
|
@ -312,28 +375,28 @@ macro_rules! ops {
|
|||
}
|
||||
}
|
||||
}
|
||||
impl ops::AddAssign<$ty> for $ty {
|
||||
impl AddAssign<$ty> for $ty {
|
||||
#[inline]
|
||||
fn add_assign(&mut self, rhs: $ty) {
|
||||
self.0 += rhs.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::SubAssign<$ty> for $ty {
|
||||
impl SubAssign<$ty> for $ty {
|
||||
#[inline]
|
||||
fn sub_assign(&mut self, rhs: $ty) {
|
||||
self.0 -= rhs.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign<usize> for $ty {
|
||||
impl AddAssign<usize> for $ty {
|
||||
#[inline]
|
||||
fn add_assign(&mut self, rhs: usize) {
|
||||
self.0 += rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::SubAssign<usize> for $ty {
|
||||
impl SubAssign<usize> for $ty {
|
||||
#[inline]
|
||||
fn sub_assign(&mut self, rhs: usize) {
|
||||
self.0 -= rhs
|
||||
|
@ -347,7 +410,7 @@ macro_rules! ops {
|
|||
}
|
||||
}
|
||||
|
||||
impl ops::Add<usize> for $ty {
|
||||
impl Add<usize> for $ty {
|
||||
type Output = $ty;
|
||||
|
||||
#[inline]
|
||||
|
@ -356,7 +419,7 @@ macro_rules! ops {
|
|||
}
|
||||
}
|
||||
|
||||
impl ops::Sub<usize> for $ty {
|
||||
impl Sub<usize> for $ty {
|
||||
type Output = $ty;
|
||||
|
||||
#[inline]
|
||||
|
|
108
src/input.rs
108
src/input.rs
|
@ -21,20 +21,26 @@
|
|||
use std::borrow::Cow;
|
||||
use std::mem;
|
||||
use std::time::Instant;
|
||||
use std::iter::once;
|
||||
|
||||
use copypasta::{Clipboard, Load, Buffer as ClipboardBuffer};
|
||||
use glutin::{ElementState, MouseButton, TouchPhase, MouseScrollDelta, ModifiersState, KeyboardInput};
|
||||
use glutin::{
|
||||
ElementState, KeyboardInput, ModifiersState, MouseButton, MouseCursor, MouseScrollDelta,
|
||||
TouchPhase,
|
||||
};
|
||||
|
||||
use crate::config::{self, Key};
|
||||
use crate::grid::Scroll;
|
||||
use crate::event::{ClickState, Mouse};
|
||||
use crate::event::{ClickState, Mouse, UrlHoverSaveState};
|
||||
use crate::index::{Line, Column, Side, Point};
|
||||
use crate::term::{Term, SizeInfo, Search};
|
||||
use crate::term::mode::TermMode;
|
||||
use crate::term::cell::Flags;
|
||||
use crate::util::fmt::Red;
|
||||
use crate::util::start_daemon;
|
||||
use crate::message_bar::{self, Message};
|
||||
use crate::ansi::{Handler, ClearMode};
|
||||
use crate::url::Url;
|
||||
|
||||
pub const FONT_SIZE_STEP: f32 = 0.5;
|
||||
|
||||
|
@ -385,19 +391,23 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
|
|||
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
|
||||
let mouse_moved = prev_line != self.ctx.mouse().line
|
||||
|| prev_col != self.ctx.mouse().column
|
||||
|| prev_side != cell_side
|
||||
{
|
||||
|| prev_side != cell_side;
|
||||
|
||||
// Don't launch URLs if mouse has moved
|
||||
if mouse_moved {
|
||||
self.ctx.mouse_mut().block_url_launcher = true;
|
||||
}
|
||||
|
||||
// Ignore motions over the message bar
|
||||
if self.message_at_point(Some(point)).is_some() {
|
||||
// Only report motions when cell changed and mouse is not over the message bar
|
||||
if self.message_at_point(Some(point)).is_some() || !mouse_moved {
|
||||
return;
|
||||
}
|
||||
|
||||
// Underline URLs and change cursor on hover
|
||||
self.update_url_highlight(point, modifiers);
|
||||
|
||||
if self.ctx.mouse().left_button_state == ElementState::Pressed
|
||||
&& (modifiers.shift || !self.ctx.terminal().mode().intersects(report_mode))
|
||||
{
|
||||
|
@ -409,8 +419,6 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
|
|||
cell_side,
|
||||
);
|
||||
} else if self.ctx.terminal().mode().intersects(motion_mode)
|
||||
// Only report motion when changing cells
|
||||
&& (prev_line != self.ctx.mouse().line || prev_col != self.ctx.mouse().column)
|
||||
&& size_info.contains_point(x, y, false)
|
||||
{
|
||||
if self.ctx.mouse().left_button_state == ElementState::Pressed {
|
||||
|
@ -425,6 +433,84 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Underline URLs and change the mouse cursor when URL hover state changes.
|
||||
fn update_url_highlight(&mut self, point: Point, modifiers: ModifiersState) {
|
||||
let mouse_mode =
|
||||
TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG | TermMode::MOUSE_REPORT_CLICK;
|
||||
|
||||
// Only show URLs as launchable when all required modifiers are pressed
|
||||
let url = if self.mouse_config.url.modifiers.relaxed_eq(modifiers)
|
||||
&& (!self.ctx.terminal().mode().contains(TermMode::ALT_SCREEN) || modifiers.shift)
|
||||
{
|
||||
self.ctx.terminal().url_search(point.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(Url { text, origin }) = url {
|
||||
let mouse_cursor = if self.ctx.terminal().mode().intersects(mouse_mode) {
|
||||
MouseCursor::Default
|
||||
} else {
|
||||
MouseCursor::Text
|
||||
};
|
||||
|
||||
let cols = self.ctx.size_info().cols().0;
|
||||
let last_line = self.ctx.size_info().lines().0 - 1;
|
||||
|
||||
// Calculate the URL's start position
|
||||
let col = (cols + point.col.0 - origin % cols) % cols;
|
||||
let line = last_line - point.line.0 + (origin + cols - point.col.0 - 1) / cols;
|
||||
let start = Point::new(line, Column(col));
|
||||
|
||||
// Update URLs only on change, so they don't all get marked as underlined
|
||||
if self.ctx.mouse().url_hover_save.as_ref().map(|hs| hs.start) == Some(start) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since the URL changed without reset, we need to clear the previous underline
|
||||
if let Some(hover_save) = self.ctx.mouse_mut().url_hover_save.take() {
|
||||
self.reset_underline(&hover_save);
|
||||
}
|
||||
|
||||
// Underline all cells and store their current underline state
|
||||
let mut underlined = Vec::with_capacity(text.len());
|
||||
let iter = once(start).chain(start.iter(Column(cols - 1), last_line));
|
||||
for point in iter.take(text.len()) {
|
||||
let cell = &mut self.ctx.terminal_mut().grid_mut()[point.line][point.col];
|
||||
underlined.push(cell.flags.contains(Flags::UNDERLINE));
|
||||
cell.flags.insert(Flags::UNDERLINE);
|
||||
}
|
||||
|
||||
// Save the higlight state for restoring it again
|
||||
self.ctx.mouse_mut().url_hover_save = Some(UrlHoverSaveState {
|
||||
mouse_cursor,
|
||||
underlined,
|
||||
start,
|
||||
});
|
||||
|
||||
self.ctx.terminal_mut().set_mouse_cursor(MouseCursor::Hand);
|
||||
self.ctx.terminal_mut().dirty = true;
|
||||
} else if let Some(hover_save) = self.ctx.mouse_mut().url_hover_save.take() {
|
||||
self.ctx.terminal_mut().set_mouse_cursor(hover_save.mouse_cursor);
|
||||
self.ctx.terminal_mut().dirty = true;
|
||||
self.reset_underline(&hover_save);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the underline state after unhovering a URL.
|
||||
fn reset_underline(&mut self, hover_save: &UrlHoverSaveState) {
|
||||
let last_col = self.ctx.size_info().cols() - 1;
|
||||
let last_line = self.ctx.size_info().lines().0 - 1;
|
||||
|
||||
let mut iter = once(hover_save.start).chain(hover_save.start.iter(last_col, last_line));
|
||||
for underlined in &hover_save.underlined {
|
||||
if let (Some(point), false) = (iter.next(), underlined) {
|
||||
let cell = &mut self.ctx.terminal_mut().grid_mut()[point.line][point.col];
|
||||
cell.flags.remove(Flags::UNDERLINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_mouse_side(&self) -> Side {
|
||||
let size_info = self.ctx.size_info();
|
||||
let x = self.ctx.mouse().x;
|
||||
|
@ -600,7 +686,7 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> {
|
|||
return None;
|
||||
}
|
||||
|
||||
let text = self.ctx.terminal().url_search(point.into())?;
|
||||
let text = self.ctx.terminal().url_search(point.into())?.text;
|
||||
|
||||
let launcher = self.mouse_config.url.launcher.as_ref()?;
|
||||
let mut args = launcher.args().to_vec();
|
||||
|
|
|
@ -52,13 +52,6 @@ mod url;
|
|||
pub use crate::grid::Grid;
|
||||
pub use crate::term::Term;
|
||||
|
||||
/// Facade around [winit's `MouseCursor`](glutin::MouseCursor)
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum MouseCursor {
|
||||
Arrow,
|
||||
Text,
|
||||
}
|
||||
|
||||
pub mod gl {
|
||||
#![allow(clippy::all)]
|
||||
include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs"));
|
||||
|
|
|
@ -433,6 +433,7 @@ impl Span {
|
|||
mod test {
|
||||
use crate::index::{Line, Column, Side, Point};
|
||||
use super::{Selection, Span, SpanType};
|
||||
use crate::url::Url;
|
||||
|
||||
struct Dimensions(Point);
|
||||
impl super::Dimensions for Dimensions {
|
||||
|
@ -453,7 +454,7 @@ mod test {
|
|||
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 }
|
||||
fn url_search(&self, _: Point<usize>) -> Option<Url> { None }
|
||||
}
|
||||
|
||||
/// Test case of single cell selection
|
||||
|
|
|
@ -20,6 +20,7 @@ use std::time::{Duration, Instant};
|
|||
|
||||
use arraydeque::ArrayDeque;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
use glutin::MouseCursor;
|
||||
|
||||
use font::{self, Size};
|
||||
use crate::ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle};
|
||||
|
@ -30,10 +31,9 @@ use crate::grid::{
|
|||
use crate::index::{self, Point, Column, Line, IndexRange, Contains, Linear};
|
||||
use crate::selection::{self, Selection, Locations};
|
||||
use crate::config::{Config, VisualBellAnimation};
|
||||
use crate::MouseCursor;
|
||||
use copypasta::{Clipboard, Load, Store};
|
||||
use crate::input::FONT_SIZE_STEP;
|
||||
use crate::url::UrlParser;
|
||||
use crate::url::{Url, UrlParser};
|
||||
use crate::message_bar::MessageBuffer;
|
||||
use crate::term::color::Rgb;
|
||||
use crate::term::cell::{LineLength, Cell};
|
||||
|
@ -54,7 +54,7 @@ pub trait Search {
|
|||
/// 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>;
|
||||
fn url_search(&self, _: Point<usize>) -> Option<Url>;
|
||||
}
|
||||
|
||||
impl Search for Term {
|
||||
|
@ -70,11 +70,11 @@ impl Search for Term {
|
|||
break;
|
||||
}
|
||||
|
||||
if iter.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) {
|
||||
if iter.cur().col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) {
|
||||
break; // cut off if on new line or hit escape char
|
||||
}
|
||||
|
||||
point = iter.cur;
|
||||
point = iter.cur();
|
||||
}
|
||||
|
||||
point
|
||||
|
@ -92,9 +92,9 @@ impl Search for Term {
|
|||
break;
|
||||
}
|
||||
|
||||
point = iter.cur;
|
||||
point = iter.cur();
|
||||
|
||||
if iter.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) {
|
||||
if point.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) {
|
||||
break; // cut off if on new line or hit escape char
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ impl Search for Term {
|
|||
point
|
||||
}
|
||||
|
||||
fn url_search(&self, mut point: Point<usize>) -> Option<String> {
|
||||
fn url_search(&self, mut point: Point<usize>) -> Option<Url> {
|
||||
// Switch first line from top to bottom
|
||||
point.line = self.grid.num_lines().0 - point.line - 1;
|
||||
|
||||
|
@ -1143,8 +1143,7 @@ impl Term {
|
|||
&self.grid
|
||||
}
|
||||
|
||||
// Mutable access for swapping out the grid during tests
|
||||
#[cfg(test)]
|
||||
/// Mutable access to the raw grid data structure
|
||||
pub fn grid_mut(&mut self) -> &mut Grid<Cell> {
|
||||
&mut self.grid
|
||||
}
|
||||
|
@ -2034,15 +2033,15 @@ impl ansi::Handler for Term {
|
|||
ansi::Mode::CursorKeys => self.mode.insert(mode::TermMode::APP_CURSOR),
|
||||
ansi::Mode::ReportMouseClicks => {
|
||||
self.mode.insert(mode::TermMode::MOUSE_REPORT_CLICK);
|
||||
self.set_mouse_cursor(MouseCursor::Arrow);
|
||||
self.set_mouse_cursor(MouseCursor::Default);
|
||||
},
|
||||
ansi::Mode::ReportCellMouseMotion => {
|
||||
self.mode.insert(mode::TermMode::MOUSE_DRAG);
|
||||
self.set_mouse_cursor(MouseCursor::Arrow);
|
||||
self.set_mouse_cursor(MouseCursor::Default);
|
||||
},
|
||||
ansi::Mode::ReportAllMouseMotion => {
|
||||
self.mode.insert(mode::TermMode::MOUSE_MOTION);
|
||||
self.set_mouse_cursor(MouseCursor::Arrow);
|
||||
self.set_mouse_cursor(MouseCursor::Default);
|
||||
},
|
||||
ansi::Mode::ReportFocusInOut => self.mode.insert(mode::TermMode::FOCUS_IN_OUT),
|
||||
ansi::Mode::BracketedPaste => self.mode.insert(mode::TermMode::BRACKETED_PASTE),
|
||||
|
|
137
src/url.rs
137
src/url.rs
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use url::Url;
|
||||
use url;
|
||||
|
||||
// See https://tools.ietf.org/html/rfc3987#page-13
|
||||
const URL_SEPARATOR_CHARS: [char; 10] = ['<', '>', '"', ' ', '{', '}', '|', '\\', '^', '`'];
|
||||
|
@ -21,21 +21,35 @@ const URL_SCHEMES: [&str; 8] = [
|
|||
"http", "https", "mailto", "news", "file", "git", "ssh", "ftp",
|
||||
];
|
||||
|
||||
// Parser for streaming inside-out detection of URLs.
|
||||
/// URL text and origin of the original click position.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Url {
|
||||
pub text: String,
|
||||
pub origin: usize,
|
||||
}
|
||||
|
||||
/// Parser for streaming inside-out detection of URLs.
|
||||
pub struct UrlParser {
|
||||
state: String,
|
||||
origin: usize,
|
||||
}
|
||||
|
||||
impl UrlParser {
|
||||
pub fn new() -> Self {
|
||||
UrlParser {
|
||||
state: String::new(),
|
||||
origin: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the parser one character to the left.
|
||||
pub fn advance_left(&mut self, c: char) -> bool {
|
||||
self.advance(c, 0)
|
||||
if self.advance(c, 0) {
|
||||
true
|
||||
} else {
|
||||
self.origin += 1;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the parser one character to the right.
|
||||
|
@ -44,7 +58,7 @@ impl UrlParser {
|
|||
}
|
||||
|
||||
/// Returns the URL if the parser has found any.
|
||||
pub fn url(mut self) -> Option<String> {
|
||||
pub fn url(mut self) -> Option<Url> {
|
||||
// Remove non-alphabetical characters before the scheme
|
||||
// https://tools.ietf.org/html/rfc3986#section-3.1
|
||||
if let Some(index) = self.state.find("://") {
|
||||
|
@ -57,6 +71,7 @@ impl UrlParser {
|
|||
match c {
|
||||
'a'...'z' | 'A'...'Z' => (),
|
||||
_ => {
|
||||
self.origin = self.origin.saturating_sub(byte_index + 1);
|
||||
self.state = self.state.split_off(byte_index + c.len_utf8());
|
||||
break;
|
||||
}
|
||||
|
@ -97,10 +112,13 @@ impl UrlParser {
|
|||
}
|
||||
|
||||
// Check if string is valid url
|
||||
match Url::parse(&self.state) {
|
||||
match url::Url::parse(&self.state) {
|
||||
Ok(url) => {
|
||||
if URL_SCHEMES.contains(&url.scheme()) {
|
||||
Some(self.state)
|
||||
if URL_SCHEMES.contains(&url.scheme()) && self.origin > 0 {
|
||||
Some(Url {
|
||||
text: self.state,
|
||||
origin: self.origin - 1,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -155,12 +173,10 @@ mod tests {
|
|||
term
|
||||
}
|
||||
|
||||
fn url_test(input: &str, expected: &str, click_index: usize) {
|
||||
fn url_test(input: &str, expected: &str) {
|
||||
let term = url_create_term(input);
|
||||
|
||||
let url = term.url_search(Point::new(0, Column(click_index)));
|
||||
|
||||
assert_eq!(url, Some(expected.into()));
|
||||
let url = term.url_search(Point::new(0, Column(15)));
|
||||
assert_eq!(url.map(|u| u.text), Some(expected.into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -168,72 +184,87 @@ mod tests {
|
|||
let term = url_create_term("no url here");
|
||||
let url = term.url_search(Point::new(0, Column(4)));
|
||||
assert_eq!(url, None);
|
||||
|
||||
let term = url_create_term(" https://example.org");
|
||||
let url = term.url_search(Point::new(0, Column(0)));
|
||||
assert_eq!(url, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_origin() {
|
||||
let term = url_create_term(" test https://example.org ");
|
||||
let url = term.url_search(Point::new(0, Column(10)));
|
||||
assert_eq!(url.map(|u| u.origin), Some(4));
|
||||
|
||||
let term = url_create_term("https://example.org");
|
||||
let url = term.url_search(Point::new(0, Column(0)));
|
||||
assert_eq!(url.map(|u| u.origin), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_matching_chars() {
|
||||
url_test("(https://example.org/test(ing))", "https://example.org/test(ing)", 5);
|
||||
url_test("https://example.org/test(ing)", "https://example.org/test(ing)", 5);
|
||||
url_test("((https://example.org))", "https://example.org", 5);
|
||||
url_test(")https://example.org(", "https://example.org", 5);
|
||||
url_test("https://example.org)", "https://example.org", 5);
|
||||
url_test("https://example.org(", "https://example.org", 5);
|
||||
url_test("(https://one.org/)(https://two.org/)", "https://one.org", 5);
|
||||
url_test("(https://example.org/test(ing))", "https://example.org/test(ing)");
|
||||
url_test("https://example.org/test(ing)", "https://example.org/test(ing)");
|
||||
url_test("((https://example.org))", "https://example.org");
|
||||
url_test(")https://example.org(", "https://example.org");
|
||||
url_test("https://example.org)", "https://example.org");
|
||||
url_test("https://example.org(", "https://example.org");
|
||||
url_test("(https://one.org/)(https://two.org/)", "https://one.org");
|
||||
|
||||
url_test("https://[2001:db8:a0b:12f0::1]:80", "https://[2001:db8:a0b:12f0::1]:80", 5);
|
||||
url_test("([(https://example.org/test(ing))])", "https://example.org/test(ing)", 5);
|
||||
url_test("https://example.org/]()", "https://example.org", 5);
|
||||
url_test("[https://example.org]", "https://example.org", 5);
|
||||
url_test("https://[2001:db8:a0b:12f0::1]:80", "https://[2001:db8:a0b:12f0::1]:80");
|
||||
url_test("([(https://example.org/test(ing))])", "https://example.org/test(ing)");
|
||||
url_test("https://example.org/]()", "https://example.org");
|
||||
url_test("[https://example.org]", "https://example.org");
|
||||
|
||||
url_test("'https://example.org/test'ing'''", "https://example.org/test'ing'", 5);
|
||||
url_test("https://example.org/test'ing'", "https://example.org/test'ing'", 5);
|
||||
url_test("'https://example.org'", "https://example.org", 5);
|
||||
url_test("'https://example.org", "https://example.org", 5);
|
||||
url_test("https://example.org'", "https://example.org", 5);
|
||||
url_test("'https://example.org/test'ing'''", "https://example.org/test'ing'");
|
||||
url_test("https://example.org/test'ing'", "https://example.org/test'ing'");
|
||||
url_test("'https://example.org'", "https://example.org");
|
||||
url_test("'https://example.org", "https://example.org");
|
||||
url_test("https://example.org'", "https://example.org");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_detect_end() {
|
||||
url_test("https://example.org/test\u{00}ing", "https://example.org/test", 5);
|
||||
url_test("https://example.org/test\u{1F}ing", "https://example.org/test", 5);
|
||||
url_test("https://example.org/test\u{7F}ing", "https://example.org/test", 5);
|
||||
url_test("https://example.org/test\u{9F}ing", "https://example.org/test", 5);
|
||||
url_test("https://example.org/test\ting", "https://example.org/test", 5);
|
||||
url_test("https://example.org/test ing", "https://example.org/test", 5);
|
||||
url_test("https://example.org/test\u{00}ing", "https://example.org/test");
|
||||
url_test("https://example.org/test\u{1F}ing", "https://example.org/test");
|
||||
url_test("https://example.org/test\u{7F}ing", "https://example.org/test");
|
||||
url_test("https://example.org/test\u{9F}ing", "https://example.org/test");
|
||||
url_test("https://example.org/test\ting", "https://example.org/test");
|
||||
url_test("https://example.org/test ing", "https://example.org/test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_remove_end_chars() {
|
||||
url_test("https://example.org/test?ing", "https://example.org/test?ing", 5);
|
||||
url_test("https://example.org.,;:)'!/?", "https://example.org", 5);
|
||||
url_test("https://example.org'.", "https://example.org", 5);
|
||||
url_test("https://example.org/test?ing", "https://example.org/test?ing");
|
||||
url_test("https://example.org.,;:)'!/?", "https://example.org");
|
||||
url_test("https://example.org'.", "https://example.org");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_remove_start_chars() {
|
||||
url_test("complicated:https://example.org", "https://example.org", 15);
|
||||
url_test("test.https://example.org", "https://example.org", 10);
|
||||
url_test(",https://example.org", "https://example.org", 5);
|
||||
url_test("\u{2502}https://example.org", "https://example.org", 5);
|
||||
url_test("complicated:https://example.org", "https://example.org");
|
||||
url_test("test.https://example.org", "https://example.org");
|
||||
url_test(",https://example.org", "https://example.org");
|
||||
url_test("\u{2502}https://example.org", "https://example.org");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_unicode() {
|
||||
url_test("https://xn--example-2b07f.org", "https://xn--example-2b07f.org", 5);
|
||||
url_test("https://example.org/\u{2008A}", "https://example.org/\u{2008A}", 5);
|
||||
url_test("https://example.org/\u{f17c}", "https://example.org/\u{f17c}", 5);
|
||||
url_test("https://üñîçøðé.com/ä", "https://üñîçøðé.com/ä", 5);
|
||||
url_test("https://xn--example-2b07f.org", "https://xn--example-2b07f.org");
|
||||
url_test("https://example.org/\u{2008A}", "https://example.org/\u{2008A}");
|
||||
url_test("https://example.org/\u{f17c}", "https://example.org/\u{f17c}");
|
||||
url_test("https://üñîçøðé.com/ä", "https://üñîçøðé.com/ä");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_schemes() {
|
||||
url_test("mailto://example.org", "mailto://example.org", 5);
|
||||
url_test("https://example.org", "https://example.org", 5);
|
||||
url_test("http://example.org", "http://example.org", 5);
|
||||
url_test("news://example.org", "news://example.org", 5);
|
||||
url_test("file://example.org", "file://example.org", 5);
|
||||
url_test("git://example.org", "git://example.org", 5);
|
||||
url_test("ssh://example.org", "ssh://example.org", 5);
|
||||
url_test("ftp://example.org", "ftp://example.org", 5);
|
||||
url_test("mailto://example.org", "mailto://example.org");
|
||||
url_test("https://example.org", "https://example.org");
|
||||
url_test("http://example.org", "http://example.org");
|
||||
url_test("news://example.org", "news://example.org");
|
||||
url_test("file://example.org", "file://example.org");
|
||||
url_test("git://example.org", "git://example.org");
|
||||
url_test("ssh://example.org", "ssh://example.org");
|
||||
url_test("ftp://example.org", "ftp://example.org");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,14 +21,12 @@ use glutin::Icon;
|
|||
use image::ImageFormat;
|
||||
use glutin::{
|
||||
self, ContextBuilder, ControlFlow, Event, EventsLoop,
|
||||
MouseCursor as GlutinMouseCursor, WindowBuilder,
|
||||
ContextTrait,
|
||||
MouseCursor, WindowBuilder, ContextTrait
|
||||
};
|
||||
use glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
|
||||
|
||||
use crate::cli::Options;
|
||||
use crate::config::{Decorations, WindowConfig};
|
||||
use crate::MouseCursor;
|
||||
|
||||
#[cfg(windows)]
|
||||
static WINDOW_ICON: &'static [u8] = include_bytes!("../assets/windows/alacritty.ico");
|
||||
|
@ -142,7 +140,7 @@ impl Window {
|
|||
window.show();
|
||||
|
||||
// Text cursor
|
||||
window.set_cursor(GlutinMouseCursor::Text);
|
||||
window.set_cursor(MouseCursor::Text);
|
||||
|
||||
// Make the context current so OpenGL operations can run
|
||||
unsafe {
|
||||
|
@ -237,10 +235,7 @@ impl Window {
|
|||
|
||||
#[inline]
|
||||
pub fn set_mouse_cursor(&self, cursor: MouseCursor) {
|
||||
self.window.set_cursor(match cursor {
|
||||
MouseCursor::Arrow => GlutinMouseCursor::Default,
|
||||
MouseCursor::Text => GlutinMouseCursor::Text,
|
||||
});
|
||||
self.window.set_cursor(cursor);
|
||||
}
|
||||
|
||||
/// Set mouse cursor visible
|
||||
|
|
Loading…
Reference in a new issue