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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ documentation.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
exclude = ["examples/*"]

[lib]
doctest = false
Expand Down
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ See `keymap-rs` in action with the [WASM example](https://rezigned.com/keymap-rs
</tbody>
</table>

<p align="center">
<table align="center">
<thead>
<tr>
<th width="500px">Runtime Reload (Rebind keys at runtime)</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">
<img src="./examples/reload.gif" alt="Runtime keymap reload demo"/>
</td>
</tr>
</tbody>
</table>
</p>

---

## 📦 Installation
Expand Down Expand Up @@ -120,8 +137,11 @@ let config = Action::keymap_config();
match config.get(&key) {
Some(action) => match action {
Action::Quit => break,
Action::Jump => println!("Jump! Symbol: {:?}, Help: {:?}",
action.keymap_item().symbol, action.keymap_item().help),
Action::Jump => println!(
"Jump! Symbol: {:?}, Help: {:?}",
action.keymap_item().symbol,
action.keymap_item().help)
),
_ => println!("Action: {action:?} - {}", action.keymap_item().description),
}
_ => {}
Expand Down
9 changes: 9 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Shows how to load key mappings exclusively from external configuration files, ig

Explores combining derive macro defaults with external configuration overrides, covering configuration precedence and key group patterns like `@digit`.

### [`capturing.rs`](./capturing.rs)
**Key group capturing using `.get_bound()`**

Demonstrates dynamic key capture with key group patterns like `@any`, `@digit`, `@alpha`, etc. The `Shoot(char)` variant captures the actual pressed character at runtime via `.get_bound()`.

### [`modes.rs`](./modes.rs)
**Multi-mode application with different key mappings**
Expand All @@ -43,6 +47,11 @@ Illustrates building applications with multiple modes (like `vim`), where differ

Explains how to handle multi-key sequences (like `j j` for double-tap actions), including sequence detection, timing-based handling, and sequence timeout management.

### [`reload.rs`](./reload.rs)
**Runtime keymap reload**

Demonstrates swapping key bindings at runtime by re-deserializing a `DerivedConfig<T>` from inline TOML configs. Press `r` to rotate between predefined configuration sets.

---

## WebAssembly Example
Expand Down
15 changes: 7 additions & 8 deletions examples/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,31 @@
#[derive(Debug, keymap::KeyMap, Hash, PartialEq, Eq, Clone)]
pub(crate) enum Action {
/// Jump over obstacles
#[key("space", "@digit")]
#[key("space", "@digit", symbol = "␣", help = "jump")]
Jump,

/// Climb or move up
#[key("up")]
#[key("up", symbol = "↑", help = "move up")]
Up,

/// Drop or crouch down
#[key("down")]
#[key("down", symbol = "↓", help = "move down")]
Down,

/// Move leftward
#[key("left")]
#[key("left", symbol = "←", help = "move left")]
Left,

/// Move rightward
#[key("right")]
#[key("right", symbol = "→", help = "move right")]
Right,

/// Exit or pause game
#[key("q", "esc")]
#[key("q", "esc", symbol = "esc", help = "quit")]
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")]
#[key("@any", help = "shoot")]
Shoot(char),
}

Expand Down
16 changes: 15 additions & 1 deletion examples/backend/crossterm.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
use std::io;

use crossterm::{
cursor,
event::{read, Event, KeyEvent},
terminal::{disable_raw_mode, enable_raw_mode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
};
use std::io::{stdout, Write};

#[allow(dead_code)]
pub(crate) fn run<F>(mut f: F) -> io::Result<()>
where
F: FnMut(KeyEvent) -> bool,
{
enable_raw_mode()?;
stdout().flush()?;

let (_, row) = cursor::position()?;

loop {
if let Event::Key(key) = read()? {
execute!(
stdout(),
cursor::MoveTo(0, row),
Clear(ClearType::FromCursorDown),
)?;

let quit = f(key);
stdout().flush()?;

if quit {
break;
}
Expand Down
35 changes: 25 additions & 10 deletions examples/backend/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::io::Write;

use keymap::Item;

#[cfg(feature = "crossterm")]
Expand Down Expand Up @@ -32,28 +34,41 @@ pub(crate) use mock::{run, Key};

#[allow(dead_code)]
pub(crate) fn print(s: &str) -> bool {
println!("{s}\r");
print!("\r{s}");
std::io::stdout().flush().ok();
false
}

#[allow(dead_code)]
pub(crate) fn quit(s: &str) -> bool {
println!("{s}\r");
std::io::stdout().flush().ok();
true
}

// ANSI colors
const RESET: &str = "\x1b[0m";
const COLOR_DIM: &str = "\x1b[38;2;68;72;85m";
const COLOR_KEY: &str = "\x1b[38;2;148;226;213m";
const COLOR_TEXT: &str = "\x1b[38;2;166;173;200m";

#[allow(dead_code)]
pub(crate) fn print_config<T: std::fmt::Debug>(items: &[(T, Item)]) {
println!("--- keymap ---");
let keys = items
.iter()
.map(|(_, v)| {
format!(
"{COLOR_KEY}{}{RESET} {COLOR_TEXT}{}{RESET}",
v.symbol.clone().unwrap_or_default(),
v.help.clone().unwrap_or(v.description.clone())
)
})
.collect::<Vec<_>>()
.join(&format!(" {COLOR_DIM}|{RESET} "));

items.iter().for_each(|(action, v)| {
println!(
"{action:?} = keys: {:?}, description: {}",
v.keys, v.description
)
});

println!("--------------");
println!("\r{keys}");
std::io::stdout().flush().ok();
}

#[allow(unused)]
fn main() {}
14 changes: 12 additions & 2 deletions examples/backend/termion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,23 @@ where
let stdin = stdin();
let mut stdout = stdout().into_raw_mode()?;

write!(stdout, "{}", termion::cursor::Save)?;
stdout.flush()?;

for key in stdin.keys() {
write!(
stdout,
"{}{}",
termion::cursor::Restore,
termion::clear::AfterCursor,
)?;

let quit = f(key.unwrap());
stdout.flush()?;

if quit {
break;
}

stdout.flush().unwrap();
}

write!(stdout, "{}", termion::cursor::Show)
Expand Down
6 changes: 2 additions & 4 deletions examples/capturing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod backend;
#[path = "./action.rs"]
mod action;

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

Expand All @@ -16,11 +16,9 @@ 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();
print_config(&config.items);

run(|key| match config.get_bound(&key) {
Some(action) => match action {
Expand Down
3 changes: 2 additions & 1 deletion examples/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod backend;
#[path = "./action.rs"]
mod action;

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

Expand All @@ -18,6 +18,7 @@ fn main() -> std::io::Result<()> {
println!("# Example: External configuration with Config<T>");

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

// 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()
Expand Down
3 changes: 2 additions & 1 deletion examples/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ mod backend;
#[path = "./action.rs"]
mod action;

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

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

// 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()
Expand Down
3 changes: 2 additions & 1 deletion examples/derived_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod backend;
#[path = "./action.rs"]
mod action;

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

Expand All @@ -19,6 +19,7 @@ fn main() -> std::io::Result<()> {
println!("# Example: Merging derive macros with external config using DerivedConfig<T>");

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

// 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()
Expand Down
8 changes: 6 additions & 2 deletions examples/modes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::HashMap;
#[path = "./backend/mod.rs"]
mod backend;

use crate::backend::{print, quit, run};
use crate::backend::{print, print_config, quit, run};
use keymap::DerivedConfig;
use serde::Deserialize;

Expand Down Expand Up @@ -45,7 +45,11 @@ fn main() -> std::io::Result<()> {
let mut mode = "home";

println!("# Example: Multi-mode application with different key mappings");
println!("mode: {mode}\r");

if let Some(Actions::Home(config)) = modes.get("home") {
print_config(&config.items);
}
println!("\rmode: {mode}");

run(move |key| match modes.get(mode).unwrap() {
Actions::Home(config) => match config.get(&key) {
Expand Down
Binary file added examples/reload.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions examples/reload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#[path = "./backend/mod.rs"]
mod backend;

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

use std::cell::RefCell;
use std::io::Write;

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

// Multiple inline configs to switch between at runtime.
// Press 'r' to reload the keymap — the active config rotates on each reload.
const CONFIG_A: &str = r#"
Jump = { keys = ["j"] }
Up = { keys = ["k"] }
"#;

const CONFIG_B: &str = r#"
Jump = { keys = ["w"] }
Down = { keys = ["s"] }
Left = { keys = ["a"] }
Right = { keys = ["d"] }
"#;

fn main() -> std::io::Result<()> {
println!("# Example: Runtime keymap reload");
println!("\rPress 'r' to rotate between configs at runtime");

let configs = [CONFIG_A, CONFIG_B];
let config = RefCell::new(toml::from_str::<DerivedConfig<Action>>(configs[0]).unwrap());
let active = RefCell::new(0usize);

print_config(&config.borrow().items);

let reload_key = KeyMap::from(keymap::node::Key::Char('r'));

run(move |key| {
// Press 'r' to reload keymap from the next inline config
if let Ok(k) = key.to_keymap() {
if k == reload_key {
let next = (*active.borrow() + 1) % configs.len();
*config.borrow_mut() = toml::from_str(configs[next]).unwrap();
*active.borrow_mut() = next;

// Replace the stale header shortcuts in-place
print!("\r\x1b[1A\x1b[J");
std::io::stdout().flush().ok();
print_config(&config.borrow().items);
print(&format!("** switched to config {next} **"));
return false;
}
}

let config = config.borrow();
match config.get(&key) {
Some(action) => match action {
Action::Quit => quit("quit!"),
Action::Shoot(_) => print("Shoot!"),
Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => print(
&format!("{action:?} = {}", action.keymap_item().description),
),
},
None => print(&format!("{key:?}")),
}
})
}
Loading
Loading