[go: up one dir, main page]

crokey/
lib.rs

1//! Crokey helps incorporate configurable keybindings in [crossterm](https://github.com/crossterm-rs/crossterm)
2//! based terminal applications by providing functions
3//! - parsing key combinations from strings
4//! - describing key combinations in strings
5//! - parsing key combinations at compile time
6//! - combining Crossterm key events in key combinations
7//!
8//! ## The KeyCombination
9//!
10//! A `KeyCombination` is made of 1 to 3 "normal" keys with some optional modifiers (alt, shift, ctrl).
11//!
12//! It can be parsed, ergonomically built with the `key!` macro, obtained from key events.
13//!
14//! ## The Combiner
15//!
16//! With a `Combiner`, you can change raw Crossterm key events into key combinations.
17//!
18//! When the terminal is modern enough and supports the Kitty protocol, complex combinations with up to three non-modifier keys may be formed, for example `Ctrl-Alt-Shift-g-y` or `i-u`.
19//!
20//! For standard ANSI terminals, only regular combinations are available, like `Shift-o`, `Ctrl-Alt-Shift-g` or `i`.
21//!
22//! The combiner works in both cases:
23//! if you presses the `ctrl`, `i`, and `u ` keys at the same time, it will result in one combination (`ctrl-i-u`) on a kitty-compatible terminal, and as a sequence of 2 key combinations (`ctrl-i` then `ctrl-u` assuming you started pressing the `i` before the `u`) in other terminals.
24//!
25//!
26//! The `print_key` example shows how to use the combiner.
27//!
28//! ```no_run
29//! # use {
30//! #     crokey::*,
31//! #     crossterm::{
32//! #         event::{read, Event},
33//! #         style::Stylize,
34//! #         terminal,
35//! #     },
36//! # };
37//! let fmt = KeyCombinationFormat::default();
38//! let mut combiner = Combiner::default();
39//! let combines = combiner.enable_combining().unwrap();
40//! if combines {
41//!     println!("Your terminal supports combining keys");
42//! } else {
43//!     println!("Your terminal doesn't support combining non-modifier keys");
44//! }
45//! println!("Type any key combination");
46//! loop {
47//!     terminal::enable_raw_mode().unwrap();
48//!     let e = read();
49//!     terminal::disable_raw_mode().unwrap();
50//!     match e {
51//!         Ok(Event::Key(key_event)) => {
52//!             if let Some(key_combination) = combiner.transform(key_event) {
53//!                 match key_combination {
54//!                     key!(ctrl-c) | key!(ctrl-q) => {
55//!                         println!("quitting");
56//!                         break;
57//!                     }
58//!                     _ => {
59//!                         println!("You typed {}", fmt.to_string(key_combination));
60//!                     }
61//!                 }
62//!             }
63//!         },
64//!         _ => {}
65//!     }
66//! }
67//! ```
68//!
69//! ## Parse a string
70//!
71//! Those strings are usually provided by a configuration file.
72//!
73//! ```
74//! use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
75//! assert_eq!(
76//!     crokey::parse("alt-enter").unwrap(),
77//!     KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT).into(),
78//! );
79//! assert_eq!(
80//!     crokey::parse("shift-F6").unwrap(),
81//!     KeyEvent::new(KeyCode::F(6), KeyModifiers::SHIFT).into(),
82//! );
83//! ```
84//!
85//! ## Use key event "literals" thanks to procedural macros
86//!
87//! Those key events are parsed at compile time and have zero runtime cost.
88//!
89//! They're efficient and convenient for matching events or defining hardcoded keybindings.
90//!
91//! ```no_run
92//! # use crokey::*;
93//! # use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
94//! # use crossterm::style::Stylize;
95//! # let key_event = key!(a);
96//! let fmt = KeyCombinationFormat::default();
97//! # loop {
98//! match key_event {
99//!     key!(ctrl-c) => {
100//!         println!("Arg! You savagely killed me with a {}", fmt.to_string(key_event).red());
101//!         break;
102//!     }
103//!     key!(ctrl-q) => {
104//!         println!("You typed {} which gracefully quits", fmt.to_string(key_event).green());
105//!         break;
106//!     }
107//!     _ => {
108//!         println!("You typed {}", fmt.to_string(key_event).blue());
109//!     }
110//! }
111//! # }
112//! ```
113//! Complete example in `/examples/print_key`
114//!
115//! ## Display a string with a configurable format
116//!
117//! ```
118//! use crokey::*;
119//! use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
120//!
121//! // The default format
122//! let format = KeyCombinationFormat::default();
123//! assert_eq!(format.to_string(key!(shift-a)), "Shift-a");
124//! assert_eq!(format.to_string(key!(ctrl-c)), "Ctrl-c");
125//!
126//! // A more compact format
127//! let format = KeyCombinationFormat::default()
128//!     .with_implicit_shift()
129//!     .with_control("^");
130//! assert_eq!(format.to_string(key!(shift-a)), "A");
131//! assert_eq!(format.to_string(key!(ctrl-c)), "^c");
132//! ```
133//!
134//! ## Deserialize keybindings using Serde
135//!
136//! With the "serde" feature enabled, you can read configuration files in a direct way:
137//!
138//! ```
139//! use {
140//!     crokey::*,
141//!     crossterm::event::KeyEvent,
142//!     serde::Deserialize,
143//!     std::collections::HashMap,
144//! };
145//! #[derive(Debug, Deserialize)]
146//! struct Config {
147//!     keybindings: HashMap<KeyCombination, String>,
148//! }
149//! static CONFIG_HJSON: &str = r#"
150//! {
151//!     keybindings: {
152//!         a: aardvark
153//!         shift-b: babirussa
154//!         ctrl-k: koala
155//!         alt-j: jaguar
156//!     }
157//! }
158//! "#;
159//! let config: Config = deser_hjson::from_str(CONFIG_HJSON).unwrap();
160//! let key: KeyCombination = key!(shift-b);
161//! assert_eq!(
162//!     config.keybindings.get(&key).unwrap(),
163//!     "babirussa",
164//! );
165//! ```
166//!
167//! Instead of Hjson, you can use any Serde compatible format such as JSON or TOML.
168//!
169
170mod combiner;
171mod format;
172mod key_event;
173mod parse;
174mod key_combination;
175
176pub use {
177    combiner::*,
178    crossterm,
179    format::*,
180    key_event::*,
181    parse::*,
182    key_combination::*,
183    strict::OneToThree,
184};
185
186use {
187    crossterm::event::{KeyCode, KeyModifiers},
188    once_cell::sync::Lazy,
189};
190
191/// A lazy initialized KeyCombinationFormat which can be considered as standard
192/// and which is used in the Display implementation of the [KeyCombination] type.
193pub static STANDARD_FORMAT: Lazy<KeyCombinationFormat> = Lazy::new(KeyCombinationFormat::default);
194
195
196/// check and expand at compile-time the provided expression
197/// into a valid KeyCombination.
198///
199///
200/// For example:
201/// ```
202/// # use crokey::key;
203/// let key_event = key!(ctrl-c);
204/// ```
205/// is expanded into (roughly):
206///
207/// ```
208/// let key_event = crokey::KeyCombination {
209///     modifiers: crossterm::event::KeyModifiers::CONTROL,
210///     codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char('c')),
211/// };
212/// ```
213///
214/// Keys which can't be valid identifiers or digits in Rust must be put between simple quotes:
215/// ```
216/// # use crokey::key;
217/// let ke = key!(shift-'?');
218/// let ke = key!(alt-']');
219/// ```
220#[macro_export]
221macro_rules! key {
222    ($($tt:tt)*) => {
223        $crate::__private::key!(($crate) $($tt)*)
224    };
225}
226
227// Not public API. This is internal and to be used only by `key!`.
228#[doc(hidden)]
229pub mod __private {
230    pub use crokey_proc_macros::key;
231    pub use crossterm;
232    pub use strict::OneToThree;
233
234    use crossterm::event::KeyModifiers;
235    pub const MODS: KeyModifiers = KeyModifiers::NONE;
236    pub const MODS_CTRL: KeyModifiers = KeyModifiers::CONTROL;
237    pub const MODS_CMD: KeyModifiers = KeyModifiers::SUPER;
238    pub const MODS_ALT: KeyModifiers = KeyModifiers::ALT;
239    pub const MODS_SHIFT: KeyModifiers = KeyModifiers::SHIFT;
240    pub const MODS_CTRL_ALT: KeyModifiers = KeyModifiers::CONTROL.union(KeyModifiers::ALT);
241    pub const MODS_CMD_ALT: KeyModifiers = KeyModifiers::SUPER.union(KeyModifiers::ALT);
242    pub const MODS_ALT_SHIFT: KeyModifiers = KeyModifiers::ALT.union(KeyModifiers::SHIFT);
243    pub const MODS_CTRL_SHIFT: KeyModifiers = KeyModifiers::CONTROL.union(KeyModifiers::SHIFT);
244    pub const MODS_CMD_SHIFT: KeyModifiers = KeyModifiers::SUPER.union(KeyModifiers::SHIFT);
245    pub const MODS_CTRL_ALT_SHIFT: KeyModifiers = KeyModifiers::CONTROL
246        .union(KeyModifiers::ALT)
247        .union(KeyModifiers::SHIFT);
248    pub const MODS_CMD_ALT_SHIFT: KeyModifiers = KeyModifiers::SUPER
249        .union(KeyModifiers::ALT)
250        .union(KeyModifiers::SHIFT);
251}
252
253#[cfg(test)]
254mod tests {
255    use {
256        crate::{key, KeyCombination, OneToThree},
257        crossterm::event::{KeyCode, KeyModifiers},
258    };
259
260    const _: () = {
261        key!(x);
262        key!(ctrl - '{');
263        key!(alt - '{');
264        key!(shift - '{');
265        key!(ctrl - alt - f10);
266        key!(alt - shift - f10);
267        key!(ctrl - shift - f10);
268        key!(cmd - shift - f10);
269        key!(ctrl - alt - shift - enter);
270    };
271
272    fn no_mod(code: KeyCode) -> KeyCombination {
273        code.into()
274    }
275
276    #[test]
277    fn key() {
278        assert_eq!(key!(backspace), no_mod(KeyCode::Backspace));
279        assert_eq!(key!(bAcKsPaCe), no_mod(KeyCode::Backspace));
280        assert_eq!(key!(0), no_mod(KeyCode::Char('0')));
281        assert_eq!(key!(9), no_mod(KeyCode::Char('9')));
282        assert_eq!(key!('x'), no_mod(KeyCode::Char('x')));
283        assert_eq!(key!('X'), no_mod(KeyCode::Char('x')));
284        assert_eq!(key!(']'), no_mod(KeyCode::Char(']')));
285        assert_eq!(key!('ඞ'), no_mod(KeyCode::Char('ඞ')));
286        assert_eq!(key!(f), no_mod(KeyCode::Char('f')));
287        assert_eq!(key!(F), no_mod(KeyCode::Char('f')));
288        assert_eq!(key!(ඞ), no_mod(KeyCode::Char('ඞ')));
289        assert_eq!(key!(f10), no_mod(KeyCode::F(10)));
290        assert_eq!(key!(F10), no_mod(KeyCode::F(10)));
291        assert_eq!(
292            key!(ctrl - c),
293            KeyCombination::new(KeyCode::Char('c'), KeyModifiers::CONTROL)
294        );
295        assert_eq!(
296            key!(alt - shift - c),
297            KeyCombination::new(KeyCode::Char('C'), KeyModifiers::ALT | KeyModifiers::SHIFT)
298        );
299        assert_eq!(key!(shift - alt - '2'), key!(ALT - SHIFT - 2));
300        assert_eq!(key!(space), key!(' '));
301        assert_eq!(key!(hyphen), key!('-'));
302        assert_eq!(key!(minus), key!('-'));
303
304        assert_eq!(
305            key!(ctrl-alt-a-b),
306            KeyCombination::new(
307                OneToThree::Two(KeyCode::Char('a'), KeyCode::Char('b')),
308                KeyModifiers::CONTROL | KeyModifiers::ALT,
309            )
310        );
311        assert_eq!(
312            key!(alt-f4-a-b),
313            KeyCombination::new(
314                OneToThree::Three(KeyCode::F(4), KeyCode::Char('a'), KeyCode::Char('b')),
315                KeyModifiers::ALT,
316            )
317        );
318        assert_eq!( // check that key codes are sorted
319            key!(alt-a-b-f4),
320            KeyCombination::new(
321                OneToThree::Three(KeyCode::F(4), KeyCode::Char('a'), KeyCode::Char('b')),
322                KeyModifiers::ALT,
323            )
324        );
325        assert_eq!(
326            key!(z-e),
327            KeyCombination::new(
328                OneToThree::Two(KeyCode::Char('e'), KeyCode::Char('z')),
329                KeyModifiers::NONE,
330            )
331        );
332    }
333
334    #[test]
335    fn format() {
336        let format = crate::KeyCombinationFormat::default();
337        assert_eq!(format.to_string(key!(insert)), "Insert");
338        assert_eq!(format.to_string(key!(space)), "Space");
339        assert_eq!(format.to_string(key!(alt-Space)), "Alt-Space");
340        assert_eq!(format.to_string(key!(shift-' ')), "Shift-Space");
341        assert_eq!(format.to_string(key!(alt-hyphen)), "Alt-Hyphen");
342        assert_eq!(format.to_string(key!(cmd-f10)), "Cmd-F10");
343    }
344
345    #[test]
346    fn key_pattern() {
347        assert!(matches!(key!(ctrl-alt-shift-c), key!(ctrl-alt-shift-c)));
348        assert!(!matches!(key!(ctrl-c), key!(ctrl-alt-shift-c)));
349        assert!(matches!(key!(ctrl-alt-b), key!(ctrl-alt-b)));
350        assert!(matches!(key!(ctrl-b), key!(ctrl-b)));
351        assert!(matches!(key!(alt-b), key!(alt-b)));
352        assert!(!matches!(key!(ctrl-b), key!(alt-b)));
353        assert!(!matches!(key!(alt-b), key!(ctrl-b)));
354        assert!(!matches!(key!(alt-b), key!(ctrl-alt-b)));
355        assert!(!matches!(key!(ctrl-b), key!(ctrl-alt-b)));
356        assert!(!matches!(key!(ctrl-alt-b), key!(alt-b)));
357        assert!(!matches!(key!(ctrl-alt-b), key!(ctrl-b)));
358    }
359
360    #[test]
361    fn ui() {
362        trybuild::TestCases::new().compile_fail("tests/ui/*.rs");
363    }
364}