diff options
Diffstat (limited to 'alacritty/src/display/hint.rs')
-rw-r--r-- | alacritty/src/display/hint.rs | 264 |
1 files changed, 264 insertions, 0 deletions
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']); + } +} |