diff options
author | Christian Duerr <contact@christianduerr.com> | 2021-03-01 19:50:39 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-01 19:50:39 +0000 |
commit | a954e076ca0b1ee9c1f272c2b119c67df3935fd4 (patch) | |
tree | f233f8622ac6ab33519bfcb70b480f23697198b1 /alacritty/src/display/content.rs | |
parent | 772afc6a8aa9db6f89de4b23df27b571a40c9118 (diff) | |
download | r-alacritty-a954e076ca0b1ee9c1f272c2b119c67df3935fd4.tar.gz r-alacritty-a954e076ca0b1ee9c1f272c2b119c67df3935fd4.tar.bz2 r-alacritty-a954e076ca0b1ee9c1f272c2b119c67df3935fd4.zip |
Add regex terminal hints
This adds support for hints, which allow opening parts of the visual
buffer with external programs if they match a certain regex.
This is done using a visual overlay triggered on a specified key
binding, which then instructs the user which keys they need to press to
pass the text to the application.
In the future it should be possible to supply some built-in actions for
Copy/Pasting the action and using this to launch text when clicking on
it with the mouse. But the current implementation should already be
useful as-is.
Fixes #2792.
Fixes #2536.
Diffstat (limited to 'alacritty/src/display/content.rs')
-rw-r--r-- | alacritty/src/display/content.rs | 171 |
1 files changed, 136 insertions, 35 deletions
diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs index 9f035a1c..6532f236 100644 --- a/alacritty/src/display/content.rs +++ b/alacritty/src/display/content.rs @@ -1,6 +1,7 @@ +use std::borrow::Cow; use std::cmp::max; use std::mem; -use std::ops::RangeInclusive; +use std::ops::{Deref, DerefMut, RangeInclusive}; use alacritty_terminal::ansi::{Color, CursorShape, NamedColor}; use alacritty_terminal::config::Config; @@ -16,6 +17,8 @@ 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; /// Minimum contrast between a fixed cursor color and the cell's background. pub const MIN_CURSOR_CONTRAST: f64 = 1.5; @@ -30,31 +33,38 @@ pub struct RenderableContent<'a> { terminal_content: TerminalContent<'a>, terminal_cursor: TerminalCursor, cursor: Option<RenderableCursor>, - search: RenderableSearch, + search: Regex<'a>, + hint: Hint<'a>, config: &'a Config<UiConfig>, colors: &'a List, } impl<'a> RenderableContent<'a> { pub fn new<T: EventListener>( - term: &'a Term<T>, - dfas: Option<&RegexSearch>, config: &'a Config<UiConfig>, - colors: &'a List, - show_cursor: bool, + display: &'a mut Display, + term: &'a Term<T>, + search_dfas: Option<&RegexSearch>, ) -> Self { - let search = dfas.map(|dfas| RenderableSearch::new(&term, dfas)).unwrap_or_default(); + let search = search_dfas.map(|dfas| Regex::new(&term, dfas)).unwrap_or_default(); let terminal_content = term.renderable_content(); // Copy the cursor and override its shape if necessary. let mut terminal_cursor = terminal_content.cursor; - if !show_cursor || terminal_cursor.shape == CursorShape::Hidden { + if terminal_cursor.shape == CursorShape::Hidden + || display.cursor_hidden + || search_dfas.is_some() + { terminal_cursor.shape = CursorShape::Hidden; } else if !term.is_focused && config.cursor.unfocused_hollow { terminal_cursor.shape = CursorShape::HollowBlock; } - Self { cursor: None, terminal_content, terminal_cursor, search, config, colors } + display.hint_state.update_matches(term); + let hint = Hint::from(&display.hint_state); + + let colors = &display.colors; + Self { cursor: None, terminal_content, terminal_cursor, search, config, colors, hint } } /// Viewport offset. @@ -193,37 +203,40 @@ impl RenderableCell { .map_or(false, |selection| selection.contains_cell(&cell, content.terminal_cursor)); let mut is_match = false; + let mut character = cell.c; + let colors = &content.config.ui_config.colors; - if is_selected { + if let Some((c, is_first)) = content.hint.advance(cell.point) { + let (config_fg, config_bg) = if is_first { + (colors.hints.start.foreground, colors.hints.start.background) + } else { + (colors.hints.end.foreground, colors.hints.end.background) + }; + Self::compute_cell_rgb(&mut fg_rgb, &mut bg_rgb, &mut bg_alpha, config_fg, config_bg); + + character = c; + } else if is_selected { + let config_fg = colors.selection.foreground; let config_bg = colors.selection.background; - let selected_fg = colors.selection.foreground.color(fg_rgb, bg_rgb); - bg_rgb = config_bg.color(fg_rgb, bg_rgb); - fg_rgb = selected_fg; + Self::compute_cell_rgb(&mut fg_rgb, &mut bg_rgb, &mut bg_alpha, config_fg, config_bg); if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) { // Reveal inversed text when fg/bg is the same. fg_rgb = content.color(NamedColor::Background as usize); bg_rgb = content.color(NamedColor::Foreground as usize); bg_alpha = 1.0; - } else if config_bg != CellRgb::CellBackground { - bg_alpha = 1.0; } } else if content.search.advance(cell.point) { // Highlight the cell if it is part of a search match. + let config_fg = colors.search.matches.foreground; let config_bg = colors.search.matches.background; - let matched_fg = colors.search.matches.foreground.color(fg_rgb, bg_rgb); - bg_rgb = config_bg.color(fg_rgb, bg_rgb); - fg_rgb = matched_fg; - - if config_bg != CellRgb::CellBackground { - bg_alpha = 1.0; - } + Self::compute_cell_rgb(&mut fg_rgb, &mut bg_rgb, &mut bg_alpha, config_fg, config_bg); is_match = true; } RenderableCell { - character: cell.c, + character, zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()), point: cell.point, fg: fg_rgb, @@ -242,6 +255,22 @@ impl RenderableCell { && self.zerowidth.is_none() } + /// Apply [`CellRgb`] colors to the cell's colors. + fn compute_cell_rgb( + cell_fg: &mut Rgb, + cell_bg: &mut Rgb, + bg_alpha: &mut f32, + fg: CellRgb, + bg: CellRgb, + ) { + let old_fg = mem::replace(cell_fg, fg.color(*cell_fg, *cell_bg)); + *cell_bg = bg.color(old_fg, *cell_bg); + + if bg != CellRgb::CellBackground { + *bg_alpha = 1.0; + } + } + /// Get the RGB color from a cell's foreground color. fn compute_fg_rgb(content: &mut RenderableContent<'_>, fg: Color, flags: Flags) -> Rgb { let ui_config = &content.config.ui_config; @@ -339,18 +368,58 @@ impl RenderableCursor { } } -/// Regex search highlight tracking. -#[derive(Default)] -pub struct RenderableSearch { - /// All visible search matches. - matches: Vec<RangeInclusive<Point>>, +/// Regex hints for keyboard shortcuts. +struct Hint<'a> { + /// Hint matches and position. + regex: Regex<'a>, - /// Index of the last match checked. - index: usize, + /// Last match checked against current cell position. + labels: &'a Vec<Vec<char>>, +} + +impl<'a> Hint<'a> { + /// Advance the hint iterator. + /// + /// If the point is within a hint, the keyboard shortcut character that should be displayed at + /// this position will be returned. + /// + /// The tuple's [`bool`] will be `true` when the character is the first for this hint. + fn advance(&mut self, point: Point) -> Option<(char, bool)> { + // Check if we're within a match at all. + if !self.regex.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| regex_match.start()) + .filter(|start| start.line == point.line)?; + + // Position within the hint label. + let label_position = point.column.0 - start.column.0; + let is_first = label_position == 0; + + // Hint label character. + self.labels[self.regex.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 } + } } -impl RenderableSearch { - /// Create a new renderable search iterator. +/// Wrapper for finding visible regex matches. +#[derive(Default, Clone)] +pub struct RegexMatches(Vec<RangeInclusive<Point>>); + +impl RegexMatches { + /// Find all visible matches. pub fn new<T>(term: &Term<T>, dfas: &RegexSearch) -> Self { let viewport_end = term.grid().display_offset(); let viewport_start = viewport_end + term.screen_lines().0 - 1; @@ -383,12 +452,44 @@ impl RenderableSearch { viewport_start..=viewport_end }); - Self { matches: iter.collect(), index: 0 } + 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. +#[derive(Default)] +struct Regex<'a> { + /// All visible matches. + matches: Cow<'a, RegexMatches>, + + /// 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 } } - /// Advance the search tracker to the next point. + /// Advance the regex tracker to the next point. /// - /// This will return `true` if the point passed is part of a search match. + /// 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 { |