tiny_dc/
hotkeys.rs

1use std::{
2    collections::{HashMap, HashSet},
3    hash::Hash,
4};
5
6use crossterm::event::{KeyCode, KeyModifiers};
7
8use crate::{
9    app::{Action, InputMode, ListMode},
10    entry::{EntryKind, EntryRenderData},
11};
12
13#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
14pub struct KeyCombo {
15    pub key_code: KeyCode,
16    pub modifiers: KeyModifiers,
17}
18
19impl From<KeyCode> for KeyCombo {
20    fn from(key_code: KeyCode) -> Self {
21        KeyCombo {
22            key_code,
23            modifiers: KeyModifiers::NONE,
24        }
25    }
26}
27
28impl From<char> for KeyCombo {
29    fn from(c: char) -> Self {
30        KeyCombo {
31            key_code: KeyCode::Char(c),
32            modifiers: KeyModifiers::NONE,
33        }
34    }
35}
36
37impl From<(char, KeyModifiers)> for KeyCombo {
38    fn from((c, modifiers): (char, KeyModifiers)) -> Self {
39        KeyCombo {
40            key_code: KeyCode::Char(c),
41            modifiers,
42        }
43    }
44}
45
46impl From<(KeyCode, KeyModifiers)> for KeyCombo {
47    fn from((key_code, modifiers): (KeyCode, KeyModifiers)) -> Self {
48        KeyCombo {
49            key_code,
50            modifiers,
51        }
52    }
53}
54
55#[derive(Debug)]
56pub struct HotkeysTrieNode<T> {
57    pub children: HashMap<KeyCombo, HotkeysTrieNode<T>>,
58    pub value: Option<T>,
59}
60
61#[derive(Debug)]
62struct HotkeysTrie<T> {
63    root: HotkeysTrieNode<T>,
64}
65
66impl<T> HotkeysTrie<T> {
67    pub fn new() -> Self {
68        HotkeysTrie {
69            root: HotkeysTrieNode {
70                children: HashMap::new(),
71                value: None,
72            },
73        }
74    }
75
76    pub fn insert(&mut self, key_combos: &[KeyCombo], value: T) {
77        // we start at the root
78        let mut current_node = &mut self.root;
79
80        for &key_combo in key_combos {
81            // if the node doesn't exist create it and move to it
82            current_node = current_node
83                .children
84                .entry(key_combo)
85                .or_insert(HotkeysTrieNode {
86                    children: HashMap::new(),
87                    value: None,
88                });
89        }
90
91        // we've reached the end, we can now append the value
92        current_node.value = Some(value);
93    }
94
95    pub fn get_value(&self, key_combos: &[KeyCombo]) -> Option<&T> {
96        // we start at the root
97        let node = self.get_node(key_combos)?;
98        node.value.as_ref()
99    }
100
101    pub fn get_node(&self, key_combos: &[KeyCombo]) -> Option<&HotkeysTrieNode<T>> {
102        // we start at the root
103        let mut current_node = &self.root;
104
105        for &key_combo in key_combos {
106            if let Some(node) = current_node.children.get(&key_combo) {
107                current_node = node;
108            } else {
109                return None;
110            }
111        }
112
113        Some(current_node)
114    }
115
116    pub fn clear(&mut self) {
117        self.root.children.clear();
118        self.root.value = None;
119    }
120}
121
122impl<T> Default for HotkeysTrie<T> {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128#[derive(Debug)]
129pub struct HotkeysRegistry<C, T>
130where
131    C: Eq + Hash,
132    T: std::fmt::Debug,
133{
134    /// System hotkeys are those needed for interracting with the app (for example: NextItem,
135    /// PrevItem, FirstItem, LastItem etc.)
136    system_hotkeys: HashMap<C, HotkeysTrie<T>>,
137    system_hotkeys_count: usize,
138
139    /// Entry hotkeys are those that are assigned on each entry so thtat the user can quickly jump
140    /// into an entry without going to it
141    entry_hotkeys: HotkeysTrie<T>,
142    entry_hotkeys_count: usize,
143}
144
145impl<C, T> HotkeysRegistry<C, T>
146where
147    C: Eq + Hash,
148    T: std::fmt::Debug,
149{
150    pub fn new() -> Self {
151        HotkeysRegistry {
152            system_hotkeys: HashMap::new(),
153            system_hotkeys_count: 0,
154            entry_hotkeys: HotkeysTrie::new(),
155            entry_hotkeys_count: 0,
156        }
157    }
158
159    pub fn register_system_hotkey(&mut self, context: C, key_combos: &[KeyCombo], value: T) {
160        self.system_hotkeys_count += 1;
161        let trie = self.system_hotkeys.entry(context).or_default();
162        trie.insert(key_combos, value);
163    }
164
165    pub fn register_entry_hotkey(&mut self, key_combos: &[KeyCombo], value: T) {
166        self.entry_hotkeys_count += 1;
167        self.entry_hotkeys.insert(key_combos, value);
168    }
169
170    pub fn clear_entry_hotkeys(&mut self) {
171        self.entry_hotkeys.clear();
172        self.entry_hotkeys_count = 0;
173    }
174
175    pub fn get_hotkey_value(&self, context: C, key_combos: &[KeyCombo]) -> Option<&T> {
176        if self.system_hotkeys_count == 0 && self.entry_hotkeys_count == 0 {
177            return None;
178        }
179
180        // System hotkeys take priority
181        self.system_hotkeys
182            .get(&context)
183            .and_then(|trie| trie.get_value(key_combos))
184            .or_else(|| self.entry_hotkeys.get_value(key_combos))
185    }
186
187    pub fn get_hotkey_node(
188        &self,
189        context: C,
190        key_combos: &[KeyCombo],
191    ) -> Option<&HotkeysTrieNode<T>> {
192        if self.system_hotkeys_count == 0 && self.entry_hotkeys_count == 0 {
193            return None;
194        }
195
196        // System hotkeys take priority
197        self.system_hotkeys
198            .get(&context)
199            .and_then(|trie| trie.get_node(key_combos))
200            .or_else(|| self.entry_hotkeys.get_node(key_combos))
201    }
202}
203
204impl<C, T> Default for HotkeysRegistry<C, T>
205where
206    C: Eq + Hash,
207    T: std::fmt::Debug,
208{
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214const fn key_combo_from_char(c: char) -> KeyCombo {
215    KeyCombo {
216        key_code: KeyCode::Char(c),
217        modifiers: KeyModifiers::NONE,
218    }
219}
220
221/// The preferred shortcuts for the entries in the list. These will be used to quickly jump to an
222/// entry and will be chosed based on the order that they appear in this array, this way we can
223/// prioritize ergonomics. In future versions, we might allow the user to customize these
224/// shortcuts.
225pub const PREFERRED_KEY_COMBOS_IN_ORDER: [KeyCombo; 31] = [
226    key_combo_from_char('a'),
227    key_combo_from_char('s'),
228    key_combo_from_char('w'),
229    key_combo_from_char('e'),
230    key_combo_from_char('r'),
231    key_combo_from_char('t'),
232    key_combo_from_char('z'),
233    key_combo_from_char('x'),
234    key_combo_from_char('c'),
235    key_combo_from_char('v'),
236    key_combo_from_char('b'),
237    key_combo_from_char('y'),
238    key_combo_from_char('u'),
239    key_combo_from_char('i'),
240    key_combo_from_char('o'),
241    key_combo_from_char('p'),
242    key_combo_from_char('n'),
243    key_combo_from_char('m'),
244    key_combo_from_char(','),
245    key_combo_from_char('1'),
246    key_combo_from_char('2'),
247    key_combo_from_char('3'),
248    key_combo_from_char('4'),
249    key_combo_from_char('5'),
250    key_combo_from_char('6'),
251    key_combo_from_char('7'),
252    key_combo_from_char('8'),
253    key_combo_from_char('9'),
254    key_combo_from_char('0'),
255    key_combo_from_char('-'),
256    key_combo_from_char('='),
257];
258
259impl HotkeysRegistry<InputMode, Action> {
260    pub fn new_with_default_system_hotkeys() -> Self {
261        let mut registry = HotkeysRegistry::new();
262
263        registry.register_system_hotkey(
264            InputMode::Normal,
265            &[KeyCombo::from('g'), KeyCombo::from('g')],
266            Action::SelectFirst,
267        );
268
269        registry.register_system_hotkey(
270            InputMode::Normal,
271            &[KeyCombo::from(KeyCode::Home)],
272            Action::SelectFirst,
273        );
274
275        registry.register_system_hotkey(
276            InputMode::Normal,
277            &[KeyCombo::from(('G', KeyModifiers::SHIFT))],
278            Action::SelectLast,
279        );
280
281        registry.register_system_hotkey(
282            InputMode::Normal,
283            &[KeyCombo::from(KeyCode::End)],
284            Action::SelectLast,
285        );
286
287        registry.register_system_hotkey(
288            InputMode::Normal,
289            &[KeyCombo::from('j')],
290            Action::SelectNext,
291        );
292
293        registry.register_system_hotkey(
294            InputMode::Normal,
295            &[KeyCombo::from(KeyCode::Down)],
296            Action::SelectNext,
297        );
298
299        registry.register_system_hotkey(
300            InputMode::Normal,
301            &[KeyCombo::from('k')],
302            Action::SelectPrevious,
303        );
304
305        registry.register_system_hotkey(
306            InputMode::Normal,
307            &[KeyCombo::from(KeyCode::Up)],
308            Action::SelectPrevious,
309        );
310
311        registry.register_system_hotkey(
312            InputMode::Normal,
313            &[KeyCombo::from(('d', KeyModifiers::CONTROL))],
314            Action::SwitchToListMode(ListMode::Directory),
315        );
316
317        registry.register_system_hotkey(
318            InputMode::Normal,
319            &[KeyCombo::from('l')],
320            Action::ChangeDirectoryToSelectedEntry,
321        );
322
323        registry.register_system_hotkey(
324            InputMode::Normal,
325            &[KeyCombo::from(KeyCode::Right)],
326            Action::ChangeDirectoryToSelectedEntry,
327        );
328
329        registry.register_system_hotkey(
330            InputMode::Normal,
331            &[KeyCombo::from('h')],
332            Action::ChangeDirectoryToParent,
333        );
334
335        registry.register_system_hotkey(
336            InputMode::Normal,
337            &[KeyCombo::from(KeyCode::Left)],
338            Action::ChangeDirectoryToParent,
339        );
340
341        registry.register_system_hotkey(
342            InputMode::Normal,
343            &[KeyCombo::from(KeyCode::Backspace)],
344            Action::ChangeDirectoryToParent,
345        );
346
347        registry.register_system_hotkey(
348            InputMode::Normal,
349            &[KeyCombo::from(('f', KeyModifiers::CONTROL))],
350            Action::SwitchToListMode(ListMode::Frecent),
351        );
352
353        registry.register_system_hotkey(
354            InputMode::Normal,
355            &[KeyCombo::from('?')],
356            Action::ToggleHelp,
357        );
358
359        registry.register_system_hotkey(
360            InputMode::Normal,
361            &[KeyCombo::from('/')],
362            Action::SwitchToInputMode(InputMode::Search),
363        );
364
365        registry.register_system_hotkey(
366            InputMode::Normal,
367            &[KeyCombo::from(KeyCode::Esc)],
368            Action::Exit,
369        );
370
371        registry.register_system_hotkey(InputMode::Normal, &[KeyCombo::from('q')], Action::Exit);
372
373        registry.register_system_hotkey(
374            InputMode::Normal,
375            &[KeyCombo::from(KeyCode::Enter)],
376            Action::ChangeDirectoryToSelectedEntry,
377        );
378
379        registry.register_system_hotkey(
380            InputMode::Normal,
381            &[KeyCombo::from('_')],
382            Action::ResetSearchInput,
383        );
384
385        registry.register_system_hotkey(
386            InputMode::Search,
387            &[KeyCombo::from(KeyCode::Enter)],
388            Action::ChangeDirectoryToSelectedEntry,
389        );
390
391        registry.register_system_hotkey(
392            InputMode::Search,
393            &[KeyCombo::from(KeyCode::Right)],
394            Action::ChangeDirectoryToSelectedEntry,
395        );
396
397        registry.register_system_hotkey(
398            InputMode::Search,
399            &[KeyCombo::from(KeyCode::Down)],
400            Action::SelectNext,
401        );
402
403        registry.register_system_hotkey(
404            InputMode::Search,
405            &[KeyCombo::from(KeyCode::Up)],
406            Action::SelectPrevious,
407        );
408
409        registry.register_system_hotkey(
410            InputMode::Search,
411            &[KeyCombo::from(KeyCode::Esc)],
412            Action::ExitSearchInput,
413        );
414
415        registry.register_system_hotkey(
416            InputMode::Search,
417            &[KeyCombo::from(KeyCode::Backspace)],
418            Action::SearchInputBackspace,
419        );
420
421        registry
422    }
423
424    fn generate_sequence_permutations(
425        key_combos: &[KeyCombo],
426        length: usize,
427    ) -> Vec<Vec<KeyCombo>> {
428        let mut result = Vec::new();
429        let mut current = vec![key_combos[0]; length];
430
431        fn generate(
432            key_combos: &[KeyCombo],
433            current: &mut Vec<KeyCombo>,
434            result: &mut Vec<Vec<KeyCombo>>,
435            pos: usize,
436        ) {
437            if pos == current.len() {
438                result.push(current.clone());
439                return;
440            }
441
442            for &key_combo in key_combos {
443                current[pos] = key_combo;
444                generate(key_combos, current, result, pos + 1);
445            }
446        }
447
448        generate(key_combos, &mut current, &mut result, 0);
449        result
450    }
451
452    pub fn assign_hotkeys(
453        &mut self,
454        entry_render_data: &mut [EntryRenderData],
455        preferred_key_combos_in_order: &[KeyCombo],
456    ) {
457        self.clear_entry_hotkeys();
458
459        let mut directory_indexes: Vec<usize> = Vec::new();
460
461        for (i, entry_render_datum) in entry_render_data.iter().enumerate() {
462            if *entry_render_datum.kind == EntryKind::Directory {
463                directory_indexes.push(i);
464            }
465        }
466
467        let directory_indexes_count = directory_indexes.len();
468
469        if directory_indexes_count == 0 {
470            // We don't even need hotkeys, we don't a have any directories
471            return;
472        }
473
474        // Collect all the next_chars for the entries, they should all be illegal hotkeys (so that
475        // the user can continue typing if in search mode)
476        let illegal_key_codes = entry_render_data
477            .iter()
478            .filter_map(|x| x.illegal_char_for_hotkey)
479            .map(KeyCode::Char)
480            .collect::<HashSet<_>>();
481
482        let mut available_key_combos: Vec<KeyCombo> = Vec::new();
483
484        for &key_combo in preferred_key_combos_in_order.iter() {
485            if !illegal_key_codes.contains(&key_combo.key_code) {
486                available_key_combos.push(key_combo);
487            }
488        }
489
490        let available_key_codes_count = available_key_combos.len();
491        if available_key_codes_count < 2 && directory_indexes_count > 1 {
492            // We can't generate key sequences if we have a single key code and more than one
493            // directory
494            return;
495        }
496
497        let mut sequence_length = 1;
498
499        while available_key_codes_count.pow(sequence_length) < directory_indexes_count {
500            sequence_length += 1;
501        }
502
503        let permutations = Self::generate_sequence_permutations(
504            available_key_combos.as_slice(),
505            sequence_length as usize,
506        );
507
508        assert!(permutations.len() >= directory_indexes_count);
509
510        let mut i = 0;
511        while i < directory_indexes_count {
512            // TODO: See if we can remove this clone
513            let directory_index = directory_indexes[i];
514            entry_render_data[directory_index].key_combo_sequence = Some(permutations[i].clone());
515            self.register_entry_hotkey(
516                permutations[i].as_slice(),
517                Action::ChangeDirectoryToEntryWithIndex(directory_index),
518            );
519            i += 1;
520        }
521    }
522}
523
524#[cfg(test)]
525mod tests {
526    use std::path::PathBuf;
527
528    use crate::entry::Entry;
529
530    use super::*;
531
532    #[test]
533    fn hotkeys_trie_works_correctly() {
534        let mut trie = HotkeysTrie::new();
535        trie.insert(&[KeyCombo::from('a'), KeyCombo::from('b')], 1);
536
537        trie.insert(&[KeyCombo::from('a'), KeyCombo::from('c')], 2);
538
539        trie.insert(&[KeyCombo::from('c'), KeyCombo::from('d')], 3);
540
541        trie.insert(&[KeyCombo::from('a'), KeyCombo::from('z')], 1);
542
543        assert_eq!(
544            trie.get_value(&[KeyCombo::from('a'), KeyCombo::from('b'),]),
545            Some(&1)
546        );
547        assert_eq!(
548            trie.get_value(&[KeyCombo::from('a'), KeyCombo::from('c'),]),
549            Some(&2)
550        );
551        assert_eq!(
552            trie.get_value(&[KeyCombo::from('c'), KeyCombo::from('d'),]),
553            Some(&3)
554        );
555
556        assert_eq!(
557            trie.get_value(&[KeyCombo::from('a'), KeyCombo::from('d'),]),
558            None
559        );
560
561        assert_eq!(
562            trie.get_value(&[KeyCombo::from('a'), KeyCombo::from('z'),]),
563            Some(&1)
564        );
565    }
566
567    #[test]
568    fn hotkeys_trie_clear_works_correctly() {
569        let mut trie = HotkeysTrie::new();
570        trie.insert(&[KeyCombo::from('a'), KeyCombo::from('b')], 1);
571
572        assert_eq!(
573            trie.get_value(&[KeyCombo::from('a'), KeyCombo::from('b'),]),
574            Some(&1)
575        );
576
577        trie.clear();
578
579        assert_eq!(
580            trie.get_value(&[KeyCombo::from('a'), KeyCombo::from('b'),]),
581            None
582        );
583    }
584
585    #[test]
586    fn generate_sequence_permutations_works_correctly() {
587        let available_key_combos = &[
588            KeyCombo::from('a'),
589            KeyCombo::from('b'),
590            KeyCombo::from('c'),
591        ];
592
593        let result: Vec<Vec<KeyCombo>> =
594            HotkeysRegistry::generate_sequence_permutations(available_key_combos, 1);
595
596        assert_eq!(result.len(), 3);
597        assert_eq!(
598            result[0],
599            vec![KeyCombo {
600                key_code: KeyCode::Char('a'),
601                modifiers: KeyModifiers::NONE
602            }]
603        );
604        assert_eq!(
605            result[1],
606            vec![KeyCombo {
607                key_code: KeyCode::Char('b'),
608                modifiers: KeyModifiers::NONE
609            }]
610        );
611        assert_eq!(
612            result[2],
613            vec![KeyCombo {
614                key_code: KeyCode::Char('c'),
615                modifiers: KeyModifiers::NONE
616            }]
617        );
618
619        let result: Vec<Vec<KeyCombo>> =
620            HotkeysRegistry::generate_sequence_permutations(available_key_combos, 2);
621
622        assert_eq!(result.len(), 9);
623
624        let expected_characters = [
625            ['a', 'a'],
626            ['a', 'b'],
627            ['a', 'c'],
628            ['b', 'a'],
629            ['b', 'b'],
630            ['b', 'c'],
631            ['c', 'a'],
632            ['c', 'b'],
633            ['c', 'c'],
634        ];
635
636        for (i, key_combos) in result.iter().enumerate() {
637            assert_eq!(key_combos.len(), 2);
638            assert_eq!(
639                key_combos[0].key_code,
640                KeyCode::Char(expected_characters[i][0])
641            );
642            assert_eq!(
643                key_combos[1].key_code,
644                KeyCode::Char(expected_characters[i][1])
645            );
646        }
647
648        let result: Vec<Vec<KeyCombo>> =
649            HotkeysRegistry::generate_sequence_permutations(available_key_combos, 3);
650
651        assert_eq!(result.len(), 27);
652
653        let expected_characters = [
654            ['a', 'a', 'a'],
655            ['a', 'a', 'b'],
656            ['a', 'a', 'c'],
657            ['a', 'b', 'a'],
658            ['a', 'b', 'b'],
659            ['a', 'b', 'c'],
660            ['a', 'c', 'a'],
661            ['a', 'c', 'b'],
662            ['a', 'c', 'c'],
663            ['b', 'a', 'a'],
664            ['b', 'a', 'b'],
665            ['b', 'a', 'c'],
666            ['b', 'b', 'a'],
667            ['b', 'b', 'b'],
668            ['b', 'b', 'c'],
669            ['b', 'c', 'a'],
670            ['b', 'c', 'b'],
671            ['b', 'c', 'c'],
672            ['c', 'a', 'a'],
673            ['c', 'a', 'b'],
674            ['c', 'a', 'c'],
675            ['c', 'b', 'a'],
676            ['c', 'b', 'b'],
677            ['c', 'b', 'c'],
678            ['c', 'c', 'a'],
679            ['c', 'c', 'b'],
680            ['c', 'c', 'c'],
681        ];
682
683        for (i, key_combos) in result.iter().enumerate() {
684            assert_eq!(key_combos.len(), 3);
685            assert_eq!(
686                key_combos[0].key_code,
687                KeyCode::Char(expected_characters[i][0])
688            );
689            assert_eq!(
690                key_combos[1].key_code,
691                KeyCode::Char(expected_characters[i][1])
692            );
693            assert_eq!(
694                key_combos[2].key_code,
695                KeyCode::Char(expected_characters[i][2])
696            );
697        }
698
699        let result: Vec<Vec<KeyCombo>> =
700            HotkeysRegistry::generate_sequence_permutations(available_key_combos, 4);
701
702        assert_eq!(result.len(), 81);
703    }
704
705    #[test]
706    fn assign_hotkeys_works_correctly() {
707        let entries = [
708            Entry {
709                name: "s-dir1".into(),
710                kind: EntryKind::Directory,
711                path: PathBuf::from("/home/user/s-dir/"),
712            },
713            Entry {
714                name: "d-dir2".into(),
715                kind: EntryKind::Directory,
716                path: PathBuf::from("/home/user/d-dir/"),
717            },
718            Entry {
719                name: "w-dir3".into(),
720                kind: EntryKind::Directory,
721                path: PathBuf::from("/home/user/w-dir/"),
722            },
723            Entry {
724                name: "e-dir4".into(),
725                kind: EntryKind::Directory,
726                path: PathBuf::from("/home/user/e-dir/"),
727            },
728            Entry {
729                name: "r-dir5".into(),
730                kind: EntryKind::Directory,
731                path: PathBuf::from("/home/user/Cargo.toml"),
732            },
733            Entry {
734                name: "Cargo.toml".into(),
735                kind: EntryKind::File {
736                    extension: Some("toml".into()),
737                },
738                path: PathBuf::from("/home/user/Cargo.toml"),
739            },
740        ];
741
742        let mut entry_render_data: Vec<EntryRenderData> = entries
743            .iter()
744            .map(|entry| EntryRenderData::from_entry(entry, ""))
745            .collect();
746
747        let mut hotkeys_registry = HotkeysRegistry::new();
748
749        hotkeys_registry.assign_hotkeys(
750            &mut entry_render_data,
751            &[
752                KeyCombo::from('b'),
753                KeyCombo::from('a'),
754                KeyCombo::from('c'),
755                KeyCombo::from('y'),
756            ],
757        );
758
759        assert_eq!(hotkeys_registry.entry_hotkeys_count, 5);
760
761        assert_eq!(
762            entry_render_data[0].key_combo_sequence,
763            Some(vec![KeyCombo::from('b'), KeyCombo::from('b')])
764        );
765
766        assert_eq!(
767            entry_render_data[1].key_combo_sequence,
768            Some(vec![KeyCombo::from('b'), KeyCombo::from('a')])
769        );
770
771        assert_eq!(
772            entry_render_data[2].key_combo_sequence,
773            Some(vec![KeyCombo::from('b'), KeyCombo::from('y')])
774        );
775
776        assert_eq!(
777            entry_render_data[3].key_combo_sequence,
778            Some(vec![KeyCombo::from('a'), KeyCombo::from('b')])
779        );
780
781        assert_eq!(
782            entry_render_data[4].key_combo_sequence,
783            Some(vec![KeyCombo::from('a'), KeyCombo::from('a')])
784        );
785
786        assert_eq!(entry_render_data[5].key_combo_sequence, None);
787    }
788}