From e6774ca3d4f0455e45c45bd8ed9165a1ba252b84 Mon Sep 17 00:00:00 2001 From: Marut Khumtong Date: Tue, 14 Apr 2026 15:28:47 +0700 Subject: [PATCH 1/4] fix: #[key(ignore)] should not require Default trait Ignored variants no longer need the Default trait implemented, even when they have fields. Previously, the macro generated Default::default() for all variants in deserialization code, causing compile errors for ignored variants without Default. --- keymap_derive/src/lib.rs | 59 +++++++++++++++++------------------ keymap_derive/tests/derive.rs | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 30 deletions(-) diff --git a/keymap_derive/src/lib.rs b/keymap_derive/src/lib.rs index aaedab1..295b01b 100644 --- a/keymap_derive/src/lib.rs +++ b/keymap_derive/src/lib.rs @@ -143,21 +143,6 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre } }; - let variant_expr_default = match &item.variant.fields { - Fields::Unit => quote! { #name::#ident }, - Fields::Unnamed(fields) => { - let defaults = fields.unnamed.iter().map(|_| quote! { Default::default() }); - quote! { #name::#ident(#(#defaults),*) } - } - Fields::Named(fields) => { - let defaults = fields.named.iter().map(|f| { - let name = &f.ident; - quote! { #name: Default::default() } - }); - quote! { #name::#ident { #(#defaults),* } } - } - }; - let variant_pat = match &item.variant.fields { Fields::Unit => quote! { #name::#ident }, Fields::Unnamed(_) => quote! { #name::#ident(..) }, @@ -170,24 +155,36 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre #variant_pat => #variant_name_str, }); - match_arms_deserialize.push(quote! { - #variant_name_str => Ok(#variant_expr_default), - }); + if !item.ignore { + match_arms_bind.push(quote! { + #variant_pat => #variant_expr, + }); - match_arms_bind.push(quote! { - #variant_pat => #variant_expr, - }); + let variant_expr_default = match &item.variant.fields { + Fields::Unit => quote! { #name::#ident }, + Fields::Unnamed(fields) => { + let defaults = fields.unnamed.iter().map(|_| quote! { Default::default() }); + quote! { #name::#ident(#(#defaults),*) } + } + Fields::Named(fields) => { + let defaults = fields.named.iter().map(|f| { + let name = &f.ident; + quote! { #name: Default::default() } + }); + quote! { #name::#ident { #(#defaults),* } } + } + }; - // keymap_item - match_arms.push(quote! { - #variant_pat => ::keymap::Item::new( - vec![#(#keys),*], - #doc.to_string() - ), - }); + match_arms_deserialize.push(quote! { + #variant_name_str => Ok(#variant_expr_default), + }); + match_arms.push(quote! { + #variant_pat => ::keymap::Item::new( + vec![#(#keys),*], + #doc.to_string() + ), + }); - // keymap_config - if !item.ignore { entries.push(quote! { ( #variant_expr_default, @@ -250,6 +247,7 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre fn keymap_item(&self) -> ::keymap::Item { match self { #(#match_arms)* + _ => ::core::unreachable!("ignored variant has no keymap"), } } @@ -259,6 +257,7 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre { match self { #(#match_arms_bind)* + _ => ::core::unreachable!("ignored variant cannot be bound"), } } } diff --git a/keymap_derive/tests/derive.rs b/keymap_derive/tests/derive.rs index bb65236..46d650a 100644 --- a/keymap_derive/tests/derive.rs +++ b/keymap_derive/tests/derive.rs @@ -23,6 +23,28 @@ enum Action { Jump(char), } +#[derive(Debug, PartialEq, Eq, Clone)] +enum NoDefault { + A, + B, +} + +#[derive(Debug, PartialEq, Eq, keymap_derive::KeyMap, Clone)] +enum IgnoreTest { + /// Active variant + #[key("a")] + Active, + /// Ignored variant (should NOT appear in keymap_config) + #[key(ignore)] + Ignored, + /// Another ignored variant + #[key(ignore)] + AlsoIgnored, + /// Ignored with field (should NOT require Default) + #[key(ignore)] + IgnoredWithData(NoDefault), +} + #[cfg(test)] mod tests { use keymap_dev::{Error, Item, KeyMap, KeyMapConfig, ToKeyMap}; @@ -159,4 +181,39 @@ mod tests { let bound_action = config.get_bound_seq(&keys).unwrap(); assert_eq!(bound_action, Action::Create); } + + #[test] + fn test_key_ignore_not_in_config() { + let config = IgnoreTest::keymap_config(); + + assert_eq!(config.items.len(), 1); + assert_eq!( + config.items[0], + ( + IgnoreTest::Active, + Item::new( + ["a"].map(ToString::to_string).to_vec(), + "Active variant".to_string() + ) + ) + ); + } + + #[test] + fn test_ignored_variant_no_default_required() { + let config = IgnoreTest::keymap_config(); + + assert!(!config + .items + .iter() + .any(|(v, _)| matches!(v, IgnoreTest::Ignored))); + assert!(!config + .items + .iter() + .any(|(v, _)| matches!(v, IgnoreTest::AlsoIgnored))); + assert!(!config + .items + .iter() + .any(|(v, _)| matches!(v, IgnoreTest::IgnoredWithData(_)))); + } } From 000b2b8aacc9927b1b38223f652915e7854ad23f Mon Sep 17 00:00:00 2001 From: Marut Khumtong Date: Tue, 14 Apr 2026 15:31:55 +0700 Subject: [PATCH 2/4] fix: use correct keymap_parser path in derive macro Previously used ::keymap_parser which fails when keymap is not the root crate. Now uses keymap_parser (without ::) which resolves correctly. --- keymap_derive/src/lib.rs | 4 ++-- keymap_derive/tests/derive.rs | 9 ++------- src/lib.rs | 1 + 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/keymap_derive/src/lib.rs b/keymap_derive/src/lib.rs index 295b01b..2fb9621 100644 --- a/keymap_derive/src/lib.rs +++ b/keymap_derive/src/lib.rs @@ -99,7 +99,7 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre let mut char_idx: Option = None; if let Some(first_node_seq) = item.nodes.first() { for (idx, node) in first_node_seq.iter().enumerate() { - if let ::keymap_parser::node::Key::Group(_) = node.key { + if let keymap_parser::node::Key::Group(_) = node.key { char_idx = Some(idx); } } @@ -108,7 +108,7 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre let extract_char = if let Some(idx) = char_idx { quote! { match keys.get(#idx).map(|n| &n.key) { - Some(::keymap_parser::node::Key::Char(c)) => *c, + Some(::keymap::node::Key::Char(c)) => *c, _ => Default::default(), } } diff --git a/keymap_derive/tests/derive.rs b/keymap_derive/tests/derive.rs index 46d650a..db05a37 100644 --- a/keymap_derive/tests/derive.rs +++ b/keymap_derive/tests/derive.rs @@ -23,12 +23,14 @@ enum Action { Jump(char), } +#[allow(dead_code)] #[derive(Debug, PartialEq, Eq, Clone)] enum NoDefault { A, B, } +#[allow(dead_code)] #[derive(Debug, PartialEq, Eq, keymap_derive::KeyMap, Clone)] enum IgnoreTest { /// Active variant @@ -37,9 +39,6 @@ enum IgnoreTest { /// Ignored variant (should NOT appear in keymap_config) #[key(ignore)] Ignored, - /// Another ignored variant - #[key(ignore)] - AlsoIgnored, /// Ignored with field (should NOT require Default) #[key(ignore)] IgnoredWithData(NoDefault), @@ -207,10 +206,6 @@ mod tests { .items .iter() .any(|(v, _)| matches!(v, IgnoreTest::Ignored))); - assert!(!config - .items - .iter() - .any(|(v, _)| matches!(v, IgnoreTest::AlsoIgnored))); assert!(!config .items .iter() diff --git a/src/lib.rs b/src/lib.rs index 3d48793..afcd35c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ // Re-exports pub use config::{Config, DerivedConfig, Item, KeyMapConfig}; pub use keymap::{Error, FromKeyMap, IntoKeyMap, KeyMap, ToKeyMap}; +pub use keymap_parser::node; pub use keymap_parser::parser; pub use matcher::Matcher; From e5aff2041b4b9da82903e1258ca3abcae8985bbf Mon Sep 17 00:00:00 2001 From: Marut Khumtong Date: Thu, 16 Apr 2026 13:54:15 +0700 Subject: [PATCH 3/4] fix: make description field optional in Config --- src/config.rs | 49 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/config.rs b/src/config.rs index c0e01e7..0dce367 100644 --- a/src/config.rs +++ b/src/config.rs @@ -253,6 +253,7 @@ pub struct Item { pub keys: Vec, /// A short description for display or documentation purposes. + #[serde(default)] pub description: String, } @@ -565,13 +566,16 @@ where let mut config = T::keymap_config(); // Merge user-specified entries: replace or append - while let Some((t, item)) = map.next_entry::()? { + while let Some((t, mut item)) = map.next_entry::()? { if let Some(pos) = config .items .iter() .position(|(existing_key, _)| existing_key == &t) { // Override the default Item if the key matches + if item.description.is_empty() { + item.description = config.items[pos].1.description.clone(); + } config.items[pos].1 = item; } else { // Append a new entry @@ -608,17 +612,26 @@ mod tests { impl KeyMapConfig for Action { fn keymap_config() -> Config { Config::new(vec![ - (Action::Create, Item::new(vec!["n".into()], "".into())), - (Action::Update, Item::new(vec!["u".into()], "".into())), - (Action::Delete, Item::new(vec![], "".into())), + ( + Action::Create, + Item::new(vec!["c".into()], "Default Create".into()), + ), + ( + Action::Update, + Item::new(vec!["u".into()], "Default Update".into()), + ), + ( + Action::Delete, + Item::new(vec!["d".into()], "Default Delete".into()), + ), ]) } fn keymap_item(&self) -> Item { match self { - Action::Create => Item::new(vec!["n".into()], "".into()), - Action::Update => Item::new(vec!["u".into()], "".into()), - Action::Delete => Item::new(vec![], "".into()), + Action::Create => Item::new(vec!["c".into()], "Default Create".into()), + Action::Update => Item::new(vec!["u".into()], "Default Update".into()), + Action::Delete => Item::new(vec!["d".into()], "Default Delete".into()), } } } @@ -669,15 +682,31 @@ mod tests { let config: DerivedConfig = toml::from_str(CONFIG).unwrap(); // "c" was provided by user config - let (action, _) = config.get_item_by_key_str("c").unwrap(); + let (action, item) = config.get_item_by_key_str("c").unwrap(); assert_eq!(*action, Action::Create); + assert_eq!(item.description, "Create a new item"); // "u" falls back to default from KeyMapConfig - let (action, _) = config.get_item_by_key_str("u").unwrap(); + let (action, item) = config.get_item_by_key_str("u").unwrap(); assert_eq!(*action, Action::Update); + assert_eq!(item.description, "Default Update"); // "d" was provided by user config - let (action, _) = config.get_item_by_key_str("d").unwrap(); + let (action, item) = config.get_item_by_key_str("d").unwrap(); assert_eq!(*action, Action::Delete); + assert_eq!(item.description, "Delete an item"); + } + + #[test] + fn test_derive_config_merges_description_when_empty() { + let toml = r#" + Create = { keys = ["c"] } + "#; + + let config: DerivedConfig = toml::from_str(toml).unwrap(); + + let (action, item) = config.get_item_by_key_str("c").unwrap(); + assert_eq!(*action, Action::Create); + assert_eq!(item.description, "Default Create"); } } From 0015e26083ac690d5245ab0ef414741544168efd Mon Sep 17 00:00:00 2001 From: Marut Khumtong Date: Sat, 18 Apr 2026 16:11:46 +0700 Subject: [PATCH 4/4] feat(keymap_derive): allow unsigned int types for @digit key group binding Add support for u8, u16, u32, u64, and usize field types when using @digit key group. The character from key event is automatically converted to its numeric digit value and cast to the target type. --- examples/action.rs | 1 + keymap_derive/src/item.rs | 2 +- keymap_derive/src/lib.rs | 48 +++++++++------ keymap_derive/tests/derive.rs | 10 +-- keymap_derive/tests/derive_uint.rs | 98 ++++++++++++++++++++++++++++++ keymap_parser/src/parser.rs | 38 +++++++----- src/keymap.rs | 68 ++++++++++++++++++++- src/lib.rs | 5 +- 8 files changed, 228 insertions(+), 42 deletions(-) create mode 100644 keymap_derive/tests/derive_uint.rs diff --git a/examples/action.rs b/examples/action.rs index 8f9fc26..4be3481 100644 --- a/examples/action.rs +++ b/examples/action.rs @@ -1,4 +1,5 @@ #[cfg(feature = "derive")] +#[allow(dead_code)] #[derive(Debug, keymap::KeyMap, Hash, PartialEq, Eq, Clone)] pub(crate) enum Action { /// Jump over obstacles diff --git a/keymap_derive/src/item.rs b/keymap_derive/src/item.rs index 1ca9420..590ed9d 100644 --- a/keymap_derive/src/item.rs +++ b/keymap_derive/src/item.rs @@ -7,7 +7,7 @@ const DOC_IDENT: &str = "doc"; pub(crate) struct Item<'a> { pub variant: &'a Variant, - /// Raw string representations of the keys (e.g., ["ctrl-c", "@any"]). + /// Raw string representations of the keys (e.g., ["ctrl-c", "@any", "g g"]). pub keys: Vec, /// Fully parsed nodes for each key sequence. Used for inspecting /// key groups (like @any, @digit) during Key Group Capturing. diff --git a/keymap_derive/src/lib.rs b/keymap_derive/src/lib.rs index 2fb9621..913508f 100644 --- a/keymap_derive/src/lib.rs +++ b/keymap_derive/src/lib.rs @@ -93,9 +93,16 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre .collect::>(); let doc = &item.description; - // Find the index of a key group (like @any, @digit, etc.) in the parsed nodes. - // For simplicity, we only check the first key mapped to this variant. - // If the first key contains a group, the matched node at that index will be a `Char`. + // `char_idx` is the position of a key group node (e.g. `@any`, `@digit`) within + // the first key sequence of this variant. It is `None` when no group is present. + // + // Example: `#[key("@digit")]` → sequence is `[@digit]` → char_idx = Some(0) + // `#[key("d")]` → sequence is `[d]` → char_idx = None + // + // Only the first key sequence is inspected because all keys for a given variant + // must share the same group position (they map to the same field type). + // At runtime, `char_idx` tells `extract_via_trait` which node to pass to + // `KeyGroupValue::from_keymap_node` when binding the matched character/digit. let mut char_idx: Option = None; if let Some(first_node_seq) = item.nodes.first() { for (idx, node) in first_node_seq.iter().enumerate() { @@ -105,24 +112,29 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre } } - let extract_char = if let Some(idx) = char_idx { - quote! { - match keys.get(#idx).map(|n| &n.key) { - Some(::keymap::node::Key::Char(c)) => *c, - _ => Default::default(), + // Generates an expression for extracting a value at the key group index using the + // `KeyGroupValue` trait. This works for any type that implements the trait, including + // type aliases, because the trait bound is resolved at monomorphisation time rather + // than by inspecting the token string of the type. + let extract_via_trait = |ty: &syn::Type| -> proc_macro2::TokenStream { + if let Some(idx) = char_idx { + quote! { + match keys.get(#idx) { + Some(node) => <#ty as ::keymap::KeyGroupValue>::from_keymap_node(node), + None => Default::default(), + } } + } else { + quote! { Default::default() } } - } else { - quote! { Default::default() } }; let variant_expr = match &item.variant.fields { Fields::Unit => quote! { #name::#ident }, Fields::Unnamed(fields) => { let defaults = fields.unnamed.iter().map(|f| { - let ty_str = quote!(#f).to_string(); - if ty_str == "char" { - extract_char.clone() + if char_idx.is_some() { + extract_via_trait(&f.ty) } else { quote! { Default::default() } } @@ -131,12 +143,12 @@ fn impl_keymap_config(name: &Ident, items: &Vec) -> proc_macro2::TokenStre } Fields::Named(fields) => { let defaults = fields.named.iter().map(|f| { - let name = f.ident.as_ref().unwrap(); - let ty_str = quote!(#f).to_string(); - if ty_str.contains("char") { - quote! { #name: #extract_char } + let field_name = f.ident.as_ref().unwrap(); + if char_idx.is_some() { + let expr = extract_via_trait(&f.ty); + quote! { #field_name: #expr } } else { - quote! { #name: Default::default() } + quote! { #field_name: Default::default() } } }); quote! { #name::#ident { #(#defaults),* } } diff --git a/keymap_derive/tests/derive.rs b/keymap_derive/tests/derive.rs index db05a37..aad5445 100644 --- a/keymap_derive/tests/derive.rs +++ b/keymap_derive/tests/derive.rs @@ -68,9 +68,10 @@ mod tests { (Action::Delete, "d d"), (Action::Delete, "delete"), ] - .map(|(action, input)| { + .iter() + .for_each(|(action, input)| { let key = keymap_parser::parse_seq(input).unwrap(); - assert_eq!(&action, config.get_item_by_keymaps(&key).unwrap().0); + assert_eq!(action, config.get_item_by_keymaps(&key).unwrap().0); }); } @@ -82,9 +83,10 @@ mod tests { (Action::Delete, "x"), // @lower (Action::Digit('\0'), "1"), // @digit ] - .map(|(action, input)| { + .iter() + .for_each(|(action, input)| { let key = keymap_parser::parse_seq(input).unwrap(); - assert_eq!(&action, config.get_item_by_keymaps(&key).unwrap().0); + assert_eq!(action, config.get_item_by_keymaps(&key).unwrap().0); }); } diff --git a/keymap_derive/tests/derive_uint.rs b/keymap_derive/tests/derive_uint.rs new file mode 100644 index 0000000..ccbc462 --- /dev/null +++ b/keymap_derive/tests/derive_uint.rs @@ -0,0 +1,98 @@ +extern crate keymap_dev as keymap; + +#[derive(Debug, PartialEq, Eq, keymap_derive::KeyMap, Clone)] +enum Action { + #[key("enter")] + Create, + #[key("@digit")] + Digit(char), +} + +#[derive(Debug, PartialEq, Eq, keymap_derive::KeyMap, Clone)] +enum DigitAction { + #[key("@digit")] + DigitU8(u8), + #[key("@any")] + DigitAny(char), +} + +#[derive(Debug, PartialEq, Eq, keymap_derive::KeyMap, Clone)] +enum DigitAction2 { + #[key("@digit")] + DigitU16(u16), + #[key("a")] + Letter(char), +} + +// Type alias — the old string-based approach would fail here. +type MyDigit = u32; + +#[derive(Debug, PartialEq, Eq, keymap_derive::KeyMap, Clone)] +enum AliasAction { + #[key("@digit")] + Count(MyDigit), + #[key("@any")] + Any(char), +} + +#[cfg(test)] +mod tests { + use keymap_dev::{Error, KeyMap, KeyMapConfig, ToKeyMap}; + + use super::*; + + struct Wrapper(keymap_parser::Node); + impl ToKeyMap for Wrapper { + fn to_keymap(&self) -> Result { + Ok(self.0.clone()) + } + } + + #[test] + fn test_digit_char() { + let config = Action::keymap_config(); + let keys = keymap_parser::parse_seq("1") + .unwrap() + .into_iter() + .map(Wrapper) + .collect::>(); + let bound = config.get_bound_seq(&keys).unwrap(); + assert_eq!(bound, Action::Digit('1')); + } + + #[test] + fn test_digit_u8() { + let config = DigitAction::keymap_config(); + let keys = keymap_parser::parse_seq("5") + .unwrap() + .into_iter() + .map(Wrapper) + .collect::>(); + let bound = config.get_bound_seq(&keys).unwrap(); + assert_eq!(bound, DigitAction::DigitU8(5)); + } + + #[test] + fn test_digit_u16() { + let config = DigitAction2::keymap_config(); + let keys = keymap_parser::parse_seq("7") + .unwrap() + .into_iter() + .map(Wrapper) + .collect::>(); + let bound = config.get_bound_seq(&keys).unwrap(); + assert_eq!(bound, DigitAction2::DigitU16(7)); + } + + #[test] + fn test_digit_type_alias() { + let config = AliasAction::keymap_config(); + let keys = keymap_parser::parse_seq("3") + .unwrap() + .into_iter() + .map(Wrapper) + .collect::>(); + let bound = config.get_bound_seq(&keys).unwrap(); + assert_eq!(bound, AliasAction::Count(3)); + } +} diff --git a/keymap_parser/src/parser.rs b/keymap_parser/src/parser.rs index c806f33..c9267c4 100644 --- a/keymap_parser/src/parser.rs +++ b/keymap_parser/src/parser.rs @@ -358,9 +358,10 @@ mod tests { ("shift-a-delete", err("expect end of input, found: -", 7)), ("al", err("expect end of input, found: l", 1)), ] - .map(|(input, result)| { + .iter() + .for_each(|(input, result)| { let output = parse(input); - assert_eq!(result, output); + assert_eq!(result, &output); }); } @@ -373,8 +374,10 @@ mod tests { Ok(vec![parse("ctrl-b").unwrap(), parse("l").unwrap()]), ), ("ctrl-b -l", Err(parse("-l").unwrap_err())), // Invalid: dangling separator + ("b", Ok(vec![parse("b").unwrap()])), ] - .map(|(s, v)| assert_eq!(super::parse_seq(s), v)); + .iter() + .for_each(|(s, v)| assert_eq!(&super::parse_seq(s), v)); } #[test] @@ -387,7 +390,7 @@ mod tests { }); // Invalid: above f12 - [13, 15].map(|n| { + [13, 15].iter().for_each(|n| { let input = format!("f{n}"); let result = parse(&input); assert!(result.is_err()); @@ -397,10 +400,12 @@ mod tests { #[test] fn test_parse_enum() { // Check named keys - [("up", Key::Up), ("esc", Key::Esc), ("del", Key::Delete)].map(|(s, key)| { - let result = parse(s); - assert_eq!(result.unwrap().key, key); - }); + [("up", Key::Up), ("esc", Key::Esc), ("del", Key::Delete)] + .iter() + .for_each(|(s, key)| { + let result = parse(s); + assert_eq!(&result.unwrap().key, key); + }); } #[test] @@ -413,9 +418,10 @@ mod tests { ("@alnum", Key::Group(CharGroup::Alnum)), ("@any", Key::Group(CharGroup::Any)), ] - .map(|(input, expected_key)| { + .iter() + .for_each(|(input, expected_key)| { let result = parse(input); - assert_eq!(result.unwrap().key, expected_key); + assert_eq!(&result.unwrap().key, expected_key); }); // Test invalid group names @@ -455,8 +461,9 @@ mod tests { "cmd-shift-f", ), ] - .map(|(node, expected)| { - assert_eq!(expected, format!("{node}")); + .iter() + .for_each(|(node, expected)| { + assert_eq!(expected, &format!("{node}")); }); } @@ -489,9 +496,10 @@ delete = "d" Node::new(0, Key::Group(CharGroup::Digit)), Node::new(Modifier::Alt as u8, Key::Group(CharGroup::Lower)), ] - .map(|n| { - let (key, _) = result.keys.get_key_value(&n).unwrap(); - assert_eq!(key, &n); + .iter() + .for_each(|n| { + let (key, _) = result.keys.get_key_value(n).unwrap(); + assert_eq!(key, n); }); } diff --git a/src/keymap.rs b/src/keymap.rs index d2e6889..f514f17 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -8,7 +8,7 @@ //! The main goal is to decouple application logic from backend-specific input handling, enabling easier //! testing, configuration, and cross-platform support. -use keymap_parser::{parser::ParseError, Node}; +use keymap_parser::{node::Key, parser::ParseError, Node}; /// A type alias for a parsed keymap node tree. /// @@ -62,6 +62,72 @@ pub trait ToKeyMap { fn to_keymap(&self) -> Result; } +/// A trait for types that can be extracted from a matched key group node. +/// +/// When a variant field is bound via a key group (e.g. `@digit`, `@any`), the +/// derive macro calls `KeyGroupValue::from_keymap_node` on the matched [`KeyMap`] +/// node to produce the field value. This replaces the old string-based type +/// inspection, so type aliases (e.g. `type Bar = u32`) work transparently as +/// long as the underlying type implements this trait. +/// +/// # Built-in implementations +/// +/// | Type | Behaviour | +/// |---------|------------------------------------------------------------| +/// | `char` | Returns the matched character, or `'\0'` as the default. | +/// | `u8` | Parses the digit character as a decimal number. | +/// | `u16` | Same as `u8`, widened to `u16`. | +/// | `u32` | Same as `u8`, widened to `u32`. | +/// | `u64` | Same as `u8`, widened to `u64`. | +/// | `usize` | Same as `u8`, widened to `usize`. | +/// +/// # Example +/// +/// ```ignore +/// use keymap::KeyGroupValue; +/// +/// type MyDigit = u32; +/// +/// #[derive(keymap::KeyMap)] +/// enum Action { +/// #[key("@digit")] +/// Count(MyDigit), // works because u32 implements KeyGroupValue +/// } +/// ``` +pub trait KeyGroupValue: Default { + /// Extracts a value from the matched key node. + /// + /// Receives the [`KeyMap`] node that was matched by the key group pattern. + /// Returns `Self::default()` when the node does not carry a suitable value. + fn from_keymap_node(node: &KeyMap) -> Self; +} + +impl KeyGroupValue for char { + fn from_keymap_node(node: &KeyMap) -> Self { + match node.key { + Key::Char(c) => c, + _ => '\0', + } + } +} + +macro_rules! impl_key_group_value_uint { + ($($t:ty),+) => { + $( + impl KeyGroupValue for $t { + fn from_keymap_node(node: &KeyMap) -> Self { + match node.key { + Key::Char(c) => c.to_digit(10).unwrap_or(0) as $t, + _ => 0, + } + } + } + )+ + }; +} + +impl_key_group_value_uint!(u8, u16, u32, u64, usize); + /// Represents errors that can occur during keymap parsing or conversion. #[derive(Debug)] pub enum Error { diff --git a/src/lib.rs b/src/lib.rs index afcd35c..6fd1f5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,8 @@ // Re-exports pub use config::{Config, DerivedConfig, Item, KeyMapConfig}; -pub use keymap::{Error, FromKeyMap, IntoKeyMap, KeyMap, ToKeyMap}; -pub use keymap_parser::node; -pub use keymap_parser::parser; +pub use keymap::{Error, FromKeyMap, IntoKeyMap, KeyGroupValue, KeyMap, ToKeyMap}; +pub use keymap_parser::{node, parser}; pub use matcher::Matcher; #[cfg(feature = "derive")]