mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-18 13:55:23 -05:00
Add vi/mouse hint highlighting support
This patch removes the old url highlighting code and replaces it with a new implementation making use of hints as sources for finding matches in the terminal.
This commit is contained in:
parent
40bcdb1133
commit
96fc9ecc9a
15 changed files with 494 additions and 647 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
# - 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: G
|
||||
# key: U
|
||||
# mods: Control|Shift
|
||||
#enabled: []
|
||||
|
||||
# Mouse bindings
|
||||
#
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
use alacritty_terminal::term::search::Match;
|
||||
use std::cmp::{max, min};
|
||||
|
||||
use glutin::event::ModifiersState;
|
||||
|
||||
use alacritty_terminal::grid::BidirectionalIterator;
|
||||
use alacritty_terminal::index::{Boundary, Point};
|
||||
use alacritty_terminal::term::search::{Match, RegexSearch};
|
||||
use alacritty_terminal::term::Term;
|
||||
|
||||
use crate::config::ui_config::{Hint, HintAction};
|
||||
use crate::config::Config;
|
||||
use crate::display::content::RegexMatches;
|
||||
use crate::display::MAX_SEARCH_LINES;
|
||||
|
||||
/// Percentage of characters in the hints alphabet used for the last character.
|
||||
const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
|
||||
|
@ -63,7 +71,20 @@ impl HintState {
|
|||
};
|
||||
|
||||
// Find visible matches.
|
||||
self.matches = hint.regex.with_compiled(|regex| RegexMatches::new(term, regex));
|
||||
self.matches.0 = hint.regex.with_compiled(|regex| {
|
||||
let mut matches = RegexMatches::new(term, regex);
|
||||
|
||||
// Apply post-processing and search for sub-matches if necessary.
|
||||
if hint.post_processing {
|
||||
matches
|
||||
.drain(..)
|
||||
.map(|rm| HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>())
|
||||
.flatten()
|
||||
.collect()
|
||||
} else {
|
||||
matches.0
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel highlight with no visible matches.
|
||||
if self.matches.is_empty() {
|
||||
|
@ -144,6 +165,7 @@ impl HintState {
|
|||
}
|
||||
|
||||
/// Hint match which was selected by the user.
|
||||
#[derive(Clone)]
|
||||
pub struct HintMatch {
|
||||
/// Action for handling the text.
|
||||
pub action: HintAction,
|
||||
|
@ -217,6 +239,159 @@ impl HintLabels {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check if there is a hint highlighted at the specified point.
|
||||
pub fn highlighted_at<T>(
|
||||
term: &Term<T>,
|
||||
config: &Config,
|
||||
point: Point,
|
||||
mouse_mods: ModifiersState,
|
||||
) -> Option<HintMatch> {
|
||||
config.ui_config.hints.enabled.iter().find_map(|hint| {
|
||||
// Check if all required modifiers are pressed.
|
||||
if hint.mouse.map_or(true, |mouse| !mouse.enabled || !mouse_mods.contains(mouse.mods.0)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
hint.regex.with_compiled(|regex| {
|
||||
// Setup search boundaries.
|
||||
let mut start = term.line_search_left(point);
|
||||
start.line = max(start.line, point.line - MAX_SEARCH_LINES);
|
||||
let mut end = term.line_search_right(point);
|
||||
end.line = min(end.line, point.line + MAX_SEARCH_LINES);
|
||||
|
||||
// Function to verify if the specified point is inside the match.
|
||||
let at_point = |rm: &Match| *rm.start() <= point && *rm.end() >= point;
|
||||
|
||||
// Check if there's any match at the specified point.
|
||||
let regex_match = term.regex_search_right(regex, start, end).filter(at_point)?;
|
||||
|
||||
// Apply post-processing and search for sub-matches if necessary.
|
||||
let regex_match = if hint.post_processing {
|
||||
HintPostProcessor::new(term, regex, regex_match).find(at_point)
|
||||
} else {
|
||||
Some(regex_match)
|
||||
};
|
||||
|
||||
regex_match.map(|bounds| HintMatch { action: hint.action.clone(), bounds })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterator over all post-processed matches inside an existing hint match.
|
||||
struct HintPostProcessor<'a, T> {
|
||||
/// Regex search DFAs.
|
||||
regex: &'a RegexSearch,
|
||||
|
||||
/// Terminal reference.
|
||||
term: &'a Term<T>,
|
||||
|
||||
/// Next hint match in the iterator.
|
||||
next_match: Option<Match>,
|
||||
|
||||
/// Start point for the next search.
|
||||
start: Point,
|
||||
|
||||
/// End point for the hint match iterator.
|
||||
end: Point,
|
||||
}
|
||||
|
||||
impl<'a, T> HintPostProcessor<'a, T> {
|
||||
/// Create a new iterator for an unprocessed match.
|
||||
fn new(term: &'a Term<T>, regex: &'a RegexSearch, regex_match: Match) -> Self {
|
||||
let end = *regex_match.end();
|
||||
let mut post_processor = Self { next_match: None, start: end, end, term, regex };
|
||||
|
||||
// Post-process the first hint match.
|
||||
let next_match = post_processor.hint_post_processing(®ex_match);
|
||||
post_processor.start = next_match.end().add(term, Boundary::Grid, 1);
|
||||
post_processor.next_match = Some(next_match);
|
||||
|
||||
post_processor
|
||||
}
|
||||
|
||||
/// Apply some hint post processing heuristics.
|
||||
///
|
||||
/// This will check the end of the hint and make it shorter if certain characters are determined
|
||||
/// to be unlikely to be intentionally part of the hint.
|
||||
///
|
||||
/// This is most useful for identifying URLs appropriately.
|
||||
fn hint_post_processing(&self, regex_match: &Match) -> Match {
|
||||
let mut iter = self.term.grid().iter_from(*regex_match.start());
|
||||
|
||||
let mut c = iter.cell().c;
|
||||
|
||||
// Truncate uneven number of brackets.
|
||||
let end = *regex_match.end();
|
||||
let mut open_parents = 0;
|
||||
let mut open_brackets = 0;
|
||||
loop {
|
||||
match c {
|
||||
'(' => open_parents += 1,
|
||||
'[' => open_brackets += 1,
|
||||
')' => {
|
||||
if open_parents == 0 {
|
||||
iter.prev();
|
||||
break;
|
||||
} else {
|
||||
open_parents -= 1;
|
||||
}
|
||||
},
|
||||
']' => {
|
||||
if open_brackets == 0 {
|
||||
iter.prev();
|
||||
break;
|
||||
} else {
|
||||
open_brackets -= 1;
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if iter.point() == end {
|
||||
break;
|
||||
}
|
||||
|
||||
match iter.next() {
|
||||
Some(indexed) => c = indexed.cell.c,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate trailing characters which are likely to be delimiters.
|
||||
let start = *regex_match.start();
|
||||
while iter.point() != start {
|
||||
if !matches!(c, '.' | ',' | ':' | ';' | '?' | '!' | '(' | '[' | '\'') {
|
||||
break;
|
||||
}
|
||||
|
||||
match iter.prev() {
|
||||
Some(indexed) => c = indexed.cell.c,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
start..=iter.point()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for HintPostProcessor<'a, T> {
|
||||
type Item = Match;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let next_match = self.next_match.take()?;
|
||||
|
||||
if self.start <= self.end {
|
||||
if let Some(rm) = self.term.regex_search_right(self.regex, self.start, self.end) {
|
||||
let regex_match = self.hint_post_processing(&rm);
|
||||
self.start = regex_match.end().add(self.term, Boundary::Grid, 1);
|
||||
self.next_match = Some(regex_match);
|
||||
}
|
||||
}
|
||||
|
||||
Some(next_match)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -45,7 +45,6 @@ mod message_bar;
|
|||
mod panic;
|
||||
mod renderer;
|
||||
mod scheduler;
|
||||
mod url;
|
||||
|
||||
mod gl {
|
||||
#![allow(clippy::all)]
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Reference in a new issue