1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3use darling::{FromAttributes, FromMeta, ast::NestedMeta};
6use itertools::MultiUnzip;
7use proc_macro2::TokenStream;
8use quote::{ToTokens, quote};
9use syn::{
10 Expr, ExprLit, ImplItem, ItemImpl, Lit, Meta, MetaNameValue, parse_macro_input, parse_quote,
11 spanned::Spanned,
12};
13
14#[derive(FromMeta)]
15struct KeyMapAttrs {
16 #[darling(default)]
17 backend: Backend,
18}
19
20impl KeyMapAttrs {
21 fn parse_attrs(attrs: TokenStream) -> darling::Result<Self> {
22 KeyMapAttrs::from_list(&NestedMeta::parse_meta_list(attrs)?)
23 }
24}
25
26#[derive(Default, FromMeta)]
27enum Backend {
28 #[cfg_attr(all(feature = "crossterm",), default)]
29 Crossterm,
30 #[cfg_attr(all(not(feature = "crossterm"), feature = "termion",), default)]
31 Termion,
32 #[cfg_attr(
33 all(
34 not(feature = "crossterm"),
35 not(feature = "termion"),
36 feature = "termwiz"
37 ),
38 default
39 )]
40 Termwiz,
41}
42
43impl Backend {
44 fn backend_type(&self) -> Expr {
45 match self {
46 Self::Crossterm => parse_quote!(::ratatui_input_manager::CrosstermBackend),
47 Self::Termion => parse_quote!(::ratatui_input_manager::TermionBackend),
48 Self::Termwiz => parse_quote!(::ratatui_input_manager::TermwizBackend),
49 }
50 }
51
52 fn combine_modifiers(&self, modifiers: &[Expr]) -> Expr {
53 match self {
54 Self::Crossterm => {
55 parse_quote! {::crossterm::event::KeyModifiers::NONE #(.union(#modifiers))*}
56 }
57 Self::Termion => parse_quote! { () },
58 Self::Termwiz => {
59 parse_quote! { ::termwiz::input::Modifiers::NONE #(.union(#modifiers))* }
60 }
61 }
62 }
63
64 fn input_event(&self, key_code: &Expr, combined_modifiers: &Expr) -> TokenStream {
65 match self {
66 Self::Crossterm => {
67 quote! {
68 ::crossterm::event::Event::Key(
69 ::crossterm::event::KeyEvent {
70 code: #key_code,
71 modifiers,
72 kind: ::crossterm::event::KeyEventKind::Press,
73 ..
74 }
75 ) if modifiers.contains(#combined_modifiers)
76 }
77 }
78 Self::Termion => quote! {
79 ::termion::event::Event::Key(
80 ::termion::event::#key_code
81 )
82 },
83 Self::Termwiz => {
84 quote! {
85 ::termwiz::input::InputEvent::Key(
86 ::termwiz::input::KeyEvent {
87 key: #key_code,
88 modifiers,
89 }
90 ) if modifiers.contains(#combined_modifiers)
91 }
92 }
93 }
94 }
95}
96
97#[derive(Debug, FromMeta)]
98struct Pressed {
99 key: syn::Expr,
100 #[darling(multiple)]
101 modifiers: Vec<syn::Expr>,
102}
103
104#[derive(FromAttributes)]
105#[darling(attributes(keybind), forward_attrs)]
106struct KeyBindAttrs {
107 #[darling(multiple)]
108 pressed: Vec<Pressed>,
109 attrs: Vec<syn::Attribute>,
110}
111
112#[allow(rustdoc::broken_intra_doc_links)]
115#[proc_macro_attribute]
116pub fn keymap(
117 attrs: proc_macro::TokenStream,
118 input: proc_macro::TokenStream,
119) -> proc_macro::TokenStream {
120 let args = match KeyMapAttrs::parse_attrs(attrs.into()) {
121 Ok(args) => args,
122 Err(err) => return err.write_errors().into(),
123 };
124 let input = parse_macro_input!(input as ItemImpl);
125 match keymap_impl(args, input) {
126 Ok((original_impl, keymap_impl)) => TokenStream::from_iter([
127 original_impl.to_token_stream(),
128 keymap_impl.to_token_stream(),
129 ])
130 .into(),
131 Err(err) => err.into_compile_error().into(),
132 }
133}
134
135fn keymap_impl(args: KeyMapAttrs, input: ItemImpl) -> syn::Result<(ItemImpl, ItemImpl)> {
136 let ItemImpl { self_ty, items, .. } = input;
137
138 let (keybinds, orig_impls) = items
139 .into_iter()
140 .map(|item| match item {
141 ImplItem::Fn(mut item_fn) => {
142 let KeyBindAttrs { pressed, attrs } =
143 KeyBindAttrs::from_attributes(&item_fn.attrs)?;
144 let doc = attrs
145 .iter()
146 .find_map(|attr| {
147 if let Meta::NameValue(MetaNameValue { path, value, .. }) = &attr.meta
148 && path.is_ident("doc")
149 && let Expr::Lit(ExprLit {
150 lit: Lit::Str(doc), ..
151 }) = value
152 {
153 Some(doc.value().trim().to_string())
154 } else {
155 None
156 }
157 })
158 .ok_or_else(|| {
159 syn::Error::new(
160 item_fn.sig.ident.span(),
161 "Keybind functions must have a doc comment for the description",
162 )
163 })?;
164 item_fn.attrs = attrs;
165 Ok(((item_fn.sig.ident.clone(), pressed, doc), item_fn))
166 }
167 _ => Err(syn::Error::new(
168 item.span(),
169 "Only function definitions are permitted with a keymap",
170 )),
171 })
172 .collect::<Result<(Vec<_>, Vec<_>), _>>()?;
173
174 let orig_impl = parse_quote! {
175 impl #self_ty {
176 #(#orig_impls)*
177 }
178 };
179
180 let (fn_names, match_arms, key_codes, combined_modifiers, descriptions): (
181 Vec<_>,
182 Vec<Vec<_>>,
183 Vec<Vec<_>>,
184 Vec<Vec<_>>,
185 Vec<_>,
186 ) = keybinds
187 .into_iter()
188 .map(|(fn_name, pressed, description)| {
189 let (match_arm, key_codes, combined_modifiers) = pressed
190 .into_iter()
191 .map(|Pressed { key, modifiers }| {
192 let combined_modifiers = args.backend.combine_modifiers(&modifiers);
193 let match_arm = args.backend.input_event(&key, &combined_modifiers);
194 (match_arm, key, combined_modifiers)
195 })
196 .multiunzip();
197 (
198 fn_name,
199 match_arm,
200 key_codes,
201 combined_modifiers,
202 description,
203 )
204 })
205 .multiunzip();
206
207 let backend_type = args.backend.backend_type();
208
209 let keymap_impl = parse_quote! {
210 impl ::ratatui_input_manager::KeyMap::<#backend_type> for #self_ty {
211 const KEYBINDS: &'static [::ratatui_input_manager::KeyBind::<#backend_type>] = &[
212 #(
213 ::ratatui_input_manager::KeyBind::<#backend_type> {
214 pressed: &[
215 #(::ratatui_input_manager::KeyPress::<#backend_type> {
216 key: #key_codes,
217 modifiers: #combined_modifiers,
218 }),*
219 ],
220 description: #descriptions,
221 },
222 )*
223 ];
224
225 fn handle(&mut self, event: &<#backend_type as ::ratatui_input_manager::Backend>::Event) -> bool {
226 match event {
227 #(#(#match_arms => {self.#fn_names(); true},)*)*
228 _ => false
229 }
230 }
231 }
232 };
233
234 Ok((orig_impl, keymap_impl))
235}
236
237#[cfg(test)]
238mod tests {
239 use super::{Backend, KeyMapAttrs, keymap_impl};
240 use pretty_assertions::assert_eq;
241 use prettyplease::unparse;
242 use quote::quote;
243 use syn::{Item, ItemImpl, parse_quote, parse2};
244
245 fn format_item<I>(item: I) -> String
246 where
247 I: Into<Item>,
248 {
249 let file = syn::File {
250 attrs: vec![],
251 items: vec![item.into()],
252 shebang: None,
253 };
254 unparse(&file)
255 }
256
257 #[test]
258 fn test_generated_impl_crossterm() {
259 let args = KeyMapAttrs {
260 backend: Backend::Crossterm,
261 };
262 let input = parse_quote! {
263 impl Foo {
264 #[keybind(pressed(key=KeyCode::Esc))]
266 #[keybind(pressed(key=KeyCode::Char('q')))]
267 fn bar(&mut self) {
268 todo!()
269 }
270
271 #[keybind(pressed(key=KeyCode::Char('a'), modifiers=KeyModifiers::CONTROL, modifiers=KeyModifiers::SHIFT))]
273 fn baz(&mut self) {
274 todo!()
275 }
276 }
277 };
278 let (orig_impl, keymap_impl) = keymap_impl(args, input).unwrap();
279 let expected_orig = parse2::<ItemImpl>(quote! {
280 impl Foo {
281 fn bar(&mut self) {
283 todo!()
284 }
285
286 fn baz(&mut self) {
288 todo!()
289 }
290 }
291 })
292 .unwrap();
293 let expected_keymap: ItemImpl = parse_quote! {
294 impl ::ratatui_input_manager::KeyMap::<::ratatui_input_manager::CrosstermBackend> for Foo {
295 const KEYBINDS: &'static [::ratatui_input_manager::KeyBind::<::ratatui_input_manager::CrosstermBackend>] = &[
296 ::ratatui_input_manager::KeyBind::<::ratatui_input_manager::CrosstermBackend> {
297 pressed: &[
298 ::ratatui_input_manager::KeyPress::<::ratatui_input_manager::CrosstermBackend> {
299 key: KeyCode::Esc,
300 modifiers: ::crossterm::event::KeyModifiers::NONE,
301 },
302 ::ratatui_input_manager::KeyPress::<::ratatui_input_manager::CrosstermBackend> {
303 key: KeyCode::Char('q'),
304 modifiers: ::crossterm::event::KeyModifiers::NONE,
305 },
306 ],
307 description: "The first keybind",
308 },
309 ::ratatui_input_manager::KeyBind::<::ratatui_input_manager::CrosstermBackend> {
310 pressed: &[
311 ::ratatui_input_manager::KeyPress::<::ratatui_input_manager::CrosstermBackend> {
312 key: KeyCode::Char('a'),
313 modifiers: ::crossterm::event::KeyModifiers::NONE
314 .union(KeyModifiers::CONTROL)
315 .union(KeyModifiers::SHIFT),
316 },
317 ],
318 description: "The second keybind",
319 }
320 ];
321
322 fn handle(&mut self, event: &<::ratatui_input_manager::CrosstermBackend as ::ratatui_input_manager::Backend>::Event) -> bool {
323 match event {
324 ::crossterm::event::Event::Key(
325 ::crossterm::event::KeyEvent {
326 code: KeyCode::Esc,
327 modifiers,
328 kind: ::crossterm::event::KeyEventKind::Press,
329 ..
330 },
331 ) if modifiers.contains(::crossterm::event::KeyModifiers::NONE) => { self.bar(); true }
332 ::crossterm::event::Event::Key(
333 ::crossterm::event::KeyEvent {
334 code: KeyCode::Char('q'),
335 modifiers,
336 kind: ::crossterm::event::KeyEventKind::Press,
337 ..
338 },
339 ) if modifiers.contains(::crossterm::event::KeyModifiers::NONE) => { self.bar(); true }
340 ::crossterm::event::Event::Key(
341 ::crossterm::event::KeyEvent {
342 code: KeyCode::Char('a'),
343 modifiers,
344 kind: ::crossterm::event::KeyEventKind::Press,
345 ..
346 },
347 ) if modifiers
348 .contains(
349 ::crossterm::event::KeyModifiers::NONE
350 .union(KeyModifiers::CONTROL)
351 .union(KeyModifiers::SHIFT),
352 ) => { self.baz(); true }
353 _ => false,
354 }
355 }
356 }
357 };
358
359 assert_eq!(format_item(expected_orig), format_item(orig_impl));
360 assert_eq!(
361 format_item::<ItemImpl>(expected_keymap),
362 format_item(keymap_impl)
363 );
364 }
365
366 #[test]
367 fn test_generated_impl_termion() {
368 let args = KeyMapAttrs {
369 backend: Backend::Termion,
370 };
371 let input = parse_quote! {
372 impl Foo {
373 #[keybind(pressed(key=Key::Esc))]
375 #[keybind(pressed(key=Key::Char('q')))]
376 fn bar(&mut self) {
377 todo!()
378 }
379
380 #[keybind(pressed(key=Key::Char('a')))]
382 fn baz(&mut self) {
383 todo!()
384 }
385 }
386 };
387 let (orig_impl, keymap_impl) = keymap_impl(args, input).unwrap();
388 let expected_orig = parse2::<ItemImpl>(quote! {
389 impl Foo {
390 fn bar(&mut self) {
392 todo!()
393 }
394
395 fn baz(&mut self) {
397 todo!()
398 }
399 }
400 })
401 .unwrap();
402 let expected_keymap = parse_quote! {
403 impl ::ratatui_input_manager::KeyMap::<::ratatui_input_manager::TermionBackend> for Foo {
404 const KEYBINDS: &'static [::ratatui_input_manager::KeyBind::<::ratatui_input_manager::TermionBackend>] = &[
405 ::ratatui_input_manager::KeyBind::<::ratatui_input_manager::TermionBackend> {
406 pressed: &[
407 ::ratatui_input_manager::KeyPress::<::ratatui_input_manager::TermionBackend> {
408 key: Key::Esc,
409 modifiers: (),
410 },
411 ::ratatui_input_manager::KeyPress::<::ratatui_input_manager::TermionBackend> {
412 key: Key::Char('q'),
413 modifiers: (),
414 },
415 ],
416 description: "The first keybind",
417 },
418 ::ratatui_input_manager::KeyBind::<::ratatui_input_manager::TermionBackend> {
419 pressed: &[
420 ::ratatui_input_manager::KeyPress::<::ratatui_input_manager::TermionBackend> {
421 key: Key::Char('a'),
422 modifiers: (),
423 },
424 ],
425 description: "The second keybind",
426 }
427 ];
428
429 fn handle(&mut self, event: &<::ratatui_input_manager::TermionBackend as ::ratatui_input_manager::Backend>::Event) -> bool {
430 match event {
431 ::termion::event::Event::Key(
432 ::termion::event::Key::Esc
433 ) => { self.bar(); true }
434 ::termion::event::Event::Key(
435 ::termion::event::Key::Char('q')
436 ) => { self.bar(); true }
437 ::termion::event::Event::Key(
438 ::termion::event::Key::Char('a')
439 ) => { self.baz(); true }
440 _ => false,
441 }
442 }
443 }
444 };
445
446 assert_eq!(format_item(expected_orig), format_item(orig_impl));
447 assert_eq!(
448 format_item::<ItemImpl>(expected_keymap),
449 format_item(keymap_impl)
450 );
451 }
452
453 #[test]
454 fn test_generated_impl_termwiz() {
455 let args = KeyMapAttrs {
456 backend: Backend::Termwiz,
457 };
458 let input = parse_quote! {
459 impl Foo {
460 #[keybind(pressed(key=KeyCode::Escape))]
462 #[keybind(pressed(key=KeyCode::Char('q')))]
463 fn bar(&mut self) {
464 todo!()
465 }
466
467 #[keybind(pressed(key=KeyCode::Char('a'), modifiers=Modifiers::CTRL, modifiers=Modifiers::SHIFT))]
469 fn baz(&mut self) {
470 todo!()
471 }
472 }
473 };
474 let (orig_impl, keymap_impl) = keymap_impl(args, input).unwrap();
475 let expected_orig = parse2::<ItemImpl>(quote! {
476 impl Foo {
477 fn bar(&mut self) {
479 todo!()
480 }
481
482 fn baz(&mut self) {
484 todo!()
485 }
486 }
487 })
488 .unwrap();
489 let expected_keymap = parse_quote! {
490 impl ::ratatui_input_manager::KeyMap::<::ratatui_input_manager::TermwizBackend> for Foo {
491 const KEYBINDS: &'static [::ratatui_input_manager::KeyBind::<::ratatui_input_manager::TermwizBackend>] = &[
492 ::ratatui_input_manager::KeyBind::<::ratatui_input_manager::TermwizBackend> {
493 pressed: &[
494 ::ratatui_input_manager::KeyPress::<::ratatui_input_manager::TermwizBackend> {
495 key: KeyCode::Escape,
496 modifiers: ::termwiz::input::Modifiers::NONE,
497 },
498 ::ratatui_input_manager::KeyPress::<::ratatui_input_manager::TermwizBackend> {
499 key: KeyCode::Char('q'),
500 modifiers: ::termwiz::input::Modifiers::NONE,
501 },
502 ],
503 description: "The first keybind",
504 },
505 ::ratatui_input_manager::KeyBind::<::ratatui_input_manager::TermwizBackend> {
506 pressed: &[
507 ::ratatui_input_manager::KeyPress::<::ratatui_input_manager::TermwizBackend> {
508 key: KeyCode::Char('a'),
509 modifiers: ::termwiz::input::Modifiers::NONE
510 .union(Modifiers::CTRL)
511 .union(Modifiers::SHIFT),
512 },
513 ],
514 description: "The second keybind",
515 }
516 ];
517
518 fn handle(&mut self, event: &<::ratatui_input_manager::TermwizBackend as ::ratatui_input_manager::Backend>::Event) -> bool {
519 match event {
520 ::termwiz::input::InputEvent::Key(
521 ::termwiz::input::KeyEvent {
522 key: KeyCode::Escape,
523 modifiers,
524 },
525 ) if modifiers.contains(::termwiz::input::Modifiers::NONE) => { self.bar(); true }
526 ::termwiz::input::InputEvent::Key(
527 ::termwiz::input::KeyEvent {
528 key: KeyCode::Char('q'),
529 modifiers,
530 },
531 ) if modifiers.contains(::termwiz::input::Modifiers::NONE) => { self.bar(); true }
532 ::termwiz::input::InputEvent::Key(
533 ::termwiz::input::KeyEvent {
534 key: KeyCode::Char('a'),
535 modifiers,
536 },
537 ) if modifiers
538 .contains(
539 ::termwiz::input::Modifiers::NONE
540 .union(Modifiers::CTRL)
541 .union(Modifiers::SHIFT),
542 ) => { self.baz(); true }
543 _ => false,
544 }
545 }
546 }
547 };
548
549 assert_eq!(format_item(expected_orig), format_item(orig_impl));
550 assert_eq!(
551 format_item::<ItemImpl>(expected_keymap),
552 format_item(keymap_impl)
553 );
554 }
555
556 #[test]
557 fn test_missing_doc_comment_error() {
558 let args = KeyMapAttrs {
559 backend: Backend::Crossterm,
560 };
561 let input = parse_quote! {
562 impl Foo {
563 #[keybind(pressed(key=KeyCode::Esc))]
564 fn bar(&mut self) {
565 todo!()
566 }
567 }
568 };
569 let result = keymap_impl(args, input);
570 assert!(result.is_err());
571 let err = result.unwrap_err();
572 assert_eq!(
573 err.to_string(),
574 "Keybind functions must have a doc comment for the description"
575 );
576 }
577}