Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* ✅ **Declarative Key Mappings**: Define keymaps via simple configuration files (e.g., TOML, YAML) or directly in your code using derive macros.
* ⌨️ **Key Patterns**: Supports single keys (`a`), combinations (`ctrl-b`), and multi-key sequences (`ctrl-b n`).
* 🧠 **Key Groups**: Use built-in pattern matching for common key groups (`@upper`, `@lower`, `@alpha`, `@alnum`, and `@any`).
* 📸 **Key Group Capturing**: Capture specific keypress data (like the actual `char` from `@any` or `@digit`) directly into your action enum variants at runtime.
* 🧬 **Compile-Time Safety**: The `keymap_derive` macro validates key syntax at compile time, preventing runtime errors.
* 🌐 **Backend-Agnostic**: Works with multiple backends, including `crossterm`, `termion`, and `wasm`.
* 🪶 **Lightweight & Extensible**: Designed to be minimal and easy to extend with new backends or features.
Expand Down Expand Up @@ -97,6 +98,11 @@ pub enum Action {
/// Jump.
#[key("space")]
Jump,

/// Key Group Capturing action (e.g. tracking which character was pressed).
/// `char` will be captured from any matched key group macro (like `@any` or `@digit`) at runtime.
#[key("@any")]
Shoot(char),
}
```

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

// `key` is a key code from the input backend, e.g., `crossterm::event::KeyCode`
// You can lookup the default pre-instantiated action reference:
match config.get(&key) {
Some(action) => match action {
Action::Quit => break,
Expand All @@ -117,8 +124,15 @@ match config.get(&key) {
}
_ => {}
}

// Or use Key Group Capturing to extract the actual `char` from `@any` or `@digit`!
if let Some(Action::Shoot(c)) = config.get_bound(&key) {
println!("Captured key: {c}");
}
```

> **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.

### 2. Using External Configuration

`keymap-rs` also supports loading keymaps from external files (e.g., `config.toml`). This is useful for user-configurable keybindings.
Expand Down
9 changes: 6 additions & 3 deletions examples/action.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use serde::Deserialize;

