Skip to main content

crokey/
combiner.rs

1use {
2    crate::*,
3    crossterm::{
4        event::{
5            KeyCode,
6            KeyEvent,
7            KeyboardEnhancementFlags,
8            KeyEventKind,
9            KeyModifiers,
10            ModifierKeyCode,
11            PopKeyboardEnhancementFlags,
12            PushKeyboardEnhancementFlags,
13        },
14        execute,
15        terminal,
16    },
17    std::{
18        io,
19        ops::Drop,
20    },
21};
22
23/// This is the maximum number of keys we can combine.
24/// It can't be changed just here, as the `KeyCombination` type doesn't support
25/// more than 3 non-modifier keys
26const MAX_PRESS_COUNT: usize = 3;
27
28/// Consumes key events and combines them into key combinations.
29///
30/// See the `print_key_events` example.
31#[derive(Debug)]
32pub struct Combiner {
33    combining: bool,
34    keyboard_enhancement_flags_pushed: bool,
35    keyboard_enhancement_flags_externally_managed: bool,
36    mandate_modifier_for_multiple_keys: bool,
37    down_keys: Vec<KeyEvent>,
38    shift_pressed: bool,
39}
40
41impl Default for Combiner {
42    fn default() -> Self {
43        Self {
44            combining: false,
45            keyboard_enhancement_flags_pushed: false,
46            keyboard_enhancement_flags_externally_managed: false,
47            mandate_modifier_for_multiple_keys: true,
48            down_keys: Vec::new(),
49            shift_pressed: false,
50        }
51    }
52}
53
54impl Combiner {
55    /// Try to enable combining more than one non-modifier key into a combination.
56    ///
57    /// Return Ok(false) when the terminal doesn't support the kitty protocol.
58    ///
59    /// Behind the scene, this function pushes the keyboard enhancement flags
60    /// to the terminal. The flags are popped, and the normal state of the terminal
61    /// restored, when the Combiner is dropped.
62    ///
63    /// This function does nothing if combining is already enabled.
64    pub fn enable_combining(&mut self) -> io::Result<bool> {
65        if self.combining {
66            return Ok(true);
67        }
68        if !self.keyboard_enhancement_flags_externally_managed {
69            if self.keyboard_enhancement_flags_pushed {
70                return Ok(self.combining);
71            }
72            if !terminal::supports_keyboard_enhancement()? {
73                return Ok(false);
74            }
75            push_keyboard_enhancement_flags()?;
76            self.keyboard_enhancement_flags_pushed = true;
77        }
78        self.combining = true;
79        Ok(true)
80    }
81    /// Disable combining.
82    pub fn disable_combining(&mut self) -> io::Result<()> {
83        if !self.keyboard_enhancement_flags_externally_managed && self.keyboard_enhancement_flags_pushed {
84            pop_keyboard_enhancement_flags()?;
85            self.keyboard_enhancement_flags_pushed = false;
86        }
87        self.combining = false;
88        Ok(())
89    }
90    /// Tell the Combiner not to push/pop the keyboard enhancement flags.
91    ///
92    /// Call before `enable_combining` if you want to manage the flags yourself.
93    /// (for example: if you need to use stderr instead of stdout in `crossterm::execute!`)
94    pub fn set_keyboard_enhancement_flags_externally_managed(&mut self) {
95        self.keyboard_enhancement_flags_externally_managed = true;
96    }
97    pub fn is_combining(&self) -> bool {
98        self.combining
99    }
100    /// When combining is enabled, you may either want "simple" keys
101    /// (i.e. without modifier or space) to be handled on key press,
102    /// or to wait for a key release so that maybe they may
103    /// be part of a combination like 'a-b'.
104    /// If combinations without modifier or space are unlikely in your application, you
105    /// may make it feel snappier by setting this to true.
106    ///
107    /// This setting has no effect when combining isn't enabled.
108    pub fn set_mandate_modifier_for_multiple_keys(&mut self, mandate: bool) {
109        self.mandate_modifier_for_multiple_keys = mandate;
110    }
111    /// Take all the `down_keys`, combine them into a `KeyCombination`
112    fn combine(&mut self, clear: bool) -> Option<KeyCombination> {
113        let mut key_combination = KeyCombination::try_from(self.down_keys.as_slice())
114            .ok(); // it may be empty, in which case we return None
115        if self.shift_pressed {
116            if let Some(ref mut key_combination) = key_combination {
117                key_combination.modifiers |= KeyModifiers::SHIFT;
118            }
119        }
120        if clear {
121            self.down_keys.clear();
122            self.shift_pressed = false;
123        }
124        key_combination
125    }
126    /// Receive a key event and return a key combination if one is ready.
127    ///
128    /// When combining is enabled, the key combination is only returned on a
129    /// key release event.
130    pub fn transform(&mut self, key: KeyEvent) -> Option<KeyCombination> {
131        if self.combining {
132            self.transform_combining(key)
133        } else {
134            self.transform_ansi(key)
135        }
136    }
137    fn transform_combining(&mut self, key: KeyEvent) -> Option<KeyCombination> {
138        if let KeyCode::Modifier(modifier) = key.code {
139            if modifier == ModifierKeyCode::LeftShift || modifier == ModifierKeyCode::RightShift {
140                self.shift_pressed = key.kind != KeyEventKind::Release;
141            }
142            // we ignore modifier keys as independent events
143            // (which means we never return a combination with only modifiers)
144            return None;
145        }
146        if
147                self.mandate_modifier_for_multiple_keys
148                && is_key_simple(key)
149                && !self.shift_pressed
150                && self.down_keys.is_empty()
151        {
152            // "simple key" are handled differently: they're returned on press and repeat
153            match key.kind {
154                KeyEventKind::Press | KeyEventKind::Repeat => {
155                    self.down_keys.push(key);
156                    self.combine(true)
157                }
158                KeyEventKind::Release => {
159                    None
160                }
161            }
162        } else {
163            // not a single simple key
164            match key.kind {
165                KeyEventKind::Press => {
166                    self.down_keys.push(key);
167                    if self.down_keys.len() == MAX_PRESS_COUNT {
168                        self.combine(true)
169                    } else {
170                        None
171                    }
172                }
173                KeyEventKind::Release => {
174                    // this release ends the combination in progress
175                    self.combine(true)
176                }
177                KeyEventKind::Repeat => {
178                    self.combine(false)
179                }
180            }
181        }
182    }
183    /// In ansi mode, no combination is possible, and we don't expect to
184    /// receive anything else than a single key or than key presses.
185    fn transform_ansi(&mut self, key: KeyEvent) -> Option<KeyCombination> {
186        match key.kind {
187            KeyEventKind::Press => Some(key.into()),
188            _ => {
189                // this is unexpected, we don't seem to be really in ansi mode
190                // but for consistency we must filter out this event
191                None
192            }
193        }
194    }
195}
196
197/// For the purpose of key combination, we consider that a key is "simple"
198/// when it's neither a modifier (ctrl,alt,shift) nor a space.
199pub fn is_key_simple(key: KeyEvent) -> bool {
200    key.modifiers.is_empty()
201        && key.code != KeyCode::Char(' ')
202}
203
204impl Drop for Combiner {
205    fn drop(&mut self) {
206        if self.keyboard_enhancement_flags_pushed {
207            let _ = pop_keyboard_enhancement_flags();
208        }
209    }
210}
211
212/// Change the state of the terminal to enable combining keys.
213/// This is done automatically by `Combiner::enable_combining`
214/// so you should usually not need to call this function.
215pub fn push_keyboard_enhancement_flags() -> io::Result<()> {
216    let mut stdout = io::stdout();
217    execute!(
218        stdout,
219        PushKeyboardEnhancementFlags(
220            KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
221                | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
222                | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
223                | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
224        )
225    )
226}
227
228/// Restore the "normal" state of the terminal.
229/// This is done automatically by the combiner on drop,
230/// so you should usually not need to call this function.
231pub fn pop_keyboard_enhancement_flags() -> io::Result<()>{
232    let mut stdout = io::stdout();
233    execute!(stdout, PopKeyboardEnhancementFlags)
234}