Skip to main content

crokey_proc_macros/
mod.rs

1use {
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
13struct KeyCombinationKey {
14    pub crate_path: TokenStream,
15    pub ctrl: bool,
16    pub cmd: bool,
17    pub alt: bool,
18    pub shift: bool,
19    pub codes: OneToThree<TokenStream>,
20}
21
22
23// TODO to allow sorted codes:
24// [x] implement map in OneToThree
25// [x] extract parse_key_code from crokey::parse (returning a crossterm::KeyCode)
26// [ ] write function KeyCode->TokenStream
27// [ ] first build a OneToThree<crossterm::KeyCode>
28// [ ] sort it
29// [ ] then map it to a OneToThree<TokenStream> using the function KeyCode->TokenStream
30
31// must be kept identical to crokey::parse_key_code
32// (and yes, this duplication isn't ideal)
33fn parse_key_code(
34    raw: &str,
35    shift: bool,
36    code_span: Span,
37) -> Result<KeyCode> {
38    use KeyCode::*;
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.chars().count() == 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(Error::new(
93                code_span,
94                format_args!("unrecognized key code {raw:?}"),
95            ));
96        }
97    };
98    Ok(code)
99}
100
101
102fn key_code_to_token_stream(key_code: KeyCode, code_span: Span) -> Result<TokenStream> {
103    let ts = match key_code {
104        KeyCode::Backspace => quote! { Backspace },
105        KeyCode::Enter => quote! { Enter },
106        KeyCode::Left => quote! { Left },
107        KeyCode::Right => quote! { Right },
108        KeyCode::Up => quote! { Up },
109        KeyCode::Down => quote! { Down },
110        KeyCode::Home => quote! { Home },
111        KeyCode::End => quote! { End },
112        KeyCode::PageUp => quote! { PageUp },
113        KeyCode::PageDown => quote! { PageDown },
114        KeyCode::Tab => quote! { Tab },
115        KeyCode::BackTab => quote! { BackTab },
116        KeyCode::Delete => quote! { Delete },
117        KeyCode::Insert => quote! { Insert },
118        KeyCode::F(n) => quote! { F(#n) },
119        KeyCode::Char(c) => quote! { Char(#c) },
120        KeyCode::Null => quote! { Null },
121        KeyCode::Esc => quote! { Esc },
122        KeyCode::CapsLock => quote! { CapsLock },
123        KeyCode::ScrollLock => quote! { ScrollLock },
124        KeyCode::NumLock => quote! { NumLock },
125        KeyCode::PrintScreen => quote! { PrintScreen },
126        KeyCode::Pause => quote! { Pause },
127        KeyCode::Menu => quote! { Menu },
128        KeyCode::KeypadBegin => quote! { KeypadBegin },
129        // Media(MediaKeyCode),
130        // Modifier(ModifierKeyCode),
131        _ => {
132            return Err(Error::new(
133                code_span,
134                format_args!("failed to encode {key_code:?}"),
135            ));
136        }
137    };
138    Ok(ts)
139}
140
141impl Parse for KeyCombinationKey {
142    fn parse(input: ParseStream<'_>) -> Result<Self> {
143        let crate_path = input.parse::<Group>()?.stream();
144
145        let mut ctrl = false;
146        let mut cmd = false;
147        let mut alt = false;
148        let mut shift = false;
149
150        let (code, code_span) = loop {
151            let lookahead = input.lookahead1();
152
153            if lookahead.peek(LitChar) {
154                let lit = input.parse::<LitChar>()?;
155                break (lit.value().to_lowercase().collect(), lit.span());
156            }
157
158            if lookahead.peek(LitInt) {
159                let int = input.parse::<LitInt>()?;
160                let digits = int.base10_digits();
161                if digits.len() > 1 {
162                    return Err(Error::new(int.span(), "invalid key; must be between 0-9"));
163                }
164                break (digits.to_owned(), int.span());
165            }
166
167            if !lookahead.peek(Ident) {
168                return Err(lookahead.error());
169            }
170
171            let ident = input.parse::<Ident>()?;
172            let ident_value = ident.to_string().to_lowercase();
173            let modifier = match &*ident_value {
174                "ctrl" => &mut ctrl,
175                "cmd" => &mut cmd,
176                "alt" => &mut alt,
177                "shift" => &mut shift,
178                _ => break (ident_value, ident.span()),
179            };
180            if *modifier {
181                return Err(Error::new(
182                    ident.span(),
183                    format_args!("duplicate modifier {ident_value}"),
184                ));
185            }
186            *modifier = true;
187
188            input.parse::<Token![-]>()?;
189        };
190
191        // parse the key codes
192        let first_code = parse_key_code(&code, shift, code_span)?;
193        let codes = if input.parse::<Token![-]>().is_ok() {
194            let ident = input.parse::<Ident>()?;
195            let second_code = parse_key_code(&ident.to_string().to_lowercase(), shift, ident.span())?;
196            if input.parse::<Token![-]>().is_ok() {
197                let ident = input.parse::<Ident>()?;
198                let third_code = parse_key_code(&ident.to_string().to_lowercase(), shift, ident.span())?;
199                OneToThree::Three(first_code, second_code, third_code)
200            } else {
201                OneToThree::Two(first_code, second_code)
202            }
203        } else {
204            OneToThree::One(first_code)
205        };
206
207        // sort according to key codes because comparing with pattern matching
208        // received key combinations with parsed ones requires code ordering to
209        // be consistent
210        let codes = codes.sorted();
211
212        // Produce the token stream which will build pattern matching comparable initializers
213        let codes = codes.try_map(|key_code| key_code_to_token_stream(key_code, input.span()))?;
214
215        Ok(KeyCombinationKey {
216            crate_path,
217            ctrl,
218            cmd,
219            alt,
220            shift,
221            codes,
222        })
223    }
224}
225
226// Not public API. This is internal and to be used only by `key!`.
227#[doc(hidden)]
228#[proc_macro]
229pub fn key(input: TokenStream1) -> TokenStream1 {
230    let KeyCombinationKey {
231        crate_path,
232        ctrl,
233        cmd,
234        alt,
235        shift,
236        codes,
237    } = parse_macro_input!(input);
238
239    let mut modifier_constant = "MODS".to_owned();
240    if ctrl {
241        modifier_constant.push_str("_CTRL");
242    }
243    if cmd {
244        modifier_constant.push_str("_CMD");
245    }
246    if alt {
247        modifier_constant.push_str("_ALT");
248    }
249    if shift {
250        modifier_constant.push_str("_SHIFT");
251    }
252    let modifier_constant = Ident::new(&modifier_constant, Span::call_site());
253
254    match codes {
255        OneToThree::One(code) => {
256            quote! {
257                #crate_path::KeyCombination {
258                    codes: #crate_path::__private::OneToThree::One(
259                       #crate_path::__private::crossterm::event::KeyCode::#code
260                    ),
261                    modifiers: #crate_path::__private::#modifier_constant,
262                }
263            }
264        }
265        OneToThree::Two(a, b) => {
266            quote! {
267                #crate_path::KeyCombination {
268                    codes: #crate_path::__private::OneToThree::Two(
269                       #crate_path::__private::crossterm::event::KeyCode::#a,
270                       #crate_path::__private::crossterm::event::KeyCode::#b,
271                    ),
272                    modifiers: #crate_path::__private::#modifier_constant,
273                }
274            }
275        }
276        OneToThree::Three(a, b, c) => {
277            quote! {
278                #crate_path::KeyCombination {
279                    codes: #crate_path::__private::OneToThree::Three(
280                       #crate_path::__private::crossterm::event::KeyCode::#a,
281                       #crate_path::__private::crossterm::event::KeyCode::#b,
282                       #crate_path::__private::crossterm::event::KeyCode::#c,
283                    ),
284                    modifiers: #crate_path::__private::#modifier_constant,
285                }
286            }
287        }
288    }
289    .into()
290}