mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-25 14:05:41 -05:00
Add support for hyperlink escape sequence
This commit adds support for hyperlink escape sequence `OSC 8 ; params ; URI ST`. The configuration option responsible for those is `hints.enabled.hyperlinks`. Fixes #922.
This commit is contained in:
parent
8451b75689
commit
694a52bcff
23 changed files with 1054 additions and 262 deletions
|
@ -22,6 +22,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Vi mode keybinding (z) to center view around vi mode cursor
|
||||
- Accept hexadecimal values starting with `0x` for `--embed`
|
||||
- Config option `cursor.blink_timeout` to timeout cursor blinking after inactivity
|
||||
- Escape sequence to set hyperlinks (`OSC 8 ; params ; URI ST`)
|
||||
- Config `hints.enabled.hyperlinks` for hyperlink escape sequence hint highlight
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -29,6 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- OSC 52 is now disabled on unfocused windows
|
||||
- `SpawnNewInstance` no longer inherits initial `--command`
|
||||
- Blinking cursor will timeout after `5` seconds by default
|
||||
- Deprecated `colors.search.bar`, use `colors.footer_bar` instead
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -237,11 +237,7 @@
|
|||
# foreground: '#ffffff'
|
||||
# background: '#000000'
|
||||
|
||||
#bar:
|
||||
# background: '#c5c8c6'
|
||||
# foreground: '#1d1f21'
|
||||
|
||||
# Keyboard regex hints
|
||||
# Keyboard hints
|
||||
#hints:
|
||||
# First character in the hint label
|
||||
#
|
||||
|
@ -269,6 +265,15 @@
|
|||
# foreground: None
|
||||
# background: None
|
||||
|
||||
# Footer bar
|
||||
#
|
||||
# Color used for the footer bar on the bottom, used by search regex input,
|
||||
# hyperlink URI preview, etc.
|
||||
#
|
||||
#footer_bar:
|
||||
# background: '#c5c8c6'
|
||||
# foreground: '#1d1f21'
|
||||
|
||||
# Selection colors
|
||||
#
|
||||
# Colors which should be used to draw the selection area.
|
||||
|
@ -467,18 +472,22 @@
|
|||
# If this is `true`, the cursor is temporarily hidden when typing.
|
||||
#hide_when_typing: false
|
||||
|
||||
# Regex hints
|
||||
# Hints
|
||||
#
|
||||
# Terminal hints can be used to find text in the visible part of the terminal
|
||||
# and pipe it to other applications.
|
||||
# Terminal hints can be used to find text or hyperlink in the visible part of
|
||||
# the terminal and pipe it to other applications.
|
||||
#hints:
|
||||
# Keys used for the hint labels.
|
||||
#alphabet: "jfkdls;ahgurieowpq"
|
||||
|
||||
# List with all available hints
|
||||
#
|
||||
# Each hint must have a `regex` and either an `action` or a `command` field.
|
||||
# The fields `mouse`, `binding` and `post_processing` are optional.
|
||||
# Each hint must have any of `regex` or `hyperlinks` field and either an
|
||||
# `action` or a `command` field. The fields `mouse`, `binding` and
|
||||
# `post_processing` are optional.
|
||||
#
|
||||
# The `hyperlinks` option will cause OSC 8 escape sequence hyperlinks to be
|
||||
# highlighted.
|
||||
#
|
||||
# The fields `command`, `binding.key`, `binding.mods`, `binding.mode` and
|
||||
# `mouse.mods` accept the same values as they do in the `key_bindings` section.
|
||||
|
@ -488,7 +497,8 @@
|
|||
#
|
||||
# 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.
|
||||
# (e.g. a trailing `.`). This is most useful for URIs and applies only to
|
||||
# `regex` matches.
|
||||
#
|
||||
# Values for `action`:
|
||||
# - Copy
|
||||
|
@ -502,6 +512,7 @@
|
|||
#enabled:
|
||||
# - regex: "(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)\
|
||||
# [^\u0000-\u001F\u007F-\u009F<>\"\\s{-}\\^⟨⟩`]+"
|
||||
# hyperlinks: true
|
||||
# command: xdg-open
|
||||
# post_processing: true
|
||||
# mouse:
|
||||
|
|
|
@ -18,15 +18,16 @@ pub struct Colors {
|
|||
pub line_indicator: LineIndicatorColors,
|
||||
pub hints: HintColors,
|
||||
pub transparent_background_colors: bool,
|
||||
footer_bar: BarColors,
|
||||
}
|
||||
|
||||
impl Colors {
|
||||
pub fn search_bar_foreground(&self) -> Rgb {
|
||||
self.search.bar.foreground.unwrap_or(self.primary.background)
|
||||
pub fn footer_bar_foreground(&self) -> Rgb {
|
||||
self.search.bar.foreground.or(self.footer_bar.foreground).unwrap_or(self.primary.background)
|
||||
}
|
||||
|
||||
pub fn search_bar_background(&self) -> Rgb {
|
||||
self.search.bar.background.unwrap_or(self.primary.foreground)
|
||||
pub fn footer_bar_background(&self) -> Rgb {
|
||||
self.search.bar.background.or(self.footer_bar.background).unwrap_or(self.primary.foreground)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,6 +126,7 @@ impl Default for InvertedCellColors {
|
|||
pub struct SearchColors {
|
||||
pub focused_match: FocusedMatchColors,
|
||||
pub matches: MatchColors,
|
||||
#[config(deprecated = "use `colors.footer_bar` instead")]
|
||||
bar: BarColors,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use std::cell::RefCell;
|
||||
use std::fmt::{self, Formatter};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use glutin::event::{ModifiersState, VirtualKeyCode};
|
||||
use log::error;
|
||||
use serde::de::Error as SerdeError;
|
||||
use serde::de::{Error as SerdeError, MapAccess, Visitor};
|
||||
use serde::{self, Deserialize, Deserializer};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
|
@ -236,6 +237,7 @@ impl Default for Hints {
|
|||
// 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)));
|
||||
let content = HintContent::new(Some(regex), true);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
let action = HintAction::Command(Program::Just(String::from("xdg-open")));
|
||||
|
@ -249,7 +251,7 @@ impl Default for Hints {
|
|||
|
||||
Self {
|
||||
enabled: vec![Hint {
|
||||
regex,
|
||||
content,
|
||||
action,
|
||||
post_processing: true,
|
||||
mouse: Some(HintMouse { enabled: true, mods: Default::default() }),
|
||||
|
@ -332,7 +334,8 @@ pub enum HintAction {
|
|||
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Hint {
|
||||
/// Regex for finding matches.
|
||||
pub regex: LazyRegex,
|
||||
#[serde(flatten)]
|
||||
pub content: HintContent,
|
||||
|
||||
/// Action executed when this hint is triggered.
|
||||
#[serde(flatten)]
|
||||
|
@ -349,6 +352,80 @@ pub struct Hint {
|
|||
binding: Option<HintBinding>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct HintContent {
|
||||
/// Regex for finding matches.
|
||||
pub regex: Option<LazyRegex>,
|
||||
|
||||
/// Escape sequence hyperlinks.
|
||||
pub hyperlinks: bool,
|
||||
}
|
||||
|
||||
impl HintContent {
|
||||
pub fn new(regex: Option<LazyRegex>, hyperlinks: bool) -> Self {
|
||||
Self { regex, hyperlinks }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for HintContent {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct HintContentVisitor;
|
||||
impl<'a> Visitor<'a> for HintContentVisitor {
|
||||
type Value = HintContent;
|
||||
|
||||
fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("a mapping")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'a>,
|
||||
{
|
||||
let mut content = Self::Value::default();
|
||||
|
||||
while let Some((key, value)) = map.next_entry::<String, serde_yaml::Value>()? {
|
||||
match key.as_str() {
|
||||
"regex" => match Option::<LazyRegex>::deserialize(value) {
|
||||
Ok(regex) => content.regex = regex,
|
||||
Err(err) => {
|
||||
error!(
|
||||
target: LOG_TARGET_CONFIG,
|
||||
"Config error: hint's regex: {}", err
|
||||
);
|
||||
},
|
||||
},
|
||||
"hyperlinks" => match bool::deserialize(value) {
|
||||
Ok(hyperlink) => content.hyperlinks = hyperlink,
|
||||
Err(err) => {
|
||||
error!(
|
||||
target: LOG_TARGET_CONFIG,
|
||||
"Config error: hint's hyperlinks: {}", err
|
||||
);
|
||||
},
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Require at least one of hyperlinks or regex trigger hint matches.
|
||||
if content.regex.is_none() && !content.hyperlinks {
|
||||
return Err(M::Error::custom(
|
||||
"Config error: At least on of the hint's regex or hint's hyperlinks must \
|
||||
be set",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(HintContentVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// Binding for triggering a keyboard hint.
|
||||
#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct HintBinding {
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
use std::borrow::Cow;
|
||||
use std::cmp::{max, min};
|
||||
use std::mem;
|
||||
use std::ops::{Deref, DerefMut, RangeInclusive};
|
||||
use std::ops::Deref;
|
||||
use std::{cmp, mem};
|
||||
|
||||
use alacritty_terminal::ansi::{Color, CursorShape, NamedColor};
|
||||
use alacritty_terminal::event::EventListener;
|
||||
use alacritty_terminal::grid::{Dimensions, Indexed};
|
||||
use alacritty_terminal::index::{Column, Direction, Line, Point};
|
||||
use alacritty_terminal::grid::Indexed;
|
||||
use alacritty_terminal::index::{Column, Line, Point};
|
||||
use alacritty_terminal::selection::SelectionRange;
|
||||
use alacritty_terminal::term::cell::{Cell, Flags};
|
||||
use alacritty_terminal::term::cell::{Cell, Flags, Hyperlink};
|
||||
use alacritty_terminal::term::color::{CellRgb, Rgb};
|
||||
use alacritty_terminal::term::search::{Match, RegexIter, RegexSearch};
|
||||
use alacritty_terminal::term::search::{Match, RegexSearch};
|
||||
use alacritty_terminal::term::{self, RenderableContent as TerminalContent, Term, TermMode};
|
||||
|
||||
use crate::config::UiConfig;
|
||||
use crate::display::color::{List, DIM_FACTOR};
|
||||
use crate::display::hint::HintState;
|
||||
use crate::display::{Display, MAX_SEARCH_LINES};
|
||||
use crate::display::hint::{self, HintState};
|
||||
use crate::display::Display;
|
||||
use crate::event::SearchState;
|
||||
|
||||
/// Minimum contrast between a fixed cursor color and the cell's background.
|
||||
|
@ -30,7 +29,7 @@ pub struct RenderableContent<'a> {
|
|||
cursor: RenderableCursor,
|
||||
cursor_shape: CursorShape,
|
||||
cursor_point: Point<usize>,
|
||||
search: Option<Regex<'a>>,
|
||||
search: Option<HintMatches<'a>>,
|
||||
hint: Option<Hint<'a>>,
|
||||
config: &'a UiConfig,
|
||||
colors: &'a List,
|
||||
|
@ -44,7 +43,7 @@ impl<'a> RenderableContent<'a> {
|
|||
term: &'a Term<T>,
|
||||
search_state: &'a SearchState,
|
||||
) -> Self {
|
||||
let search = search_state.dfas().map(|dfas| Regex::new(term, dfas));
|
||||
let search = search_state.dfas().map(|dfas| HintMatches::visible_regex_matches(term, dfas));
|
||||
let focused_match = search_state.focused_match();
|
||||
let terminal_content = term.renderable_content();
|
||||
|
||||
|
@ -181,13 +180,21 @@ impl<'a> Iterator for RenderableContent<'a> {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct RenderableCell {
|
||||
pub character: char,
|
||||
pub zerowidth: Option<Vec<char>>,
|
||||
pub point: Point<usize>,
|
||||
pub fg: Rgb,
|
||||
pub bg: Rgb,
|
||||
pub bg_alpha: f32,
|
||||
pub underline: Rgb,
|
||||
pub flags: Flags,
|
||||
pub extra: Option<Box<RenderableCellExtra>>,
|
||||
}
|
||||
|
||||
/// Extra storage with rarely present fields for [`RenderableCell`], to reduce the cell size we
|
||||
/// pass around.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RenderableCellExtra {
|
||||
pub zerowidth: Option<Vec<char>>,
|
||||
pub hyperlink: Option<Hyperlink>,
|
||||
}
|
||||
|
||||
impl RenderableCell {
|
||||
|
@ -257,23 +264,24 @@ impl RenderableCell {
|
|||
.underline_color()
|
||||
.map_or(fg, |underline| Self::compute_fg_rgb(content, underline, flags));
|
||||
|
||||
RenderableCell {
|
||||
zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()),
|
||||
flags,
|
||||
character,
|
||||
bg_alpha,
|
||||
point,
|
||||
fg,
|
||||
bg,
|
||||
underline,
|
||||
}
|
||||
let zerowidth = cell.zerowidth();
|
||||
let hyperlink = cell.hyperlink();
|
||||
|
||||
let extra = (zerowidth.is_some() || hyperlink.is_some()).then(|| {
|
||||
Box::new(RenderableCellExtra {
|
||||
zerowidth: zerowidth.map(|zerowidth| zerowidth.to_vec()),
|
||||
hyperlink,
|
||||
})
|
||||
});
|
||||
|
||||
RenderableCell { flags, character, bg_alpha, point, fg, bg, underline, extra }
|
||||
}
|
||||
|
||||
/// Check if cell contains any renderable content.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.bg_alpha == 0.
|
||||
&& self.character == ' '
|
||||
&& self.zerowidth.is_none()
|
||||
&& self.extra.is_none()
|
||||
&& !self.flags.intersects(Flags::ALL_UNDERLINES | Flags::STRIKEOUT)
|
||||
}
|
||||
|
||||
|
@ -406,7 +414,7 @@ impl RenderableCursor {
|
|||
/// Regex hints for keyboard shortcuts.
|
||||
struct Hint<'a> {
|
||||
/// Hint matches and position.
|
||||
regex: Regex<'a>,
|
||||
matches: HintMatches<'a>,
|
||||
|
||||
/// Last match checked against current cell position.
|
||||
labels: &'a Vec<Vec<char>>,
|
||||
|
@ -421,16 +429,15 @@ impl<'a> Hint<'a> {
|
|||
/// The tuple's [`bool`] will be `true` when the character is the first for this hint.
|
||||
fn advance(&mut self, viewport_start: Point, point: Point) -> Option<(char, bool)> {
|
||||
// Check if we're within a match at all.
|
||||
if !self.regex.advance(point) {
|
||||
if !self.matches.advance(point) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Match starting position on this line; linebreaks interrupt the hint labels.
|
||||
let start = self
|
||||
.regex
|
||||
.matches
|
||||
.get(self.regex.index)
|
||||
.map(|regex_match| max(*regex_match.start(), viewport_start))
|
||||
.get(self.matches.index)
|
||||
.map(|bounds| cmp::max(*bounds.start(), viewport_start))
|
||||
.filter(|start| start.line == point.line)?;
|
||||
|
||||
// Position within the hint label.
|
||||
|
@ -438,85 +445,47 @@ impl<'a> Hint<'a> {
|
|||
let is_first = label_position == 0;
|
||||
|
||||
// Hint label character.
|
||||
self.labels[self.regex.index].get(label_position).copied().map(|c| (c, is_first))
|
||||
self.labels[self.matches.index].get(label_position).copied().map(|c| (c, is_first))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a HintState> for Hint<'a> {
|
||||
fn from(hint_state: &'a HintState) -> Self {
|
||||
let regex = Regex { matches: Cow::Borrowed(hint_state.matches()), index: 0 };
|
||||
Self { labels: hint_state.labels(), regex }
|
||||
let matches = HintMatches::new(hint_state.matches());
|
||||
Self { labels: hint_state.labels(), matches }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for finding visible regex matches.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct RegexMatches(pub Vec<RangeInclusive<Point>>);
|
||||
|
||||
impl RegexMatches {
|
||||
/// Find all visible matches.
|
||||
pub fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
|
||||
let viewport_start = Line(-(term.grid().display_offset() as i32));
|
||||
let viewport_end = viewport_start + term.bottommost_line();
|
||||
|
||||
// Compute start of the first and end of the last line.
|
||||
let start_point = Point::new(viewport_start, Column(0));
|
||||
let mut start = term.line_search_left(start_point);
|
||||
let end_point = Point::new(viewport_end, term.last_column());
|
||||
let mut end = term.line_search_right(end_point);
|
||||
|
||||
// Set upper bound on search before/after the viewport to prevent excessive blocking.
|
||||
start.line = max(start.line, viewport_start - MAX_SEARCH_LINES);
|
||||
end.line = min(end.line, viewport_end + MAX_SEARCH_LINES);
|
||||
|
||||
// Create an iterater for the current regex search for all visible matches.
|
||||
let iter = RegexIter::new(start, end, Direction::Right, term, dfas)
|
||||
.skip_while(move |rm| rm.end().line < viewport_start)
|
||||
.take_while(move |rm| rm.start().line <= viewport_end);
|
||||
|
||||
Self(iter.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RegexMatches {
|
||||
type Target = Vec<RangeInclusive<Point>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for RegexMatches {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Visible regex match tracking.
|
||||
/// Visible hint match tracking.
|
||||
#[derive(Default)]
|
||||
struct Regex<'a> {
|
||||
struct HintMatches<'a> {
|
||||
/// All visible matches.
|
||||
matches: Cow<'a, RegexMatches>,
|
||||
matches: Cow<'a, [Match]>,
|
||||
|
||||
/// Index of the last match checked.
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Regex<'a> {
|
||||
/// Create a new renderable regex iterator.
|
||||
fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
|
||||
let matches = Cow::Owned(RegexMatches::new(term, dfas));
|
||||
Self { index: 0, matches }
|
||||
impl<'a> HintMatches<'a> {
|
||||
/// Create new renderable matches iterator..
|
||||
fn new(matches: impl Into<Cow<'a, [Match]>>) -> Self {
|
||||
Self { matches: matches.into(), index: 0 }
|
||||
}
|
||||
|
||||
/// Create from regex matches on term visable part.
|
||||
fn visible_regex_matches<T>(term: &Term<T>, dfas: &RegexSearch) -> Self {
|
||||
let matches = hint::visible_regex_match_iter(term, dfas).collect::<Vec<_>>();
|
||||
Self::new(matches)
|
||||
}
|
||||
|
||||
/// Advance the regex tracker to the next point.
|
||||
///
|
||||
/// This will return `true` if the point passed is part of a regex match.
|
||||
fn advance(&mut self, point: Point) -> bool {
|
||||
while let Some(regex_match) = self.matches.get(self.index) {
|
||||
if regex_match.start() > &point {
|
||||
while let Some(bounds) = self.get(self.index) {
|
||||
if bounds.start() > &point {
|
||||
break;
|
||||
} else if regex_match.end() < &point {
|
||||
} else if bounds.end() < &point {
|
||||
self.index += 1;
|
||||
} else {
|
||||
return true;
|
||||
|
@ -525,3 +494,11 @@ impl<'a> Regex<'a> {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for HintMatches<'a> {
|
||||
type Target = [Match];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.matches.deref()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
use std::cmp::{max, min};
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::HashSet;
|
||||
use std::iter;
|
||||
|
||||
use glutin::event::ModifiersState;
|
||||
|
||||
use alacritty_terminal::grid::BidirectionalIterator;
|
||||
use alacritty_terminal::index::{Boundary, Direction, Point};
|
||||
use alacritty_terminal::grid::{BidirectionalIterator, Dimensions};
|
||||
use alacritty_terminal::index::{Boundary, Column, Direction, Line, Point};
|
||||
use alacritty_terminal::term::cell::Hyperlink;
|
||||
use alacritty_terminal::term::search::{Match, RegexIter, RegexSearch};
|
||||
use alacritty_terminal::term::{Term, TermMode};
|
||||
|
||||
use crate::config::ui_config::{Hint, HintAction};
|
||||
use crate::config::UiConfig;
|
||||
use crate::display::content::RegexMatches;
|
||||
use crate::display::MAX_SEARCH_LINES;
|
||||
|
||||
/// Maximum number of linewraps followed outside of the viewport during search highlighting.
|
||||
pub const MAX_SEARCH_LINES: usize = 100;
|
||||
|
||||
/// Percentage of characters in the hints alphabet used for the last character.
|
||||
const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
|
||||
|
@ -24,7 +28,7 @@ pub struct HintState {
|
|||
alphabet: String,
|
||||
|
||||
/// Visible matches.
|
||||
matches: RegexMatches,
|
||||
matches: Vec<Match>,
|
||||
|
||||
/// Key label for each visible match.
|
||||
labels: Vec<Vec<char>>,
|
||||
|
@ -70,20 +74,29 @@ impl HintState {
|
|||
None => return,
|
||||
};
|
||||
|
||||
// Find visible matches.
|
||||
self.matches.0 = hint.regex.with_compiled(|regex| {
|
||||
let mut matches = RegexMatches::new(term, regex);
|
||||
// Clear current matches.
|
||||
self.matches.clear();
|
||||
|
||||
// Apply post-processing and search for sub-matches if necessary.
|
||||
if hint.post_processing {
|
||||
matches
|
||||
.drain(..)
|
||||
.flat_map(|rm| HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>())
|
||||
.collect()
|
||||
} else {
|
||||
matches.0
|
||||
}
|
||||
});
|
||||
// Add escape sequence hyperlinks.
|
||||
if hint.content.hyperlinks {
|
||||
self.matches.extend(visible_unique_hyperlink_iter(term));
|
||||
}
|
||||
|
||||
// Add visible regex matches.
|
||||
if let Some(regex) = hint.content.regex.as_ref() {
|
||||
regex.with_compiled(|regex| {
|
||||
let matches = visible_regex_match_iter(term, regex);
|
||||
|
||||
// Apply post-processing and search for sub-matches if necessary.
|
||||
if hint.post_processing {
|
||||
self.matches.extend(matches.flat_map(|rm| {
|
||||
HintPostProcessor::new(term, regex, rm).collect::<Vec<_>>()
|
||||
}));
|
||||
} else {
|
||||
self.matches.extend(matches);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel highlight with no visible matches.
|
||||
if self.matches.is_empty() {
|
||||
|
@ -91,6 +104,10 @@ impl HintState {
|
|||
return;
|
||||
}
|
||||
|
||||
// Sort and dedup ranges. Currently overlapped but not exactly same ranges are kept.
|
||||
self.matches.sort_by_key(|bounds| (*bounds.start(), Reverse(*bounds.end())));
|
||||
self.matches.dedup_by_key(|bounds| *bounds.start());
|
||||
|
||||
let mut generator = HintLabels::new(&self.alphabet, HINT_SPLIT_PERCENTAGE);
|
||||
let match_count = self.matches.len();
|
||||
let keys_len = self.keys.len();
|
||||
|
@ -135,7 +152,9 @@ impl HintState {
|
|||
|
||||
self.stop();
|
||||
|
||||
Some(HintMatch { action, bounds })
|
||||
// Hyperlinks take precedence over regex matches.
|
||||
let hyperlink = term.grid()[*bounds.start()].hyperlink();
|
||||
Some(HintMatch { action, bounds, hyperlink })
|
||||
} else {
|
||||
// Store character to preserve the selection.
|
||||
self.keys.push(c);
|
||||
|
@ -150,7 +169,7 @@ impl HintState {
|
|||
}
|
||||
|
||||
/// Visible hint regex matches.
|
||||
pub fn matches(&self) -> &RegexMatches {
|
||||
pub fn matches(&self) -> &[Match] {
|
||||
&self.matches
|
||||
}
|
||||
|
||||
|
@ -167,10 +186,33 @@ impl HintState {
|
|||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub struct HintMatch {
|
||||
/// Action for handling the text.
|
||||
pub action: HintAction,
|
||||
action: HintAction,
|
||||
|
||||
/// Terminal range matching the hint.
|
||||
pub bounds: Match,
|
||||
bounds: Match,
|
||||
|
||||
hyperlink: Option<Hyperlink>,
|
||||
}
|
||||
|
||||
impl HintMatch {
|
||||
#[inline]
|
||||
pub fn should_highlight(&self, point: Point, pointed_hyperlink: Option<&Hyperlink>) -> bool {
|
||||
self.bounds.contains(&point) && self.hyperlink.as_ref() == pointed_hyperlink
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn action(&self) -> &HintAction {
|
||||
&self.action
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bounds(&self) -> &Match {
|
||||
&self.bounds
|
||||
}
|
||||
|
||||
pub fn hyperlink(&self) -> Option<&Hyperlink> {
|
||||
self.hyperlink.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generator for creating new hint labels.
|
||||
|
@ -238,6 +280,77 @@ impl HintLabels {
|
|||
}
|
||||
}
|
||||
|
||||
/// Iterate over all visible regex matches.
|
||||
pub fn visible_regex_match_iter<'a, T>(
|
||||
term: &'a Term<T>,
|
||||
regex: &'a RegexSearch,
|
||||
) -> impl Iterator<Item = Match> + 'a {
|
||||
let viewport_start = Line(-(term.grid().display_offset() as i32));
|
||||
let viewport_end = viewport_start + term.bottommost_line();
|
||||
let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
|
||||
let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
|
||||
start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
|
||||
end.line = end.line.min(viewport_start + MAX_SEARCH_LINES);
|
||||
|
||||
RegexIter::new(start, end, Direction::Right, term, regex)
|
||||
.skip_while(move |rm| rm.end().line < viewport_start)
|
||||
.take_while(move |rm| rm.start().line <= viewport_end)
|
||||
}
|
||||
|
||||
/// Iterate over all visible hyperlinks, yanking only unique ones.
|
||||
pub fn visible_unique_hyperlink_iter<T>(term: &Term<T>) -> impl Iterator<Item = Match> + '_ {
|
||||
let mut display_iter = term.grid().display_iter().peekable();
|
||||
|
||||
// Avoid creating hints for the same hyperlinks, but from a different places.
|
||||
let mut unique_hyperlinks = HashSet::new();
|
||||
|
||||
iter::from_fn(move || {
|
||||
// Find the start of the next unique hyperlink.
|
||||
let (cell, hyperlink) = display_iter.find_map(|cell| {
|
||||
let hyperlink = cell.hyperlink()?;
|
||||
unique_hyperlinks.contains(&hyperlink).then(|| {
|
||||
unique_hyperlinks.insert(hyperlink.clone());
|
||||
(cell, hyperlink)
|
||||
})
|
||||
})?;
|
||||
|
||||
let start = cell.point;
|
||||
let mut end = start;
|
||||
|
||||
// Find the end bound of just found unique hyperlink.
|
||||
while let Some(next_cell) = display_iter.peek() {
|
||||
// Cell at display iter doesn't match, yield the hyperlink and start over with
|
||||
// `find_map`.
|
||||
if next_cell.hyperlink().as_ref() != Some(&hyperlink) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Advance to the next cell.
|
||||
end = next_cell.point;
|
||||
let _ = display_iter.next();
|
||||
}
|
||||
|
||||
Some(start..=end)
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve the match, if the specified point is inside the content matching the regex.
|
||||
fn regex_match_at<T>(
|
||||
term: &Term<T>,
|
||||
point: Point,
|
||||
regex: &RegexSearch,
|
||||
post_processing: bool,
|
||||
) -> Option<Match> {
|
||||
let regex_match = visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))?;
|
||||
|
||||
// Apply post-processing and search for sub-matches if necessary.
|
||||
if post_processing {
|
||||
HintPostProcessor::new(term, regex, regex_match).find(|rm| rm.contains(&point))
|
||||
} else {
|
||||
Some(regex_match)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if there is a hint highlighted at the specified point.
|
||||
pub fn highlighted_at<T>(
|
||||
term: &Term<T>,
|
||||
|
@ -258,32 +371,75 @@ pub fn highlighted_at<T>(
|
|||
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);
|
||||
if let Some((hyperlink, bounds)) =
|
||||
hint.content.hyperlinks.then(|| hyperlink_at(term, point)).flatten()
|
||||
{
|
||||
return Some(HintMatch {
|
||||
bounds,
|
||||
action: hint.action.clone(),
|
||||
hyperlink: Some(hyperlink),
|
||||
});
|
||||
}
|
||||
|
||||
// Function to verify that the specified point is inside the match.
|
||||
let at_point = |rm: &Match| *rm.end() >= point && *rm.start() <= point;
|
||||
if let Some(bounds) = hint.content.regex.as_ref().and_then(|regex| {
|
||||
regex.with_compiled(|regex| regex_match_at(term, point, regex, hint.post_processing))
|
||||
}) {
|
||||
return Some(HintMatch { bounds, action: hint.action.clone(), hyperlink: None });
|
||||
}
|
||||
|
||||
// Check if there's any match at the specified point.
|
||||
let mut iter = RegexIter::new(start, end, Direction::Right, term, regex);
|
||||
let regex_match = iter.find(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 })
|
||||
})
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve the hyperlink with its range, if there is one at the specified point.
|
||||
fn hyperlink_at<T>(term: &Term<T>, point: Point) -> Option<(Hyperlink, Match)> {
|
||||
let hyperlink = term.grid()[point].hyperlink()?;
|
||||
|
||||
let viewport_start = Line(-(term.grid().display_offset() as i32));
|
||||
let viewport_end = viewport_start + term.bottommost_line();
|
||||
|
||||
let mut match_start = Point::new(point.line, Column(0));
|
||||
let mut match_end = Point::new(point.line, Column(term.columns() - 1));
|
||||
let grid = term.grid();
|
||||
|
||||
// Find adjacent lines that have the same `hyperlink`. The end purpose to highlight hyperlinks
|
||||
// that span across multiple lines or not directly attached to each other.
|
||||
|
||||
// Find the closest to the viewport start adjucent line.
|
||||
while match_start.line > viewport_start {
|
||||
let next_line = match_start.line - 1i32;
|
||||
// Iterate over all the cells in the grid's line and check if any of those cells contains
|
||||
// the hyperlink we've found at original `point`.
|
||||
let line_contains_hyperlink = grid[next_line]
|
||||
.into_iter()
|
||||
.any(|cell| cell.hyperlink().map(|h| h == hyperlink).unwrap_or(false));
|
||||
|
||||
// There's no hyperlink on the next line, break.
|
||||
if !line_contains_hyperlink {
|
||||
break;
|
||||
}
|
||||
|
||||
match_start.line = next_line;
|
||||
}
|
||||
|
||||
// Ditto for the end.
|
||||
while match_end.line < viewport_end {
|
||||
let next_line = match_end.line + 1i32;
|
||||
|
||||
let line_contains_hyperlink = grid[next_line]
|
||||
.into_iter()
|
||||
.any(|cell| cell.hyperlink().map(|h| h == hyperlink).unwrap_or(false));
|
||||
|
||||
if !line_contains_hyperlink {
|
||||
break;
|
||||
}
|
||||
|
||||
match_end.line = next_line;
|
||||
}
|
||||
|
||||
Some((hyperlink, match_start..=match_end))
|
||||
}
|
||||
|
||||
/// Iterator over all post-processed matches inside an existing hint match.
|
||||
struct HintPostProcessor<'a, T> {
|
||||
/// Regex search DFAs.
|
||||
|
|
|
@ -16,7 +16,6 @@ use glutin::Rect as DamageRect;
|
|||
use log::{debug, info};
|
||||
use parking_lot::MutexGuard;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
#[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
|
||||
use wayland_client::EventQueue;
|
||||
|
||||
|
@ -49,6 +48,7 @@ use crate::event::{Mouse, SearchState};
|
|||
use crate::message_bar::{MessageBuffer, MessageType};
|
||||
use crate::renderer::rects::{RenderLines, RenderRect};
|
||||
use crate::renderer::{self, GlyphCache, Renderer};
|
||||
use crate::string::{ShortenDirection, StrShortener};
|
||||
|
||||
pub mod content;
|
||||
pub mod cursor;
|
||||
|
@ -60,15 +60,15 @@ mod color;
|
|||
mod damage;
|
||||
mod meter;
|
||||
|
||||
/// 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: ";
|
||||
|
||||
/// The character used to shorten the visible text like uri preview or search regex.
|
||||
const SHORTENER: char = '…';
|
||||
|
||||
/// Color which is used to highlight damaged rects when debugging.
|
||||
const DAMAGE_RECT_COLOR: Rgb = Rgb { r: 255, g: 0, b: 255 };
|
||||
|
||||
|
@ -362,9 +362,13 @@ pub struct Display {
|
|||
/// The renderer update that takes place only once before the actual rendering.
|
||||
pub pending_renderer_update: Option<RendererUpdate>,
|
||||
|
||||
// Mouse point position when highlighting hints.
|
||||
hint_mouse_point: Option<Point>,
|
||||
|
||||
is_damage_supported: bool,
|
||||
debug_damage: bool,
|
||||
damage_rects: Vec<DamageRect>,
|
||||
next_frame_damage_rects: Vec<DamageRect>,
|
||||
renderer: Renderer,
|
||||
glyph_cache: GlyphCache,
|
||||
meter: Meter,
|
||||
|
@ -515,10 +519,11 @@ impl Display {
|
|||
let hint_state = HintState::new(config.hints.alphabet());
|
||||
let is_damage_supported = window.swap_buffers_with_damage_supported();
|
||||
let debug_damage = config.debug.highlight_damage;
|
||||
let damage_rects = if is_damage_supported || debug_damage {
|
||||
Vec::with_capacity(size_info.screen_lines())
|
||||
let (damage_rects, next_frame_damage_rects) = if is_damage_supported || debug_damage {
|
||||
let vec = Vec::with_capacity(size_info.screen_lines());
|
||||
(vec.clone(), vec)
|
||||
} else {
|
||||
Vec::new()
|
||||
(Vec::new(), Vec::new())
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
|
@ -540,6 +545,8 @@ impl Display {
|
|||
is_damage_supported,
|
||||
debug_damage,
|
||||
damage_rects,
|
||||
next_frame_damage_rects,
|
||||
hint_mouse_point: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -757,7 +764,7 @@ impl Display {
|
|||
let size_info = self.size_info;
|
||||
|
||||
let vi_mode = terminal.mode().contains(TermMode::VI);
|
||||
let vi_mode_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None };
|
||||
let vi_cursor_point = if vi_mode { Some(terminal.vi_mode_cursor.point) } else { None };
|
||||
|
||||
if self.collect_damage() {
|
||||
self.update_damage(&mut terminal, selection_range, search_state);
|
||||
|
@ -772,6 +779,10 @@ impl Display {
|
|||
self.renderer.clear(background_color, config.window_opacity());
|
||||
let mut lines = RenderLines::new();
|
||||
|
||||
// Optimize loop hint comparator.
|
||||
let has_highlighted_hint =
|
||||
self.highlighted_hint.is_some() || self.vi_highlighted_hint.is_some();
|
||||
|
||||
// Draw grid.
|
||||
{
|
||||
let _sampler = self.meter.sampler();
|
||||
|
@ -783,16 +794,26 @@ impl Display {
|
|||
let glyph_cache = &mut self.glyph_cache;
|
||||
let highlighted_hint = &self.highlighted_hint;
|
||||
let vi_highlighted_hint = &self.vi_highlighted_hint;
|
||||
|
||||
self.renderer.draw_cells(
|
||||
&size_info,
|
||||
glyph_cache,
|
||||
grid_cells.into_iter().map(|mut cell| {
|
||||
// Underline hints hovered by mouse or vi mode cursor.
|
||||
let point = term::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);
|
||||
|
||||
if has_highlighted_hint {
|
||||
let hyperlink =
|
||||
cell.extra.as_ref().and_then(|extra| extra.hyperlink.as_ref());
|
||||
if highlighted_hint
|
||||
.as_ref()
|
||||
.map_or(false, |hint| hint.should_highlight(point, hyperlink))
|
||||
|| vi_highlighted_hint
|
||||
.as_ref()
|
||||
.map_or(false, |hint| hint.should_highlight(point, hyperlink))
|
||||
{
|
||||
cell.flags.insert(Flags::UNDERLINE);
|
||||
}
|
||||
}
|
||||
|
||||
// Update underline/strikeout.
|
||||
|
@ -805,18 +826,17 @@ impl Display {
|
|||
|
||||
let mut rects = lines.rects(&metrics, &size_info);
|
||||
|
||||
if let Some(vi_mode_cursor) = vi_mode_cursor {
|
||||
if let Some(vi_cursor_point) = vi_cursor_point {
|
||||
// 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;
|
||||
let obstructed_column = Some(vi_point)
|
||||
let line = (-vi_cursor_point.line.0 + size_info.bottommost_line().0) as usize;
|
||||
let obstructed_column = Some(vi_cursor_point)
|
||||
.filter(|point| point.line == -(display_offset as i32))
|
||||
.map(|point| point.column);
|
||||
self.draw_line_indicator(config, &size_info, total_lines, obstructed_column, line);
|
||||
self.draw_line_indicator(config, total_lines, obstructed_column, line);
|
||||
} else if search_state.regex().is_some() {
|
||||
// Show current display offset in vi-less search to indicate match position.
|
||||
self.draw_line_indicator(config, &size_info, total_lines, None, display_offset);
|
||||
}
|
||||
self.draw_line_indicator(config, total_lines, None, display_offset);
|
||||
};
|
||||
|
||||
// Draw cursor.
|
||||
for rect in cursor.rects(&size_info, config.terminal_config.cursor.thickness()) {
|
||||
|
@ -868,14 +888,21 @@ impl Display {
|
|||
let fg = config.colors.primary.background;
|
||||
for (i, message_text) in text.iter().enumerate() {
|
||||
let point = Point::new(start_line + i, Column(0));
|
||||
self.renderer.draw_string(point, fg, bg, message_text, &size_info, glyph_cache);
|
||||
self.renderer.draw_string(
|
||||
point,
|
||||
fg,
|
||||
bg,
|
||||
message_text.chars(),
|
||||
&size_info,
|
||||
glyph_cache,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Draw rectangles.
|
||||
self.renderer.draw_rects(&size_info, &metrics, rects);
|
||||
}
|
||||
|
||||
self.draw_render_timer(config, &size_info);
|
||||
self.draw_render_timer(config);
|
||||
|
||||
// Handle search and IME positioning.
|
||||
let ime_position = match search_state.regex() {
|
||||
|
@ -885,10 +912,10 @@ impl Display {
|
|||
Direction::Left => BACKWARD_SEARCH_LABEL,
|
||||
};
|
||||
|
||||
let search_text = Self::format_search(&size_info, regex, search_label);
|
||||
let search_text = Self::format_search(regex, search_label, size_info.columns());
|
||||
|
||||
// Render the search bar.
|
||||
self.draw_search(config, &size_info, &search_text);
|
||||
self.draw_search(config, &search_text);
|
||||
|
||||
// Compute IME position.
|
||||
let line = Line(size_info.screen_lines() as i32 + 1);
|
||||
|
@ -897,6 +924,11 @@ impl Display {
|
|||
None => cursor_point,
|
||||
};
|
||||
|
||||
// Draw hyperlink uri preview.
|
||||
if has_highlighted_hint {
|
||||
self.draw_hyperlink_preview(config, vi_cursor_point, display_offset);
|
||||
}
|
||||
|
||||
// Update IME position.
|
||||
self.window.update_ime_position(ime_position, &self.size_info);
|
||||
|
||||
|
@ -921,6 +953,9 @@ impl Display {
|
|||
}
|
||||
|
||||
self.damage_rects.clear();
|
||||
|
||||
// Append damage rects we've enqueued for the next frame.
|
||||
mem::swap(&mut self.damage_rects, &mut self.next_frame_damage_rects);
|
||||
}
|
||||
|
||||
/// Update to a new configuration.
|
||||
|
@ -964,8 +999,13 @@ impl Display {
|
|||
|
||||
// Update cursor shape.
|
||||
if highlighted_hint.is_some() {
|
||||
// If mouse changed the line, we should update the hyperlink preview, since the
|
||||
// highlighted hint could be disrupted by the old preview.
|
||||
dirty = self.hint_mouse_point.map(|p| p.line != point.line).unwrap_or(false);
|
||||
self.hint_mouse_point = Some(point);
|
||||
self.window.set_mouse_cursor(CursorIcon::Hand);
|
||||
} else if self.highlighted_hint.is_some() {
|
||||
self.hint_mouse_point = None;
|
||||
if term.mode().intersects(TermMode::MOUSE_MODE) && !term.mode().contains(TermMode::VI) {
|
||||
self.window.set_mouse_cursor(CursorIcon::Default);
|
||||
} else {
|
||||
|
@ -980,74 +1020,148 @@ impl Display {
|
|||
}
|
||||
|
||||
/// 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.
|
||||
let mut formatted_regex = String::with_capacity(search_regex.len());
|
||||
for c in search_regex.chars() {
|
||||
formatted_regex.push(c);
|
||||
if c.width() == Some(2) {
|
||||
formatted_regex.push(' ');
|
||||
}
|
||||
fn format_search(search_regex: &str, search_label: &str, max_width: usize) -> String {
|
||||
let label_len = search_label.len();
|
||||
|
||||
// Skip `search_regex` formatting if only label is visible.
|
||||
if label_len > max_width {
|
||||
return search_label[..max_width].to_owned();
|
||||
}
|
||||
|
||||
// Add cursor to show whitespace.
|
||||
formatted_regex.push('_');
|
||||
// The search string consists of `search_label` + `search_regex` + `cursor`.
|
||||
let mut bar_text = String::from(search_label);
|
||||
bar_text.extend(StrShortener::new(
|
||||
search_regex,
|
||||
max_width.wrapping_sub(label_len + 1),
|
||||
ShortenDirection::Left,
|
||||
Some(SHORTENER),
|
||||
));
|
||||
|
||||
// Truncate beginning of the search regex if it exceeds the viewport width.
|
||||
let num_cols = size_info.columns();
|
||||
let label_len = search_label.chars().count();
|
||||
let regex_len = formatted_regex.chars().count();
|
||||
let truncate_len = cmp::min((regex_len + label_len).saturating_sub(num_cols), regex_len);
|
||||
let index = formatted_regex.char_indices().nth(truncate_len).map(|(i, _c)| i).unwrap_or(0);
|
||||
let truncated_regex = &formatted_regex[index..];
|
||||
|
||||
// Add search label to the beginning of the search regex.
|
||||
let mut bar_text = format!("{}{}", search_label, truncated_regex);
|
||||
|
||||
// Make sure the label alone doesn't exceed the viewport width.
|
||||
bar_text.truncate(num_cols);
|
||||
bar_text.push('_');
|
||||
|
||||
bar_text
|
||||
}
|
||||
|
||||
/// Draw current search regex.
|
||||
fn draw_search(&mut self, config: &UiConfig, size_info: &SizeInfo, text: &str) {
|
||||
let glyph_cache = &mut self.glyph_cache;
|
||||
let num_cols = size_info.columns();
|
||||
/// Draw preview for the currently highlighted `Hyperlink`.
|
||||
#[inline(never)]
|
||||
fn draw_hyperlink_preview(
|
||||
&mut self,
|
||||
config: &UiConfig,
|
||||
vi_cursor_point: Option<Point>,
|
||||
display_offset: usize,
|
||||
) {
|
||||
let num_cols = self.size_info.columns();
|
||||
let uris: Vec<_> = self
|
||||
.highlighted_hint
|
||||
.iter()
|
||||
.chain(&self.vi_highlighted_hint)
|
||||
.filter_map(|hint| hint.hyperlink().map(|hyperlink| hyperlink.uri()))
|
||||
.map(|uri| StrShortener::new(uri, num_cols, ShortenDirection::Right, Some(SHORTENER)))
|
||||
.collect();
|
||||
|
||||
if uris.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// The maximum amount of protected lines including the ones we'll show preview on.
|
||||
let max_protected_lines = uris.len() * 2;
|
||||
|
||||
// Lines we shouldn't shouldn't show preview on, because it'll obscure the highlighted
|
||||
// hint.
|
||||
let mut protected_lines = Vec::with_capacity(max_protected_lines);
|
||||
if self.size_info.screen_lines() >= max_protected_lines {
|
||||
// Prefer to show preview even when it'll likely obscure the highlighted hint, when
|
||||
// there's no place left for it.
|
||||
protected_lines.push(self.hint_mouse_point.map(|point| point.line));
|
||||
protected_lines.push(vi_cursor_point.map(|point| point.line));
|
||||
}
|
||||
|
||||
// Find the line in viewport we can draw preview on without obscuring protected lines.
|
||||
let viewport_bottom = self.size_info.bottommost_line() - Line(display_offset as i32);
|
||||
let viewport_top = viewport_bottom - (self.size_info.screen_lines() - 1);
|
||||
let uri_lines = (viewport_top.0..=viewport_bottom.0)
|
||||
.rev()
|
||||
.map(|line| Some(Line(line)))
|
||||
.filter_map(|line| {
|
||||
if protected_lines.contains(&line) {
|
||||
None
|
||||
} else {
|
||||
protected_lines.push(line);
|
||||
line
|
||||
}
|
||||
})
|
||||
.take(uris.len())
|
||||
.flat_map(|line| term::point_to_viewport(display_offset, Point::new(line, Column(0))));
|
||||
|
||||
let fg = config.colors.footer_bar_foreground();
|
||||
let bg = config.colors.footer_bar_background();
|
||||
for (uri, point) in uris.into_iter().zip(uri_lines) {
|
||||
// Damage the uri preview.
|
||||
if self.collect_damage() {
|
||||
let uri_preview_damage = self.damage_from_point(point, num_cols as u32);
|
||||
self.damage_rects.push(uri_preview_damage);
|
||||
|
||||
// Damage the uri preview for the next frame as well.
|
||||
self.next_frame_damage_rects.push(uri_preview_damage);
|
||||
}
|
||||
|
||||
self.renderer.draw_string(point, fg, bg, uri, &self.size_info, &mut self.glyph_cache);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw current search regex.
|
||||
#[inline(never)]
|
||||
fn draw_search(&mut self, config: &UiConfig, text: &str) {
|
||||
// Assure text length is at least num_cols.
|
||||
let num_cols = self.size_info.columns();
|
||||
let text = format!("{:<1$}", text, num_cols);
|
||||
|
||||
let point = Point::new(size_info.screen_lines(), Column(0));
|
||||
let fg = config.colors.search_bar_foreground();
|
||||
let bg = config.colors.search_bar_background();
|
||||
let point = Point::new(self.size_info.screen_lines(), Column(0));
|
||||
|
||||
self.renderer.draw_string(point, fg, bg, &text, size_info, glyph_cache);
|
||||
let fg = config.colors.footer_bar_foreground();
|
||||
let bg = config.colors.footer_bar_background();
|
||||
|
||||
self.renderer.draw_string(
|
||||
point,
|
||||
fg,
|
||||
bg,
|
||||
text.chars(),
|
||||
&self.size_info,
|
||||
&mut self.glyph_cache,
|
||||
);
|
||||
}
|
||||
|
||||
/// Draw render timer.
|
||||
fn draw_render_timer(&mut self, config: &UiConfig, size_info: &SizeInfo) {
|
||||
#[inline(never)]
|
||||
fn draw_render_timer(&mut self, config: &UiConfig) {
|
||||
if !config.debug.render_timer {
|
||||
return;
|
||||
}
|
||||
|
||||
let timing = format!("{:.3} usec", self.meter.average());
|
||||
let point = Point::new(size_info.screen_lines().saturating_sub(2), Column(0));
|
||||
let point = Point::new(self.size_info.screen_lines().saturating_sub(2), Column(0));
|
||||
let fg = config.colors.primary.background;
|
||||
let bg = config.colors.normal.red;
|
||||
|
||||
// Damage the entire line.
|
||||
self.damage_from_point(point, self.size_info.columns() as u32);
|
||||
if self.collect_damage() {
|
||||
// Damage the entire line.
|
||||
let render_timer_damage =
|
||||
self.damage_from_point(point, self.size_info.columns() as u32);
|
||||
self.damage_rects.push(render_timer_damage);
|
||||
|
||||
// Damage the render timer for the next frame.
|
||||
self.next_frame_damage_rects.push(render_timer_damage)
|
||||
}
|
||||
|
||||
let glyph_cache = &mut self.glyph_cache;
|
||||
self.renderer.draw_string(point, fg, bg, &timing, size_info, glyph_cache);
|
||||
self.renderer.draw_string(point, fg, bg, timing.chars(), &self.size_info, glyph_cache);
|
||||
}
|
||||
|
||||
/// Draw an indicator for the position of a line in history.
|
||||
#[inline(never)]
|
||||
fn draw_line_indicator(
|
||||
&mut self,
|
||||
config: &UiConfig,
|
||||
size_info: &SizeInfo,
|
||||
total_lines: usize,
|
||||
obstructed_column: Option<Column>,
|
||||
line: usize,
|
||||
|
@ -1064,14 +1178,16 @@ impl Display {
|
|||
}
|
||||
|
||||
let text = format!("[{}/{}]", line, total_lines - 1);
|
||||
let column = Column(size_info.columns().saturating_sub(text.len()));
|
||||
let column = Column(self.size_info.columns().saturating_sub(text.len()));
|
||||
let point = Point::new(0, column);
|
||||
|
||||
// Damage the maximum possible length of the format text, which could be achieved when
|
||||
// using `MAX_SCROLLBACK_LINES` as current and total lines adding a `3` for formatting.
|
||||
const MAX_SIZE: usize = 2 * num_digits(MAX_SCROLLBACK_LINES) + 3;
|
||||
let damage_point = Point::new(0, Column(size_info.columns().saturating_sub(MAX_SIZE)));
|
||||
self.damage_from_point(damage_point, MAX_SIZE as u32);
|
||||
let damage_point = Point::new(0, Column(self.size_info.columns().saturating_sub(MAX_SIZE)));
|
||||
if self.collect_damage() {
|
||||
self.damage_rects.push(self.damage_from_point(damage_point, MAX_SIZE as u32));
|
||||
}
|
||||
|
||||
let colors = &config.colors;
|
||||
let fg = colors.line_indicator.foreground.unwrap_or(colors.primary.background);
|
||||
|
@ -1080,23 +1196,20 @@ impl Display {
|
|||
// Do not render anything if it would obscure the vi mode cursor.
|
||||
if obstructed_column.map_or(true, |obstructed_column| obstructed_column < column) {
|
||||
let glyph_cache = &mut self.glyph_cache;
|
||||
self.renderer.draw_string(point, fg, bg, &text, size_info, glyph_cache);
|
||||
self.renderer.draw_string(point, fg, bg, text.chars(), &self.size_info, glyph_cache);
|
||||
}
|
||||
}
|
||||
|
||||
/// Damage `len` starting from a `point`.
|
||||
#[inline]
|
||||
fn damage_from_point(&mut self, point: Point<usize>, len: u32) {
|
||||
if !self.collect_damage() {
|
||||
return;
|
||||
}
|
||||
|
||||
///
|
||||
/// This method also enqueues damage for the next frame automatically.
|
||||
fn damage_from_point(&self, point: Point<usize>, len: u32) -> DamageRect {
|
||||
let size_info: SizeInfo<u32> = self.size_info.into();
|
||||
let x = size_info.padding_x() + point.column.0 as u32 * size_info.cell_width();
|
||||
let y_top = size_info.height() - size_info.padding_y();
|
||||
let y = y_top - (point.line as u32 + 1) * size_info.cell_height();
|
||||
let width = len as u32 * size_info.cell_width();
|
||||
self.damage_rects.push(DamageRect { x, y, width, height: size_info.cell_height() })
|
||||
DamageRect { x, y, width, height: size_info.cell_height() }
|
||||
}
|
||||
|
||||
/// Damage currently highlighted `Display` hints.
|
||||
|
@ -1105,10 +1218,12 @@ impl Display {
|
|||
let display_offset = terminal.grid().display_offset();
|
||||
let last_visible_line = terminal.screen_lines() - 1;
|
||||
for hint in self.highlighted_hint.iter().chain(&self.vi_highlighted_hint) {
|
||||
for point in (hint.bounds.start().line.0..=hint.bounds.end().line.0).flat_map(|line| {
|
||||
term::point_to_viewport(display_offset, Point::new(Line(line), Column(0)))
|
||||
.filter(|point| point.line <= last_visible_line)
|
||||
}) {
|
||||
for point in
|
||||
(hint.bounds().start().line.0..=hint.bounds().end().line.0).flat_map(|line| {
|
||||
term::point_to_viewport(display_offset, Point::new(Line(line), Column(0)))
|
||||
.filter(|point| point.line <= last_visible_line)
|
||||
})
|
||||
{
|
||||
terminal.damage_line(point.line, 0, terminal.columns() - 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -679,28 +679,31 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
|
|||
return;
|
||||
}
|
||||
|
||||
match &hint.action {
|
||||
let hint_bounds = hint.bounds();
|
||||
let text = match hint.hyperlink() {
|
||||
Some(hyperlink) => hyperlink.uri().to_owned(),
|
||||
None => self.terminal.bounds_to_string(*hint_bounds.start(), *hint_bounds.end()),
|
||||
};
|
||||
|
||||
match &hint.action() {
|
||||
// Launch an external program.
|
||||
HintAction::Command(command) => {
|
||||
let text = self.terminal.bounds_to_string(*hint.bounds.start(), *hint.bounds.end());
|
||||
let mut args = command.args().to_vec();
|
||||
args.push(text);
|
||||
self.spawn_daemon(command.program(), &args);
|
||||
},
|
||||
// Copy the text to the clipboard.
|
||||
HintAction::Action(HintInternalAction::Copy) => {
|
||||
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(*hint.bounds.start(), *hint.bounds.end());
|
||||
self.paste(&text);
|
||||
},
|
||||
// Select the text.
|
||||
HintAction::Action(HintInternalAction::Select) => {
|
||||
self.start_selection(SelectionType::Simple, *hint.bounds.start(), Side::Left);
|
||||
self.update_selection(*hint.bounds.end(), Side::Right);
|
||||
self.start_selection(SelectionType::Simple, *hint_bounds.start(), Side::Left);
|
||||
self.update_selection(*hint_bounds.end(), Side::Right);
|
||||
self.copy_selection(ClipboardType::Selection);
|
||||
},
|
||||
// Move the vi mode cursor.
|
||||
|
@ -710,7 +713,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
|
|||
self.terminal.toggle_vi_mode();
|
||||
}
|
||||
|
||||
self.terminal.vi_goto_point(*hint.bounds.start());
|
||||
self.terminal.vi_goto_point(*hint_bounds.start());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -922,9 +922,10 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
|
|||
fn cursor_state(&mut self) -> CursorIcon {
|
||||
let display_offset = self.ctx.terminal().grid().display_offset();
|
||||
let point = self.ctx.mouse().point(&self.ctx.size_info(), display_offset);
|
||||
let hyperlink = self.ctx.terminal().grid()[point].hyperlink();
|
||||
|
||||
// Function to check if mouse is on top of a hint.
|
||||
let hint_highlighted = |hint: &HintMatch| hint.bounds.contains(&point);
|
||||
let hint_highlighted = |hint: &HintMatch| hint.should_highlight(point, hyperlink.as_ref());
|
||||
|
||||
if let Some(mouse_state) = self.message_bar_cursor_state() {
|
||||
mouse_state
|
||||
|
|
|
@ -46,6 +46,7 @@ mod message_bar;
|
|||
mod panic;
|
||||
mod renderer;
|
||||
mod scheduler;
|
||||
mod string;
|
||||
mod window_context;
|
||||
|
||||
mod gl {
|
||||
|
|
|
@ -125,14 +125,14 @@ impl Renderer {
|
|||
point: Point<usize>,
|
||||
fg: Rgb,
|
||||
bg: Rgb,
|
||||
string: &str,
|
||||
string_chars: impl Iterator<Item = char>,
|
||||
size_info: &SizeInfo,
|
||||
glyph_cache: &mut GlyphCache,
|
||||
) {
|
||||
let cells = string.chars().enumerate().map(|(i, character)| RenderableCell {
|
||||
let cells = string_chars.enumerate().map(|(i, character)| RenderableCell {
|
||||
point: Point::new(point.line, point.column + i),
|
||||
character,
|
||||
zerowidth: None,
|
||||
extra: None,
|
||||
flags: Flags::empty(),
|
||||
bg_alpha: 1.0,
|
||||
fg,
|
||||
|
|
|
@ -159,7 +159,9 @@ pub trait TextRenderApi<T: TextRenderBatch>: LoadGlyph {
|
|||
self.add_render_item(&cell, &glyph, size_info);
|
||||
|
||||
// Render visible zero-width characters.
|
||||
if let Some(zerowidth) = cell.zerowidth.take().filter(|_| !hidden) {
|
||||
if let Some(zerowidth) =
|
||||
cell.extra.as_mut().and_then(|extra| extra.zerowidth.take().filter(|_| !hidden))
|
||||
{
|
||||
for character in zerowidth {
|
||||
glyph_key.character = character;
|
||||
let glyph = glyph_cache.get(glyph_key, self, false);
|
||||
|
|
314
alacritty/src/string.rs
Normal file
314
alacritty/src/string.rs
Normal file
|
@ -0,0 +1,314 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::iter::Skip;
|
||||
use std::str::Chars;
|
||||
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
/// The action performed by [`StrShortener`].
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TextAction {
|
||||
/// Yield a spacer.
|
||||
Spacer,
|
||||
/// Terminate state reached.
|
||||
Terminate,
|
||||
/// Yield a shortener.
|
||||
Shortener,
|
||||
/// Yield a character.
|
||||
Char,
|
||||
}
|
||||
|
||||
/// The direction which we should shorten.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ShortenDirection {
|
||||
/// Shorten to the start of the string.
|
||||
Left,
|
||||
|
||||
/// Shorten to the end of the string.
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Iterator that yield shortened version of the text.
|
||||
pub struct StrShortener<'a> {
|
||||
chars: Skip<Chars<'a>>,
|
||||
accumulted_len: usize,
|
||||
max_width: usize,
|
||||
direction: ShortenDirection,
|
||||
shortener: Option<char>,
|
||||
text_action: TextAction,
|
||||
}
|
||||
|
||||
impl<'a> StrShortener<'a> {
|
||||
pub fn new(
|
||||
text: &'a str,
|
||||
max_width: usize,
|
||||
direction: ShortenDirection,
|
||||
mut shortener: Option<char>,
|
||||
) -> Self {
|
||||
if text.is_empty() {
|
||||
// If we don't have any text don't produce a shortener for it.
|
||||
let _ = shortener.take();
|
||||
}
|
||||
|
||||
if direction == ShortenDirection::Right {
|
||||
return Self {
|
||||
chars: text.chars().skip(0),
|
||||
accumulted_len: 0,
|
||||
text_action: TextAction::Char,
|
||||
max_width,
|
||||
direction,
|
||||
shortener,
|
||||
};
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
let mut current_len = 0;
|
||||
|
||||
let mut iter = text.chars().rev().enumerate();
|
||||
|
||||
while let Some((idx, ch)) = iter.next() {
|
||||
let ch_width = ch.width().unwrap_or(1);
|
||||
current_len += ch_width;
|
||||
|
||||
match current_len.cmp(&max_width) {
|
||||
// We can only be here if we've faced wide character or we've already
|
||||
// handled equality situation. Anyway, break.
|
||||
Ordering::Greater => break,
|
||||
Ordering::Equal => {
|
||||
if shortener.is_some() && iter.clone().next().is_some() {
|
||||
// We have one more character after, shortener will accumulate for
|
||||
// the `current_len`.
|
||||
break;
|
||||
} else {
|
||||
// The match is exact, consume shortener.
|
||||
let _ = shortener.take();
|
||||
}
|
||||
},
|
||||
Ordering::Less => (),
|
||||
}
|
||||
|
||||
offset = idx + 1;
|
||||
}
|
||||
|
||||
// Consume the iterator to count the number of characters in it.
|
||||
let num_chars = iter.last().map(|(idx, _)| idx + 1).unwrap_or(offset);
|
||||
let skip_chars = num_chars - offset;
|
||||
|
||||
let text_action = if num_chars <= max_width || shortener.is_none() {
|
||||
TextAction::Char
|
||||
} else {
|
||||
TextAction::Shortener
|
||||
};
|
||||
|
||||
let chars = text.chars().skip(skip_chars);
|
||||
|
||||
Self { chars, accumulted_len: 0, text_action, max_width, direction, shortener }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for StrShortener<'a> {
|
||||
type Item = char;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.text_action {
|
||||
TextAction::Spacer => {
|
||||
self.text_action = TextAction::Char;
|
||||
Some(' ')
|
||||
},
|
||||
TextAction::Terminate => {
|
||||
// We've reached the termination state.
|
||||
None
|
||||
},
|
||||
TextAction::Shortener => {
|
||||
// When we shorten from the left we yield the shortener first and process the rest.
|
||||
self.text_action = if self.direction == ShortenDirection::Left {
|
||||
TextAction::Char
|
||||
} else {
|
||||
TextAction::Terminate
|
||||
};
|
||||
|
||||
// Consume the shortener to avoid yielding it later when shortening left.
|
||||
self.shortener.take()
|
||||
},
|
||||
TextAction::Char => {
|
||||
let ch = self.chars.next()?;
|
||||
let ch_width = ch.width().unwrap_or(1);
|
||||
|
||||
// Advance width.
|
||||
self.accumulted_len += ch_width;
|
||||
|
||||
if self.accumulted_len > self.max_width {
|
||||
self.text_action = TextAction::Terminate;
|
||||
return self.shortener;
|
||||
} else if self.accumulted_len == self.max_width && self.shortener.is_some() {
|
||||
// Check if we have a next char.
|
||||
let has_next = self.chars.clone().next().is_some();
|
||||
|
||||
// We should terminate after that.
|
||||
self.text_action = TextAction::Terminate;
|
||||
|
||||
return has_next.then(|| self.shortener.unwrap()).or(Some(ch));
|
||||
}
|
||||
|
||||
// Add a spacer for wide character.
|
||||
if ch_width == 2 {
|
||||
self.text_action = TextAction::Spacer;
|
||||
}
|
||||
|
||||
Some(ch)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn into_shortened_with_shortener() {
|
||||
let s = "Hello";
|
||||
let len = s.chars().count();
|
||||
assert_eq!(
|
||||
"",
|
||||
StrShortener::new("", 1, ShortenDirection::Left, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
".",
|
||||
StrShortener::new(s, 1, ShortenDirection::Right, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
".",
|
||||
StrShortener::new(s, 1, ShortenDirection::Left, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"H.",
|
||||
StrShortener::new(s, 2, ShortenDirection::Right, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
".o",
|
||||
StrShortener::new(s, 2, ShortenDirection::Left, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
&StrShortener::new(s, len * 2, ShortenDirection::Right, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
&StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
let s = "こJんにちはP";
|
||||
let len = 2 + 1 + 2 + 2 + 2 + 2 + 1;
|
||||
assert_eq!(
|
||||
".",
|
||||
&StrShortener::new(s, 1, ShortenDirection::Right, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&".",
|
||||
&StrShortener::new(s, 1, ShortenDirection::Left, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
".",
|
||||
&StrShortener::new(s, 2, ShortenDirection::Right, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
".P",
|
||||
&StrShortener::new(s, 2, ShortenDirection::Left, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"こ .",
|
||||
&StrShortener::new(s, 3, ShortenDirection::Right, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
".P",
|
||||
&StrShortener::new(s, 3, ShortenDirection::Left, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"こ Jん に ち は P",
|
||||
&StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"こ Jん に ち は P",
|
||||
&StrShortener::new(s, len * 2, ShortenDirection::Right, Some('.')).collect::<String>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_shortened_without_shortener() {
|
||||
let s = "Hello";
|
||||
assert_eq!("", StrShortener::new("", 1, ShortenDirection::Left, None).collect::<String>());
|
||||
|
||||
assert_eq!(
|
||||
"H",
|
||||
&StrShortener::new(s, 1, ShortenDirection::Right, None).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!("o", &StrShortener::new(s, 1, ShortenDirection::Left, None).collect::<String>());
|
||||
|
||||
assert_eq!(
|
||||
"He",
|
||||
&StrShortener::new(s, 2, ShortenDirection::Right, None).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"lo",
|
||||
&StrShortener::new(s, 2, ShortenDirection::Left, None).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&s,
|
||||
&StrShortener::new(s, s.len(), ShortenDirection::Right, None).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&s,
|
||||
&StrShortener::new(s, s.len(), ShortenDirection::Left, None).collect::<String>()
|
||||
);
|
||||
|
||||
let s = "こJんにちはP";
|
||||
let len = 2 + 1 + 2 + 2 + 2 + 2 + 1;
|
||||
assert_eq!("", &StrShortener::new(s, 1, ShortenDirection::Right, None).collect::<String>());
|
||||
|
||||
assert_eq!("P", &StrShortener::new(s, 1, ShortenDirection::Left, None).collect::<String>());
|
||||
|
||||
assert_eq!(
|
||||
"こ ",
|
||||
&StrShortener::new(s, 2, ShortenDirection::Right, None).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!("P", &StrShortener::new(s, 2, ShortenDirection::Left, None).collect::<String>());
|
||||
|
||||
assert_eq!(
|
||||
"こ J",
|
||||
&StrShortener::new(s, 3, ShortenDirection::Right, None).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"は P",
|
||||
&StrShortener::new(s, 3, ShortenDirection::Left, None).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"こ Jん に ち は P",
|
||||
&StrShortener::new(s, len, ShortenDirection::Left, None).collect::<String>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"こ Jん に ち は P",
|
||||
&StrShortener::new(s, len, ShortenDirection::Right, None).collect::<String>()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ use vte::{Params, ParamsIter};
|
|||
use alacritty_config_derive::ConfigDeserialize;
|
||||
|
||||
use crate::index::{Column, Line};
|
||||
use crate::term::cell::Hyperlink;
|
||||
use crate::term::color::Rgb;
|
||||
|
||||
/// Maximum time before a synchronized update is aborted.
|
||||
|
@ -457,6 +458,9 @@ pub trait Handler {
|
|||
|
||||
/// Report text area size in characters.
|
||||
fn text_area_size_chars(&mut self) {}
|
||||
|
||||
/// Set hyperlink.
|
||||
fn set_hyperlink(&mut self, _: Option<Hyperlink>) {}
|
||||
}
|
||||
|
||||
/// Terminal cursor configuration.
|
||||
|
@ -1007,6 +1011,27 @@ where
|
|||
}
|
||||
},
|
||||
|
||||
// Hyperlink.
|
||||
b"8" if params.len() > 2 => {
|
||||
let link_params = params[1];
|
||||
let uri = str::from_utf8(params[2]).unwrap_or_default();
|
||||
|
||||
// The OSC 8 escape sequence must be stopped when getting an empty `uri`.
|
||||
if uri.is_empty() {
|
||||
self.handler.set_hyperlink(None);
|
||||
return;
|
||||
}
|
||||
|
||||
// Link parameters are in format of `key1=value1:key2=value2`. Currently only key
|
||||
// `id` is defined.
|
||||
let id = link_params
|
||||
.split(|&b| b == b':')
|
||||
.find_map(|kv| kv.strip_prefix(b"id="))
|
||||
.and_then(|kv| str::from_utf8(kv).ok());
|
||||
|
||||
self.handler.set_hyperlink(Some(Hyperlink::new(id, uri)));
|
||||
},
|
||||
|
||||
// Get/set Foreground, Background, Cursor colors.
|
||||
b"10" | b"11" | b"12" => {
|
||||
if params.len() >= 2 {
|
||||
|
|
|
@ -171,6 +171,16 @@ impl<T> Row<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoIterator for &'a Row<T> {
|
||||
type IntoIter = slice::Iter<'a, T>;
|
||||
type Item = &'a T;
|
||||
|
||||
#[inline]
|
||||
fn into_iter(self) -> slice::Iter<'a, T> {
|
||||
self.inner.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoIterator for &'a mut Row<T> {
|
||||
type IntoIter = slice::IterMut<'a, T>;
|
||||
type Item = &'a mut T;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
@ -33,6 +34,53 @@ bitflags! {
|
|||
}
|
||||
}
|
||||
|
||||
/// Counter for hyperlinks without explicit ID.
|
||||
static HYPERLINK_ID_SUFFIX: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Hyperlink {
|
||||
inner: Arc<HyperlinkInner>,
|
||||
}
|
||||
|
||||
impl Hyperlink {
|
||||
pub fn new<T: ToString>(id: Option<T>, uri: T) -> Self {
|
||||
let inner = Arc::new(HyperlinkInner::new(id, uri));
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
&self.inner.id
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> &str {
|
||||
&self.inner.uri
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
|
||||
struct HyperlinkInner {
|
||||
/// Identifier for the given hyperlink.
|
||||
id: String,
|
||||
|
||||
/// Resource identifier of the hyperlink.
|
||||
uri: String,
|
||||
}
|
||||
|
||||
impl HyperlinkInner {
|
||||
pub fn new<T: ToString>(id: Option<T>, uri: T) -> Self {
|
||||
let id = match id {
|
||||
Some(id) => id.to_string(),
|
||||
None => {
|
||||
let mut id = HYPERLINK_ID_SUFFIX.fetch_add(1, Ordering::Relaxed).to_string();
|
||||
id.push_str("_alacritty");
|
||||
id
|
||||
},
|
||||
};
|
||||
|
||||
Self { id, uri: uri.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for determining if a reset should be performed.
|
||||
pub trait ResetDiscriminant<T> {
|
||||
/// Value based on which equality for the reset will be determined.
|
||||
|
@ -61,6 +109,8 @@ pub struct CellExtra {
|
|||
zerowidth: Vec<char>,
|
||||
|
||||
underline_color: Option<Color>,
|
||||
|
||||
hyperlink: Option<Hyperlink>,
|
||||
}
|
||||
|
||||
/// Content and attributes of a single cell in the terminal grid.
|
||||
|
@ -113,14 +163,17 @@ impl Cell {
|
|||
/// Set underline color on the cell.
|
||||
pub fn set_underline_color(&mut self, color: Option<Color>) {
|
||||
// If we reset color and we don't have zerowidth we should drop extra storage.
|
||||
if color.is_none() && self.extra.as_ref().map_or(true, |extra| !extra.zerowidth.is_empty())
|
||||
if color.is_none()
|
||||
&& self
|
||||
.extra
|
||||
.as_ref()
|
||||
.map_or(true, |extra| !extra.zerowidth.is_empty() || extra.hyperlink.is_some())
|
||||
{
|
||||
self.extra = None;
|
||||
return;
|
||||
} else {
|
||||
let extra = self.extra.get_or_insert(Default::default());
|
||||
Arc::make_mut(extra).underline_color = color;
|
||||
}
|
||||
|
||||
let extra = self.extra.get_or_insert(Default::default());
|
||||
Arc::make_mut(extra).underline_color = color;
|
||||
}
|
||||
|
||||
/// Underline color stored in this cell.
|
||||
|
@ -128,6 +181,27 @@ impl Cell {
|
|||
pub fn underline_color(&self) -> Option<Color> {
|
||||
self.extra.as_ref()?.underline_color
|
||||
}
|
||||
|
||||
/// Set hyperlink.
|
||||
pub fn set_hyperlink(&mut self, hyperlink: Option<Hyperlink>) {
|
||||
let should_drop = hyperlink.is_none()
|
||||
&& self.extra.as_ref().map_or(true, |extra| {
|
||||
!extra.zerowidth.is_empty() || extra.underline_color.is_some()
|
||||
});
|
||||
|
||||
if should_drop {
|
||||
self.extra = None;
|
||||
} else {
|
||||
let extra = self.extra.get_or_insert(Default::default());
|
||||
Arc::make_mut(extra).hyperlink = hyperlink;
|
||||
}
|
||||
}
|
||||
|
||||
/// Hyperlink stored in this cell.
|
||||
#[inline]
|
||||
pub fn hyperlink(&self) -> Option<Hyperlink> {
|
||||
self.extra.as_ref()?.hyperlink.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl GridCell for Cell {
|
||||
|
|
|
@ -16,7 +16,7 @@ use crate::event::{Event, EventListener};
|
|||
use crate::grid::{Dimensions, Grid, GridIterator, Scroll};
|
||||
use crate::index::{self, Boundary, Column, Direction, Line, Point, Side};
|
||||
use crate::selection::{Selection, SelectionRange, SelectionType};
|
||||
use crate::term::cell::{Cell, Flags, LineLength};
|
||||
use crate::term::cell::{Cell, Flags, Hyperlink, LineLength};
|
||||
use crate::term::color::{Colors, Rgb};
|
||||
use crate::vi_mode::{ViModeCursor, ViMotion};
|
||||
|
||||
|
@ -1679,6 +1679,12 @@ impl<T: EventListener> Handler for Term<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_hyperlink(&mut self, hyperlink: Option<Hyperlink>) {
|
||||
trace!("Setting hyperlink: {:?}", hyperlink);
|
||||
self.grid.cursor.template.set_hyperlink(hyperlink);
|
||||
}
|
||||
|
||||
/// Set a terminal attribute.
|
||||
#[inline]
|
||||
fn terminal_attribute(&mut self, attr: Attr) {
|
||||
|
|
|
@ -28,15 +28,36 @@ macro_rules! ref_tests {
|
|||
}
|
||||
|
||||
ref_tests! {
|
||||
alt_reset
|
||||
clear_underline
|
||||
colored_reset
|
||||
colored_underline
|
||||
csi_rep
|
||||
decaln_reset
|
||||
deccolm_reset
|
||||
delete_chars_reset
|
||||
delete_lines
|
||||
erase_chars_reset
|
||||
fish_cc
|
||||
grid_reset
|
||||
history
|
||||
hyperlinks
|
||||
indexed_256_colors
|
||||
insert_blank_reset
|
||||
issue_855
|
||||
ll
|
||||
newline_with_cursor_beyond_scroll_region
|
||||
region_scroll_down
|
||||
row_reset
|
||||
saved_cursor
|
||||
saved_cursor_alt
|
||||
scroll_up_reset
|
||||
selective_erasure
|
||||
sgr
|
||||
tab_rendering
|
||||
tmux_git_log
|
||||
tmux_htop
|
||||
underline
|
||||
vim_24bitcolors_bce
|
||||
vim_large_window_scroll
|
||||
vim_simple_edit
|
||||
|
@ -46,29 +67,9 @@ ref_tests! {
|
|||
vttest_origin_mode_2
|
||||
vttest_scroll
|
||||
vttest_tab_clear_set
|
||||
zsh_tab_completion
|
||||
history
|
||||
grid_reset
|
||||
row_reset
|
||||
zerowidth
|
||||
selective_erasure
|
||||
colored_reset
|
||||
colored_underline
|
||||
delete_lines
|
||||
delete_chars_reset
|
||||
alt_reset
|
||||
deccolm_reset
|
||||
decaln_reset
|
||||
insert_blank_reset
|
||||
erase_chars_reset
|
||||
scroll_up_reset
|
||||
clear_underline
|
||||
region_scroll_down
|
||||
wrapline_alt_toggle
|
||||
saved_cursor
|
||||
saved_cursor_alt
|
||||
sgr
|
||||
underline
|
||||
zerowidth
|
||||
zsh_tab_completion
|
||||
}
|
||||
|
||||
fn read_u8<P>(path: P) -> Vec<u8>
|
||||
|
|
10
alacritty_terminal/tests/ref/hyperlinks/alacritty.recording
Normal file
10
alacritty_terminal/tests/ref/hyperlinks/alacritty.recording
Normal file
|
@ -0,0 +1,10 @@
|
|||
[?2004hsh-5.1$ [7mprintf '\e]8;;https://example.com\e\\foo\e]8;;\e\\\n'[27m
[C[C[C[C[C[C[C[Cprintf '\e]8;;https://example.com\e\\foo\e]8;;\e\\\n'
|
||||
[?2004l
]8;;https://example.com\foo]8;;\
|
||||
[?2004hsh-5.1$ [7mprintf '\e]8;;https://example.com\e\\foo\e]8;;\e\\\n'[27m
[C[C[C[C[C[C[C[Cprintf '\e]8;;https://example.com\e\\foo\e]8;;\e\\\n'
|
||||
[?2004l
]8;;https://example.com\foo]8;;\
|
||||
[?2004hsh-5.1$ [7mprintf '\e]8;id=42;https://example.com\e\\bar\e]8;;\e\\\n'[27m
[C[C[C[C[C[C[C[Cprintf '\e]8;id=42;https://example.com\e\\bar\e]8;;\e\\\n'
|
||||
[?2004l
]8;id=42;https://example.com\bar]8;;\
|
||||
[?2004hsh-5.1$ [7mprintf '\e]8;id=42;https://example.com\e\\bar\e]8;;\e\\\n'[27m
[C[C[C[C[C[C[C[Cprintf '\e]8;id=42;https://example.com\e\\bar\e]8;;\e\\\n'
|
||||
[?2004l
]8;id=42;https://example.com\bar]8;;\
|
||||
[?2004hsh-5.1$ [?2004l
|
||||
exit
|
1
alacritty_terminal/tests/ref/hyperlinks/config.json
Normal file
1
alacritty_terminal/tests/ref/hyperlinks/config.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"history_size":0}
|
1
alacritty_terminal/tests/ref/hyperlinks/grid.json
Normal file
1
alacritty_terminal/tests/ref/hyperlinks/grid.json
Normal file
File diff suppressed because one or more lines are too long
1
alacritty_terminal/tests/ref/hyperlinks/size.json
Normal file
1
alacritty_terminal/tests/ref/hyperlinks/size.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"columns":140,"screen_lines":38}
|
|
@ -90,6 +90,7 @@ brevity.
|
|||
| `OSC 1` | REJECTED | Icon names are not supported |
|
||||
| `OSC 2` | IMPLEMENTED | |
|
||||
| `OSC 4` | IMPLEMENTED | |
|
||||
| `OSC 8` | IMPLEMENTED | |
|
||||
| `OSC 10` | IMPLEMENTED | |
|
||||
| `OSC 11` | IMPLEMENTED | |
|
||||
| `OSC 12` | IMPLEMENTED | |
|
||||
|
|
Loading…
Reference in a new issue