summaryrefslogtreecommitdiffstats
path: root/alacritty
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2021-03-01 19:50:39 +0000
committerGitHub <noreply@github.com>2021-03-01 19:50:39 +0000
commita954e076ca0b1ee9c1f272c2b119c67df3935fd4 (patch)
treef233f8622ac6ab33519bfcb70b480f23697198b1 /alacritty
parent772afc6a8aa9db6f89de4b23df27b571a40c9118 (diff)
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')
-rw-r--r--alacritty/src/config/bindings.rs8
-rw-r--r--alacritty/src/config/color.rs37
-rw-r--r--alacritty/src/config/mod.rs3
-rw-r--r--alacritty/src/config/ui_config.rs168
-rw-r--r--alacritty/src/display/content.rs171
-rw-r--r--alacritty/src/display/hint.rs264
-rw-r--r--alacritty/src/display/mod.rs14
-rw-r--r--alacritty/src/event.rs45
-rw-r--r--alacritty/src/input.rs77
9 files changed, 693 insertions, 94 deletions
diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs
index 4d0fcadd..732875db 100644
--- a/alacritty/src/config/bindings.rs
+++ b/alacritty/src/config/bindings.rs
@@ -16,6 +16,8 @@ use alacritty_terminal::config::Program;
use alacritty_terminal::term::TermMode;
use alacritty_terminal::vi_mode::ViMotion;
+use crate::config::ui_config::Hint;
+
/// Describes a state and action to take in that state.
///
/// This is the shared component of `MouseBinding` and `KeyBinding`.
@@ -91,6 +93,10 @@ pub enum Action {
#[config(skip)]
Command(Program),
+ /// Regex keyboard hints.
+ #[config(skip)]
+ Hint(Hint),
+
/// Move vi mode cursor.
#[config(skip)]
ViMotion(ViMotion),
@@ -1132,7 +1138,7 @@ impl<'a> Deserialize<'a> for KeyBinding {
/// Our deserialize impl wouldn't be covered by a derive(Deserialize); see the
/// impl below.
#[derive(Debug, Copy, Clone, Hash, Default, Eq, PartialEq)]
-pub struct ModsWrapper(ModifiersState);
+pub struct ModsWrapper(pub ModifiersState);
impl ModsWrapper {
pub fn into_inner(self) -> ModifiersState {
diff --git a/alacritty/src/config/color.rs b/alacritty/src/config/color.rs
index cd5d964d..d55cf26f 100644
--- a/alacritty/src/config/color.rs
+++ b/alacritty/src/config/color.rs
@@ -16,6 +16,7 @@ pub struct Colors {
pub indexed_colors: Vec<IndexedColor>,
pub search: SearchColors,
pub line_indicator: LineIndicatorColors,
+ pub hints: HintColors,
}
impl Colors {
@@ -34,6 +35,42 @@ pub struct LineIndicatorColors {
pub background: Option<Rgb>,
}
+#[derive(ConfigDeserialize, Default, Copy, Clone, Debug, PartialEq, Eq)]
+pub struct HintColors {
+ pub start: HintStartColors,
+ pub end: HintEndColors,
+}
+
+#[derive(ConfigDeserialize, Copy, Clone, Debug, PartialEq, Eq)]
+pub struct HintStartColors {
+ pub foreground: CellRgb,
+ pub background: CellRgb,
+}
+
+impl Default for HintStartColors {
+ fn default() -> Self {
+ Self {
+ foreground: CellRgb::Rgb(Rgb { r: 0x1d, g: 0x1f, b: 0x21 }),
+ background: CellRgb::Rgb(Rgb { r: 0xe9, g: 0xff, b: 0x5e }),
+ }
+ }
+}
+
+#[derive(ConfigDeserialize, Copy, Clone, Debug, PartialEq, Eq)]
+pub struct HintEndColors {
+ pub foreground: CellRgb,
+ pub background: CellRgb,
+}
+
+impl Default for HintEndColors {
+ fn default() -> Self {
+ Self {
+ foreground: CellRgb::Rgb(Rgb { r: 0xe9, g: 0xff, b: 0x5e }),
+ background: CellRgb::Rgb(Rgb { r: 0x1d, g: 0x1f, b: 0x21 }),
+ }
+ }
+}
+
#[derive(Deserialize, Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct IndexedColor {
pub color: Rgb,
diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs
index c321915e..4ddc81c2 100644
--- a/alacritty/src/config/mod.rs
+++ b/alacritty/src/config/mod.rs
@@ -159,6 +159,9 @@ fn read_config(path: &Path, cli_config: Value) -> Result<Config> {
let mut config = Config::deserialize(config_value)?;
config.ui_config.config_paths = config_paths;
+ // Create key bindings for regex hints.
+ config.ui_config.generate_hint_bindings();
+
Ok(config)
}
diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs
index 2d7b5c98..6d06aa7d 100644
--- a/alacritty/src/config/ui_config.rs
+++ b/alacritty/src/config/ui_config.rs
@@ -1,13 +1,20 @@
+use std::cell::RefCell;
use std::path::PathBuf;
+use std::rc::Rc;
use log::error;
+use serde::de::Error as SerdeError;
use serde::{Deserialize, Deserializer};
+use unicode_width::UnicodeWidthChar;
use alacritty_config_derive::ConfigDeserialize;
-use alacritty_terminal::config::{Percentage, LOG_TARGET_CONFIG};
+use alacritty_terminal::config::{Percentage, Program, LOG_TARGET_CONFIG};
+use alacritty_terminal::term::search::RegexSearch;
use crate::config::bell::BellConfig;
-use crate::config::bindings::{self, Binding, KeyBinding, MouseBinding};
+use crate::config::bindings::{
+ self, Action, Binding, BindingMode, Key, KeyBinding, ModsWrapper, MouseBinding,
+};
use crate::config::color::Colors;
use crate::config::debug::Debug;
use crate::config::font::Font;
@@ -46,6 +53,9 @@ pub struct UiConfig {
#[config(skip)]
pub config_paths: Vec<PathBuf>,
+ /// Regex hints for interacting with terminal content.
+ pub hints: Hints,
+
/// Keybindings.
key_bindings: KeyBindings,
@@ -72,11 +82,27 @@ impl Default for UiConfig {
bell: Default::default(),
colors: Default::default(),
draw_bold_text_with_bright_colors: Default::default(),
+ hints: Default::default(),
}
}
}
impl UiConfig {
+ /// Generate key bindings for all keyboard hints.
+ pub fn generate_hint_bindings(&mut self) {
+ for hint in self.hints.enabled.drain(..) {
+ let binding = KeyBinding {
+ trigger: hint.binding.key,
+ mods: hint.binding.mods.0,
+ mode: BindingMode::empty(),
+ notmode: BindingMode::empty(),
+ action: Action::Hint(hint),
+ };
+
+ self.key_bindings.0.push(binding);
+ }
+ }
+
#[inline]
pub fn background_opacity(&self) -> f32 {
self.background_opacity.as_f32()
@@ -169,3 +195,141 @@ pub struct Delta<T: Default> {
/// Vertical change.
pub y: T,
}
+
+/// Regex terminal hints.
+#[derive(ConfigDeserialize, Default, Debug, PartialEq, Eq)]
+pub struct Hints {
+ /// Characters for the hint labels.
+ alphabet: HintsAlphabet,
+
+ /// All configured terminal hints.
+ enabled: Vec<Hint>,
+}
+
+impl Hints {
+ /// Characters for the hint labels.
+ pub fn alphabet(&self) -> &str {
+ &self.alphabet.0
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct HintsAlphabet(String);
+
+impl Default for HintsAlphabet {
+ fn default() -> Self {
+ Self(String::from("jfkdls;ahgurieowpq"))
+ }
+}
+
+impl<'de> Deserialize<'de> for HintsAlphabet {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+
+ let mut character_count = 0;
+ for character in value.chars() {
+ if character.width() != Some(1) {
+ return Err(D::Error::custom("characters must be of width 1"));
+ }
+ character_count += 1;
+ }
+
+ if character_count < 2 {
+ return Err(D::Error::custom("must include at last 2 characters"));
+ }
+
+ Ok(Self(value))
+ }
+}
+
+/// Hint configuration.
+#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
+pub struct Hint {
+ /// Command the text will be piped to.
+ pub command: Program,
+
+ /// Regex for finding matches.
+ pub regex: LazyRegex,
+
+ /// Binding required to search for this hint.
+ binding: HintBinding,
+}
+
+/// Binding for triggering a keyboard hint.
+#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
+pub struct HintBinding {
+ pub key: Key,
+ pub mods: ModsWrapper,
+}
+
+/// Lazy regex with interior mutability.
+#[derive(Clone, Debug)]
+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
+ where
+ F: Fn(&RegexSearch) -> T,
+ {
+ f(self.0.borrow_mut().compiled())
+ }
+}
+
+impl<'de> Deserialize<'de> for LazyRegex {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let regex = LazyRegexVariant::Pattern(String::deserialize(deserializer)?);
+ Ok(Self(Rc::new(RefCell::new(regex))))
+ }
+}
+
+/// 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 {
+ Compiled(Box<RegexSearch>),
+ Pattern(String),
+}
+
+impl LazyRegexVariant {
+ /// Get a reference to the compiled regex.
+ ///
+ /// If the regex is not already compiled, this will compile the DFAs and store them for future
+ /// access.
+ fn compiled(&mut self) -> &RegexSearch {
+ // Check if the regex has already been compiled.
+ let regex = match self {
+ Self::Compiled(regex_search) => return regex_search,
+ Self::Pattern(regex) => regex,
+ };
+
+ // Compile the regex.
+ let regex_search = match RegexSearch::new(&regex) {
+ Ok(regex_search) => regex_search,
+ Err(error) => {
+ error!("hint regex is invalid: {}", error);
+ RegexSearch::new("").unwrap()
+ },
+ };
+ *self = Self::Compiled(Box::new(regex_search));
+
+ // Return a reference to the compiled DFAs.
+ match self {
+ Self::Compiled(dfas) => dfas,
+ Self::Pattern(_) => unreachable!(),
+ }
+ }
+}
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 {
diff --git a/alacritty/src/display/hint.rs b/alacritty/src/display/hint.rs
new file mode 100644
index 00000000..6499a959
--- /dev/null
+++ b/alacritty/src/display/hint.rs
@@ -0,0 +1,264 @@
+use alacritty_terminal::term::Term;
+
+use crate::config::ui_config::Hint;
+use crate::daemon::start_daemon;
+use crate::display::content::RegexMatches;
+
+/// Percentage of characters in the hints alphabet used for the last character.
+const HINT_SPLIT_PERCENTAGE: f32 = 0.5;
+
+/// Keyboard regex hint state.
+pub struct HintState {
+ /// Hint currently in use.
+ hint: Option<Hint>,
+
+ /// Alphabet for hint labels.
+ alphabet: String,
+
+ /// Visible matches.
+ matches: RegexMatches,
+
+ /// Key label for each visible match.
+ labels: Vec<Vec<char>>,
+
+ /// Keys pressed for hint selection.
+ keys: Vec<char>,
+}
+
+impl HintState {
+ /// Initialize an inactive hint state.
+ pub fn new<S: Into<String>>(alphabet: S) -> Self {
+ Self {
+ alphabet: alphabet.into(),
+ hint: Default::default(),
+ matches: Default::default(),
+ labels: Default::default(),
+ keys: Default::default(),
+ }
+ }
+
+ /// Check if a hint selection is in progress.
+ pub fn active(&self) -> bool {
+ self.hint.is_some()
+ }
+
+ /// Start the hint selection process.
+ pub fn start(&mut self, hint: Hint) {
+ self.hint = Some(hint);
+ }
+
+ /// Cancel the hint highlighting process.
+ fn stop(&mut self) {
+ self.matches.clear();
+ self.labels.clear();
+ self.keys.clear();
+ self.hint = None;
+ }
+
+ /// Update the visible hint matches and key labels.
+ pub fn update_matches<T>(&mut self, term: &Term<T>) {
+ let hint = match self.hint.as_mut() {
+ Some(hint) => hint,
+ None => return,
+ };
+
+ // Find visible matches.
+ self.matches = hint.regex.with_compiled(|regex| RegexMatches::new(term, regex));
+
+ // Cancel highlight with no visible matches.
+ if self.matches.is_empty() {
+ self.stop();
+ return;
+ }
+
+ let mut generator = HintLabels::new(&self.alphabet, HINT_SPLIT_PERCENTAGE);
+ let match_count = self.matches.len();
+ let keys_len = self.keys.len();
+
+ // Get the label for each match.
+ self.labels.resize(match_count, Vec::new());
+ for i in (0..match_count).rev() {
+ let mut label = generator.next();
+ if label.len() >= keys_len && label[..keys_len] == self.keys[..] {
+ self.labels[i] = label.split_off(keys_len);
+ } else {
+ self.labels[i] = Vec::new();
+ }
+ }
+ }
+
+ /// Handle keyboard input during hint selection.
+ pub fn keyboard_input<T>(&mut self, term: &Term<T>, c: char) {
+ match c {
+ // Use backspace to remove the last character pressed.
+ '\x08' | '\x1f' => {
+ self.keys.pop();
+ },
+ // Cancel hint highlighting on ESC.
+ '\x1b' => self.stop(),
+ _ => (),
+ }
+
+ // Update the visible matches.
+ self.update_matches(term);
+
+ let hint = match self.hint.as_ref() {
+ Some(hint) => hint,
+ None => return,
+ };
+
+ // Find the last label starting with the input character.
+ let mut labels = self.labels.iter().enumerate().rev();
+ let (index, label) = match labels.find(|(_, label)| !label.is_empty() && label[0] == c) {
+ Some(last) => last,
+ None => return,
+ };
+
+ // Check if the selected label is fully matched.
+ if label.len() == 1 {
+ // Get text for the hint's regex match.
+ let hint_match = &self.matches[index];
+ let start = term.visible_to_buffer(*hint_match.start());
+ let end = term.visible_to_buffer(*hint_match.end());
+ let text = term.bounds_to_string(start, end);
+
+ // Append text as last argument and launch command.
+ let program = hint.command.program();
+ let mut args = hint.command.args().to_vec();
+ args.push(text);
+ start_daemon(program, &args);
+
+ self.stop();
+ } else {
+ // Store character to preserve the selection.
+ self.keys.push(c);
+ }
+ }
+
+ /// Hint key labels.
+ pub fn labels(&self) -> &Vec<Vec<char>> {
+ &self.labels
+ }
+
+ /// Visible hint regex matches.
+ pub fn matches(&self) -> &RegexMatches {
+ &self.matches
+ }
+
+ /// Update the alphabet used for hint labels.
+ pub fn update_alphabet(&mut self, alphabet: &str) {
+ if self.alphabet != alphabet {
+ self.alphabet = alphabet.to_owned();
+ self.keys.clear();
+ }
+ }
+}
+
+/// Generator for creating new hint labels.
+struct HintLabels {
+ /// Full character set available.
+ alphabet: Vec<char>,
+
+ /// Alphabet indices for the next label.
+ indices: Vec<usize>,
+
+ /// Point separating the alphabet's head and tail characters.
+ ///
+ /// To make identification of the tail character easy, part of the alphabet cannot be used for
+ /// any other position.
+ ///
+ /// All characters in the alphabet before this index will be used for the last character, while
+ /// the rest will be used for everything else.
+ split_point: usize,
+}
+
+impl HintLabels {
+ /// Create a new label generator.
+ ///
+ /// The `split_ratio` should be a number between 0.0 and 1.0 representing the percentage of
+ /// elements in the alphabet which are reserved for the tail of the hint label.
+ fn new(alphabet: impl Into<String>, split_ratio: f32) -> Self {
+ let alphabet: Vec<char> = alphabet.into().chars().collect();
+ let split_point = ((alphabet.len() - 1) as f32 * split_ratio.min(1.)) as usize;
+
+ Self { indices: vec![0], split_point, alphabet }
+ }
+
+ /// Get the characters for the next label.
+ fn next(&mut self) -> Vec<char> {
+ let characters = self.indices.iter().rev().map(|index| self.alphabet[*index]).collect();
+ self.increment();
+ characters
+ }
+
+ /// Increment the character sequence.
+ fn increment(&mut self) {
+ // Increment the last character; if it's not at the split point we're done.
+ let tail = &mut self.indices[0];
+ if *tail < self.split_point {
+ *tail += 1;
+ return;
+ }
+ *tail = 0;
+
+ // Increment all other characters in reverse order.
+ let alphabet_len = self.alphabet.len();
+ for index in self.indices.iter_mut().skip(1) {
+ if *index + 1 == alphabet_len {
+ // Reset character and move to the next if it's already at the limit.
+ *index = self.split_point + 1;
+ } else {
+ // If the character can be incremented, we're done.
+ *index += 1;
+ return;
+ }
+ }
+
+ // Extend the sequence with another character when nothing could be incremented.
+ self.indices.push(self.split_point + 1);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn hint_label_generation() {
+ let mut generator = HintLabels::new("0123", 0.5);
+
+ assert_eq!(generator.next(), vec!['0']);
+ assert_eq!(generator.next(), vec!['1']);
+
+ assert_eq!(generator.next(), vec!['2', '0']);
+ assert_eq!(generator.next(), vec!['2', '1']);
+ assert_eq!(generator.next(), vec!['3', '0']);
+ assert_eq!(generator.next(), vec!['3', '1']);
+
+ assert_eq!(generator.next(), vec!['2', '2', '0']);
+ assert_eq!(generator.next(), vec!['2', '2', '1']);
+ assert_eq!(generator.next(), vec!['2', '3', '0']);
+ assert_eq!(generator.next(), vec!['2', '3', '1']);
+ assert_eq!(generator.next(), vec!['3', '2', '0']);
+ assert_eq!(generator.next(), vec!['3', '2', '1']);
+ assert_eq!(generator.next(), vec!['3', '3', '0']);
+ assert_eq!(generator.next(), vec!['3', '3', '1']);
+
+ assert_eq!(generator.next(), vec!['2', '2', '2', '0']);
+ assert_eq!(generator.next(), vec!['2', '2', '2', '1']);
+ assert_eq!(generator.next(), vec!['2', '2', '3', '0']);
+ assert_eq!(generator.next(), vec!['2', '2', '3', '1']);
+ assert_eq!(generator.next(), vec!['2', '3', '2', '0']);
+ assert_eq!(generator.next(), vec!['2', '3', '2', '1']);
+ assert_eq!(generator.next(), vec!['2', '3', '3', '0']);
+ assert_eq!(generator.next(), vec!['2', '3', '3', '1']);
+ assert_eq!(generator.next(), vec!['3', '2', '2', '0']);
+ assert_eq!(generator.next(), vec!['3', '2', '2', '1']);
+ assert_eq!(generator.next(), vec!['3', '2', '3', '0']);
+ assert_eq!(generator.next(), vec!['3', '2', '3', '1']);
+ assert_eq!(generator.next(), vec!['3', '3', '2', '0']);
+ assert_eq!(generator.next(), vec!['3', '3', '2', '1']);
+ assert_eq!(generator.next(), vec!['3', '3', '3', '0']);
+ assert_eq!(generator.next(), vec!['3', '3', '3', '1']);
+ }
+}
diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs
index 9c37bd0e..d44013c4 100644
--- a/alacritty/src/display/mod.rs
+++ b/alacritty/src/display/mod.rs
@@ -38,6 +38,7 @@ 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::meter::Meter;
use crate::display::window::Window;
use crate::event::{Mouse, SearchState};
@@ -48,6 +49,7 @@ use crate::url::{Url, Urls};
pub mod content;
pub mod cursor;
+pub mod hint;
pub mod window;
mod bell;
@@ -181,6 +183,9 @@ pub struct Display {
/// Mapped RGB values for each terminal color.
pub colors: List,
+ /// State of the keyboard hints.
+ pub hint_state: HintState,
+
renderer: QuadRenderer,
glyph_cache: GlyphCache,
meter: Meter,
@@ -317,10 +322,13 @@ impl Display {
_ => (),
}
+ let hint_state = HintState::new(config.ui_config.hints.alphabet());
+
Ok(Self {
window,
renderer,
glyph_cache,
+ hint_state,
meter: Meter::new(),
size_info,
urls: Urls::new(),
@@ -474,12 +482,10 @@ impl Display {
let viewport_match = search_state
.focused_match()
.and_