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}