summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2021-04-13 03:24:42 +0000
committerGitHub <noreply@github.com>2021-04-13 03:24:42 +0000
commit96fc9ecc9a62c8a766da745d05fbe60e6c2e1efe (patch)
tree16e237d750d6c650713b58df98453dc4df16d33a
parent40bcdb11335cc49d4d42694b953be746cb383cb9 (diff)
downloadalacritty-96fc9ecc9a62c8a766da745d05fbe60e6c2e1efe.tar.gz
alacritty-96fc9ecc9a62c8a766da745d05fbe60e6c2e1efe.zip
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.
-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]
@@ -385,30 +383,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
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) {
*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);
@@ -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.
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;
+ }
- let HintMatch { action, bounds } = match action {
- Some(action) => action,
- None => return,
- };
+ /// Trigger a hint action.
+ fn trigger_hint(&mut self, hint: &HintMatch) {
+ if self.mouse.block_hint_launcher {
+ return;
+ }
- match action {
+ match &hint.action {
// Launch an external program.
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();
args.push(text);
start_daemon(command.program(), &args);
},
// Copy the text to the clipboard.
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);
},
// Write the text to the PTY/search.
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);
},
// Select the text.
HintAction::Action(HintInternalAction::Select) => {
- self.start_selection(SelectionType::Simple, *bounds.start(), Side::Left);
- self.update_selection(*bounds.end(), Side::Right);
+ self.start_selection(SelectionType::Simple, *hint.bounds.start(), Side::Left);
+ 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
}
- fn urls(&self) -> &Urls {
- &self.display.urls
- }
-
fn clipboard_mut(&mut self) -> &mut Clipboard {
self.clipboard
}
@@ -908,7 +879,8 @@ pub struct Mouse {
pub scroll_px: f64,
pub cell_side: Side,
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 point: Point<usize>,
pub x: usize,
@@ -925,7 +897,8 @@ impl Default for Mouse {
right_button_state: ElementState::Released,
click_state: ClickState::None,
cell_side: Side::Left,
- block_url_launcher: Default::default(),
+ hint_highlight_dirty: Default::default(),
+ block_hint_launcher: Default::default(),
inside_text_area: Default::default(),
lines_scrolled: Default::default(),
scroll_px: Default::default(),
@@ -1115,6 +1088,17 @@ impl<N: Notify + OnResize> Processor<N> {
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 {
self.dirty = false;
@@ -1127,14 +1111,7 @@ impl<N: Notify + OnResize> Processor<N> {
}
// Redraw screen.
- self.display.draw(
- terminal,
- &self.message_buffer,
- &self.config,
- &self.mouse,
- self.modifiers,
- &self.search_state,
- );
+ self.display.draw(terminal, &self.message_buffer, &self.config, &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.
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;
},
Event::Message(message) => {
@@ -1184,7 +1161,7 @@ impl<N: Notify + OnResize> Processor<N> {
TerminalEvent::Title(title) => {
let ui_config = &processor.ctx.config.ui_config;
if ui_config.window.dynamic_title {
- processor.ctx.window_mut().set_title(&title);
+ processor.ctx.window().set_title(&title);
}
},
TerminalEvent::ResetTitle => {
@@ -1198,7 +1175,7 @@ impl<N: Notify + OnResize> Processor<N> {
// Set window urgency.
if processor.ctx.terminal.mode().contains(TermMode::URGENCY_HINTS) {
let focused = processor.ctx.terminal.is_focused;
- processor.ctx.window_mut().set_urgent(!focused);
+ processor.ctx.window().set_urgent(!focused);
}
// Ring visual bell.
@@ -1251,16 +1228,16 @@ impl<N: Notify + OnResize> Processor<N> {
},
WindowEvent::ReceivedCharacter(c) => processor.received_char(c),
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.ctx.dirty = true;
},
WindowEvent::CursorMoved { position, .. } => {
- processor.ctx.window_mut().set_mouse_visible(true);
+ processor.ctx.window().set_mouse_visible(true);
processor.mouse_moved(position);
},
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);
},
WindowEvent::Focused(is_focused) => {
@@ -1269,9 +1246,9 @@ impl<N: Notify + OnResize> Processor<N> {
*processor.ctx.dirty = true;
if is_focused {
- processor.ctx.window_mut().set_urgent(false);
+ processor.ctx.window().set_urgent(false);
} else {
- processor.ctx.window_mut().set_mouse_visible(true);
+ processor.ctx.window().set_mouse_visible(true);
}
processor.ctx.update_cursor_blinking();
@@ -1285,7 +1262,7 @@ impl<N: Notify + OnResize> Processor<N> {
WindowEvent::CursorLeft { .. } => {
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;
}
},
@@ -1382,12 +1359,12 @@ impl<N: Notify + OnResize> Processor<N> {
if !config.ui_config.window.dynamic_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))))]
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.
@@ -1396,7 +1373,7 @@ impl<N: Notify + OnResize> Processor<N> {
// Disable shadows for transparent windows on 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.
processor.ctx.display.hint_state.update_alphabet(config.ui_config.hints.alphabet());
diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs
index 38a346d9..7dd47803 100644
--- a/alacritty/src/input.rs
+++ b/alacritty/src/input.rs
@@ -22,7 +22,7 @@ use glutin::window::CursorIcon;
use alacritty_terminal::ansi::{ClearMode, Handler};
use alacritty_terminal::event::EventListener;
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::term::search::Match;
use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode};
@@ -31,12 +31,12 @@ use alacritty_terminal::vi_mode::ViMotion;
use crate::clipboard::Clipboard;
use crate::config::{Action, BindingMode, Config, Key, SearchAction, ViAction};
use crate::daemon::start_daemon;
-use crate::display::hint::HintState;
+use crate::display::hint::HintMatch;
use crate::display::window::Window;
+use crate::display::{self, Display};
use crate::event::{ClickState, Event, Mouse, TYPING_SEARCH_DELAY};
use crate::message_bar::{self, Message};
use crate::scheduler::{Scheduler, TimerId};
-use crate::url::{Url, Urls};
/// Font size change interval.
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 modifiers(&mut self) -> &mut ModifiersState;
fn scroll(&mut self, _scroll: Scroll) {}
- fn window(&self) -> &Window;
- fn window_mut(&mut self) -> &mut Window;
+ fn window(&mut self) -> &mut Window;
+ fn display(&mut self) -> &mut Display;
fn terminal(&self) -> &Term<T>;
fn terminal_mut(&mut self) -> &mut Term<T>;
fn spawn_new_instance(&mut self) {}
@@ -86,9 +86,6 @@ pub trait ActionContext<T: EventListener> {
fn message(&self) -> Option<&Message>;
fn config(&self) -> &Config;
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 clipboard_mut(&mut self) -> &mut Clipboard;
fn scheduler_mut(&mut self) -> &mut Scheduler;
@@ -105,8 +102,8 @@ pub trait ActionContext<T: EventListener> {
fn search_active(&self) -> bool;
fn on_typing_start(&mut self) {}
fn toggle_vi_mode(&mut self) {}
- fn hint_state(&mut self) -> &mut HintState;
fn hint_input(&mut self, _character: char) {}
+ fn trigger_hint(&mut self, _hint: &HintMatch) {}
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::Hint(hint) => {
- ctx.hint_state().start(hint.clone());
+ ctx.display().hint_state.start(hint.clone());
ctx.mark_dirty();
},
Action::ToggleViMode => ctx.toggle_vi_mode(),
@@ -164,12 +161,12 @@ impl<T: EventListener> Execute<T> for Action {
Self::toggle_selection(ctx, SelectionType::Semantic);
},
Action::ViAction(ViAction::Open) => {
- ctx.mouse_mut().block_url_launcher = false;
- let vi_point = ctx.terminal().vi_mode_cursor.point;
- let line = (vi_point.line + ctx.terminal().grid().display_offset()).0 as usize;
- if let Some(url) = ctx.urls().find_at(Point::new(line, vi_point.column)) {
- ctx.launch_url(url);
+ let hint = ctx.display().vi_highlighted_hint.take();
+ if let Some(hint) = &hint {
+ ctx.mouse_mut().block_hint_launcher = false;
+ ctx.trigger_hint(hint);
}
+ ctx.display().vi_highlighted_hint = hint;
},
Action::ViAction(ViAction::SearchNext) => {
let terminal = ctx.terminal();
@@ -250,9 +247,9 @@ impl<T: EventListener> Execute<T> for Action {
let text = ctx.clipboard_mut().load(ClipboardType::Selection);
ctx.paste(&text);
},
- Action::ToggleFullscreen => ctx.window_mut().toggle_fullscreen(),
+ Action::ToggleFullscreen => ctx.window().toggle_fullscreen(),
#[cfg(target_os = "macos")]
- Action::ToggleSimpleFullscreen => ctx.window_mut().toggle_simple_fullscreen(),
+ Action::ToggleSimpleFullscreen => ctx.window().toggle_simple_fullscreen(),
#[cfg(target_os = "macos")]
Action::Hide => ctx.event_loop().hide_application(),
#[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> {
pub fn new(ctx: A) -> Self {
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;
- // 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 !cell_changed
&& 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().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.
- 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())
{
- let line = Line(point.line as i32) - self.ctx.terminal().grid().display_offset();
- let point = Point::new(line, point.column);
+ let display_offset = self.ctx.terminal().grid().display_offset();
+ let point = display::viewport_to_point(display_offset, point);
self.ctx.update_selection(point, cell_side);
} else if cell_changed
&& 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.
- let point = self.ctx.mouse().point;
let display_offset = self.ctx.terminal().grid().display_offset();
- let absolute_line = Line(point.line as i32) - display_offset;
- let point = Point::new(absolute_line, point.column);
+ let point = display::viewport_to_point(display_offset, self.ctx.mouse().point);
match button {
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 {
ClickState::Click => {
// 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();
@@ -631,11 +609,11 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}
},
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);
},
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);
},
ClickState::None => (),
@@ -658,10 +636,15 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
};
self.mouse_report(code, ElementState::Released);
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);
}
@@ -748,7 +731,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}
// 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
{
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 {
match state {
ElementState::Pressed => {
@@ -788,7 +771,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
/// Process key input.
pub fn key_input(&mut self, input: KeyboardInput) {
// 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;
return;
}
@@ -813,17 +796,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
pub fn modifiers_input(&mut self, modifiers: ModifiersState) {
*self.ctx.modifiers() = modifiers;
+ // Prompt hint highlight update.
+ self.ctx.mouse_mut().hint_highlight_dirty = true;
+
// 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());
+ let mouse_state = self.cursor_state();
+ self.ctx.window().set_mouse_cursor(mouse_state);
}
/// Reset mouse cursor based on modifier and terminal state.
#[inline]
pub fn reset_mouse_cursor(&mut self) {
- let mouse_state = self.mouse_state();
- self.ctx.window_mut().set_mouse_cursor(mouse_state.into());
+ let mouse_state = self.cursor_state();
+ self.ctx.window().set_mouse_cursor(mouse_state);
}
/// Process a received character.
@@ -831,7 +816,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
let suppress_chars = *self.ctx.suppress_chars();
// 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);
return;
}
@@ -925,8 +910,8 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}
}
- /// Check mouse state in relation to the message bar.
- fn message_bar_mouse_state(&self) -> Option<MouseState> {
+ /// Check mouse icon state in relation to the message bar.
+ fn message_bar_cursor_state(&self) -> Option<CursorIcon> {
// 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 };
@@ -941,53 +926,30 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
} else if mouse.y <= terminal_end + size.cell_height() as usize
&& mouse.point.column + message_bar::CLOSE_BUTTON_TEXT.len() >= size.columns()
{
- Some(MouseState::MessageBarButton)
+ Some(CursorIcon::Hand)
} else {
- Some(MouseState::MessageBar)
- }
- }
-
- /// Trigger redraw when URL highlight changed.
- #[inline]
- fn update_url_state(&mut self, mouse_state: &MouseState) {
- let highlighted_url = self.ctx.highlighted_url();
- if let MouseState::Url(url) = mouse_state {
- if Some(url) != highlighted_url {
- self.ctx.mark_dirty();
- }
- } else if highlighted_url.is_some() {
- self.ctx.mark_dirty();
+ Some(CursorIcon::Default)
}
}
- /// Location of the mouse cursor.
- fn mouse_state(&mut self) -> MouseState {
- // Check message bar before URL to ignore URLs in the message bar.
- if let Some(mouse_state) = self.message_bar_mouse_state() {
- return mouse_state;
- }
-
- 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);
- }
+ /// Icon state of the cursor.
+ fn cursor_state(&mut self) -> CursorIcon {
+ // Define function to check if mouse is on top of a hint.
+ let display_offset = self.ctx.terminal().grid().display_offset();
+ let mouse_point = self.ctx.mouse().point;
+ let hint_highlighted = |hint: &HintMatch| {
+ let point = display::viewport_to_point(display_offset, mouse_point);
+ hint.bounds.contains(&point)
+ };
- // Check mouse mode if location is not special.
- if !self.ctx.modifiers().shift() && mouse_mode {
- MouseState::Mouse
+ if let Some(mouse_state) = self.message_bar_cursor_state() {
+ mouse_state
+ } else if self.ctx.display().highlighted_hint.as_ref().map_or(false, hint_highlighted) {
+ CursorIcon::Hand
+ } else if !self.ctx.modifiers().shift() && self.ctx.mouse_mode() {
+ CursorIcon::Default
} else {
- MouseState::Text
+ CursorIcon::Text
}
}
@@ -1129,11 +1091,11 @@ mod tests {
&mut self.modifiers
}
- fn window(&self) -> &Window {
+ fn window(&mut self) -> &mut Window {
unimplemented!();
}
- fn window_mut(&mut self) -> &mut Window {
+ fn display(&mut self) -> &mut Display {
unimplemented!();
}
@@ -1157,21 +1119,9 @@ mod tests {
unimplemented!();
}
- fn urls(&self) -> &Urls {
- unimplemented!();
- }
-
- fn highlighted_url(&self) -> Option<&Url> {
- unimplemented!();
- }
-
fn scheduler_mut(&mut self) -> &mut Scheduler {
unimplemented!();
}
-
- fn hint_state(&mut self) -> &mut HintState {
- unimplemented!();
- }
}
macro_rules! test_clickstate {
diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs
index 0914aee4..c68f3cfb 100644
--- a/alacritty/src/main.rs
+++ b/alacritty/src/main.rs
@@ -45,7 +45,6 @@ mod message_bar;
mod panic;
mod renderer;
mod scheduler;
-mod url;
mod gl {
#![allow(clippy::all)]
diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs
deleted file mode 100644
index add4a1aa..00000000
--- a/alacritty/src/url.rs
+++ /dev/null
@@ -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));
- }
-}
diff --git a/alacritty_terminal/src/term/search.rs b/alacritty_terminal/src/term/search.rs
index 638df670..93345e4f 100644
--- a/alacritty_terminal/src/term/search.rs
+++ b/alacritty_terminal/src/term/search.rs
@@ -82,7 +82,7 @@ impl<T> Term<T> {
// Limit maximum number of lines searched.
end = match 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())
},
_ => end.sub(self, Boundary::None, 1),
@@ -121,7 +121,7 @@ impl<T> Term<T> {
// Limit maximum number of lines searched.
end = match 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))
},
_ => end.add(self, Boundary::None, 1),
diff --git a/docs/features.md b/docs/features.md
index 3aa87aab..fd9a9ad2 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -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
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 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
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
Alacritty configuration file.