Add vi/mouse hint highlighting support

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

9
Cargo.lock generated
View File

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# 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",
@ -1664,12 +1665,6 @@ version = "0.2.1"
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"

View File

@ -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` and `binding.mods` accept the same
# values as they do in the `key_bindings` section.
# 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.
#
# 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
#

View File

@ -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"

View File

@ -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>

View File

@ -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)]

View File

@ -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 {}

View File

@ -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.

View File

@ -1,8 +1,16 @@
use alacritty_terminal::term::search::Match;
use std::cmp::{max, min};
use glutin::event::ModifiersState;
use alacritty_terminal::grid::BidirectionalIterator;
use alacritty_terminal::index::{Boundary, Point};
use alacritty_terminal::term::search::{Match, RegexSearch};
use alacritty_terminal::term::Term;
use 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::*;

View File

@ -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.

View File

@ -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());

View File

@ -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)
Some(CursorIcon::Default)
}
}
/// 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();
}
}
/// 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)
};
/// 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);
}
// 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 {

View File

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

View File

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

View File

@ -82,7 +82,7 @@ impl<T> Term<T> {
// Limit maximum number of lines searched.
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),

View File

@ -27,13 +27,6 @@ line (<kbd>Shift</kbd> <kbd>v</kbd>) and block selection (<kbd>Ctrl</kbd>
<kbd>v</kbd>). You can also toggle between them while the selection is still
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.