1
0
Fork 0
mirror of https://github.com/alacritty/alacritty.git synced 2024-11-18 13:55:23 -05:00

Add vi/mouse hint highlighting support

This patch removes the old url highlighting code and replaces it with a
new implementation making use of hints as sources for finding matches in
the terminal.
This commit is contained in:
Christian Duerr 2021-04-13 03:24:42 +00:00 committed by GitHub
parent 40bcdb1133
commit 96fc9ecc9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 494 additions and 647 deletions

9
Cargo.lock generated
View file

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "ab_glyph_rasterizer" name = "ab_glyph_rasterizer"
version = "0.1.4" version = "0.1.4"
@ -40,7 +42,6 @@ dependencies = [
"serde_yaml", "serde_yaml",
"time", "time",
"unicode-width", "unicode-width",
"urlocator",
"wayland-client", "wayland-client",
"winapi 0.3.9", "winapi 0.3.9",
"x11-dl", "x11-dl",
@ -1664,12 +1665,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "urlocator"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14e39a4f106dafb0a748b951494667a44e62b55fd7942b4fc12706d63cc535a0"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.0" version = "0.2.0"

View file

@ -445,29 +445,6 @@
# If this is `true`, the cursor is temporarily hidden when typing. # If this is `true`, the cursor is temporarily hidden when typing.
#hide_when_typing: false #hide_when_typing: false
#url:
# 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.
#
# When set to `launcher: None`, URL launching will be disabled completely.
#
# Default:
# - (macOS) open
# - (Linux/BSD) xdg-open
# - (Windows) cmd /c start ""
#launcher:
# program: xdg-open
# args: []
# URL modifiers
#
# These are the modifiers that need to be held down for opening URLs when
# clicking on them. The available modifiers are documented in the key
# binding section.
#modifiers: None
# Regex hints # Regex hints
# #
# Terminal hints can be used to find text in the visible part of the terminal # Terminal hints can be used to find text in the visible part of the terminal
@ -478,10 +455,18 @@
# List with all available hints # List with all available hints
# #
# Each hint takes a `regex`, `binding` and either a `command` or an `action`. # Each hint must have a `regex` and either an `action` or a `command` field.
# The fields `mouse`, `binding` and `post_processing` are optional.
# #
# The fields `command`, `binding.key` and `binding.mods` accept the same # The fields `command`, `binding.key`, `binding.mods` and `mouse.mods` accept
# values as they do in the `key_bindings` section. # the same values as they do in the `key_bindings` section.
#
# The `mouse.enabled` field controls if the hint should be underlined while
# the mouse with all `mouse.mods` keys held or the vi mode cursor is above it.
#
# If the `post_processing` field is set to `true`, heuristics will be used to
# shorten the match if there are characters likely not to be part of the hint
# (e.g. a trailing `.`). This is most useful for URIs.
# #
# Values for `action`: # Values for `action`:
# - Copy # - Copy
@ -490,16 +475,17 @@
# Paste the hint's text to the terminal or search. # Paste the hint's text to the terminal or search.
# - Select # - Select
# Select the hint's text. # Select the hint's text.
# #enabled:
# Example # - regex: "(mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\
# # [^\u0000-\u001F\u007F-\u009F<>\" {-}\\^⟨⟩`]+"
# enabled: # command: xdg-open
# - regex: "alacritty/alacritty#\\d*" # post_processing: true
# command: firefox # mouse:
# binding: # enabled: true
# key: G # mods: None
# mods: Control|Shift # binding:
#enabled: [] # key: U
# mods: Control|Shift
# Mouse bindings # Mouse bindings
# #

View file

@ -29,7 +29,6 @@ glutin = { version = "0.26.0", default-features = false, features = ["serde"] }
notify = "4" notify = "4"
parking_lot = "0.11.0" parking_lot = "0.11.0"
crossfont = { version = "0.2.0", features = ["force_system_fontconfig"] } crossfont = { version = "0.2.0", features = ["force_system_fontconfig"] }
urlocator = "0.1.3"
copypasta = { version = "0.7.0", default-features = false } copypasta = { version = "0.7.0", default-features = false }
libc = "0.2" libc = "0.2"
unicode-width = "0.1" unicode-width = "0.1"

View file

@ -1162,7 +1162,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper {
type Value = ModsWrapper; type Value = ModsWrapper;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a subset of Shift|Control|Super|Command|Alt|Option") f.write_str("None or a subset of Shift|Control|Super|Command|Alt|Option")
} }
fn visit_str<E>(self, value: &str) -> Result<ModsWrapper, E> fn visit_str<E>(self, value: &str) -> Result<ModsWrapper, E>

View file

@ -1,50 +1,12 @@
use std::time::Duration; use std::time::Duration;
use glutin::event::ModifiersState;
use alacritty_config_derive::ConfigDeserialize; use alacritty_config_derive::ConfigDeserialize;
use alacritty_terminal::config::Program;
use crate::config::bindings::ModsWrapper;
#[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq, Eq)] #[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq, Eq)]
pub struct Mouse { pub struct Mouse {
pub double_click: ClickHandler, pub double_click: ClickHandler,
pub triple_click: ClickHandler, pub triple_click: ClickHandler,
pub hide_when_typing: bool, pub hide_when_typing: bool,
pub url: Url,
}
#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
pub struct Url {
/// Program for opening links.
pub launcher: Option<Program>,
/// Modifier used to open links.
modifiers: ModsWrapper,
}
impl Url {
pub fn mods(&self) -> ModifiersState {
self.modifiers.into_inner()
}
}
impl Default for Url {
fn default() -> Url {
Url {
#[cfg(not(any(target_os = "macos", windows)))]
launcher: Some(Program::Just(String::from("xdg-open"))),
#[cfg(target_os = "macos")]
launcher: Some(Program::Just(String::from("open"))),
#[cfg(windows)]
launcher: Some(Program::WithArgs {
program: String::from("cmd"),
args: vec!["/c".to_string(), "start".to_string(), "".to_string()],
}),
modifiers: Default::default(),
}
}
} }
#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] #[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]

View file

