Skip to content

Commit 29ae3cd

Browse files
authored
feat: support Key Group Capturing for dynamic variant data (#35)
- Added `KeyMapConfig::bind()` for mapping matched key groups to enum payloads. - Implemented `get_bound_*` lookup methods to return owned, captured variants. - Modified macro to identify key group indices for all macros (@any, @Digit, etc.). - Automated `Serialize` and `Deserialize` generation to prevent map-key collisions. - Refreshed project documentation and examples with the new terminology.
1 parent b0b604b commit 29ae3cd

17 files changed

Lines changed: 448 additions & 94 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
***Declarative Key Mappings**: Define keymaps via simple configuration files (e.g., TOML, YAML) or directly in your code using derive macros.
2828
* ⌨️ **Key Patterns**: Supports single keys (`a`), combinations (`ctrl-b`), and multi-key sequences (`ctrl-b n`).
2929
* 🧠 **Key Groups**: Use built-in pattern matching for common key groups (`@upper`, `@lower`, `@alpha`, `@alnum`, and `@any`).
30+
* 📸 **Key Group Capturing**: Capture specific keypress data (like the actual `char` from `@any` or `@digit`) directly into your action enum variants at runtime.
3031
* 🧬 **Compile-Time Safety**: The `keymap_derive` macro validates key syntax at compile time, preventing runtime errors.
3132
* 🌐 **Backend-Agnostic**: Works with multiple backends, including `crossterm`, `termion`, and `wasm`.
3233
* 🪶 **Lightweight & Extensible**: Designed to be minimal and easy to extend with new backends or features.
@@ -97,6 +98,11 @@ pub enum Action {
9798
/// Jump.
9899
#[key("space")]
99100
Jump,
101+
102+
/// Key Group Capturing action (e.g. tracking which character was pressed).
103+
/// `char` will be captured from any matched key group macro (like `@any` or `@digit`) at runtime.
104+
#[key("@any")]
105+
Shoot(char),
100106
}
101107
```
102108

@@ -109,6 +115,7 @@ The `KeyMap` derive macro generates an associated `keymap_config()` method, whic
109115
let config = Action::keymap_config();
110116

111117
// `key` is a key code from the input backend, e.g., `crossterm::event::KeyCode`
118+
// You can lookup the default pre-instantiated action reference:
112119
match config.get(&key) {
113120
Some(action) => match action {
114121
Action::Quit => break,
@@ -117,8 +124,15 @@ match config.get(&key) {
117124
}
118125
_ => {}
119126
}
127+
128+
// Or use Key Group Capturing to extract the actual `char` from `@any` or `@digit`!
129+
if let Some(Action::Shoot(c)) = config.get_bound(&key) {
130+
println!("Captured key: {c}");
131+
}
120132
```
121133

134+
> **Note**: `keymap_derive` automatically generates custom `Serialize` and `Deserialize` implementations for the derived `enum`, making your variants with captured data serialize as simple tags (e.g. `"Shoot"`) out of the box so that Map deserialization continues to work flawlessly.
135+
122136
### 2. Using External Configuration
123137

124138
`keymap-rs` also supports loading keymaps from external files (e.g., `config.toml`). This is useful for user-configurable keybindings.

examples/action.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
use serde::Deserialize;
2-
31
#[cfg(feature = "derive")]
4-
#[derive(Debug, keymap::KeyMap, Deserialize, Hash, PartialEq, Eq)]
2+
#[derive(Debug, keymap::KeyMap, Hash, PartialEq, Eq, Clone)]
53
pub(crate) enum Action {
64
/// Jump over obstacles
75
#[key("space", "@digit")]
@@ -26,6 +24,11 @@ pub(crate) enum Action {
2624
/// Exit or pause game
2725
#[key("q", "esc")]
2826
Quit,
27+
28+
/// Key Group Capturing action (e.g. tracking which character was pressed).
29+
/// `char` will be captured from any matched key group macro (like `@any` or `@digit`) at runtime.
30+
#[key("@any")]
31+
Shoot(char),
2932
}
3033

3134
#[allow(dead_code)]

examples/backend/mock.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ impl ToKeyMap for Key {
1313
}
1414

1515
#[allow(dead_code)]
16-
pub(crate) fn run<F>(mut f: F) -> io::Result<()>
16+
pub(crate) fn run<F>(_f: F) -> io::Result<()>
1717
where
1818
F: FnMut(Key) -> bool,
1919
{

examples/capturing.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#[path = "./backend/mod.rs"]
2+
mod backend;
3+
4+
#[path = "./action.rs"]
5+
mod action;
6+
7+
use crate::backend::{print, quit, run};
8+
use action::Action;
9+
use keymap::{DerivedConfig, KeyMapConfig};
10+
11+
// In this example, we showcase Key Group Capturing using .get_bound()
12+
// The Action::Shoot(char) variant is mapped to @any in action.rs.
13+
pub(crate) const CONFIG: &str = r#"
14+
Jump = { keys = ["j"], description = "Jump!" }
15+
"#;
16+
17+
fn main() -> std::io::Result<()> {
18+
println!("# Example: Key Group Capturing using .get_bound()");
19+
println!("- Press any key to see it captured by Action::Shoot(char)");
20+
println!("- Press 'j' to see Action::Jump (unit variant)");
21+
println!("- Press 'q' or 'esc' to quit");
22+
23+
let config: DerivedConfig<Action> = toml::from_str(CONFIG).unwrap();
24+
25+
run(|key| match config.get_bound(&key) {
26+
Some(action) => match action {
27+
Action::Quit => quit("quit!"),
28+
// This is matched via @any and the char is dynamically bound
29+
Action::Shoot(c) => print(&format!("Matched @any! Captured character: '{c}'")),
30+
// Standard unit variants work as before
31+
Action::Jump | Action::Up | Action::Down | Action::Left | Action::Right => {
32+
print(&format!(
33+
"Action: {action:?} (Description: {})",
34+
action.keymap_item().description
35+
))
36+
}
37+
},
38+
None => print(&format!("Unknown key {key:?}")),
39+
})
40+
}

examples/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@ fn main() -> std::io::Result<()> {
1919

2020
let config: Config<Action> = toml::from_str(CONFIG).unwrap();
2121

22+
// Use .get() for high-performance reference lookup of the "default" variant.
23+
// To capture the actual key pressed (e.g. the 'a' in @any), use .get_bound()
24+
// or see the `capturing` example.
2225
run(|key| match config.get(&key) {
2326
Some(action) => match action {
2427
Action::Quit => quit("quit!"),
28+
// Standard unit variants work as before
2529
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => print(
2630
&format!("{action:?} = {}", action.keymap_item().description),
2731
),
32+
Action::Shoot(_) => print("Shoot! (Use .get_bound() to capture the character)"),
2833
},
2934
None => print(&format!("Unknown key {key:?}")),
3035
})

examples/derive.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,24 @@ mod backend;
55
mod action;
66

77
use crate::backend::{print, quit, run};
8-
use keymap::KeyMapConfig;
98
use action::Action;
9+
use keymap::KeyMapConfig;
1010

1111
fn main() -> std::io::Result<()> {
1212
println!("# Example: Using the KeyMap derive macro");
1313
let config = Action::keymap_config();
1414

15+
// Use .get() for high-performance reference lookup of the "default" variant.
16+
// To capture the actual key pressed (e.g. the 'a' in @any), use .get_bound()
17+
// or see the `capturing` example.
1518
run(|key| match config.get(&key) {
1619
Some(action) => match action {
1720
Action::Quit => quit("quit!"),
18-
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => {
19-
print(&format!("{action:?}"))
20-
}
21+
Action::Shoot(_) => print("Shoot! (Use .get_bound() to capture the character)"),
22+
// Standard unit variants work as before
23+
Action::Jump | Action::Up | Action::Down | Action::Left | Action::Right => print(
24+
&format!("{action:?} = {}", action.keymap_item().description),
25+
),
2126
},
2227
None => print(&format!("Unknown key {key:?}")),
2328
})

examples/derived_config.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ fn main() -> std::io::Result<()> {
2020

2121
let config: DerivedConfig<Action> = toml::from_str(CONFIG).unwrap();
2222

23+
// Use .get() for high-performance reference lookup of the "default" variant.
24+
// To capture the actual key pressed (e.g. the 'a' in @any), use .get_bound()
25+
// or see the `capturing` example.
2326
run(|key| match config.get(&key) {
2427
Some(action) => match action {
2528
Action::Quit => quit("quit!"),
26-
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => {
27-
print(&format!("{action:?} = {}", action.keymap_item().description))
28-
}
29+
Action::Shoot(_) => print("Shoot! (Use .get_bound() to capture the character)"),
30+
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => print(
31+
&format!("{action:?} = {}", action.keymap_item().description),
32+
),
2933
},
3034
None => print(&format!("Unknown key {key:?}")),
3135
})

examples/modes.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ use crate::backend::{print, quit, run};
77
use keymap::DerivedConfig;
88
use serde::Deserialize;
99

10-
#[derive(keymap::KeyMap, Deserialize, Debug, Hash, Eq, PartialEq)]
10+
#[derive(keymap::KeyMap, Debug, Hash, Eq, PartialEq, Clone)]
1111
enum HomeAction {
1212
#[key("esc")]
1313
Quit,
1414
#[key("e")]
1515
Edit,
1616
}
1717

18-
#[derive(keymap::KeyMap, Deserialize, Debug, Hash, Eq, PartialEq)]
18+
#[derive(keymap::KeyMap, Debug, Hash, Eq, PartialEq, Clone)]
1919
enum EditAction {
2020
#[key("esc")]
2121
Exit,

examples/sequences.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::time::{Duration, Instant};
88

99
use crate::backend::{print, quit, run, Key};
1010
use action::Action;
11-
use keymap::DerivedConfig;
11+
use keymap::{DerivedConfig, KeyMapConfig};
1212

1313
// Override default key mapping defined via #[derive(KeyMap)] in Action.
1414
pub(crate) const CONFIG: &str = r#"
@@ -26,9 +26,10 @@ fn main() -> std::io::Result<()> {
2626
let ret = match config.get(&key) {
2727
Some(action) => match action {
2828
Action::Quit => quit("quit!"),
29-
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => {
30-
print(&format!("{action:?}"))
31-
}
29+
Action::Shoot(_) => print("Shoot!"),
30+
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => print(
31+
&format!("{action:?} = {}", action.keymap_item().description),
32+
),
3233
},
3334
None => {
3435
// Handle key sequence

examples/wasm/game.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,9 @@ class ObstacleManager {
279279

280280
const spawnX = lastObstacle
281281
? Math.max(
282-
canvas.width,
283-
lastObstacle.x + CONFIG.OBSTACLE.MIN_GAP + randomOffset,
284-
)
282+
canvas.width,
283+
lastObstacle.x + CONFIG.OBSTACLE.MIN_GAP + randomOffset,
284+
)
285285
: canvas.width + randomOffset;
286286

287287
const type =
@@ -412,8 +412,11 @@ class RainbowTrail {
412412

413413
draw() {
414414
this.particles.forEach((particle) => {
415-
ctx.fillStyle = Utils.hexToRgba(particle.color, particle.alpha);
415+
ctx.save();
416+
ctx.globalAlpha = particle.alpha;
417+
ctx.fillStyle = particle.color;
416418
ctx.fillRect(particle.x, particle.y, particle.width, particle.height);
419+
ctx.restore();
417420
});
418421
}
419422
}
@@ -693,3 +696,16 @@ export function pauseGame() {
693696
export function setKey(key, description) {
694697
game.setKey(key, description);
695698
}
699+
700+
export function setSkin(c) {
701+
// Handle char code or string character
702+
const char = typeof c === 'number' ? String.fromCharCode(c) : c;
703+
const digit = parseInt(char);
704+
if (isNaN(digit)) return;
705+
706+
// Change rainbow trail colors based on digit
707+
const baseHue = (digit * 36) % 360;
708+
game.rainbowTrail.colors = Array.from({ length: 6 }, (_, i) => {
709+
return `hsl(${(baseHue + i * 20) % 360}, 100%, 50%)`;
710+
});
711+
}

0 commit comments

Comments
 (0)