aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock9
-rw-r--r--alacritty.yml58
-rw-r--r--alacritty/Cargo.toml1
-rw-r--r--alacritty/src/config/bindings.rs2
-rw-r--r--alacritty/src/config/mouse.rs38
-rw-r--r--alacritty/src/config/ui_config.rs99
-rw-r--r--alacritty/src/display/content.rs15
-rw-r--r--alacritty/src/display/hint.rs179
-rw-r--r--alacritty/src/display/mod.rs122
-rw-r--r--alacritty/src/event.rs127
-rw-r--r--alacritty/src/input.rs186
-rw-r--r--alacritty/src/main.rs1
-rw-r--r--alacritty/src/url.rs288
-rw-r--r--alacritty_terminal/src/term/search.rs4
-rw-r--r--docs/features.md12
15 files changed, 494 insertions, 647 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 505b64d2..5b77d602 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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(&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)]
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]