@ -21,6 +21,11 @@ use crate::config::font::Font;
use crate::config::mouse::Mouse; use crate::config::mouse::Mouse;
use crate::config::window::WindowConfig; use crate::config::window::WindowConfig;
/// Regex used for the default URL hint.
#[rustfmt::skip]
const URL_REGEX: &str = "(mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\
[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\" {-}\\^⟨⟩`]+";
#[derive(ConfigDeserialize, Debug, PartialEq)] #[derive(ConfigDeserialize, Debug, PartialEq)]
pub struct UiConfig { pub struct UiConfig {
/// Font configuration. /// Font configuration.
@ -90,13 +95,18 @@ impl Default for UiConfig {
impl UiConfig { impl UiConfig {
/// Generate key bindings for all keyboard hints. /// Generate key bindings for all keyboard hints.
pub fn generate_hint_bindings(&mut self) { pub fn generate_hint_bindings(&mut self) {
for hint in self.hints.enabled.drain(..) { for hint in &self.hints.enabled {
let binding = match hint.binding {
Some(binding) => binding,
None => continue,
};
let binding = KeyBinding { let binding = KeyBinding {
trigger: hint.binding.key, trigger: binding.key,
mods: hint.binding.mods.0, mods: binding.mods.0,
mode: BindingMode::empty(), mode: BindingMode::empty(),
notmode: BindingMode::empty(), notmode: BindingMode::empty(),
action: Action::Hint(hint), action: Action::Hint(hint.clone()),
}; };
self.key_bindings.0.push(binding); self.key_bindings.0.push(binding);
@ -197,13 +207,42 @@ pub struct Delta<T: Default> {
} }
/// Regex terminal hints. /// Regex terminal hints.
#[derive(ConfigDeserialize, Default, Debug, PartialEq, Eq)] #[derive(ConfigDeserialize, Debug, PartialEq, Eq)]
pub struct Hints { pub struct Hints {
/// Characters for the hint labels. /// Characters for the hint labels.
alphabet: HintsAlphabet, alphabet: HintsAlphabet,
/// All configured terminal hints. /// All configured terminal hints.
enabled: Vec<Hint>, pub enabled: Vec<Hint>,
}
impl Default for Hints {
fn default() -> Self {
// Add URL hint by default when no other hint is present.
let pattern = LazyRegexVariant::Pattern(String::from(URL_REGEX));
let regex = LazyRegex(Rc::new(RefCell::new(pattern)));
#[cfg(not(any(target_os = "macos", windows)))]
let action = HintAction::Command(Program::Just(String::from("xdg-open")));
#[cfg(target_os = "macos")]
let action = HintAction::Command(Program::Just(String::from("open")));
#[cfg(windows)]
let action = HintAction::Command(Program::WithArgs {
program: String::from("cmd"),
args: vec!["/c".to_string(), "start".to_string(), "".to_string()],
});
Self {
enabled: vec![Hint {
regex,
action,
post_processing: true,
mouse: Some(HintMouse { enabled: true, mods: Default::default() }),
binding: Default::default(),
}],
alphabet: Default::default(),
}
}
} }
impl Hints { impl Hints {
@ -271,33 +310,51 @@ pub enum HintAction {
/// Hint configuration. /// Hint configuration.
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Hint { pub struct Hint {
/// Regex for finding matches.
pub regex: LazyRegex,
/// Action executed when this hint is triggered. /// Action executed when this hint is triggered.
#[serde(flatten)] #[serde(flatten)]
pub action: HintAction, pub action: HintAction,
/// Regex for finding matches. /// Hint text post processing.
pub regex: LazyRegex, #[serde(default)]
pub post_processing: bool,
/// Hint mouse highlighting.
pub mouse: Option<HintMouse>,
/// Binding required to search for this hint. /// Binding required to search for this hint.
binding: HintBinding, binding: Option<HintBinding>,
} }
/// Binding for triggering a keyboard hint. /// Binding for triggering a keyboard hint.
#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)] #[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
pub struct HintBinding { pub struct HintBinding {
pub key: Key, pub key: Key,
#[serde(default)]
pub mods: ModsWrapper,
}
/// Hint mouse highlighting.
#[derive(ConfigDeserialize, Default, Copy, Clone, Debug, PartialEq, Eq)]
pub struct HintMouse {
/// Hint mouse highlighting availability.
pub enabled: bool,
/// Required mouse modifiers for hint highlighting.
pub mods: ModsWrapper, pub mods: ModsWrapper,
} }
/// Lazy regex with interior mutability. /// Lazy regex with interior mutability.
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct LazyRegex(Rc<RefCell<LazyRegexVariant>>); pub struct LazyRegex(Rc<RefCell<LazyRegexVariant>>);
impl LazyRegex { impl LazyRegex {
/// Execute a function with the compiled regex DFAs as parameter. /// Execute a function with the compiled regex DFAs as parameter.
pub fn with_compiled<T, F>(&self, f: F) -> T pub fn with_compiled<T, F>(&self, mut f: F) -> T
where where
F: Fn(&RegexSearch) -> T, F: FnMut(&RegexSearch) -> T,
{ {
f(self.0.borrow_mut().compiled()) f(self.0.borrow_mut().compiled())
} }
@ -313,14 +370,6 @@ impl<'de> Deserialize<'de> for LazyRegex {
} }
} }
/// Implement placeholder to allow derive upstream, since we never need it for this struct itself.
impl PartialEq for LazyRegex {
fn eq(&self, _other: &Self) -> bool {
false
}
}
impl Eq for LazyRegex {}
/// Regex which is compiled on demand, to avoid expensive computations at startup. /// Regex which is compiled on demand, to avoid expensive computations at startup.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum LazyRegexVariant { pub enum LazyRegexVariant {
@ -357,3 +406,13 @@ impl LazyRegexVariant {
} }
} }
} }
impl PartialEq for LazyRegexVariant {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Pattern(regex), Self::Pattern(other_regex)) => regex == other_regex,
_ => false,
}
}
}
impl Eq for LazyRegexVariant {}

View file

@ -18,15 +18,12 @@ use alacritty_terminal::term::{
use crate::config::ui_config::UiConfig; use crate::config::ui_config::UiConfig;
use crate::display::color::{List, DIM_FACTOR}; use crate::display::color::{List, DIM_FACTOR};
use crate::display::hint::HintState; use crate::display::hint::HintState;
use crate::display::Display; use crate::display::{self, Display, MAX_SEARCH_LINES};
use crate::event::SearchState; use crate::event::SearchState;
/// Minimum contrast between a fixed cursor color and the cell's background. /// Minimum contrast between a fixed cursor color and the cell's background.
pub const MIN_CURSOR_CONTRAST: f64 = 1.5; pub const MIN_CURSOR_CONTRAST: f64 = 1.5;
/// Maximum number of linewraps followed outside of the viewport during search highlighting.
const MAX_SEARCH_LINES: usize = 100;
/// Renderable terminal content. /// Renderable terminal content.
/// ///
/// This provides the terminal cursor and an iterator over all non-empty cells. /// This provides the terminal cursor and an iterator over all non-empty cells.
@ -138,8 +135,8 @@ impl<'a> RenderableContent<'a> {
// Convert cursor point to viewport position. // Convert cursor point to viewport position.
let cursor_point = self.terminal_cursor.point; let cursor_point = self.terminal_cursor.point;
let line = (cursor_point.line + self.terminal_content.display_offset as i32).0 as usize; let display_offset = self.terminal_content.display_offset;
let point = Point::new(line, cursor_point.column); let point = display::point_to_viewport(display_offset, cursor_point).unwrap();
Some(RenderableCursor { Some(RenderableCursor {
shape: self.terminal_cursor.shape, shape: self.terminal_cursor.shape,
@ -258,8 +255,8 @@ impl RenderableCell {
// Convert cell point to viewport position. // Convert cell point to viewport position.
let cell_point = cell.point; let cell_point = cell.point;
let line = (cell_point.line + content.terminal_content.display_offset as i32).0 as usize; let display_offset = content.terminal_content.display_offset;
let point = Point::new(line, cell_point.column); let point = display::point_to_viewport(display_offset, cell_point).unwrap();
RenderableCell { RenderableCell {
zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()), zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()),
@ -441,7 +438,7 @@ impl<'a> From<&'a HintState> for Hint<'a> {
/// Wrapper for finding visible regex matches. /// Wrapper for finding visible regex matches.
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct RegexMatches(Vec<RangeInclusive<Point>>); pub struct RegexMatches(pub Vec<RangeInclusive<Point>>);
impl RegexMatches { impl RegexMatches {
/// Find all visible matches. /// Find all visible matches.

View file

@ -1,8 +1,16 @@
use alacritty_terminal::term::search::Match; use std::cmp::{max, min};
use glutin::event::ModifiersState;
use alacritty_terminal::grid::BidirectionalIterator;
use alacritty_terminal::index::{Boundary, Point};
use alacritty_terminal::term::search::{Match, RegexSearch};
use alacritty_terminal::term::Term; use alacritty_terminal::term::Term;
use crate::config::ui_config::{Hint, HintAction}; use crate::config::ui_config::{Hint, HintAction};
use crate::config::Config;
use crate::display::content::RegexMatches; use crate::display::content::RegexMatches;
use crate::display::MAX_SEARCH_LINES;
/// Percentage of characters in the hints alphabet used for the last character. /// Percentage of characters in the hints alphabet used for the last character.
const HINT_SPLIT_PERCENTAGE: f32 = 0.5; const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
@ -63,7 +71,20 @@ impl HintState {
}; };
// Find visible matches. // Find visible matches.
self.matches = hint.regex.with_compiled(|regex| RegexMatches::new(term, regex)); self.matches.0 = hint.regex.with_compiled(|regex| {
let mut matches = RegexMatches::new(term, regex);
// Apply post-processing and search for sub-matches if necessary.
if hint.post_processing {
matches
.drain(..)
.map(|rm| HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>())
.flatten()
.collect()
} else {
matches.0
}
});
// Cancel highlight with no visible matches. // Cancel highlight with no visible matches.
if self.matches.is_empty() { if self.matches.is_empty() {
@ -144,6 +165,7 @@ impl HintState {
} }
/// Hint match which was selected by the user. /// Hint match which was selected by the user.
#[derive(Clone)]
pub struct HintMatch { pub struct HintMatch {
/// Action for handling the text. /// Action for handling the text.
pub action: HintAction, pub action: HintAction,
@ -217,6 +239,159 @@ impl HintLabels {
} }
} }
/// Check if there is a hint highlighted at the specified point.
pub fn highlighted_at<T>(
term: &Term<T>,
config: &Config,
point: Point,
mouse_mods: ModifiersState,
) -> Option<HintMatch> {
config.ui_config.hints.enabled.iter().find_map(|hint| {
// Check if all required modifiers are pressed.
if hint.mouse.map_or(true, |mouse| !mouse.enabled || !mouse_mods.contains(mouse.mods.0)) {
return None;
}
hint.regex.with_compiled(|regex| {
// Setup search boundaries.
let mut start = term.line_search_left(point);
start.line = max(start.line, point.line - MAX_SEARCH_LINES);
let mut end = term.line_search_right(point);
end.line = min(end.line, point.line + MAX_SEARCH_LINES);
// Function to verify if the specified point is inside the match.
let at_point = |rm: &Match| *rm.start() <= point && *rm.end() >= point;
// Check if there's any match at the specified point.
let regex_match = term.regex_search_right(regex, start, end).filter(at_point)?;
// Apply post-processing and search for sub-matches if necessary.
let regex_match = if hint.post_processing {
HintPostProcessor::new(term, regex, regex_match).find(at_point)
} else {
Some(regex_match)
};
regex_match.map(|bounds| HintMatch { action: hint.action.clone(), bounds })
})
})
}
/// Iterator over all post-processed matches inside an existing hint match.
struct HintPostProcessor<'a, T> {
/// Regex search DFAs.
regex: &'a RegexSearch,
/// Terminal reference.
term: &'a Term<T>,
/// Next hint match in the iterator.
next_match: Option<Match>,
/// Start point for the next search.
start: Point,
/// End point for the hint match iterator.
end: Point,
}
impl<'a, T> HintPostProcessor<'a, T> {
/// Create a new iterator for an unprocessed match.
fn new(term: &'a Term<T>, regex: &'a RegexSearch, regex_match: Match) -> Self {
let end = *regex_match.end();
let mut post_processor = Self { next_match: None, start: end, end, term, regex };
// Post-process the first hint match.
let next_match = post_processor.hint_post_processing(&regex_match);
post_processor.start = next_match.end().add(term, Boundary::Grid, 1);
post_processor.next_match = Some(next_match);
post_processor
}
/// Apply some hint post processing heuristics.
///
/// This will check the end of the hint and make it shorter if certain characters are determined
/// to be unlikely to be intentionally part of the hint.
///
/// This is most useful for identifying URLs appropriately.
fn hint_post_processing(&self, regex_match: &Match) -> Match {
let mut iter = self.term.grid().iter_from(*regex_match.start());
let mut c = iter.cell().c;
// Truncate uneven number of brackets.
let end = *regex_match.end();
let mut open_parents = 0;
let mut open_brackets = 0;
loop {
match c {
'(' => open_parents += 1,
'[' => open_brackets += 1,
')' => {
if open_parents == 0 {
iter.prev();
break;
} else {
open_parents -= 1;
}
},
']' => {
if open_brackets == 0 {
iter.prev();
break;
} else {
open_brackets -= 1;
}
},
_ => (),
}
if iter.point() == end {
break;
}
match iter.next() {
Some(indexed) => c = indexed.cell.c,
None => break,
}
}
// Truncate trailing characters which are likely to be delimiters.
let start = *regex_match.start();
while iter.point() != start {
if !matches!(c, '.' | ',' | ':' | ';' | '?' | '!' | '(' | '[' | '\'') {
break;
}
match iter.prev() {
Some(indexed) => c = indexed.cell.c,
None => break,
}
}
start..=iter.point()
}
}
impl<'a, T> Iterator for HintPostProcessor<'a, T> {
type Item = Match;
fn next(&mut self) -> Option<Self::Item> {
let next_match = self.next_match.take()?;
if self.start <= self.end {
if let Some(rm) = self.term.regex_search_right(self.regex, self.start, self.end) {
let regex_match = self.hint_post_processing(&rm);
self.start = regex_match.end().add(self.term, Boundary::Grid, 1);
self.next_match = Some(regex_match);
}
}
Some(next_match)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -2,6 +2,7 @@
//! GPU drawing. //! GPU drawing.
use std::cmp::min; use std::cmp::min;
use std::convert::TryFrom;
use std::f64; use std::f64;
use std::fmt::{self, Formatter}; use std::fmt::{self, Formatter};
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
@ -27,6 +28,7 @@ use alacritty_terminal::event::{EventListener, OnResize};
use alacritty_terminal::grid::Dimensions as _; use alacritty_terminal::grid::Dimensions as _;
use alacritty_terminal::index::{Column, Direction, Line, Point}; use alacritty_terminal::index::{Column, Direction, Line, Point};
use alacritty_terminal::selection::Selection; use alacritty_terminal::selection::Selection;
use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::{SizeInfo, Term, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES}; use alacritty_terminal::term::{SizeInfo, Term, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES};
use crate::config::font::Font; use crate::config::font::Font;
@ -38,14 +40,13 @@ use crate::display::bell::VisualBell;
use crate::display::color::List; use crate::display::color::List;
use crate::display::content::RenderableContent; use crate::display::content::RenderableContent;
use crate::display::cursor::IntoRects; use crate::display::cursor::IntoRects;
use crate::display::hint::HintState; use crate::display::hint::{HintMatch, HintState};
use crate::display::meter::Meter; use crate::display::meter::Meter;
use crate::display::window::Window; use crate::display::window::Window;
use crate::event::{Mouse, SearchState}; use crate::event::{Mouse, SearchState};
use crate::message_bar::{MessageBuffer, MessageType}; use crate::message_bar::{MessageBuffer, MessageType};
use crate::renderer::rects::{RenderLines, RenderRect}; use crate::renderer::rects::{RenderLines, RenderRect};
use crate::renderer::{self, GlyphCache, QuadRenderer}; use crate::renderer::{self, GlyphCache, QuadRenderer};
use crate::url::{Url, Urls};
pub mod content; pub mod content;
pub mod cursor; pub mod cursor;
@ -58,7 +59,13 @@ mod meter;
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
mod wayland_theme; mod wayland_theme;
/// Maximum number of linewraps followed outside of the viewport during search highlighting.
pub const MAX_SEARCH_LINES: usize = 100;
/// Label for the forward terminal search bar.
const FORWARD_SEARCH_LABEL: &str = "Search: "; const FORWARD_SEARCH_LABEL: &str = "Search: ";
/// Label for the backward terminal search bar.
const BACKWARD_SEARCH_LABEL: &str = "Backward Search: "; const BACKWARD_SEARCH_LABEL: &str = "Backward Search: ";
#[derive(Debug)] #[derive(Debug)]
@ -164,10 +171,12 @@ impl DisplayUpdate {
pub struct Display { pub struct Display {
pub size_info: SizeInfo, pub size_info: SizeInfo,
pub window: Window, pub window: Window,
pub urls: Urls,
/// Currently highlighted URL. /// Hint highlighted by the mouse.
pub highlighted_url: Option<Url>, pub highlighted_hint: Option<HintMatch>,
/// Hint highlighted by the vi mode cursor.
pub vi_highlighted_hint: Option<HintMatch>,
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
pub wayland_event_queue: Option<EventQueue>, pub wayland_event_queue: Option<EventQueue>,
@ -331,8 +340,8 @@ impl Display {
hint_state, hint_state,
meter: Meter::new(), meter: Meter::new(),
size_info, size_info,
urls: Urls::new(), highlighted_hint: None,
highlighted_url: None, vi_highlighted_hint: None,
#[cfg(not(any(target_os = "macos", windows)))] #[cfg(not(any(target_os = "macos", windows)))]
is_x11, is_x11,
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
@ -473,8 +482,6 @@ impl Display {
terminal: MutexGuard<'_, Term<T>>, terminal: MutexGuard<'_, Term<T>>,
message_buffer: &MessageBuffer, message_buffer: &MessageBuffer,
config: &Config, config: &Config,
mouse: &Mouse,
mods: ModifiersState,
search_state: &SearchState, search_state: &SearchState,
) { ) {
// Collect renderable content before the terminal is dropped. // Collect renderable content before the terminal is dropped.
@ -492,10 +499,6 @@ impl Display {
let metrics = self.glyph_cache.font_metrics(); let metrics = self.glyph_cache.font_metrics();
let size_info = self.size_info; let size_info = self.size_info;
let selection = !terminal.selection.as_ref().map(Selection::is_empty).unwrap_or(true);
let mouse_mode = terminal.mode().intersects(TermMode::MOUSE_MODE)
&& !terminal.mode().contains(TermMode::VI);
let vi_mode = terminal.mode().contains(TermMode::VI); let vi_mode = terminal.mode().contains(TermMode::VI);
let vi_mode_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None }; let vi_mode_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None };
@ -507,18 +510,24 @@ impl Display {
}); });
let mut lines = RenderLines::new(); let mut lines = RenderLines::new();
let mut urls = Urls::new();
// Draw grid. // Draw grid.
{ {
let _sampler = self.meter.sampler(); let _sampler = self.meter.sampler();
let glyph_cache = &mut self.glyph_cache; let glyph_cache = &mut self.glyph_cache;
let highlighted_hint = &self.highlighted_hint;
let vi_highlighted_hint = &self.vi_highlighted_hint;
self.renderer.with_api(&config.ui_config, &size_info, |mut api| { self.renderer.with_api(&config.ui_config, &size_info, |mut api| {
// Iterate over all non-empty cells in the grid. // Iterate over all non-empty cells in the grid.
for cell in grid_cells { for mut cell in grid_cells {
// Update URL underlines. // Underline hints hovered by mouse or vi mode cursor.
urls.update(&size_info, &cell); let point = viewport_to_point(display_offset, cell.point);
if highlighted_hint.as_ref().map_or(false, |h| h.bounds.contains(&point))
|| vi_highlighted_hint.as_ref().map_or(false, |h| h.bounds.contains(&point))
{
cell.flags.insert(Flags::UNDERLINE);
}
// Update underline/strikeout. // Update underline/strikeout.
lines.update(&cell); lines.update(&cell);
@ -531,33 +540,9 @@ impl Display {
let mut rects = lines.rects(&metrics, &size_info); let mut rects = lines.rects(&metrics, &size_info);
// Update visible URLs.
self.urls = urls;
if let Some(url) = self.urls.highlighted(config, mouse, mods, mouse_mode, selection) {
rects.append(&mut url.rects(&metrics, &size_info));
self.window.set_mouse_cursor(CursorIcon::Hand);
self.highlighted_url = Some(url);
} else if self.highlighted_url.is_some() {
self.highlighted_url = None;
if mouse_mode {
self.window.set_mouse_cursor(CursorIcon::Default);
} else {
self.window.set_mouse_cursor(CursorIcon::Text);
}
}
if let Some(vi_mode_cursor) = vi_mode_cursor { if let Some(vi_mode_cursor) = vi_mode_cursor {
// Highlight URLs at the vi mode cursor position.
let vi_point = vi_mode_cursor.point;
let line = (vi_point.line + display_offset).0 as usize;
if let Some(url) = self.urls.find_at(Point::new(line, vi_point.column)) {
rects.append(&mut url.rects(&metrics, &size_info));
}
// Indicate vi mode by showing the cursor's position in the top right corner. // Indicate vi mode by showing the cursor's position in the top right corner.
let vi_point = vi_mode_cursor.point;
let line = (-vi_point.line.0 + size_info.bottommost_line().0) as usize; let line = (-vi_point.line.0 + size_info.bottommost_line().0) as usize;
self.draw_line_indicator(config, &size_info, total_lines, Some(vi_point), line); self.draw_line_indicator(config, &size_info, total_lines, Some(vi_point), line);
} else if search_state.regex().is_some() { } else if search_state.regex().is_some() {
@ -671,6 +656,47 @@ impl Display {
self.colors = List::from(&config.ui_config.colors); self.colors = List::from(&config.ui_config.colors);
} }
/// Update the mouse/vi mode cursor hint highlighting.
pub fn update_highlighted_hints<T>(
&mut self,
term: &Term<T>,
config: &Config,
mouse: &Mouse,
modifiers: ModifiersState,
) {
// Update vi mode cursor hint.
if term.mode().contains(TermMode::VI) {
let mods = ModifiersState::all();
let point = term.vi_mode_cursor.point;
self.vi_highlighted_hint = hint::highlighted_at(&term, config, point, mods);
} else {
self.vi_highlighted_hint = None;
}
// Abort if mouse highlighting conditions are not met.
if !mouse.inside_text_area || !term.selection.as_ref().map_or(true, Selection::is_empty) {
self.highlighted_hint = None;
return;
}
// Find highlighted hint at mouse position.
let point = viewport_to_point(term.grid().display_offset(), mouse.point);
let highlighted_hint = hint::highlighted_at(&term, config, point, modifiers);
// Update cursor shape.
if highlighted_hint.is_some() {
self.window.set_mouse_cursor(CursorIcon::Hand);
} else if self.highlighted_hint.is_some() {
if term.mode().intersects(TermMode::MOUSE_MODE) && !term.mode().contains(TermMode::VI) {
self.window.set_mouse_cursor(CursorIcon::Default);
} else {
self.window.set_mouse_cursor(CursorIcon::Text);
}
}
self.highlighted_hint = highlighted_hint;
}
/// Format search regex to account for the cursor and fullwidth characters. /// Format search regex to account for the cursor and fullwidth characters.
fn format_search(size_info: &SizeInfo, search_regex: &str, search_label: &str) -> String { fn format_search(size_info: &SizeInfo, search_regex: &str, search_label: &str) -> String {
// Add spacers for wide chars. // Add spacers for wide chars.
@ -782,6 +808,18 @@ impl Display {
} }
} }
/// Convert a terminal point to a viewport relative point.
pub fn point_to_viewport(display_offset: usize, point: Point) -> Option<Point<usize>> {
let viewport_line = point.line.0 + display_offset as i32;
usize::try_from(viewport_line).ok().map(|line| Point::new(line, point.column))
}
/// Convert a viewport relative point to a terminal point.
pub fn viewport_to_point(display_offset: usize, point: Point<usize>) -> Point {
let line = Line(point.line as i32) - display_offset;
Point::new(line, point.column)
}
/// Calculate the cell dimensions based on font metrics. /// Calculate the cell dimensions based on font metrics.
/// ///
/// This will return a tuple of the cell width and height. /// This will return a tuple of the cell width and height.

View file

@ -44,15 +44,14 @@ use crate::clipboard::Clipboard;
use crate::config::ui_config::{HintAction, HintInternalAction}; use crate::config::ui_config::{HintAction, HintInternalAction};
use crate::config::{self, Config}; use crate::config::{self, Config};
use crate::daemon::start_daemon; use crate::daemon::start_daemon;
use crate::display::hint::{HintMatch, HintState}; use crate::display::hint::HintMatch;
use crate::display::window::Window; use crate::display::window::Window;
use crate::display::{Display, DisplayUpdate}; use crate::display::{self, Display, DisplayUpdate};
use crate::input::{self, ActionContext as _, FONT_SIZE_STEP}; use crate::input::{self, ActionContext as _, FONT_SIZE_STEP};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use crate::macos; use crate::macos;
use crate::message_bar::{Message, MessageBuffer}; use crate::message_bar::{Message, MessageBuffer};
use crate::scheduler::{Scheduler, TimerId}; use crate::scheduler::{Scheduler, TimerId};
use crate::url::{Url, Urls};
/// Duration after the last user input until an unlimited search is performed. /// Duration after the last user input until an unlimited search is performed.
pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500); pub const TYPING_SEARCH_DELAY: Duration = Duration::from_millis(500);
@ -213,9 +212,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
} else if self.mouse().left_button_state == ElementState::Pressed } else if self.mouse().left_button_state == ElementState::Pressed
|| self.mouse().right_button_state == ElementState::Pressed || self.mouse().right_button_state == ElementState::Pressed
{ {
let point = self.mouse().point; let display_offset = self.terminal.grid().display_offset();
let line = Line(point.line as i32) - self.terminal.grid().display_offset(); let point = display::viewport_to_point(display_offset, self.mouse().point);
let point = Point::new(line, point.column);
self.update_selection(point, self.mouse().cell_side); self.update_selection(point, self.mouse().cell_side);
} }
@ -322,13 +320,13 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
} }
#[inline] #[inline]
fn window(&self) -> &Window { fn window(&mut self) -> &mut Window {
&self.display.window &mut self.display.window
} }
#[inline] #[inline]
fn window_mut(&mut self) -> &mut Window { fn display(&mut self) -> &mut Display {
&mut self.display.window &mut self.display
} }
#[inline] #[inline]
@ -385,30 +383,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
start_daemon(&alacritty, &args); start_daemon(&alacritty, &args);
} }
/// Spawn URL launcher when clicking on URLs.
fn launch_url(&self, url: Url) {
if self.mouse.block_url_launcher {
return;
}
if let Some(ref launcher) = self.config.ui_config.mouse.url.launcher {
let display_offset = self.terminal.grid().display_offset();
let start = url.start();
let start = Point::new(Line(start.line as i32 - display_offset as i32), start.column);
let end = url.end();
let end = Point::new(Line(end.line as i32 - display_offset as i32), end.column);
let mut args = launcher.args().to_vec();
args.push(self.terminal.bounds_to_string(start, end));
start_daemon(launcher.program(), &args);
}
}
fn highlighted_url(&self) -> Option<&Url> {
self.display.highlighted_url.as_ref()
}
fn change_font_size(&mut self, delta: f32) { fn change_font_size(&mut self, delta: f32) {
*self.font_size = max(*self.font_size + delta, Size::new(FONT_SIZE_STEP)); *self.font_size = max(*self.font_size + delta, Size::new(FONT_SIZE_STEP));
let font = self.config.ui_config.font.clone().with_size(*self.font_size); let font = self.config.ui_config.font.clone().with_size(*self.font_size);
@ -645,42 +619,43 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
} }
} }
fn hint_state(&mut self) -> &mut HintState {
&mut self.display.hint_state
}
/// Process a new character for keyboard hints. /// Process a new character for keyboard hints.
fn hint_input(&mut self, c: char) { fn hint_input(&mut self, c: char) {
let action = self.display.hint_state.keyboard_input(self.terminal, c); if let Some(hint) = self.display.hint_state.keyboard_input(self.terminal, c) {
self.mouse.block_hint_launcher = false;
self.trigger_hint(&hint);
}
*self.dirty = true; *self.dirty = true;
}
let HintMatch { action, bounds } = match action { /// Trigger a hint action.
Some(action) => action, fn trigger_hint(&mut self, hint: &HintMatch) {
None => return, if self.mouse.block_hint_launcher {
}; return;
}
match action { match &hint.action {
// Launch an external program. // Launch an external program.
HintAction::Command(command) => { HintAction::Command(command) => {
let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end());
let mut args = command.args().to_vec(); let mut args = command.args().to_vec();
args.push(text); args.push(text);
start_daemon(command.program(), &args); start_daemon(command.program(), &args);
}, },
// Copy the text to the clipboard. // Copy the text to the clipboard.
HintAction::Action(HintInternalAction::Copy) => { HintAction::Action(HintInternalAction::Copy) => {
let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end());
self.clipboard.store(ClipboardType::Clipboard, text); self.clipboard.store(ClipboardType::Clipboard, text);
}, },
// Write the text to the PTY/search. // Write the text to the PTY/search.
HintAction::Action(HintInternalAction::Paste) => { HintAction::Action(HintInternalAction::Paste) => {
let text = self.terminal.bounds_to_string(*bounds.start(), *bounds.end()); let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end());
self.paste(&text); self.paste(&text);
}, },
// Select the text. // Select the text.
HintAction::Action(HintInternalAction::Select) => { HintAction::Action(HintInternalAction::Select) => {
self.start_selection(SelectionType::Simple, *bounds.start(), Side::Left); self.start_selection(SelectionType::Simple, *hint.bounds.start(), Side::Left);
self.update_selection(*bounds.end(), Side::Right); self.update_selection(*hint.bounds.end(), Side::Right);
}, },
} }
} }
@ -731,10 +706,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
self.event_loop self.event_loop
} }
fn urls(&self) -> &Urls {
&self.display.urls
}
fn clipboard_mut(&mut self) -> &mut Clipboard { fn clipboard_mut(&mut self) -> &mut Clipboard {
self.clipboard self.clipboard
} }
@ -908,7 +879,8 @@ pub struct Mouse {
pub scroll_px: f64, pub scroll_px: f64,
pub cell_side: Side, pub cell_side: Side,
pub lines_scrolled: f32, pub lines_scrolled: f32,
pub block_url_launcher: bool, pub block_hint_launcher: bool,
pub hint_highlight_dirty: bool,
pub inside_text_area: bool, pub inside_text_area: bool,
pub point: Point<usize>, pub point: Point<usize>,
pub x: usize, pub x: usize,
@ -925,7 +897,8 @@ impl Default for Mouse {
right_button_state: ElementState::Released, right_button_state: ElementState::Released,
click_state: ClickState::None, click_state: ClickState::None,
cell_side: Side::Left, cell_side: Side::Left,
block_url_launcher: Default::default(), hint_highlight_dirty: Default::default(),
block_hint_launcher: Default::default(),
inside_text_area: Default::default(), inside_text_area: Default::default(),
lines_scrolled: Default::default(), lines_scrolled: Default::default(),
scroll_px: Default::default(), scroll_px: Default::default(),
@ -1115,6 +1088,17 @@ impl<N: Notify + OnResize> Processor<N> {
return; return;
} }
if self.dirty || self.mouse.hint_highlight_dirty {
self.display.update_highlighted_hints(
&terminal,
&self.config,
&self.mouse,
self.modifiers,
);
self.mouse.hint_highlight_dirty = false;
self.dirty = true;
}
if self.dirty { if self.dirty {
self.dirty = false; self.dirty = false;
@ -1127,14 +1111,7 @@ impl<N: Notify + OnResize> Processor<N> {
} }
// Redraw screen. // Redraw screen.
self.display.draw( self.display.draw(terminal, &self.message_buffer, &self.config, &self.search_state);
terminal,
&self.message_buffer,
&self.config,
&self.mouse,
self.modifiers,
&self.search_state,
);
} }
}); });
@ -1165,7 +1142,7 @@ impl<N: Notify + OnResize> Processor<N> {
// Resize to event's dimensions, since no resize event is emitted on Wayland. // Resize to event's dimensions, since no resize event is emitted on Wayland.
display_update_pending.set_dimensions(PhysicalSize::new(width, height)); display_update_pending.set_dimensions(PhysicalSize::new(width, height));
processor.ctx.window_mut().dpr = scale_factor; processor.ctx.window().dpr = scale_factor;
*processor.ctx.dirty = true; *processor.ctx.dirty = true;
}, },
Event::Message(message) => { Event::Message(message) => {
@ -1184,7 +1161,7 @@ impl<N: Notify + OnResize> Processor<N> {
TerminalEvent::Title(title) => { TerminalEvent::Title(title) => {
let ui_config = &processor.ctx.config.ui_config; let ui_config = &processor.ctx.config.ui_config;
if ui_config.window.dynamic_title { if ui_config.window.dynamic_title {
processor.ctx.window_mut().set_title(&title); processor.ctx.window().set_title(&title);
} }
}, },
TerminalEvent::ResetTitle => { TerminalEvent::ResetTitle => {
@ -1198,7 +1175,7 @@ impl<N: Notify + OnResize> Processor<N> {
// Set window urgency. // Set window urgency.
if processor.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) { if processor.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) {
let focused = processor.ctx.terminal.is_focused; let focused = processor.ctx.terminal.is_focused;
processor.ctx.window_mut().set_urgent(!focused); processor.ctx.window().set_urgent(!focused);
} }
// Ring visual bell. // Ring visual bell.
@ -1251,16 +1228,16 @@ impl<N: Notify + OnResize> Processor<N> {
}, },
WindowEvent::ReceivedCharacter(c) => processor.received_char(c), WindowEvent::ReceivedCharacter(c) => processor.received_char(c),
WindowEvent::MouseInput { state, button, .. } => { WindowEvent::MouseInput { state, button, .. } => {
processor.ctx.window_mut().set_mouse_visible(true); processor.ctx.window().set_mouse_visible(true);
processor.mouse_input(state, button); processor.mouse_input(state, button);
*processor.ctx.dirty = true; *processor.ctx.dirty = true;
}, },
WindowEvent::CursorMoved { position, .. } => { WindowEvent::CursorMoved { position, .. } => {
processor.ctx.window_mut().set_mouse_visible(true); processor.ctx.window().set_mouse_visible(true);
processor.mouse_moved(position); processor.mouse_moved(position);
}, },
WindowEvent::MouseWheel { delta, phase, .. } => { WindowEvent::MouseWheel { delta, phase, .. } => {
processor.ctx.window_mut().set_mouse_visible(true); processor.ctx.window().set_mouse_visible(true);
processor.mouse_wheel_input(delta, phase); processor.mouse_wheel_input(delta, phase);
}, },
WindowEvent::Focused(is_focused) => { WindowEvent::Focused(is_focused) => {
@ -1269,9 +1246,9 @@ impl<N: Notify + OnResize> Processor<N> {
*processor.ctx.dirty = true; *processor.ctx.dirty = true;
if is_focused { if is_focused {
processor.ctx.window_mut().set_urgent(false); processor.ctx.window().set_urgent(false);
} else { } else {
processor.ctx.window_mut().set_mouse_visible(true); processor.ctx.window().set_mouse_visible(true);
} }
processor.ctx.update_cursor_blinking(); processor.ctx.update_cursor_blinking();
@ -1285,7 +1262,7 @@ impl<N: Notify + OnResize> Processor<N> {
WindowEvent::CursorLeft { .. } => { WindowEvent::CursorLeft { .. } => {
processor.ctx.mouse.inside_text_area = false; processor.ctx.mouse.inside_text_area = false;
if processor.ctx.highlighted_url().is_some() { if processor.ctx.display().highlighted_hint.is_some() {
*processor.ctx.dirty = true; *processor.ctx.dirty = true;
} }
}, },
@ -1382,12 +1359,12 @@ impl<N: Notify + OnResize> Processor<N> {
if !config.ui_config.window.dynamic_title if !config.ui_config.window.dynamic_title
|| processor.ctx.config.ui_config.window.title != config.ui_config.window.title || processor.ctx.config.ui_config.window.title != config.ui_config.window.title
{ {
processor.ctx.window_mut().set_title(&config.ui_config.window.title); processor.ctx.window().set_title(&config.ui_config.window.title);
} }
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
if processor.ctx.event_loop.is_wayland() { if processor.ctx.event_loop.is_wayland() {
processor.ctx.window_mut().set_wayland_theme(&config.ui_config.colors); processor.ctx.window().set_wayland_theme(&config.ui_config.colors);
} }
// Set subpixel anti-aliasing. // Set subpixel anti-aliasing.
@ -1396,7 +1373,7 @@ impl<N: Notify + OnResize> Processor<N> {
// Disable shadows for transparent windows on macOS. // Disable shadows for transparent windows on macOS.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
processor.ctx.window_mut().set_has_shadow(config.ui_config.background_opacity() >= 1.0); processor.ctx.window().set_has_shadow(config.ui_config.background_opacity() >= 1.0);
// Update hint keys. // Update hint keys.
processor.ctx.display.hint_state.update_alphabet(config.ui_config.hints.alphabet()); processor.ctx.display.hint_state.update_alphabet(config.ui_config.hints.alphabet());

View file

@ -22,7 +22,7 @@ use glutin::window::CursorIcon;
use alacritty_terminal::ansi::{ClearMode, Handler}; use alacritty_terminal::ansi::{ClearMode, Handler};
use alacritty_terminal::event::EventListener; use alacritty_terminal::event::EventListener;
use alacritty_terminal::grid::{Dimensions, Scroll}; use alacritty_terminal::grid::{Dimensions, Scroll};
use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point, Side}; use alacritty_terminal::index::{Boundary, Column, Direction, Point, Side};
use alacritty_terminal::selection::SelectionType; use alacritty_terminal::selection::SelectionType;
use alacritty_terminal::term::search::Match; use alacritty_terminal::term::search::Match;
use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode};
@ -31,12 +31,12 @@ use alacritty_terminal::vi_mode::ViMotion;
use crate::clipboard::Clipboard; use crate::clipboard::Clipboard;
use crate::config::{Action, BindingMode, Config, Key, SearchAction, ViAction}; use crate::config::{Action, BindingMode, Config, Key, SearchAction, ViAction};
use crate::daemon::start_daemon; use crate::daemon::start_daemon;
use crate::display::hint::HintState; use crate::display::hint::HintMatch;
use crate::display::window::Window; use crate::display::window::Window;
use crate::display::{self, Display};
use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY}; use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY};
use crate::message_bar::{self, Message}; use crate::message_bar::{self, Message};
use crate::scheduler::{Scheduler, TimerId}; use crate::scheduler::{Scheduler, TimerId};
use crate::url::{Url, Urls};
/// Font size change interval. /// Font size change interval.
pub const FONT_SIZE_STEP: f32 = 0.5; pub const FONT_SIZE_STEP: f32 = 0.5;
@ -75,8 +75,8 @@ pub trait ActionContext<T: EventListener> {
fn suppress_chars(&mut self) -> &mut bool; fn suppress_chars(&mut self) -> &mut bool;
fn modifiers(&mut self) -> &mut ModifiersState; fn modifiers(&mut self) -> &mut ModifiersState;
fn scroll(&mut self, _scroll: Scroll) {} fn scroll(&mut self, _scroll: Scroll) {}
fn window(&self) -> &Window; fn window(&mut self) -> &mut Window;
fn window_mut(&mut self) -> &mut Window; fn display(&mut self) -> &mut Display;
fn terminal(&self) -> &Term<T>; fn terminal(&self) -> &Term<T>;
fn terminal_mut(&mut self) -> &mut Term<T>; fn terminal_mut(&mut self) -> &mut Term<T>;
fn spawn_new_instance(&mut self) {} fn spawn_new_instance(&mut self) {}
@ -86,9 +86,6 @@ pub trait ActionContext<T: EventListener> {
fn message(&self) -> Option<&Message>; fn message(&self) -> Option<&Message>;
fn config(&self) -> &Config; fn config(&self) -> &Config;
fn event_loop(&self) -> &EventLoopWindowTarget<Event>; fn event_loop(&self) -> &EventLoopWindowTarget<Event>;
fn urls(&self) -> &Urls;
fn launch_url(&self, _url: Url) {}
fn highlighted_url(&self) -> Option<&Url>;
fn mouse_mode(&self) -> bool; fn mouse_mode(&self) -> bool;
fn clipboard_mut(&mut self) -> &mut Clipboard; fn clipboard_mut(&mut self) -> &mut Clipboard;
fn scheduler_mut(&mut self) -> &mut Scheduler; fn scheduler_mut(&mut self) -> &mut Scheduler;
@ -105,8 +102,8 @@ pub trait ActionContext<T: EventListener> {
fn search_active(&self) -> bool; fn search_active(&self) -> bool;
fn on_typing_start(&mut self) {} fn on_typing_start(&mut self) {}
fn toggle_vi_mode(&mut self) {} fn toggle_vi_mode(&mut self) {}
fn hint_state(&mut self) -> &mut HintState;
fn hint_input(&mut self, _character: char) {} fn hint_input(&mut self, _character: char) {}
fn trigger_hint(&mut self, _hint: &HintMatch) {}
fn paste(&mut self, _text: &str) {} fn paste(&mut self, _text: &str) {}
} }
@ -142,7 +139,7 @@ impl<T: EventListener> Execute<T> for Action {
}, },
Action::Command(program) => start_daemon(program.program(), program.args()), Action::Command(program) => start_daemon(program.program(), program.args()),
Action::Hint(hint) => { Action::Hint(hint) => {
ctx.hint_state().start(hint.clone()); ctx.display().hint_state.start(hint.clone());
ctx.mark_dirty(); ctx.mark_dirty();
}, },
Action::ToggleViMode => ctx.toggle_vi_mode(), Action::ToggleViMode => ctx.toggle_vi_mode(),
@ -164,12 +161,12 @@ impl<T: EventListener> Execute<T> for Action {
Self::toggle_selection(ctx, SelectionType::Semantic); Self::toggle_selection(ctx, SelectionType::Semantic);
}, },
Action::ViAction(ViAction::Open) => { Action::ViAction(ViAction::Open) => {
ctx.mouse_mut().block_url_launcher = false; let hint = ctx.display().vi_highlighted_hint.take();
let vi_point = ctx.terminal().vi_mode_cursor.point; if let Some(hint) = &hint {
let line = (vi_point.line + ctx.terminal().grid().display_offset()).0 as usize; ctx.mouse_mut().block_hint_launcher = false;
if let Some(url) = ctx.urls().find_at(Point::new(line, vi_point.column)) { ctx.trigger_hint(hint);
ctx.launch_url(url);
} }
ctx.display().vi_highlighted_hint = hint;
}, },
Action::ViAction(ViAction::SearchNext) => { Action::ViAction(ViAction::SearchNext) => {
let terminal = ctx.terminal(); let terminal = ctx.terminal();
@ -250,9 +247,9 @@ impl<T: EventListener> Execute<T> for Action {
let text = ctx.clipboard_mut().load(ClipboardType::Selection); let text = ctx.clipboard_mut().load(ClipboardType::Selection);
ctx.paste(&text); ctx.paste(&text);
}, },
Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(), Action::ToggleFullscreen => ctx.window().toggle_fullscreen(),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(), Action::ToggleSimpleFullscreen => ctx.window().toggle_simple_fullscreen(),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
Action::Hide => ctx.event_loop().hide_application(), Action::Hide => ctx.event_loop().hide_application(),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -327,25 +324,6 @@ impl<T: EventListener> Execute<T> for Action {
} }
} }
#[derive(Debug, Clone, PartialEq)]
pub enum MouseState {
Url(Url),
MessageBar,
MessageBarButton,
Mouse,
Text,
}
impl From<MouseState> for CursorIcon {
fn from(mouse_state: MouseState) -> CursorIcon {
match mouse_state {
MouseState::Url(_) | MouseState::MessageBarButton => CursorIcon::Hand,
MouseState::Text => CursorIcon::Text,
_ => CursorIcon::Default,
}
}
}
impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
pub fn new(ctx: A) -> Self { pub fn new(ctx: A) -> Self {
Self { ctx, _phantom: Default::default() } Self { ctx, _phantom: Default::default() }
@ -375,11 +353,6 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
let cell_changed = point != self.ctx.mouse().point; let cell_changed = point != self.ctx.mouse().point;
// Update mouse state and check for URL change.
let mouse_state = self.mouse_state();
self.update_url_state(&mouse_state);
self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
// If the mouse hasn't changed cells, do nothing. // If the mouse hasn't changed cells, do nothing.
if !cell_changed if !cell_changed
&& self.ctx.mouse().cell_side == cell_side && self.ctx.mouse().cell_side == cell_side
@ -392,13 +365,20 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
self.ctx.mouse_mut().cell_side = cell_side; self.ctx.mouse_mut().cell_side = cell_side;
self.ctx.mouse_mut().point = point; self.ctx.mouse_mut().point = point;
// Update mouse state and check for URL change.
let mouse_state = self.cursor_state();
self.ctx.window().set_mouse_cursor(mouse_state);
// Prompt hint highlight update.
self.ctx.mouse_mut().hint_highlight_dirty = true;
// Don't launch URLs if mouse has moved. // Don't launch URLs if mouse has moved.
self.ctx.mouse_mut().block_url_launcher = true; self.ctx.mouse_mut().block_hint_launcher = true;
if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode())
{ {
let line = Line(point.line as i32) - self.ctx.terminal().grid().display_offset(); let display_offset = self.ctx.terminal().grid().display_offset();
let point = Point::new(line, point.column); let point = display::viewport_to_point(display_offset, point);
self.ctx.update_selection(point, cell_side); self.ctx.update_selection(point, cell_side);
} else if cell_changed } else if cell_changed
&& point.line < self.ctx.terminal().screen_lines() && point.line < self.ctx.terminal().screen_lines()
@ -562,10 +542,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}; };
// Load mouse point, treating message bar and padding as the closest cell. // Load mouse point, treating message bar and padding as the closest cell.
let point = self.ctx.mouse().point;
let display_offset = self.ctx.terminal().grid().display_offset(); let display_offset = self.ctx.terminal().grid().display_offset();
let absolute_line = Line(point.line as i32) - display_offset; let point = display::viewport_to_point(display_offset, self.ctx.mouse().point);
let point = Point::new(absolute_line, point.column);
match button { match button {
MouseButton::Left => self.on_left_click(point), MouseButton::Left => self.on_left_click(point),
@ -619,7 +597,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
match self.ctx.mouse().click_state { match self.ctx.mouse().click_state {
ClickState::Click => { ClickState::Click => {
// Don't launch URLs if this click cleared the selection. // Don't launch URLs if this click cleared the selection.
self.ctx.mouse_mut().block_url_launcher = !self.ctx.selection_is_empty(); self.ctx.mouse_mut().block_hint_launcher = !self.ctx.selection_is_empty();
self.ctx.clear_selection(); self.ctx.clear_selection();
@ -631,11 +609,11 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
} }
}, },
ClickState::DoubleClick => { ClickState::DoubleClick => {
self.ctx.mouse_mut().block_url_launcher = true; self.ctx.mouse_mut().block_hint_launcher = true;
self.ctx.start_selection(SelectionType::Semantic, point, side); self.ctx.start_selection(SelectionType::Semantic, point, side);
}, },
ClickState::TripleClick => { ClickState::TripleClick => {
self.ctx.mouse_mut().block_url_launcher = true; self.ctx.mouse_mut().block_hint_launcher = true;
self.ctx.start_selection(SelectionType::Lines, point, side); self.ctx.start_selection(SelectionType::Lines, point, side);
}, },
ClickState::None => (), ClickState::None => (),
@ -658,10 +636,15 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}; };
self.mouse_report(code, ElementState::Released); self.mouse_report(code, ElementState::Released);
return; return;
} else if let (MouseButton::Left, MouseState::Url(url)) = (button, self.mouse_state()) {
self.ctx.launch_url(url);
} }
// Trigger hints highlighted by the mouse.
let hint = self.ctx.display().highlighted_hint.take();
if let Some(hint) = hint.as_ref().filter(|_| button == MouseButton::Left) {
self.ctx.trigger_hint(hint);
}
self.ctx.display().highlighted_hint = hint;
self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling); self.ctx.scheduler_mut().unschedule(TimerId::SelectionScrolling);
} }
@ -748,7 +731,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
} }
// Skip normal mouse events if the message bar has been clicked. // Skip normal mouse events if the message bar has been clicked.
if self.message_bar_mouse_state() == Some(MouseState::MessageBarButton) if self.message_bar_cursor_state() == Some(CursorIcon::Hand)
&& state == ElementState::Pressed && state == ElementState::Pressed
{ {
let size = self.ctx.size_info(); let size = self.ctx.size_info();
@ -773,7 +756,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}, },
}; };
self.ctx.window_mut().set_mouse_cursor(new_icon); self.ctx.window().set_mouse_cursor(new_icon);
} else { } else {
match state { match state {
ElementState::Pressed => { ElementState::Pressed => {
@ -788,7 +771,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
/// Process key input. /// Process key input.
pub fn key_input(&mut self, input: KeyboardInput) { pub fn key_input(&mut self, input: KeyboardInput) {
// All key bindings are disabled while a hint is being selected. // All key bindings are disabled while a hint is being selected.
if self.ctx.hint_state().active() { if self.ctx.display().hint_state.active() {
*self.ctx.suppress_chars() = false; *self.ctx.suppress_chars() = false;
return; return;
} }
@ -813,17 +796,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
pub fn modifiers_input(&mut self, modifiers: ModifiersState) { pub fn modifiers_input(&mut self, modifiers: ModifiersState) {
*self.ctx.modifiers() = modifiers; *self.ctx.modifiers() = modifiers;
// Prompt hint highlight update.
self.ctx.mouse_mut().hint_highlight_dirty = true;
// Update mouse state and check for URL change. // Update mouse state and check for URL change.
let mouse_state = self.mouse_state(); let mouse_state = self.cursor_state();
self.update_url_state(&mouse_state); self.ctx.window().set_mouse_cursor(mouse_state);
self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
} }
/// Reset mouse cursor based on modifier and terminal state. /// Reset mouse cursor based on modifier and terminal state.
#[inline] #[inline]
pub fn reset_mouse_cursor(&mut self) { pub fn reset_mouse_cursor(&mut self) {
let mouse_state = self.mouse_state(); let mouse_state = self.cursor_state();
self.ctx.window_mut().set_mouse_cursor(mouse_state.into()); self.ctx.window().set_mouse_cursor(mouse_state);
} }
/// Process a received character. /// Process a received character.
@ -831,7 +816,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
let suppress_chars = *self.ctx.suppress_chars(); let suppress_chars = *self.ctx.suppress_chars();
// Handle hint selection over anything else. // Handle hint selection over anything else.
if self.ctx.hint_state().active() && !suppress_chars { if self.ctx.display().hint_state.active() && !suppress_chars {
self.ctx.hint_input(c); self.ctx.hint_input(c);
return; return;
} }
@ -925,8 +910,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
} }
} }
/// Check mouse state in relation to the message bar. /// Check mouse icon state in relation to the message bar.
fn message_bar_mouse_state(&self) -> Option<MouseState> { fn message_bar_cursor_state(&self) -> Option<CursorIcon> {
// Since search is above the message bar, the button is offset by search's height. // Since search is above the message bar, the button is offset by search's height.
let search_height = if self.ctx.search_active() { 1 } else { 0 }; let search_height = if self.ctx.search_active() { 1 } else { 0 };
@ -941,53 +926,30 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
} else if mouse.y <= terminal_end + size.cell_height() as usize } else if mouse.y <= terminal_end + size.cell_height() as usize
&& mouse.point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns() && mouse.point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns()
{ {
Some(MouseState::MessageBarButton) Some(CursorIcon::Hand)
} else { } else {
Some(MouseState::MessageBar) Some(CursorIcon::Default)
} }
} }
/// Trigger redraw when URL highlight changed. /// Icon state of the cursor.
#[inline] fn cursor_state(&mut self) -> CursorIcon {
fn update_url_state(&mut self, mouse_state: &MouseState) { // Define function to check if mouse is on top of a hint.
let highlighted_url = self.ctx.highlighted_url(); let display_offset = self.ctx.terminal().grid().display_offset();
if let MouseState::Url(url) = mouse_state { let mouse_point = self.ctx.mouse().point;
if Some(url) != highlighted_url { let hint_highlighted = |hint: &HintMatch| {
self.ctx.mark_dirty(); let point = display::viewport_to_point(display_offset, mouse_point);
} hint.bounds.contains(&point)
} else if highlighted_url.is_some() { };
self.ctx.mark_dirty();
}
}
/// Location of the mouse cursor. if let Some(mouse_state) = self.message_bar_cursor_state() {
fn mouse_state(&mut self) -> MouseState { mouse_state
// Check message bar before URL to ignore URLs in the message bar. } else if self.ctx.display().highlighted_hint.as_ref().map_or(false, hint_highlighted) {
if let Some(mouse_state) = self.message_bar_mouse_state() { CursorIcon::Hand
return mouse_state; } else if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() {
} CursorIcon::Default
let mouse_mode = self.ctx.mouse_mode();
// Check for URL at mouse cursor.
let mods = *self.ctx.modifiers();
let highlighted_url = self.ctx.urls().highlighted(
self.ctx.config(),
self.ctx.mouse(),
mods,
mouse_mode,
!self.ctx.selection_is_empty(),
);
if let Some(url) = highlighted_url {
return MouseState::Url(url);
}
// Check mouse mode if location is not special.
if !self.ctx.modifiers().shift() && mouse_mode {
MouseState::Mouse
} else { } else {
MouseState::Text CursorIcon::Text
} }
} }
@ -1129,11 +1091,11 @@ mod tests {
&mut self.modifiers &mut self.modifiers
} }
fn window(&self) -> &Window { fn window(&mut self) -> &mut Window {
unimplemented!(); unimplemented!();
} }
fn window_mut(&mut self) -> &mut Window { fn display(&mut self) -> &mut Display {
unimplemented!(); unimplemented!();
} }
@ -1157,21 +1119,9 @@ mod tests {
unimplemented!(); unimplemented!();
} }
fn urls(&self) -> &Urls {
unimplemented!();
}
fn highlighted_url(&self) -> Option<&Url> {
unimplemented!();
}
fn scheduler_mut(&mut self) -> &mut Scheduler { fn scheduler_mut(&mut self) -> &mut Scheduler {
unimplemented!(); unimplemented!();
} }
fn hint_state(&mut self) -> &mut HintState {
unimplemented!();
}
} }
macro_rules! test_clickstate { macro_rules! test_clickstate {

View file

@ -45,7 +45,6 @@ mod message_bar;
mod panic; mod panic;
mod renderer; mod renderer;
mod scheduler; mod scheduler;
mod url;
mod gl { mod gl {
#![allow(clippy::all)] #![allow(clippy::all)]

View file

@ -1,288 +0,0 @@
use std::cmp::min;
use std::mem;
use crossfont::Metrics;
use glutin::event::{ElementState, ModifiersState};
use urlocator::{UrlLocation, UrlLocator};
use alacritty_terminal::grid::Dimensions;
use alacritty_terminal::index::{Boundary, Column, Line, Point};
use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::color::Rgb;
use alacritty_terminal::term::SizeInfo;
use crate::config::Config;
use crate::display::content::RenderableCell;
use crate::event::Mouse;
use crate::renderer::rects::{RenderLine, RenderRect};
#[derive(Clone, Debug, PartialEq)]
pub struct Url {
lines: Vec<RenderLine>,
end_offset: u16,
size: SizeInfo,
}
impl Url {
/// Rectangles required for underlining the URL.
pub fn rects(&self, metrics: &Metrics, size: &SizeInfo) -> Vec<RenderRect> {
let end = self.end();
self.lines
.iter()
.filter(|line| line.start <= end)
.map(|line| {
let mut rect_line = *line;
rect_line.end = min(line.end, end);
rect_line.rects(Flags::UNDERLINE, metrics, size)
})
.flatten()
.collect()
}
/// Viewport start point of the URL.
pub fn start(&self) -> Point<usize> {
self.lines[0].start
}
/// Viewport end point of the URL.
pub fn end(&self) -> Point<usize> {
let end = self.lines[self.lines.len() - 1].end;
// Convert to Point<Line> to make use of the grid clamping logic.
let mut end = Point::new(Line(end.line as i32), end.column);
end = end.sub(&self.size, Boundary::Cursor, self.end_offset as usize);
Point::new(end.line.0 as usize, end.column)
}
}
pub struct Urls {
locator: UrlLocator,
urls: Vec<Url>,
scheme_buffer: Vec<(Point<usize>, Rgb)>,
next_point: Point<usize>,
state: UrlLocation,
}
impl Default for Urls {
fn default() -> Self {
Self {
locator: UrlLocator::new(),
scheme_buffer: Vec::new(),
urls: Vec::new(),
state: UrlLocation::Reset,
next_point: Point::new(0, Column(0)),
}
}
}
impl Urls {
pub fn new() -> Self {
Self::default()
}
// Update tracked URLs.
pub fn update(&mut self, size: &SizeInfo, cell: &RenderableCell) {
let point = cell.point;
let mut end = point;
// Include the following wide char spacer.
if cell.flags.contains(Flags::WIDE_CHAR) {
end.column += 1;
}
// Reset URL when empty cells have been skipped.
if point != Point::new(0, Column(0)) && point != self.next_point {
self.reset();
}
self.next_point = if end.column.0 + 1 == size.columns() {
Point::new(end.line + 1, Column(0))
} else {
Point::new(end.line, end.column + 1)
};
// Extend current state if a leading wide char spacer is encountered.
if cell.flags.intersects(Flags::LEADING_WIDE_CHAR_SPACER) {
if let UrlLocation::Url(_, mut end_offset) = self.state {
if end_offset != 0 {
end_offset += 1;
}
self.extend_url(point, end, cell.fg, end_offset);
}
return;
}
// Advance parser.
let last_state = mem::replace(&mut self.state, self.locator.advance(cell.character));
match (self.state, last_state) {
(UrlLocation::Url(_length, end_offset), UrlLocation::Scheme) => {
// Create empty URL.
self.urls.push(Url { lines: Vec::new(), end_offset, size: *size });
// Push schemes into URL.
for (scheme_point, scheme_fg) in self.scheme_buffer.split_off(0) {
self.extend_url(scheme_point, scheme_point, scheme_fg, end_offset);
}
// Push the new cell into URL.
self.extend_url(point, end, cell.fg, end_offset);
},
(UrlLocation::Url(_length, end_offset), UrlLocation::Url(..)) => {
self.extend_url(point, end, cell.fg, end_offset);
},
(UrlLocation::Scheme, _) => self.scheme_buffer.push((cell.point, cell.fg)),
(UrlLocation::Reset, _) => self.reset(),
_ => (),
}
// Reset at un-wrapped linebreak.
if cell.point.column.0 + 1 == size.columns() && !cell.flags.contains(Flags::WRAPLINE) {
self.reset();
}
}
/// Extend the last URL.
fn extend_url(&mut self, start: Point<usize>, end: Point<usize>, color: Rgb, end_offset: u16) {
let url = self.urls.last_mut().unwrap();
// If color changed, we need to insert a new line.
if url.lines.last().map(|last| last.color) == Some(color) {
url.lines.last_mut().unwrap().end = end;
} else {
url.lines.push(RenderLine { start, end, color });
}
// Update excluded cells at the end of the URL.
url.end_offset = end_offset;
}
/// Find URL below the mouse cursor.
pub fn highlighted(
&self,
config: &Config,
mouse: &Mouse,
mods: ModifiersState,
mouse_mode: bool,
selection: bool,
) -> Option<Url> {
// Require additional shift in mouse mode.
let mut required_mods = config.ui_config.mouse.url.mods();
if mouse_mode {
required_mods |= ModifiersState::SHIFT;
}
// Make sure all prerequisites for highlighting are met.
if selection
|| !mouse.inside_text_area
|| config.ui_config.mouse.url.launcher.is_none()
|| required_mods != mods
|| mouse.left_button_state == ElementState::Pressed
{
return None;
}
self.find_at(mouse.point)
}
/// Find URL at location.
pub fn find_at(&self, point: Point<usize>) -> Option<Url> {
for url in &self.urls {
if (url.start()..=url.end()).contains(&point) {
return Some(url.clone());
}
}
None
}
fn reset(&mut self) {
self.locator = UrlLocator::new();
self.state = UrlLocation::Reset;
self.scheme_buffer.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use alacritty_terminal::index::Column;
fn text_to_cells(text: &str) -> Vec<RenderableCell> {
text.chars()
.enumerate()
.map(|(i, character)| RenderableCell {
character,
zerowidth: None,
point: Point::new(0, Column(i)),
fg: Default::default(),
bg: Default::default(),
bg_alpha: 0.,
flags: Flags::empty(),
})
.collect()
}
#[test]
fn multi_color_url() {
let mut input = text_to_cells("test https://example.org ing");
let size = SizeInfo::new(input.len() as f32, 1., 1.0, 1.0, 0.0, 0.0, false);
input[10].fg = Rgb { r: 0xff, g: 0x00, b: 0xff };
let mut urls = Urls::new();
for cell in input {
urls.update(&size, &cell);
}
let url = urls.urls.first().unwrap();
assert_eq!(url.start().column, Column(5));
assert_eq!(url.end().column, Column(23));
}
#[test]
fn multiple_urls() {
let input = text_to_cells("test git:a git:b git:c ing");
let size = SizeInfo::new(input.len() as f32, 1., 1.0, 1.0, 0.0, 0.0, false);
let mut urls = Urls::new();
for cell in input {
urls.update(&size, &cell);
}
assert_eq!(urls.urls.len(), 3);
assert_eq!(urls.urls[0].start().column, Column(5));
assert_eq!(urls.urls[0].end().column, Column(9));
assert_eq!(urls.urls[1].start().column, Column(11));
assert_eq!(urls.urls[1].end().column, Column(15));
assert_eq!(urls.urls[2].start().column, Column(17));
assert_eq!(urls.urls[2].end().column, Column(21));
}
#[test]
fn wide_urls() {
let input = text_to_cells("test https://こんにちは (http:여보세요) ing");
let size = SizeInfo::new(input.len() as f32 + 9., 1., 1.0, 1.0, 0.0, 0.0, false);
let mut urls = Urls::new();
for cell in input {
urls.update(&size, &cell);
}
assert_eq!(urls.urls.len(), 2);
assert_eq!(urls.urls[0].start().column, Column(5));
assert_eq!(urls.urls[0].end().column, Column(17));
assert_eq!(urls.urls[1].start().column, Column(20));
assert_eq!(urls.urls[1].end().column, Column(28));
}
}

View file

@ -82,7 +82,7 @@ impl<T> Term<T> {
// Limit maximum number of lines searched. // Limit maximum number of lines searched.
end = match max_lines { end = match max_lines {
Some(max_lines) => { Some(max_lines) => {
let line = (start.line + max_lines).grid_clamp(self, Boundary::Grid); let line = (start.line + max_lines).grid_clamp(self, Boundary::None);
Point::new(line, self.last_column()) Point::new(line, self.last_column())
}, },
_ => end.sub(self, Boundary::None, 1), _ => end.sub(self, Boundary::None, 1),
@ -121,7 +121,7 @@ impl<T> Term<T> {
// Limit maximum number of lines searched. // Limit maximum number of lines searched.
end = match max_lines { end = match max_lines {
Some(max_lines) => { Some(max_lines) => {
let line = (start.line - max_lines).grid_clamp(self, Boundary::Grid); let line = (start.line - max_lines).grid_clamp(self, Boundary::None);
Point::new(line, Column(0)) Point::new(line, Column(0))
}, },
_ => end.add(self, Boundary::None, 1), _ => end.add(self, Boundary::None, 1),

View file

@ -27,13 +27,6 @@ line (<kbd>Shift</kbd> <kbd>v</kbd>) and block selection (<kbd>Ctrl</kbd>
<kbd>v</kbd>). You can also toggle between them while the selection is still <kbd>v</kbd>). You can also toggle between them while the selection is still
active. active.
### Opening URLs
While in vi mode you can open URLs using the <kbd>Enter</kbd> key. If some text
is recognized as a URL, it will be underlined once you move the vi cursor above
it. The program used to open these URLs can be changed in the [configuration
file].
## Search ## Search
Search allows you to find anything in Alacritty's scrollback buffer. You can Search allows you to find anything in Alacritty's scrollback buffer. You can
@ -61,6 +54,11 @@ start vi mode. They consist of a regex that detects these text elements and then
either feeds them to an external application or triggers one of Alacritty's either feeds them to an external application or triggers one of Alacritty's
built-in actions. built-in actions.
Hints can also be triggered using the mouse or vi mode cursor. If a hint is
enabled for mouse interaction and recognized as such, it will be underlined when
the mouse or vi mode cursor is on top of it. Using the left mouse button or
<kbd>Enter</kbd> key in vi mode will then trigger the hint.
Hints can be configured in the `hints` and `colors.hints` sections in the Hints can be configured in the `hints` and `colors.hints` sections in the
Alacritty configuration file. Alacritty configuration file.