diff options
| -rw-r--r-- | Cargo.lock | 9 | ||||
| -rw-r--r-- | alacritty.yml | 58 | ||||
| -rw-r--r-- | alacritty/Cargo.toml | 1 | ||||
| -rw-r--r-- | alacritty/src/config/bindings.rs | 2 | ||||
| -rw-r--r-- | alacritty/src/config/mouse.rs | 38 | ||||
| -rw-r--r-- | alacritty/src/config/ui_config.rs | 99 | ||||
| -rw-r--r-- | alacritty/src/display/content.rs | 15 | ||||
| -rw-r--r-- | alacritty/src/display/hint.rs | 179 | ||||
| -rw-r--r-- | alacritty/src/display/mod.rs | 122 | ||||
| -rw-r--r-- | alacritty/src/event.rs | 127 | ||||
| -rw-r--r-- | alacritty/src/input.rs | 186 | ||||
| -rw-r--r-- | alacritty/src/main.rs | 1 | ||||
| -rw-r--r-- | alacritty/src/url.rs | 288 | ||||
| -rw-r--r-- | alacritty_terminal/src/term/search.rs | 4 | ||||
| -rw-r--r-- | docs/features.md | 12 |
15 files changed, 494 insertions, 647 deletions
@@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "ab_glyph_rasterizer" version = "0.1.4" @@ -40,7 +42,6 @@ dependencies = [ "serde_yaml", "time", "unicode-width", - "urlocator", "wayland-client", "winapi 0.3.9", "x11-dl", @@ -1665,12 +1666,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] -name = "urlocator" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e39a4f106dafb0a748b951494667a44e62b55fd7942b4fc12706d63cc535a0" - -[[package]] name = "utf8parse" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/alacritty.yml b/alacritty.yml index 6cca5b76..840c4ed9 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -445,29 +445,6 @@ # If this is `true`, the cursor is temporarily hidden when typing. #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 # # Terminal hints can be used to find text in the visible part of the terminal @@ -478,10 +455,18 @@ # 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`, `binding.mods` and `mouse.mods` accept + # 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. # - # The fields `command`, `binding.key` and `binding.mods` accept the same - # values as they do in the `key_bindings` section. + # 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`: # - Copy @@ -490,16 +475,17 @@ # Paste the hint's text to the terminal or search. # - Select # Select the hint's text. - # - # Example - # - # enabled: - # - regex: "alacritty/alacritty#\\d*" - # command: firefox - # binding: - # key: G - # mods: Control|Shift - #enabled: [] + #enabled: + # - regex: "(mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\ + # [^\u0000-\u001F\u007F-\u009F<>\" {-}\\^⟨⟩`]+" + # command: xdg-open + # post_processing: true + # mouse: + # enabled: true + # mods: None + # binding: + # key: U + # mods: Control|Shift # Mouse bindings # diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index febfbdc7..b2fcbe9d 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -29,7 +29,6 @@ glutin = { version = "0.26.0", default-features = false, features = ["serde"] } notify = "4" parking_lot = "0.11.0" crossfont = { version = "0.2.0", features = ["force_system_fontconfig"] } -urlocator = "0.1.3" copypasta = { version = "0.7.0", default-features = false } libc = "0.2" unicode-width = "0.1" diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index 91cc09d6..4e7c2fcb 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -1162,7 +1162,7 @@ impl<'a> de::Deserialize<'a> for ModsWrapper { type Value = ModsWrapper; 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> diff --git a/alacritty/src/config/mouse.rs b/alacritty/src/config/mouse.rs index 2aa7557c..aed1ab04 100644 --- a/alacritty/src/config/mouse.rs +++ b/alacritty/src/config/mouse.rs @@ -1,50 +1,12 @@ use std::time::Duration; -use glutin::event::ModifiersState; - use alacritty_config_derive::ConfigDeserialize; -use alacritty_terminal::config::Program; - -use crate::config::bindings::ModsWrapper; #[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq, Eq)] pub struct Mouse { pub double_click: ClickHandler, pub triple_click: ClickHandler, 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)] diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 3cd2ad88..ff013d57 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -21,6 +21,11 @@ use crate::config::font::Font; use crate::config::mouse::Mouse; 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)] pub struct UiConfig { /// Font configuration. @@ -90,13 +95,18 @@ impl Default for UiConfig { impl UiConfig { /// Generate key bindings for all keyboard hints. 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 { - trigger: hint.binding.key, - mods: hint.binding.mods.0, + trigger: binding.key, + mods: binding.mods.0, mode: BindingMode::empty(), notmode: BindingMode::empty(), - action: Action::Hint(hint), + action: Action::Hint(hint.clone()), }; self.key_bindings.0.push(binding); @@ -197,13 +207,42 @@ pub struct Delta<T: Default> { } /// Regex terminal hints. -#[derive(ConfigDeserialize, Default, Debug, PartialEq, Eq)] +#[derive(ConfigDeserialize, Debug, PartialEq, Eq)] pub struct Hints { /// Characters for the hint labels. alphabet: HintsAlphabet, /// 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 { @@ -271,33 +310,51 @@ pub enum HintAction { /// Hint configuration. #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] pub struct Hint { + /// Regex for finding matches. + pub regex: LazyRegex, + /// Action executed when this hint is triggered. #[serde(flatten)] pub action: HintAction, - /// Regex for finding matches. - pub regex: LazyRegex, + /// Hint text post processing. + #[serde(default)] + pub post_processing: bool, + + /// Hint mouse highlighting. + pub mouse: Option<HintMouse>, /// Binding required to search for this hint. - binding: HintBinding, + binding: Option<HintBinding>, } /// Binding for triggering a keyboard hint. #[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)] pub struct HintBinding { 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, } /// Lazy regex with interior mutability. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct LazyRegex(Rc<RefCell<LazyRegexVariant>>); impl LazyRegex { /// 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 - F: Fn(&RegexSearch) -> T, + F: FnMut(&RegexSearch) -> T, { 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. #[derive(Clone, Debug)] 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 {} diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs index e356f1f3..23a2f8a4 100644 --- a/alacritty/src/display/content.rs +++ b/alacritty/src/display/content.rs @@ -18,15 +18,12 @@ use alacritty_terminal::term::{ use crate::config::ui_config::UiConfig; use crate::display::color::{List, DIM_FACTOR}; use crate::display::hint::HintState; -use crate::display::Display; +use crate::display::{self, Display, MAX_SEARCH_LINES}; use crate::event::SearchState; /// Minimum contrast between a fixed cursor color and the cell's background. 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. /// /// 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. let cursor_point = self.terminal_cursor.point; - let line = (cursor_point.line + self.terminal_content.display_offset as i32).0 as usize; - let point = Point::new(line, cursor_point.column); + let display_offset = self.terminal_content.display_offset; + let point = display::point_to_viewport(display_offset, cursor_point).unwrap(); Some(RenderableCursor { shape: self.terminal_cursor.shape, @@ -258,8 +255,8 @@ impl RenderableCell { // Convert cell point to viewport position. let cell_point = cell.point; - let line = (cell_point.line + content.terminal_content.display_offset as i32).0 as usize; - let point = Point::new(line, cell_point.column); + let display_offset = content.terminal_content.display_offset; + let point = display::point_to_viewport(display_offset, cell_point).unwrap(); RenderableCell { 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. #[derive(Default, Clone)] -pub struct RegexMatches(Vec<RangeInclusive<Point>>); +pub struct RegexMatches(pub Vec<RangeInclusive<Point>>); impl RegexMatches { /// Find all visible matches. diff --git a/alacritty/src/display/hint.rs b/alacritty/src/display/hint.rs index 2a5e9c65..f9ab90d4 100644 --- a/alacritty/src/display/hint.rs +++ b/alacritty/src/display/hint.rs @@ -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 crate::config::ui_config::{Hint, HintAction}; +use crate::config::Config; use crate::display::content::RegexMatches; +use crate::display::MAX_SEARCH_LINES; /// Percentage of characters in the hints alphabet used for the last character. const HINT_SPLIT_PERCENTAGE: f32 = 0.5; @@ -63,7 +71,20 @@ impl HintState { }; // 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. if self.matches.is_empty() { @@ -144,6 +165,7 @@ impl HintState { } /// Hint match which was selected by the user. +#[derive(Clone)] pub struct HintMatch { /// Action for handling the text. 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(®ex_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)] mod tests { use super::*; diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index 78220b59..6e40e35c 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -2,6 +2,7 @@ //! GPU drawing. use std::cmp::min; +use std::convert::TryFrom; use std::f64; use std::fmt::{self, Formatter}; #[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::index::{Column, Direction, Line, Point}; use alacritty_terminal::selection::Selection; +use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::{SizeInfo, Term, TermMode, MIN_COLUMNS, MIN_SCREEN_LINES}; use crate::config::font::Font; @@ -38,14 +40,13 @@ use crate::display::bell::VisualBell; use crate::display::color::List; use crate::display::content::RenderableContent; use crate::display::cursor::IntoRects; -use crate::display::hint::HintState; +use crate::display::hint::{HintMatch, HintState}; use crate::display::meter::Meter; use crate::display::window::Window; use crate::event::{Mouse, SearchState}; use crate::message_bar::{MessageBuffer, MessageType}; use crate::renderer::rects::{RenderLines, RenderRect}; use crate::renderer::{self, GlyphCache, QuadRenderer}; -use crate::url::{Url, Urls}; pub mod content; pub mod cursor; @@ -58,7 +59,13 @@ mod meter; #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] 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: "; + +/// Label for the backward terminal search bar. const BACKWARD_SEARCH_LABEL: &str = "Backward Search: "; #[derive(Debug)] @@ -164,10 +171,12 @@ impl DisplayUpdate { pub struct Display { pub size_info: SizeInfo, pub window: Window, - pub urls: Urls, - /// Currently highlighted URL. - pub highlighted_url: Option<Url>, + /// Hint highlighted by the mouse. + 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))))] pub wayland_event_queue: Option<EventQueue>, @@ -331,8 +340,8 @@ impl Display { hint_state, meter: Meter::new(), size_info, - urls: Urls::new(), - highlighted_url: None, + highlighted_hint: None, + vi_highlighted_hint: None, #[cfg(not(any(target_os = "macos", windows)))] is_x11, #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))] @@ -473,8 +482,6 @@ impl Display { terminal: MutexGuard<'_, Term<T>>, message_buffer: &MessageBuffer, config: &Config, - mouse: &Mouse, - mods: ModifiersState, search_state: &SearchState, ) { // Collect renderable content before the terminal is dropped. @@ -492,10 +499,6 @@ impl Display { let metrics = self.glyph_cache.font_metrics(); 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_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None }; @@ -507,18 +510,24 @@ impl Display { }); let mut lines = RenderLines::new(); - let mut urls = Urls::new(); // Draw grid. { let _sampler = self.meter.sampler(); 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| { // Iterate over all non-empty cells in the grid. - for cell in grid_cells { - // Update URL underlines. - urls.update(&size_info, &cell); + for mut cell in grid_cells { + // Underline hints hovered by mouse or vi mode cursor. + 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. lines.update(&cell); @@ -531,33 +540,9 @@ impl Display { 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 { - // 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. + let vi_point = vi_mode_cursor.point; 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); } else if search_state.regex().is_some() { @@ -671,6 +656,47 @@ impl Display { 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. fn format_search(size_info: &SizeInfo, search_regex: &str, search_label: &str) -> String { // 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. /// /// This will return a tuple of the cell width and height. diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 341f398a..33b15928 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -44,15 +44,14 @@ use crate::clipboard::Clipboard; use crate::config::ui_config::{HintAction, HintInternalAction}; use crate::config::{self, Config}; use crate::daemon::start_daemon; -use crate::display::hint::{HintMatch, HintState}; +use crate::display::hint::HintMatch; 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}; #[cfg(target_os = "macos")] use crate::macos; use crate::message_bar::{Message, MessageBuffer}; use crate::scheduler::{Scheduler, TimerId}; -use crate::url::{Url, Urls}; /// Duration after the last user input until an unlimited search is performed. 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 || self.mouse().right_button_state == ElementState::Pressed { - let point = self.mouse().point; - let line = Line(point.line as i32) - self.terminal.grid().display_offset(); - let point = Point::new(line, point.column); + let display_offset = self.terminal.grid().display_offset(); + let point = display::viewport_to_point(display_offset, self.mouse().point); 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] - fn window(&self) -> &Window { - &self.display.window + fn window(&mut self) -> &mut Window { + &mut self.display.window } #[inline] - fn window_mut(&mut self) -> &mut Window { - &mut self.display.window + fn display(&mut self) -> &mut Display { + &mut self.display } #[inline] |