aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/display/hint.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty/src/display/hint.rs')
-rw-r--r--alacritty/src/display/hint.rs264
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']);
+ }
+}