├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── doc └── print_key.png ├── examples ├── README.md ├── deser_keybindings │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── print_key │ ├── Cargo.toml │ └── src │ │ └── main.rs └── print_key_no_combiner │ ├── Cargo.toml │ └── src │ └── main.rs ├── src ├── combiner.rs ├── format.rs ├── key_combination.rs ├── key_event.rs ├── lib.rs ├── parse.rs └── proc_macros │ ├── Cargo.toml │ └── mod.rs └── tests ├── hygiene.rs └── ui ├── duplicate-modifier.rs ├── duplicate-modifier.stderr ├── invalid-key.rs ├── invalid-key.stderr ├── invalid-modifier.rs ├── invalid-modifier.stderr ├── unexpected-eof.rs └── unexpected-eof.stderr /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crokey" 3 | version = "1.2.0" 4 | authors = ["dystroy "] 5 | edition = "2021" 6 | keywords = ["key", "parse"] 7 | license = "MIT" 8 | categories = ["command-line-interface", "parsing"] 9 | description = "Parse and describe keys - helping incorporate keybindings in terminal applications" 10 | repository = "https://github.com/Canop/crokey" 11 | readme = "README.md" 12 | rust-version = "1.56" 13 | 14 | [features] 15 | default = ["serde"] 16 | 17 | [dependencies] 18 | crossterm = "0.29" 19 | crokey-proc_macros = { path = "src/proc_macros", version = "1.2.0" } 20 | once_cell = "1.12" 21 | serde = { optional = true, version = "1.0.130", features = ["derive"] } 22 | strict = "0.2" 23 | 24 | [dev-dependencies] 25 | deser-hjson = "1.0" 26 | trybuild = "1.0.55" 27 | 28 | [workspace] 29 | members = [ 30 | "src/proc_macros", 31 | "examples/deser_keybindings", 32 | "examples/print_key", 33 | "examples/print_key_no_combiner", 34 | ] 35 | 36 | [patch.crates-io] 37 | # strict = { path = "../strict" } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Canop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] 2 | 3 | [s1]: https://img.shields.io/crates/v/crokey.svg 4 | [l1]: https://crates.io/crates/crokey 5 | 6 | [s2]: https://img.shields.io/badge/license-MIT-blue.svg 7 | [l2]: LICENSE 8 | 9 | [s3]: https://docs.rs/crokey/badge.svg 10 | [l3]: https://docs.rs/crokey/ 11 | 12 | [s4]: https://miaou.dystroy.org/static/shields/room.svg 13 | [l4]: https://miaou.dystroy.org/3490?crokey 14 | 15 | # Crokey 16 | 17 | Crokey helps incorporate configurable keybindings in [crossterm](https://github.com/crossterm-rs/crossterm) 18 | based terminal applications by providing functions 19 | - parsing key combinations from strings 20 | - describing key combinations in strings 21 | - parsing key combinations at compile time 22 | - combining Crossterm key events in key combinations 23 | 24 | ## The KeyCombination 25 | 26 | A `KeyCombination` is made of 1 to 3 "normal" keys with some optional modifiers (alt, shift, ctrl). 27 | 28 | It can be parsed, ergonomically built with the `key!` macro, obtained from key events. 29 | 30 | ## The Combiner 31 | 32 | With a `Combiner`, you can change raw Crossterm key events into key combinations. 33 | 34 | 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 `Space-!`. 35 | 36 | For standard ANSI terminals, only regular combinations are available, like `Shift-o`, `Ctrl-Alt-Shift-g` or `i`. 37 | 38 | The combiner works in both cases: 39 | 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. 40 | 41 | The `print_key` example shows how to deal with that: 42 | 43 | ```rust 44 | let fmt = KeyCombinationFormat::default(); 45 | let mut combiner = Combiner::default(); 46 | let combines = combiner.enable_combining().unwrap(); 47 | if combines { 48 | println!("Your terminal supports combining keys"); 49 | } else { 50 | println!("Your terminal doesn't support combining non-modifier keys"); 51 | } 52 | println!("Type any key combination"); 53 | loop { 54 | terminal::enable_raw_mode().unwrap(); 55 | let e = read(); 56 | terminal::disable_raw_mode().unwrap(); 57 | match e { 58 | Ok(Event::Key(key_event)) => { 59 | if let Some(key_combination) = combiner.transform(key_event) { 60 | match key_combination { 61 | key!(ctrl-c) | key!(ctrl-q) => { 62 | println!("quitting"); 63 | break; 64 | } 65 | _ => { 66 | println!("You typed {}", fmt.to_string(key_combination)); 67 | } 68 | } 69 | } 70 | }, 71 | ... 72 | } 73 | } 74 | ``` 75 | 76 | ## Parse a string 77 | 78 | Those strings are usually provided by a configuration file. 79 | 80 | ```rust 81 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 82 | assert_eq!( 83 | crokey::parse("alt-enter").unwrap(), 84 | KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT), 85 | ); 86 | assert_eq!( 87 | crokey::parse("shift-F6").unwrap(), 88 | KeyEvent::new(KeyCode::F(6), KeyModifiers::SHIFT), 89 | ); 90 | ``` 91 | 92 | ## Use key combination "literals" thanks to procedural macros 93 | 94 | Those key combinations are parsed at compile time and have zero runtime cost. 95 | 96 | They're efficient and convenient for matching events or defining hardcoded keybindings. 97 | 98 | ```rust 99 | match key_event.into() { 100 | key!(ctrl-c) => { 101 | println!("Arg! You savagely killed me with a {}", fmt.to_string(key_event).red()); 102 | break; 103 | } 104 | key!(ctrl-q) => { 105 | println!("You typed {} which gracefully quits", fmt.to_string(key_event).green()); 106 | break; 107 | } 108 | _ => { 109 | println!("You typed {}", fmt.to_string(key_event).blue()); 110 | } 111 | } 112 | ``` 113 | 114 | Complete example in `/examples/print_key`: 115 | 116 | ![print_key](doc/print_key.png) 117 | 118 | The `key!` macro can be called in const contexts: 119 | 120 | ```rust 121 | const quit: KeyCombination = key!(ctrl-q); 122 | ``` 123 | 124 | ## Display a string with a configurable format 125 | 126 | ```rust 127 | use crokey::*; 128 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 129 | 130 | // The default format 131 | let format = KeyCombinationFormat::default(); 132 | assert_eq!(format.to_string(key!(shift-a)), "Shift-a"); 133 | assert_eq!(format.to_string(key!(ctrl-c)), "Ctrl-c"); 134 | 135 | // A more compact format 136 | let format = KeyCombinationFormat::default() 137 | .with_implicit_shift() 138 | .with_control("^"); 139 | assert_eq!(format.to_string(key!(shift-a)), "A"); 140 | assert_eq!(format.to_string(key!(ctrl-c)), "^c"); 141 | ``` 142 | 143 | ## Deserialize keybindings using Serde 144 | 145 | With the "serde" feature enabled, you can read configuration files in a direct way: 146 | 147 | ``` 148 | use { 149 | crokey::*, 150 | crossterm::event::KeyEvent, 151 | serde::Deserialize, 152 | std::collections::HashMap, 153 | }; 154 | #[derive(Debug, Deserialize)] 155 | struct Config { 156 | keybindings: HashMap, 157 | } 158 | static CONFIG_HJSON: &str = r#" 159 | { 160 | keybindings: { 161 | a: aardvark 162 | shift-b: babirussa 163 | ctrl-k: koala 164 | alt-j: jaguar 165 | } 166 | } 167 | "#; 168 | let config: Config = deser_hjson::from_str(CONFIG_HJSON).unwrap(); 169 | let key: KeyCombination = key!(shift-b); 170 | assert_eq!( 171 | config.keybindings.get(&key).unwrap(), 172 | "babirussa", 173 | ); 174 | ``` 175 | 176 | You can use any Serde compatible format such as JSON or TOML. 177 | 178 | 179 | ## Crossterm Compatibility 180 | 181 | Crokey includes and reexports Crossterm, so you don't have to import it and to avoid conflicts. 182 | 183 | -------------------------------------------------------------------------------- /doc/print_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/crokey/f016c4bc77026e420560d716f9e188286aad542e/doc/print_key.png -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Crokey Examples 3 | 4 | To run an example, `cd` to its directory then do `cargo run`. 5 | 6 | ## deser_keybindings 7 | 8 | Shows how a set of key-bindings can be read from JSON (might have been TOML, Hjson, YAML, etc.) and the action executed when the user presses the relevant key combination. 9 | 10 | ## print_key 11 | 12 | Shows how a combiner transforms crossterm key events into key combinations. 13 | 14 | The `Combiner` is configured to recognize combinations which aren't normally available, when the terminal supports the Kitty Keyboard protocol. 15 | 16 | When using a combiner, key combinations involving a modifier (ctrl, alt, shift, space) are detected on key release. 17 | 18 | ## print_key_no_combiner 19 | 20 | Similar to print_key, but simpler, uses no `Combiner`. 21 | 22 | Key combinations which are standard on ANSI terminals are handled, but the capabilities of more modern terminals won't be used and you won't get combinations like `ctrl-a-b`, or `space-n`. 23 | 24 | When not using a combiner, all combinations are detected on key press. 25 | -------------------------------------------------------------------------------- /examples/deser_keybindings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deser_keybindings" 3 | version = "0.5.0" 4 | authors = ["dystroy "] 5 | edition = "2021" 6 | description = "An example of using crokey to deserialize keybindings" 7 | license = "MIT" 8 | readme = "README.md" 9 | 10 | [dependencies] 11 | crokey = { path = "../.." } 12 | serde = { version = "1.0.130", features = ["derive"] } 13 | toml = "0.5" 14 | 15 | -------------------------------------------------------------------------------- /examples/deser_keybindings/src/main.rs: -------------------------------------------------------------------------------- 1 | //! cd to the deser_keybindings repository then do `cargo run` 2 | use { 3 | crokey::{ 4 | *, 5 | crossterm::{ 6 | event::{read, Event}, 7 | style::Stylize, 8 | terminal, 9 | }, 10 | }, 11 | serde::Deserialize, 12 | std::collections::HashMap, 13 | }; 14 | 15 | /// This is an example of a configuration structure which contains 16 | /// a map from KeyEvent to String. 17 | #[derive(Deserialize)] 18 | struct Config { 19 | keybindings: HashMap, 20 | } 21 | 22 | /// An example of what could be a configuration file 23 | static CONFIG_TOML: &str = r#" 24 | [keybindings] 25 | a = "aardvark" 26 | shift-b = "babirussa" 27 | ctrl-k = "koala" 28 | alt-j = "jaguar" 29 | h = "hexapode" 30 | shift-h = "HEXAPODE" 31 | - = "mandrill" 32 | alt-- = "nasalis" # some terminals don't distinguish between - and alt-- 33 | "#; 34 | 35 | pub fn main() { 36 | print!("Application configuration:\n{}", CONFIG_TOML.blue()); 37 | let config: Config = toml::from_str(CONFIG_TOML).unwrap(); 38 | let fmt = KeyCombinationFormat::default(); 39 | println!("\nType any key combination"); 40 | loop { 41 | terminal::enable_raw_mode().unwrap(); 42 | let e = read(); 43 | terminal::disable_raw_mode().unwrap(); 44 | if let Ok(Event::Key(key_event)) = e { 45 | let key = KeyCombination::from(key_event); 46 | if key == key!(ctrl-c) || key == key!(ctrl-q) { 47 | println!("bye!"); 48 | break; 49 | } 50 | if let Some(word) = config.keybindings.get(&key) { 51 | println!( 52 | "You hit {} which is mapped to {}", 53 | fmt.to_string(key).green(), 54 | word.clone().yellow(), 55 | ); 56 | } else { 57 | println!( 58 | "You hit {} which isn't mapped", 59 | fmt.to_string(key).red(), 60 | ); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/print_key/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "print_key" 3 | version = "0.5.0" 4 | authors = ["dystroy "] 5 | edition = "2021" 6 | description = "An example of using crokey to recognize and print key combinations" 7 | license = "MIT" 8 | readme = "README.md" 9 | 10 | [dependencies] 11 | crokey = { path = "../.." } 12 | -------------------------------------------------------------------------------- /examples/print_key/src/main.rs: -------------------------------------------------------------------------------- 1 | //! To run this example, cd to the print_key repository then do `cargo run` 2 | use { 3 | crokey::*, 4 | crossterm::{ 5 | event::{read, Event}, 6 | style::Stylize, 7 | terminal, 8 | }, 9 | }; 10 | 11 | pub fn main() { 12 | let fmt = KeyCombinationFormat::default(); 13 | let mut combiner = Combiner::default(); 14 | let combines = combiner.enable_combining().unwrap(); 15 | if combines { 16 | println!("Your terminal supports combining keys"); 17 | } else { 18 | println!("Your terminal doesn't support combining standard (non modifier) keys"); 19 | } 20 | println!("Type any key combination (remember that your terminal intercepts many ones)"); 21 | loop { 22 | terminal::enable_raw_mode().unwrap(); 23 | let e = read(); 24 | terminal::disable_raw_mode().unwrap(); 25 | match e { 26 | Ok(Event::Key(key_event)) => { 27 | let Some(key_combination) = combiner.transform(key_event) else { 28 | continue; 29 | }; 30 | let key = fmt.to_string(key_combination); 31 | match key_combination { 32 | key!(ctrl-c) => { 33 | println!("Arg! You savagely killed me with a {}", key.red()); 34 | break; 35 | } 36 | key!(ctrl-q) => { 37 | println!("You typed {} which gracefully quits", key.green()); 38 | break; 39 | } 40 | key!('?') | key!(shift-'?') => { 41 | println!("{}", "There's no help on this app".red()); 42 | } 43 | _ => { 44 | println!("You typed {}", key.blue()); 45 | } 46 | } 47 | } 48 | e => { 49 | // any other event, for example a resize, we quit 50 | eprintln!("Quitting on {:?}", e); 51 | break; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/print_key_no_combiner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "print_key_no_combiner" 3 | version = "0.5.0" 4 | authors = ["dystroy "] 5 | edition = "2021" 6 | description = "An example of using crokey to print the keys the user types" 7 | license = "MIT" 8 | readme = "README.md" 9 | 10 | [dependencies] 11 | crokey = { path = "../.." } 12 | -------------------------------------------------------------------------------- /examples/print_key_no_combiner/src/main.rs: -------------------------------------------------------------------------------- 1 | //! To run this example, cd to the print_key repository then do `cargo run` 2 | use { 3 | crokey::*, 4 | crossterm::{ 5 | event::{read, Event}, 6 | style::Stylize, 7 | terminal, 8 | }, 9 | }; 10 | 11 | pub fn main() { 12 | let fmt = KeyCombinationFormat::default(); 13 | println!("Type any key combination (remember that your terminal intercepts many ones)"); 14 | loop { 15 | terminal::enable_raw_mode().unwrap(); 16 | let e = read(); 17 | terminal::disable_raw_mode().unwrap(); 18 | match e { 19 | Ok(Event::Key(key_event)) => { 20 | let key_combination = key_event.into(); 21 | let key = fmt.to_string(key_combination); 22 | match key_combination { 23 | key!(ctrl-c) => { 24 | println!("Arg! You savagely killed me with a {}", key.red()); 25 | break; 26 | } 27 | key!(ctrl-q) => { 28 | println!("You typed {} which gracefully quits", key.green()); 29 | break; 30 | } 31 | key!('?') | key!(shift-'?') => { 32 | println!("{}", "There's no help on this app".red()); 33 | } 34 | _ => { 35 | println!("You typed {}", key.blue()); 36 | } 37 | } 38 | } 39 | e => { 40 | // any other event, for example a resize, we quit 41 | eprintln!("Quitting on {:?}", e); 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/combiner.rs: -------------------------------------------------------------------------------- 1 | use { 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 26 | const 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)] 32 | pub 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, 38 | shift_pressed: bool, 39 | } 40 | 41 | impl 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 | 54 | impl 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 { 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 { 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 { 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 { 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 { 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. 199 | pub fn is_key_simple(key: KeyEvent) -> bool { 200 | key.modifiers.is_empty() 201 | && key.code != KeyCode::Char(' ') 202 | } 203 | 204 | impl 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. 215 | pub 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. 231 | pub fn pop_keyboard_enhancement_flags() -> io::Result<()>{ 232 | let mut stdout = io::stdout(); 233 | execute!(stdout, PopKeyboardEnhancementFlags) 234 | } 235 | -------------------------------------------------------------------------------- /src/format.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 | 6 | use { 7 | crate::KeyCombination, 8 | crossterm::event::{KeyCode::*, KeyModifiers}, 9 | std::fmt, 10 | }; 11 | 12 | /// A formatter to produce key combinations descriptions. 13 | /// 14 | /// ``` 15 | /// use { 16 | /// crokey::*, 17 | /// crossterm::event::{ 18 | /// KeyCode, 19 | /// KeyEvent, 20 | /// KeyModifiers, 21 | /// }, 22 | /// }; 23 | /// 24 | /// let format = KeyCombinationFormat::default(); 25 | /// assert_eq!(format.to_string(key!(shift-a)), "Shift-a"); 26 | /// assert_eq!(format.to_string(key!(ctrl-c)), "Ctrl-c"); 27 | /// 28 | /// // A more compact format 29 | /// let format = KeyCombinationFormat::default() 30 | /// .with_implicit_shift() 31 | /// .with_control("^"); 32 | /// assert_eq!(format.to_string(key!(shift-a)), "A"); 33 | /// assert_eq!(format.to_string(key!(ctrl-c)), "^c"); 34 | /// 35 | /// // A long format with lowercased modifiers 36 | /// let format = KeyCombinationFormat::default() 37 | /// .with_lowercase_modifiers(); 38 | /// assert_eq!(format.to_string(key!(ctrl-enter)), "ctrl-Enter"); 39 | /// assert_eq!(format.to_string(key!(home)), "Home"); 40 | /// assert_eq!( 41 | /// format.to_string( 42 | /// KeyCombination::new( 43 | /// KeyCode::F(6), 44 | /// KeyModifiers::ALT, 45 | /// ) 46 | /// ), 47 | /// "alt-F6", 48 | /// ); 49 | /// assert_eq!( 50 | /// format.to_string( 51 | /// KeyCombination::new( 52 | /// (KeyCode::Char('u'), KeyCode::Char('i')), 53 | /// KeyModifiers::NONE, 54 | /// ) 55 | /// ), 56 | /// "i-u", 57 | /// ); 58 | /// 59 | /// ``` 60 | #[derive(Debug, Clone)] 61 | pub struct KeyCombinationFormat { 62 | pub control: String, 63 | pub alt: String, 64 | pub shift: String, 65 | pub enter: String, 66 | pub uppercase_shift: bool, 67 | pub key_separator: String, 68 | } 69 | 70 | impl Default for KeyCombinationFormat { 71 | fn default() -> Self { 72 | Self { 73 | control: "Ctrl-".to_string(), 74 | alt: "Alt-".to_string(), 75 | shift: "Shift-".to_string(), 76 | enter: "Enter".to_string(), 77 | uppercase_shift: false, 78 | key_separator: "-".to_string(), 79 | } 80 | } 81 | } 82 | 83 | impl KeyCombinationFormat { 84 | pub fn with_lowercase_modifiers(mut self) -> Self { 85 | self.control = self.control.to_lowercase(); 86 | self.alt = self.alt.to_lowercase(); 87 | self.shift = self.shift.to_lowercase(); 88 | self 89 | } 90 | pub fn with_control>(mut self, s: S) -> Self { 91 | self.control = s.into(); 92 | self 93 | } 94 | pub fn with_alt>(mut self, s: S) -> Self { 95 | self.alt = s.into(); 96 | self 97 | } 98 | pub fn with_shift>(mut self, s: S) -> Self { 99 | self.shift = s.into(); 100 | self 101 | } 102 | pub fn with_implicit_shift(mut self) -> Self { 103 | self.shift = "".to_string(); 104 | self.uppercase_shift = true; 105 | self 106 | } 107 | /// return a wrapper of the key implementing Display 108 | /// 109 | /// ``` 110 | /// use crokey::*; 111 | /// let format = KeyCombinationFormat::default(); 112 | /// let k = format.format(key!(f6)); 113 | /// let s = format!("k={}", k); 114 | /// assert_eq!(s, "k=F6"); 115 | /// ``` 116 | pub fn format>(&self, key: K) -> FormattedKeyCombination { 117 | FormattedKeyCombination { format: self, key: key.into() } 118 | } 119 | /// return the key formatted into a string 120 | /// 121 | /// `format.to_string(key)` is equivalent to `format.format(key).to_string()`. 122 | pub fn to_string>(&self, key: K) -> String { 123 | self.format(key).to_string() 124 | } 125 | } 126 | 127 | pub struct FormattedKeyCombination<'s> { 128 | format: &'s KeyCombinationFormat, 129 | key: KeyCombination, 130 | } 131 | 132 | impl fmt::Display for FormattedKeyCombination<'_> { 133 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 134 | let format = &self.format; 135 | let key = &self.key; 136 | if key.modifiers.contains(KeyModifiers::CONTROL) { 137 | write!(f, "{}", format.control)?; 138 | } 139 | if key.modifiers.contains(KeyModifiers::ALT) { 140 | write!(f, "{}", format.alt)?; 141 | } 142 | if key.modifiers.contains(KeyModifiers::SHIFT) { 143 | write!(f, "{}", format.shift)?; 144 | } 145 | for (i, code) in key.codes.iter().enumerate() { 146 | if i > 0 { 147 | write!(f, "{}", format.key_separator)?; 148 | } 149 | match code { 150 | Char(' ') => { 151 | write!(f, "Space")?; 152 | } 153 | Char('-') => { 154 | write!(f, "Hyphen")?; 155 | } 156 | Char('\r') | Char('\n') | Enter => { 157 | write!(f, "{}", format.enter)?; 158 | } 159 | Char(c) if key.modifiers.contains(KeyModifiers::SHIFT) && format.uppercase_shift => { 160 | write!(f, "{}", c.to_ascii_uppercase())?; 161 | } 162 | Char(c) => { 163 | write!(f, "{}", c.to_ascii_lowercase())?; 164 | } 165 | F(u) => { 166 | write!(f, "F{u}")?; 167 | } 168 | _ => { 169 | write!(f, "{:?}", code)?; 170 | } 171 | } 172 | } 173 | Ok(()) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/key_combination.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::*, 3 | crossterm::event::{ 4 | KeyEvent, 5 | KeyEventKind, 6 | KeyEventState, 7 | }, 8 | std::{ 9 | fmt, 10 | str::FromStr, 11 | }, 12 | strict::OneToThree, 13 | }; 14 | 15 | #[cfg(feature = "serde")] 16 | use serde::{ 17 | de, 18 | Deserialize, 19 | Deserializer, 20 | Serialize, 21 | Serializer, 22 | }; 23 | 24 | /// A Key combination wraps from one to three standard keys with optional modifiers 25 | /// (ctrl, alt, shift). 26 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] 27 | pub struct KeyCombination { 28 | pub codes: OneToThree, 29 | pub modifiers: KeyModifiers, 30 | } 31 | 32 | /// Change the char to uppercase when the modifier shift is present, 33 | /// otherwise if the char is uppercase, return true. 34 | /// If the key is the `\r' or '\n' char, change it to KeyCode::Enter. 35 | fn normalize_key_code(code: &mut KeyCode, modifiers: KeyModifiers) -> bool { 36 | if matches!(code, KeyCode::Char('\r') | KeyCode::Char('\n')) { 37 | *code = KeyCode::Enter; 38 | } else if modifiers.contains(KeyModifiers::SHIFT) { 39 | if let KeyCode::Char(c) = code { 40 | if c.is_ascii_lowercase() { 41 | *code = KeyCode::Char(c.to_ascii_uppercase()); 42 | } 43 | } 44 | } else if let KeyCode::Char(c) = code { 45 | if c.is_ascii_uppercase() { 46 | return true; 47 | } 48 | } 49 | false 50 | } 51 | 52 | impl KeyCombination { 53 | /// Create a new KeyCombination from one to three keycodes and a set of modifiers 54 | pub fn new>>(codes: C, modifiers: KeyModifiers) -> Self { 55 | let codes = codes.into().sorted(); 56 | Self { codes, modifiers } 57 | } 58 | /// Create a new KeyCombination from one keycode and a set of modifiers 59 | pub const fn one_key(code: KeyCode, modifiers: KeyModifiers) -> Self { 60 | let codes = OneToThree::One(code); 61 | Self { codes, modifiers } 62 | } 63 | /// Ansi terminals don't manage key press/release/repeat, so they 64 | /// don't allow to determine whether 2 keys are pressed at the same 65 | /// time. This means a combination involving several key codes can't 66 | /// be distiguished from a sequences of combinations involving a single key code. 67 | /// For this reason, only combinations involving a single key code are 68 | /// considered "ansi compatible" 69 | pub const fn is_ansi_compatible(self) -> bool { 70 | matches!(self.codes, OneToThree::One(_)) 71 | } 72 | /// Return a normailzed version of the combination. 73 | /// 74 | /// Fix the case of the code to uppercase if the shift modifier is present. 75 | /// Add the SHIFT modifier if one code is uppercase. 76 | /// 77 | /// This allows direct comparisons with the fields of crossterm::event::KeyEvent 78 | /// whose code is uppercase when the shift modifier is present. And supports the 79 | /// case where the modifier isn't mentionned but the key is uppercase. 80 | pub fn normalized(mut self) -> Self { 81 | let mut shift = normalize_key_code(self.codes.first_mut(), self.modifiers); 82 | if let Some(ref mut code) = self.codes.get_mut(1) { 83 | shift |= normalize_key_code(code, self.modifiers); 84 | } 85 | if let Some(ref mut code) = self.codes.get_mut(2) { 86 | shift |= normalize_key_code(code, self.modifiers); 87 | } 88 | if shift { 89 | self.modifiers |= KeyModifiers::SHIFT; 90 | } 91 | self 92 | } 93 | /// return the raw char if the combination is a letter event 94 | pub const fn as_letter(self) -> Option { 95 | match self { 96 | Self { 97 | codes: OneToThree::One(KeyCode::Char(l)), 98 | modifiers: KeyModifiers::NONE, 99 | } => Some(l), 100 | _ => None, 101 | } 102 | } 103 | } 104 | 105 | #[cfg(feature = "serde")] 106 | impl<'de> Deserialize<'de> for KeyCombination { 107 | fn deserialize(deserializer: D) -> Result 108 | where 109 | D: Deserializer<'de>, 110 | { 111 | let s = String::deserialize(deserializer)?; 112 | FromStr::from_str(&s).map_err(de::Error::custom) 113 | } 114 | } 115 | 116 | #[cfg(feature = "serde")] 117 | impl Serialize for KeyCombination { 118 | fn serialize(&self, serializer: S) -> Result 119 | where 120 | S: Serializer, 121 | { 122 | serializer.serialize_str(&self.to_string()) 123 | } 124 | } 125 | 126 | impl FromStr for KeyCombination { 127 | type Err = ParseKeyError; 128 | fn from_str(s: &str) -> Result { 129 | parse(s) 130 | } 131 | } 132 | 133 | impl fmt::Display for KeyCombination { 134 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 135 | STANDARD_FORMAT.format(*self).fmt(f) 136 | } 137 | } 138 | 139 | impl From for KeyCombination { 140 | fn from(key_event: KeyEvent) -> Self { 141 | let raw = Self { 142 | codes: key_event.code.into(), 143 | modifiers: key_event.modifiers, 144 | }; 145 | raw.normalized() 146 | } 147 | } 148 | 149 | impl TryFrom<&[KeyEvent]> for KeyCombination { 150 | type Error = &'static str; 151 | /// Try to create a KeyCombination from a slice of key events, 152 | /// will fail if and only if the slice is empty. 153 | fn try_from(key_events: &[KeyEvent]) -> Result { 154 | let mut modifiers = KeyModifiers::empty(); 155 | let mut codes = Vec::new(); 156 | for key_event in key_events { 157 | modifiers |= key_event.modifiers; 158 | codes.push(key_event.code); 159 | } 160 | let codes: OneToThree = codes.try_into()?; 161 | let raw = Self::new(codes, modifiers); 162 | Ok(raw.normalized()) 163 | } 164 | } 165 | 166 | impl From for KeyCombination { 167 | fn from(key_code: KeyCode) -> Self { 168 | Self { 169 | codes: key_code.into(), 170 | modifiers: KeyModifiers::empty(), 171 | } 172 | } 173 | } 174 | 175 | #[allow(clippy::from_over_into)] 176 | impl Into for KeyCombination { 177 | fn into(self) -> KeyEvent { 178 | let Self { codes, modifiers } = self; 179 | KeyEvent { 180 | code: *codes.first(), 181 | modifiers, 182 | kind: KeyEventKind::Press, // the only one in ANSI terminals 183 | state: KeyEventState::empty(), 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/key_event.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, 3 | }; 4 | 5 | /// Return the raw char if the crossterm key event is a letter event. 6 | /// 7 | /// Case of the code is not normalized, just as in the original event. 8 | pub const fn as_letter(key: KeyEvent) -> Option { 9 | match key { 10 | KeyEvent { 11 | code: KeyCode::Char(l), 12 | modifiers: KeyModifiers::NONE, 13 | .. 14 | } => Some(l), 15 | _ => None, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/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, 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 | 170 | mod combiner; 171 | mod format; 172 | mod key_event; 173 | mod parse; 174 | mod key_combination; 175 | 176 | pub use { 177 | combiner::*, 178 | crossterm, 179 | format::*, 180 | key_event::*, 181 | parse::*, 182 | key_combination::*, 183 | strict::OneToThree, 184 | }; 185 | 186 | use { 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. 193 | pub static STANDARD_FORMAT: Lazy = 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] 221 | macro_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)] 229 | pub 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_ALT: KeyModifiers = KeyModifiers::ALT; 238 | pub const MODS_SHIFT: KeyModifiers = KeyModifiers::SHIFT; 239 | pub const MODS_CTRL_ALT: KeyModifiers = KeyModifiers::CONTROL.union(KeyModifiers::ALT); 240 | pub const MODS_ALT_SHIFT: KeyModifiers = KeyModifiers::ALT.union(KeyModifiers::SHIFT); 241 | pub const MODS_CTRL_SHIFT: KeyModifiers = KeyModifiers::CONTROL.union(KeyModifiers::SHIFT); 242 | pub const MODS_CTRL_ALT_SHIFT: KeyModifiers = KeyModifiers::CONTROL 243 | .union(KeyModifiers::ALT) 244 | .union(KeyModifiers::SHIFT); 245 | } 246 | 247 | #[cfg(test)] 248 | mod tests { 249 | use { 250 | crate::{key, KeyCombination, OneToThree}, 251 | crossterm::event::{KeyCode, KeyModifiers}, 252 | }; 253 | 254 | const _: () = { 255 | key!(x); 256 | key!(ctrl - '{'); 257 | key!(alt - '{'); 258 | key!(shift - '{'); 259 | key!(ctrl - alt - f10); 260 | key!(alt - shift - f10); 261 | key!(ctrl - shift - f10); 262 | key!(ctrl - alt - shift - enter); 263 | }; 264 | 265 | fn no_mod(code: KeyCode) -> KeyCombination { 266 | code.into() 267 | } 268 | 269 | #[test] 270 | fn key() { 271 | assert_eq!(key!(backspace), no_mod(KeyCode::Backspace)); 272 | assert_eq!(key!(bAcKsPaCe), no_mod(KeyCode::Backspace)); 273 | assert_eq!(key!(0), no_mod(KeyCode::Char('0'))); 274 | assert_eq!(key!(9), no_mod(KeyCode::Char('9'))); 275 | assert_eq!(key!('x'), no_mod(KeyCode::Char('x'))); 276 | assert_eq!(key!('X'), no_mod(KeyCode::Char('x'))); 277 | assert_eq!(key!(']'), no_mod(KeyCode::Char(']'))); 278 | assert_eq!(key!('ඞ'), no_mod(KeyCode::Char('ඞ'))); 279 | assert_eq!(key!(f), no_mod(KeyCode::Char('f'))); 280 | assert_eq!(key!(F), no_mod(KeyCode::Char('f'))); 281 | assert_eq!(key!(ඞ), no_mod(KeyCode::Char('ඞ'))); 282 | assert_eq!(key!(f10), no_mod(KeyCode::F(10))); 283 | assert_eq!(key!(F10), no_mod(KeyCode::F(10))); 284 | assert_eq!( 285 | key!(ctrl - c), 286 | KeyCombination::new(KeyCode::Char('c'), KeyModifiers::CONTROL) 287 | ); 288 | assert_eq!( 289 | key!(alt - shift - c), 290 | KeyCombination::new(KeyCode::Char('C'), KeyModifiers::ALT | KeyModifiers::SHIFT) 291 | ); 292 | assert_eq!(key!(shift - alt - '2'), key!(ALT - SHIFT - 2)); 293 | assert_eq!(key!(space), key!(' ')); 294 | assert_eq!(key!(hyphen), key!('-')); 295 | assert_eq!(key!(minus), key!('-')); 296 | 297 | assert_eq!( 298 | key!(ctrl-alt-a-b), 299 | KeyCombination::new( 300 | OneToThree::Two(KeyCode::Char('a'), KeyCode::Char('b')), 301 | KeyModifiers::CONTROL | KeyModifiers::ALT, 302 | ) 303 | ); 304 | assert_eq!( 305 | key!(alt-f4-a-b), 306 | KeyCombination::new( 307 | OneToThree::Three(KeyCode::F(4), KeyCode::Char('a'), KeyCode::Char('b')), 308 | KeyModifiers::ALT, 309 | ) 310 | ); 311 | assert_eq!( // check that key codes are sorted 312 | key!(alt-a-b-f4), 313 | KeyCombination::new( 314 | OneToThree::Three(KeyCode::F(4), KeyCode::Char('a'), KeyCode::Char('b')), 315 | KeyModifiers::ALT, 316 | ) 317 | ); 318 | assert_eq!( 319 | key!(z-e), 320 | KeyCombination::new( 321 | OneToThree::Two(KeyCode::Char('e'), KeyCode::Char('z')), 322 | KeyModifiers::NONE, 323 | ) 324 | ); 325 | } 326 | 327 | #[test] 328 | fn format() { 329 | let format = crate::KeyCombinationFormat::default(); 330 | assert_eq!(format.to_string(key!(insert)), "Insert"); 331 | assert_eq!(format.to_string(key!(space)), "Space"); 332 | assert_eq!(format.to_string(key!(alt-Space)), "Alt-Space"); 333 | assert_eq!(format.to_string(key!(shift-' ')), "Shift-Space"); 334 | assert_eq!(format.to_string(key!(alt-hyphen)), "Alt-Hyphen"); 335 | } 336 | 337 | #[test] 338 | fn key_pattern() { 339 | assert!(matches!(key!(ctrl-alt-shift-c), key!(ctrl-alt-shift-c))); 340 | assert!(!matches!(key!(ctrl-c), key!(ctrl-alt-shift-c))); 341 | assert!(matches!(key!(ctrl-alt-b), key!(ctrl-alt-b))); 342 | assert!(matches!(key!(ctrl-b), key!(ctrl-b))); 343 | assert!(matches!(key!(alt-b), key!(alt-b))); 344 | assert!(!matches!(key!(ctrl-b), key!(alt-b))); 345 | assert!(!matches!(key!(alt-b), key!(ctrl-b))); 346 | assert!(!matches!(key!(alt-b), key!(ctrl-alt-b))); 347 | assert!(!matches!(key!(ctrl-b), key!(ctrl-alt-b))); 348 | assert!(!matches!(key!(ctrl-alt-b), key!(alt-b))); 349 | assert!(!matches!(key!(ctrl-alt-b), key!(ctrl-b))); 350 | } 351 | 352 | #[test] 353 | fn ui() { 354 | trybuild::TestCases::new().compile_fail("tests/ui/*.rs"); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/parse.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 | 6 | use { 7 | crate::{ 8 | OneToThree, 9 | KeyCombination, 10 | }, 11 | crossterm::event::{ 12 | KeyCode::{self, *}, 13 | KeyModifiers, 14 | }, 15 | std::fmt, 16 | }; 17 | 18 | #[derive(Debug)] 19 | pub struct ParseKeyError { 20 | /// the string which couldn't be parsed 21 | pub raw: String, 22 | } 23 | 24 | impl ParseKeyError { 25 | pub fn new>(s: S) -> Self { 26 | Self { raw: s.into() } 27 | } 28 | } 29 | 30 | impl fmt::Display for ParseKeyError { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | write!(f, "{:?} can't be parsed as a key", self.raw) 33 | } 34 | } 35 | 36 | impl std::error::Error for ParseKeyError {} 37 | 38 | pub fn parse_key_code(raw: &str, shift: bool) -> Result { 39 | let code = match raw { 40 | "esc" => Esc, 41 | "enter" => Enter, 42 | "left" => Left, 43 | "right" => Right, 44 | "up" => Up, 45 | "down" => Down, 46 | "home" => Home, 47 | "end" => End, 48 | "pageup" => PageUp, 49 | "pagedown" => PageDown, 50 | "backtab" => BackTab, 51 | "backspace" => Backspace, 52 | "del" => Delete, 53 | "delete" => Delete, 54 | "insert" => Insert, 55 | "ins" => Insert, 56 | "f1" => F(1), 57 | "f2" => F(2), 58 | "f3" => F(3), 59 | "f4" => F(4), 60 | "f5" => F(5), 61 | "f6" => F(6), 62 | "f7" => F(7), 63 | "f8" => F(8), 64 | "f9" => F(9), 65 | "f10" => F(10), 66 | "f11" => F(11), 67 | "f12" => F(12), 68 | "f13" => F(13), 69 | "f14" => F(14), 70 | "f15" => F(15), 71 | "f16" => F(16), 72 | "f17" => F(17), 73 | "f18" => F(18), 74 | "f19" => F(19), 75 | "f20" => F(20), 76 | "f21" => F(21), 77 | "f22" => F(22), 78 | "f23" => F(23), 79 | "f24" => F(24), 80 | "space" => Char(' '), 81 | "hyphen" => Char('-'), 82 | "minus" => Char('-'), 83 | "tab" => Tab, 84 | c if c.len() == 1 => { 85 | let mut c = c.chars().next().unwrap(); 86 | if shift { 87 | c = c.to_ascii_uppercase(); 88 | } 89 | Char(c) 90 | } 91 | _ => { 92 | return Err(ParseKeyError::new(raw)); 93 | } 94 | }; 95 | Ok(code) 96 | } 97 | 98 | /// parse a string as a keyboard key combination definition. 99 | /// 100 | /// About the case: 101 | /// The char we receive as code from crossterm is usually lowercase 102 | /// but uppercase when it was typed with shift (i.e. we receive 103 | /// "g" for a lowercase, and "shift-G" for an uppercase) 104 | pub fn parse(raw: &str) -> Result { 105 | let mut modifiers = KeyModifiers::empty(); 106 | let raw = raw.to_ascii_lowercase(); 107 | let mut raw: &str = raw.as_ref(); 108 | loop { 109 | if let Some(end) = raw.strip_prefix("ctrl-") { 110 | raw = end; 111 | modifiers.insert(KeyModifiers::CONTROL); 112 | } else if let Some(end) = raw.strip_prefix("alt-") { 113 | raw = end; 114 | modifiers.insert(KeyModifiers::ALT); 115 | } else if let Some(end) = raw.strip_prefix("shift-") { 116 | raw = end; 117 | modifiers.insert(KeyModifiers::SHIFT); 118 | } else { 119 | break; 120 | } 121 | } 122 | let codes = if raw == "-" { 123 | OneToThree::One(Char('-')) 124 | } else { 125 | let mut codes = Vec::new(); 126 | let shift = modifiers.contains(KeyModifiers::SHIFT); 127 | for raw in raw.split('-') { 128 | let code = parse_key_code(raw, shift)?; 129 | if code == BackTab { 130 | // Crossterm always sends SHIFT with backtab 131 | modifiers.insert(KeyModifiers::SHIFT); 132 | } 133 | codes.push(code); 134 | } 135 | codes.try_into().map_err(|_| ParseKeyError::new("".to_string()))? 136 | }; 137 | Ok(KeyCombination::new(codes, modifiers)) 138 | } 139 | 140 | #[test] 141 | fn check_key_parsing() { 142 | use crate::*; 143 | fn check_ok(raw: &str, key: KeyCombination) { 144 | let parsed = parse(raw); 145 | assert!(parsed.is_ok(), "failed to parse {:?} as key combination", raw); 146 | assert_eq!(parsed.unwrap(), key); 147 | } 148 | assert!(parse("").is_err()); 149 | check_ok("left", key!(left)); 150 | check_ok("RIGHT", key!(right)); 151 | check_ok("Home", key!(HOME)); 152 | check_ok( 153 | "backtab", 154 | KeyCombination::new(KeyCode::BackTab, KeyModifiers::SHIFT), 155 | ); 156 | check_ok("f1", KeyCombination::from(F(1))); 157 | check_ok("F2", KeyCombination::from(F(2))); 158 | check_ok("Enter", KeyCombination::from(Enter)); 159 | check_ok("alt-enter", KeyCombination::new(Enter, KeyModifiers::ALT)); 160 | check_ok("insert", KeyCombination::from(Insert)); 161 | check_ok( 162 | "ctrl-q", 163 | KeyCombination::new(Char('q'), KeyModifiers::CONTROL), 164 | ); 165 | check_ok( 166 | "shift-q", 167 | KeyCombination::new(Char('Q'), KeyModifiers::SHIFT), 168 | ); 169 | check_ok( 170 | "ctrl-Q", 171 | KeyCombination::new(Char('q'), KeyModifiers::CONTROL), 172 | ); 173 | check_ok( 174 | "shift-Q", 175 | KeyCombination::new(Char('Q'), KeyModifiers::SHIFT), 176 | ); 177 | check_ok( 178 | "ctrl-shift-Q", 179 | KeyCombination::new(Char('Q'), KeyModifiers::SHIFT | KeyModifiers::CONTROL), 180 | ); 181 | check_ok("-", KeyCombination::new(Char('-'), KeyModifiers::NONE)); 182 | check_ok("Hyphen", KeyCombination::new(Char('-'), KeyModifiers::NONE)); 183 | check_ok("alt--", KeyCombination::new(Char('-'), KeyModifiers::ALT)); 184 | check_ok( 185 | "alt-hyphen", 186 | KeyCombination::new(Char('-'), KeyModifiers::ALT), 187 | ); 188 | check_ok( 189 | "alt-hyphen", 190 | KeyCombination::new(Char('-'), KeyModifiers::ALT), 191 | ); 192 | check_ok( 193 | "ctrl-Shift-alt-space", 194 | KeyCombination::new( 195 | Char(' '), 196 | KeyModifiers::ALT | KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL, 197 | ), 198 | ); 199 | check_ok( 200 | "ctrl-shift-alt--", 201 | KeyCombination::new( 202 | Char('-'), 203 | KeyModifiers::ALT | KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL, 204 | ), 205 | ); 206 | 207 | // multiple codes 208 | check_ok( 209 | "alt-f12-@", 210 | KeyCombination::new( 211 | OneToThree::Two(F(12), Char('@')), 212 | KeyModifiers::ALT, 213 | ), 214 | ); 215 | check_ok( 216 | "alt-f12-@", 217 | KeyCombination::new( 218 | OneToThree::Two(Char('@'), F(12)), // it's the same because the codes are sorted 219 | KeyModifiers::ALT, 220 | ), 221 | ); 222 | check_ok( 223 | "a-b", 224 | KeyCombination::new( 225 | OneToThree::Two(Char('a'), Char('b')), 226 | KeyModifiers::NONE, 227 | ), 228 | ); 229 | } 230 | -------------------------------------------------------------------------------- /src/proc_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crokey-proc_macros" 3 | version = "1.2.0" 4 | authors = ["Canop "] 5 | description = "proc macros for the crokey crate" 6 | license = "MIT" 7 | edition = "2018" 8 | 9 | [dependencies] 10 | crossterm = "0.29" 11 | proc-macro2 = "1.0" 12 | quote = "1.0" 13 | strict = "0.2" 14 | syn = { version = "2.0", default-features = false, features = ["parsing", "proc-macro"] } 15 | 16 | [lib] 17 | proc-macro = true 18 | path = "mod.rs" 19 | 20 | -------------------------------------------------------------------------------- /src/proc_macros/mod.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crossterm::event::KeyCode, 3 | proc_macro::TokenStream as TokenStream1, 4 | proc_macro2::{Group, Span, TokenStream}, 5 | quote::quote, 6 | strict::OneToThree, 7 | syn::{ 8 | parse::{Error, Parse, ParseStream, Result}, 9 | parse_macro_input, Ident, LitChar, LitInt, Token, 10 | }, 11 | }; 12 | 13 | struct KeyCombinationKey { 14 | pub crate_path: TokenStream, 15 | pub ctrl: bool, 16 | pub alt: bool, 17 | pub shift: bool, 18 | pub codes: OneToThree, 19 | } 20 | 21 | 22 | // TODO to allow sorted codes: 23 | // [x] implement map in OneToThree 24 | // [x] extract parse_key_code from crokey::parse (returning a crossterm::KeyCode) 25 | // [ ] write function KeyCode->TokenStream 26 | // [ ] first build a OneToThree 27 | // [ ] sort it 28 | // [ ] then map it to a OneToThree using the function KeyCode->TokenStream 29 | 30 | // must be kept identical to crokey::parse_key_code 31 | // (and yes, this duplication isn't ideal) 32 | fn parse_key_code( 33 | raw: &str, 34 | shift: bool, 35 | code_span: Span, 36 | ) -> Result { 37 | use KeyCode::*; 38 | let code = match raw { 39 | "esc" => Esc, 40 | "enter" => Enter, 41 | "left" => Left, 42 | "right" => Right, 43 | "up" => Up, 44 | "down" => Down, 45 | "home" => Home, 46 | "end" => End, 47 | "pageup" => PageUp, 48 | "pagedown" => PageDown, 49 | "backtab" => BackTab, 50 | "backspace" => Backspace, 51 | "del" => Delete, 52 | "delete" => Delete, 53 | "insert" => Insert, 54 | "ins" => Insert, 55 | "f1" => F(1), 56 | "f2" => F(2), 57 | "f3" => F(3), 58 | "f4" => F(4), 59 | "f5" => F(5), 60 | "f6" => F(6), 61 | "f7" => F(7), 62 | "f8" => F(8), 63 | "f9" => F(9), 64 | "f10" => F(10), 65 | "f11" => F(11), 66 | "f12" => F(12), 67 | "f13" => F(13), 68 | "f14" => F(14), 69 | "f15" => F(15), 70 | "f16" => F(16), 71 | "f17" => F(17), 72 | "f18" => F(18), 73 | "f19" => F(19), 74 | "f20" => F(20), 75 | "f21" => F(21), 76 | "f22" => F(22), 77 | "f23" => F(23), 78 | "f24" => F(24), 79 | "space" => Char(' '), 80 | "hyphen" => Char('-'), 81 | "minus" => Char('-'), 82 | "tab" => Tab, 83 | c if c.chars().count() == 1 => { 84 | let mut c = c.chars().next().unwrap(); 85 | if shift { 86 | c = c.to_ascii_uppercase(); 87 | } 88 | Char(c) 89 | } 90 | _ => { 91 | return Err(Error::new( 92 | code_span, 93 | format_args!("unrecognized key code {:?}", raw), 94 | )); 95 | } 96 | }; 97 | Ok(code) 98 | } 99 | 100 | 101 | fn key_code_to_token_stream(key_code: KeyCode, code_span: Span) -> Result { 102 | let ts = match key_code { 103 | KeyCode::Backspace => quote! { Backspace }, 104 | KeyCode::Enter => quote! { Enter }, 105 | KeyCode::Left => quote! { Left }, 106 | KeyCode::Right => quote! { Right }, 107 | KeyCode::Up => quote! { Up }, 108 | KeyCode::Down => quote! { Down }, 109 | KeyCode::Home => quote! { Home }, 110 | KeyCode::End => quote! { End }, 111 | KeyCode::PageUp => quote! { PageUp }, 112 | KeyCode::PageDown => quote! { PageDown }, 113 | KeyCode::Tab => quote! { Tab }, 114 | KeyCode::BackTab => quote! { BackTab }, 115 | KeyCode::Delete => quote! { Delete }, 116 | KeyCode::Insert => quote! { Insert }, 117 | KeyCode::F(n) => quote! { F(#n) }, 118 | KeyCode::Char(c) => quote! { Char(#c) }, 119 | KeyCode::Null => quote! { Null }, 120 | KeyCode::Esc => quote! { Esc }, 121 | KeyCode::CapsLock => quote! { CapsLock }, 122 | KeyCode::ScrollLock => quote! { ScrollLock }, 123 | KeyCode::NumLock => quote! { NumLock }, 124 | KeyCode::PrintScreen => quote! { PrintScreen }, 125 | KeyCode::Pause => quote! { Pause }, 126 | KeyCode::Menu => quote! { Menu }, 127 | KeyCode::KeypadBegin => quote! { KeypadBegin }, 128 | // Media(MediaKeyCode), 129 | // Modifier(ModifierKeyCode), 130 | _ => { 131 | return Err(Error::new( 132 | code_span, 133 | format_args!("failed to encode {:?}", key_code), 134 | )); 135 | } 136 | }; 137 | Ok(ts) 138 | } 139 | 140 | impl Parse for KeyCombinationKey { 141 | fn parse(input: ParseStream<'_>) -> Result { 142 | let crate_path = input.parse::()?.stream(); 143 | 144 | let mut ctrl = false; 145 | let mut alt = false; 146 | let mut shift = false; 147 | 148 | let (code, code_span) = loop { 149 | let lookahead = input.lookahead1(); 150 | 151 | if lookahead.peek(LitChar) { 152 | let lit = input.parse::()?; 153 | break (lit.value().to_lowercase().collect(), lit.span()); 154 | } 155 | 156 | if lookahead.peek(LitInt) { 157 | let int = input.parse::()?; 158 | let digits = int.base10_digits(); 159 | if digits.len() > 1 { 160 | return Err(Error::new(int.span(), "invalid key; must be between 0-9")); 161 | } 162 | break (digits.to_owned(), int.span()); 163 | } 164 | 165 | if !lookahead.peek(Ident) { 166 | return Err(lookahead.error()); 167 | } 168 | 169 | let ident = input.parse::()?; 170 | let ident_value = ident.to_string().to_lowercase(); 171 | let modifier = match &*ident_value { 172 | "ctrl" => &mut ctrl, 173 | "alt" => &mut alt, 174 | "shift" => &mut shift, 175 | _ => break (ident_value, ident.span()), 176 | }; 177 | if *modifier { 178 | return Err(Error::new( 179 | ident.span(), 180 | format_args!("duplicate modifier {}", ident_value), 181 | )); 182 | } 183 | *modifier = true; 184 | 185 | input.parse::()?; 186 | }; 187 | 188 | // parse the key codes 189 | let first_code = parse_key_code(&code, shift, code_span)?; 190 | let codes = if input.parse::().is_ok() { 191 | let ident = input.parse::()?; 192 | let second_code = parse_key_code(&ident.to_string().to_lowercase(), shift, ident.span())?; 193 | if input.parse::().is_ok() { 194 | let ident = input.parse::()?; 195 | let third_code = parse_key_code(&ident.to_string().to_lowercase(), shift, ident.span())?; 196 | OneToThree::Three(first_code, second_code, third_code) 197 | } else { 198 | OneToThree::Two(first_code, second_code) 199 | } 200 | } else { 201 | OneToThree::One(first_code) 202 | }; 203 | 204 | // sort according to key codes because comparing with pattern matching 205 | // received key combinations with parsed ones requires code ordering to 206 | // be consistent 207 | let codes = codes.sorted(); 208 | 209 | // Produce the token stream which will build pattern matching comparable initializers 210 | let codes = codes.try_map(|key_code| key_code_to_token_stream(key_code, input.span()))?; 211 | 212 | Ok(KeyCombinationKey { 213 | crate_path, 214 | ctrl, 215 | alt, 216 | shift, 217 | codes, 218 | }) 219 | } 220 | } 221 | 222 | // Not public API. This is internal and to be used only by `key!`. 223 | #[doc(hidden)] 224 | #[proc_macro] 225 | pub fn key(input: TokenStream1) -> TokenStream1 { 226 | let KeyCombinationKey { 227 | crate_path, 228 | ctrl, 229 | alt, 230 | shift, 231 | codes, 232 | } = parse_macro_input!(input); 233 | 234 | let mut modifier_constant = "MODS".to_owned(); 235 | if ctrl { 236 | modifier_constant.push_str("_CTRL"); 237 | } 238 | if alt { 239 | modifier_constant.push_str("_ALT"); 240 | } 241 | if shift { 242 | modifier_constant.push_str("_SHIFT"); 243 | } 244 | let modifier_constant = Ident::new(&modifier_constant, Span::call_site()); 245 | 246 | match codes { 247 | OneToThree::One(code) => { 248 | quote! { 249 | #crate_path::KeyCombination { 250 | codes: #crate_path::__private::OneToThree::One( 251 | #crate_path::__private::crossterm::event::KeyCode::#code 252 | ), 253 | modifiers: #crate_path::__private::#modifier_constant, 254 | } 255 | } 256 | } 257 | OneToThree::Two(a, b) => { 258 | quote! { 259 | #crate_path::KeyCombination { 260 | codes: #crate_path::__private::OneToThree::Two( 261 | #crate_path::__private::crossterm::event::KeyCode::#a, 262 | #crate_path::__private::crossterm::event::KeyCode::#b, 263 | ), 264 | modifiers: #crate_path::__private::#modifier_constant, 265 | } 266 | } 267 | } 268 | OneToThree::Three(a, b, c) => { 269 | quote! { 270 | #crate_path::KeyCombination { 271 | codes: #crate_path::__private::OneToThree::Three( 272 | #crate_path::__private::crossterm::event::KeyCode::#a, 273 | #crate_path::__private::crossterm::event::KeyCode::#b, 274 | #crate_path::__private::crossterm::event::KeyCode::#c, 275 | ), 276 | modifiers: #crate_path::__private::#modifier_constant, 277 | } 278 | } 279 | } 280 | } 281 | .into() 282 | } 283 | -------------------------------------------------------------------------------- /tests/hygiene.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_implicit_prelude] 3 | 4 | #[allow(dead_code)] 5 | fn hygiene() { 6 | ::crokey::key!(M); 7 | ::crokey::key!(ctrl-c); 8 | ::crokey::key!(alt-shift-ctrl-']'); 9 | } 10 | -------------------------------------------------------------------------------- /tests/ui/duplicate-modifier.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | crokey::key!(ctrl-ctrl-5); 3 | crokey::key!(alt-alt-5); 4 | crokey::key!(shift-shift-5); 5 | crokey::key!(shift-alt-shift-ctrl-5); 6 | } 7 | -------------------------------------------------------------------------------- /tests/ui/duplicate-modifier.stderr: -------------------------------------------------------------------------------- 1 | error: duplicate modifier ctrl 2 | --> tests/ui/duplicate-modifier.rs:2:23 3 | | 4 | 2 | crokey::key!(ctrl-ctrl-5); 5 | | ^^^^ 6 | 7 | error: duplicate modifier alt 8 | --> tests/ui/duplicate-modifier.rs:3:22 9 | | 10 | 3 | crokey::key!(alt-alt-5); 11 | | ^^^ 12 | 13 | error: duplicate modifier shift 14 | --> tests/ui/duplicate-modifier.rs:4:24 15 | | 16 | 4 | crokey::key!(shift-shift-5); 17 | | ^^^^^ 18 | 19 | error: duplicate modifier shift 20 | --> tests/ui/duplicate-modifier.rs:5:28 21 | | 22 | 5 | crokey::key!(shift-alt-shift-ctrl-5); 23 | | ^^^^^ 24 | -------------------------------------------------------------------------------- /tests/ui/invalid-key.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | crokey::key!(10); 3 | crokey::key!(ctrl-backpace); 4 | crokey::key!(ctrl--); 5 | } 6 | -------------------------------------------------------------------------------- /tests/ui/invalid-key.stderr: -------------------------------------------------------------------------------- 1 | error: invalid key; must be between 0-9 2 | --> tests/ui/invalid-key.rs:2:18 3 | | 4 | 2 | crokey::key!(10); 5 | | ^^ 6 | 7 | error: unrecognized key code "backpace" 8 | --> tests/ui/invalid-key.rs:3:23 9 | | 10 | 3 | crokey::key!(ctrl-backpace); 11 | | ^^^^^^^^ 12 | 13 | error: expected one of: character literal, integer literal, identifier 14 | --> tests/ui/invalid-key.rs:4:23 15 | | 16 | 4 | crokey::key!(ctrl--); 17 | | ^ 18 | -------------------------------------------------------------------------------- /tests/ui/invalid-modifier.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | crokey::key!(control-c); 3 | } 4 | -------------------------------------------------------------------------------- /tests/ui/invalid-modifier.stderr: -------------------------------------------------------------------------------- 1 | error: unrecognized key code "control" 2 | --> tests/ui/invalid-modifier.rs:2:18 3 | | 4 | 2 | crokey::key!(control-c); 5 | | ^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/unexpected-eof.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | crokey::key!(); 3 | crokey::key!(ctrl); 4 | crokey::key!(ctrl-); 5 | } 6 | -------------------------------------------------------------------------------- /tests/ui/unexpected-eof.stderr: -------------------------------------------------------------------------------- 1 | error: unexpected end of input, expected one of: character literal, integer literal, identifier 2 | --> tests/ui/unexpected-eof.rs:2:5 3 | | 4 | 2 | crokey::key!(); 5 | | ^^^^^^^^^^^^^^ 6 | | 7 | = note: this error originates in the macro `$crate::__private::key` which comes from the expansion of the macro `crokey::key` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | 9 | error: expected `-` 10 | --> tests/ui/unexpected-eof.rs:3:5 11 | | 12 | 3 | crokey::key!(ctrl); 13 | | ^^^^^^^^^^^^^^^^^^ 14 | | 15 | = note: this error originates in the macro `$crate::__private::key` which comes from the expansion of the macro `crokey::key` (in Nightly builds, run with -Z macro-backtrace for more info) 16 | 17 | error: unexpected end of input, expected one of: character literal, integer literal, identifier 18 | --> tests/ui/unexpected-eof.rs:4:5 19 | | 20 | 4 | crokey::key!(ctrl-); 21 | | ^^^^^^^^^^^^^^^^^^^ 22 | | 23 | = note: this error originates in the macro `$crate::__private::key` which comes from the expansion of the macro `crokey::key` (in Nightly builds, run with -Z macro-backtrace for more info) 24 | --------------------------------------------------------------------------------