use alacritty_terminal::term::search::Match; use alacritty_terminal::term::Term; use crate::config::ui_config::{Hint, HintAction}; 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, /// Alphabet for hint labels. alphabet: String, /// Visible matches. matches: RegexMatches, /// Key label for each visible match. labels: Vec>, /// Keys pressed for hint selection. keys: Vec, } impl HintState { /// Initialize an inactive hint state. pub fn new>(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(&mut self, term: &Term) { 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(&mut self, term: &Term, c: char) -> Option { 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 = self.hint.as_ref()?; // Find the last label starting with the input character. let mut labels = self.labels.iter().enumerate().rev(); let (index, label) = labels.find(|(_, label)| !label.is_empty() && label[0] == c)?; // Check if the selected label is fully matched. if label.len() == 1 { let bounds = self.matches[index].clone(); let action = hint.action.clone(); self.stop(); Some(HintMatch { action, bounds }) } else { // Store character to preserve the selection. self.keys.push(c); None } } /// Hint key labels. pub fn labels(&self) -> &Vec> { &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(); } } } /// Hint match which was selected by the user. pub struct HintMatch { /// Action for handling the text. pub action: HintAction, /// Terminal range matching the hint. pub bounds: Match, } /// Generator for creating new hint labels. struct HintLabels { /// Full character set available. alphabet: Vec, /// Alphabet indices for the next label. indices: Vec, /// 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, split_ratio: f32) -> Self { let alphabet: Vec = 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 { 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']); } }