#[cfg(feature = "derive")]
#[derive(Debug, keymap::KeyMap, Deserialize, Hash, PartialEq, Eq)]
#[derive(Debug, keymap::KeyMap, Hash, PartialEq, Eq, Clone)]
pub(crate) enum Action {
/// Jump over obstacles
#[key("space", "@digit")]
Expand All @@ -26,6 +24,11 @@ pub(crate) enum Action {
/// Exit or pause game
#[key("q", "esc")]
Quit,

/// Key Group Capturing action (e.g. tracking which character was pressed).
/// `char` will be captured from any matched key group macro (like `@any` or `@digit`) at runtime.
#[key("@any")]
Shoot(char),
}

#[allow(dead_code)]
Expand Down
2 changes: 1 addition & 1 deletion examples/backend/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ impl ToKeyMap for Key {
}

#[allow(dead_code)]
pub(crate) fn run<F>(mut f: F) -> io::Result<()>
pub(crate) fn run<F>(_f: F) -> io::Result<()>
where
F: FnMut(Key) -> bool,
{
Expand Down
40 changes: 40 additions & 0 deletions examples/capturing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#[path = "./backend/mod.rs"]
mod backend;

#[path = "./action.rs"]
mod action;

use crate::backend::{print, quit, run};
use action::Action;
use keymap::{DerivedConfig, KeyMapConfig};

// In this example, we showcase Key Group Capturing using .get_bound()
// The Action::Shoot(char) variant is mapped to @any in action.rs.
pub(crate) const CONFIG: &str = r#"
Jump = { keys = ["j"], description = "Jump!" }
"#;

fn main() -> std::io::Result<()> {
println!("# Example: Key Group Capturing using .get_bound()");
println!("- Press any key to see it captured by Action::Shoot(char)");
println!("- Press 'j' to see Action::Jump (unit variant)");
println!("- Press 'q' or 'esc' to quit");

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

run(|key| match config.get_bound(&key) {
Some(action) => match action {
Action::Quit => quit("quit!"),
// This is matched via @any and the char is dynamically bound
Action::Shoot(c) => print(&format!("Matched @any! Captured character: '{c}'")),
// Standard unit variants work as before
Action::Jump | Action::Up | Action::Down | Action::Left | Action::Right => {
print(&format!(
"Action: {action:?} (Description: {})",
action.keymap_item().description
))
}
},
None => print(&format!("Unknown key {key:?}")),
})
}
5 changes: 5 additions & 0 deletions examples/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ fn main() -> std::io::Result<()> {

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

// Use .get() for high-performance reference lookup of the "default" variant.
// To capture the actual key pressed (e.g. the 'a' in @any), use .get_bound()
// or see the `capturing` example.
run(|key| match config.get(&key) {
Some(action) => match action {
Action::Quit => quit("quit!"),
// Standard unit variants work as before
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => print(
&format!("{action:?} = {}", action.keymap_item().description),
),
Action::Shoot(_) => print("Shoot! (Use .get_bound() to capture the character)"),
},
None => print(&format!("Unknown key {key:?}")),
})
Expand Down
13 changes: 9 additions & 4 deletions examples/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@ mod backend;
mod action;

use crate::backend::{print, quit, run};
use keymap::KeyMapConfig;
use action::Action;
use keymap::KeyMapConfig;

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

// Use .get() for high-performance reference lookup of the "default" variant.
// To capture the actual key pressed (e.g. the 'a' in @any), use .get_bound()
// or see the `capturing` example.
run(|key| match config.get(&key) {
Some(action) => match action {
Action::Quit => quit("quit!"),
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => {
print(&format!("{action:?}"))
}
Action::Shoot(_) => print("Shoot! (Use .get_bound() to capture the character)"),
// Standard unit variants work as before
Action::Jump | Action::Up | Action::Down | Action::Left | Action::Right => print(
&format!("{action:?} = {}", action.keymap_item().description),
),
},
None => print(&format!("Unknown key {key:?}")),
})
Expand Down
10 changes: 7 additions & 3 deletions examples/derived_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ fn main() -> std::io::Result<()> {

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

// Use .get() for high-performance reference lookup of the "default" variant.
// To capture the actual key pressed (e.g. the 'a' in @any), use .get_bound()
// or see the `capturing` example.
run(|key| match config.get(&key) {
Some(action) => match action {
Action::Quit => quit("quit!"),
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => {
print(&format!("{action:?} = {}", action.keymap_item().description))
}
Action::Shoot(_) => print("Shoot! (Use .get_bound() to capture the character)"),
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => print(
&format!("{action:?} = {}", action.keymap_item().description),
),
},
None => print(&format!("Unknown key {key:?}")),
})
Expand Down
4 changes: 2 additions & 2 deletions examples/modes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ use crate::backend::{print, quit, run};
use keymap::DerivedConfig;
use serde::Deserialize;

#[derive(keymap::KeyMap, Deserialize, Debug, Hash, Eq, PartialEq)]
#[derive(keymap::KeyMap, Debug, Hash, Eq, PartialEq, Clone)]
enum HomeAction {
#[key("esc")]
Quit,
#[key("e")]
Edit,
}

#[derive(keymap::KeyMap, Deserialize, Debug, Hash, Eq, PartialEq)]
#[derive(keymap::KeyMap, Debug, Hash, Eq, PartialEq, Clone)]
enum EditAction {
#[key("esc")]
Exit,
Expand Down
9 changes: 5 additions & 4 deletions examples/sequences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::time::{Duration, Instant};

use crate::backend::{print, quit, run, Key};
use action::Action;
use keymap::DerivedConfig;
use keymap::{DerivedConfig, KeyMapConfig};

// Override default key mapping defined via #[derive(KeyMap)] in Action.
pub(crate) const CONFIG: &str = r#"
Expand All @@ -26,9 +26,10 @@ fn main() -> std::io::Result<()> {
let ret = match config.get(&key) {
Some(action) => match action {
Action::Quit => quit("quit!"),
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => {
print(&format!("{action:?}"))
}
Action::Shoot(_) => print("Shoot!"),
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => print(
&format!("{action:?} = {}", action.keymap_item().description),
),
},
None => {
// Handle key sequence
Expand Down
24 changes: 20 additions & 4 deletions examples/wasm/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,9 @@ class ObstacleManager {

const spawnX = lastObstacle
? Math.max(
canvas.width,
lastObstacle.x + CONFIG.OBSTACLE.MIN_GAP + randomOffset,
)
canvas.width,
lastObstacle.x + CONFIG.OBSTACLE.MIN_GAP + randomOffset,
)
: canvas.width + randomOffset;

const type =
Expand Down Expand Up @@ -412,8 +412,11 @@ class RainbowTrail {

draw() {
this.particles.forEach((particle) => {
ctx.fillStyle = Utils.hexToRgba(particle.color, particle.alpha);
ctx.save();
ctx.globalAlpha = particle.alpha;
ctx.fillStyle = particle.color;
ctx.fillRect(particle.x, particle.y, particle.width, particle.height);
ctx.restore();
});
}
}
Expand Down Expand Up @@ -693,3 +696,16 @@ export function pauseGame() {
export function setKey(key, description) {
game.setKey(key, description);
}

export function setSkin(c) {
// Handle char code or string character
const char = typeof c === 'number' ? String.fromCharCode(c) : c;
const digit = parseInt(char);
if (isNaN(digit)) return;

// Change rainbow trail colors based on digit
const baseHue = (digit * 36) % 360;
game.rainbowTrail.colors = Array.from({ length: 6 }, (_, i) => {
return `hsl(${(baseHue + i * 20) % 360}, 100%, 50%)`;
});
}
8 changes: 4 additions & 4 deletions examples/wasm/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ <h4>EXAMPLE: Derive Macro</h4>
<p>
Define an enum and automatically derive key bindings using the <code>#[derive(KeyMap)]</code> macro.
</p>
<pre><code class="language-rust">#[derive(keymap::KeyMap, Deserialize, Hash, PartialEq, Eq)]
<pre><code class="language-rust">#[derive(keymap::KeyMap, Hash, PartialEq, Eq, Clone)]
pub enum Action {
/// Jump over obstacles
#[key("space")]
Expand All @@ -52,9 +52,9 @@ <h4>EXAMPLE: Derive Macro</h4>
#[key("right")]
Right,

/// Pause
#[key("p")]
Pause,
/// Select a skin (Key Group Capturing!)
#[key("@digit")]
SelectSkin(char),

/// Restart
#[key("q", "esc")]
Expand Down
19 changes: 16 additions & 3 deletions examples/wasm/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use keymap::{DerivedConfig, ToKeyMap};
use serde::Deserialize;

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{window, KeyboardEvent};
Expand All @@ -13,9 +13,10 @@ extern "C" {
fn resetGame();
fn pauseGame();
fn setKey(key: String, desc: String);
fn setSkin(c: char);
}

#[derive(Debug, Clone, keymap::KeyMap, Deserialize, Hash, PartialEq, Eq)]
#[derive(Debug, Clone, keymap::KeyMap, Hash, PartialEq, Eq)]
pub enum Action {
/// Jump over obstacles
#[key("space")]
Expand All @@ -36,6 +37,10 @@ pub enum Action {
/// Restart
#[key("q", "esc")]
Quit,

/// Select a skin
#[key("@digit")]
SelectSkin(char),
}

/// Overrides the default keymap
Expand All @@ -45,6 +50,7 @@ Jump = { keys = ["space", "up"], description = "Jump Jump!" }
Quit = { keys = ["q", "esc"], description = "Quit!" }
Left = { keys = ["left", "alt-l"], description = "Move Left" }
Right = { keys = ["right", "alt-r"], description = "Move Right" }
SelectSkin = { keys = ["@digit"], description = "Select a skin" }
"#;

#[allow(unused)]
Expand Down Expand Up @@ -111,13 +117,20 @@ pub fn handle_key_event(event: &KeyboardEvent, is_keydown: bool) {
setKey(key.to_string(), desc);
}

if let Some(action) = config.get(event) {
// Use .get_bound() to support Key Group Capturing for SelectSkin
if let Some(action) = config.get_bound(event) {
match action {
Action::Quit => {
if is_keydown {
resetGame();
}
}
Action::SelectSkin(c) => {
if is_keydown {
setSkin(c);
setKey(format!("Skin selected: {c}"), "Key Group Capturing!".to_string());
}
}
_ if !is_game_over => match action {
Action::Left => moveLeft(is_keydown),
Action::Right => moveRight(is_keydown),
Expand Down
Loading