diff --git a/Cargo.toml b/Cargo.toml index cefc039..3355dd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ documentation.workspace = true repository.workspace = true license.workspace = true edition.workspace = true +exclude = ["examples/*"] [lib] doctest = false diff --git a/README.md b/README.md index 235bbaa..cd4bfe9 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,23 @@ See `keymap-rs` in action with the [WASM example](https://rezigned.com/keymap-rs +

+ + + + + + + + + + + +
Runtime Reload (Rebind keys at runtime)
+ Runtime keymap reload demo +
+

+ --- ## 📦 Installation @@ -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), } _ => {} diff --git a/examples/README.md b/examples/README.md index d93428b..50d46ea 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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** @@ -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` from inline TOML configs. Press `r` to rotate between predefined configuration sets. + --- ## WebAssembly Example diff --git a/examples/action.rs b/examples/action.rs index 4be3481..1dbd152 100644 --- a/examples/action.rs +++ b/examples/action.rs @@ -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), } diff --git a/examples/backend/crossterm.rs b/examples/backend/crossterm.rs index 5d196f7..b386f6d 100644 --- a/examples/backend/crossterm.rs +++ b/examples/backend/crossterm.rs @@ -1,9 +1,12 @@ 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(mut f: F) -> io::Result<()> @@ -11,10 +14,21 @@ 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; } diff --git a/examples/backend/mod.rs b/examples/backend/mod.rs index 932f061..a69d350 100644 --- a/examples/backend/mod.rs +++ b/examples/backend/mod.rs @@ -1,3 +1,5 @@ +use std::io::Write; + use keymap::Item; #[cfg(feature = "crossterm")] @@ -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(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::>() + .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() {} diff --git a/examples/backend/termion.rs b/examples/backend/termion.rs index b041e87..ccc5337 100644 --- a/examples/backend/termion.rs +++ b/examples/backend/termion.rs @@ -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) diff --git a/examples/capturing.rs b/examples/capturing.rs index a82dc09..db9e437 100644 --- a/examples/capturing.rs +++ b/examples/capturing.rs @@ -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}; @@ -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 = toml::from_str(CONFIG).unwrap(); + print_config(&config.items); run(|key| match config.get_bound(&key) { Some(action) => match action { diff --git a/examples/config.rs b/examples/config.rs index c9014b1..066f2d6 100644 --- a/examples/config.rs +++ b/examples/config.rs @@ -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}; @@ -18,6 +18,7 @@ fn main() -> std::io::Result<()> { println!("# Example: External configuration with Config"); let config: Config = 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() diff --git a/examples/derive.rs b/examples/derive.rs index 1ee26f2..1dd0aad 100644 --- a/examples/derive.rs +++ b/examples/derive.rs @@ -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() diff --git a/examples/derived_config.rs b/examples/derived_config.rs index 38e9f3b..77e87d4 100644 --- a/examples/derived_config.rs +++ b/examples/derived_config.rs @@ -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}; @@ -19,6 +19,7 @@ fn main() -> std::io::Result<()> { println!("# Example: Merging derive macros with external config using DerivedConfig"); let config: DerivedConfig = 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() diff --git a/examples/modes.rs b/examples/modes.rs index 8aa5c0d..9005c95 100644 --- a/examples/modes.rs +++ b/examples/modes.rs @@ -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; @@ -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) { diff --git a/examples/reload.gif b/examples/reload.gif new file mode 100644 index 0000000..4b0afc9 Binary files /dev/null and b/examples/reload.gif differ diff --git a/examples/reload.rs b/examples/reload.rs new file mode 100644 index 0000000..94af50f --- /dev/null +++ b/examples/reload.rs @@ -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::>(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:?}")), + } + }) +} diff --git a/examples/reload.tape b/examples/reload.tape new file mode 100644 index 0000000..4fcc0f0 --- /dev/null +++ b/examples/reload.tape @@ -0,0 +1,29 @@ +Output examples/reload.gif + +Require + cargo + +Type "cargo run --example reload --features crossterm" +Enter +Sleep 1s + +Type "j" +Sleep 1s + +Type "k" +Sleep 1s + +# Press 'r' — rotate to CONFIG_B +Type "r" +Sleep 2s + +# From CONFIG_B +Type "w" +Sleep 1s + +Type "a" +Sleep 1s + +# Press 'q' — quit +Type "q" +Sleep 1s diff --git a/examples/sequences.rs b/examples/sequences.rs index 311d5e7..1a2ac56 100644 --- a/examples/sequences.rs +++ b/examples/sequences.rs @@ -6,22 +6,26 @@ mod action; use std::time::{Duration, Instant}; -use crate::backend::{print, quit, run, Key}; +use crate::backend::{print, print_config, quit, run, Key}; use action::Action; use keymap::{DerivedConfig, KeyMapConfig}; // Override default key mapping defined via #[derive(KeyMap)] in Action. pub(crate) const CONFIG: &str = r#" Jump = { keys = ["j j"], description = "Jump Jump!" } +Shoot = { keys = ["enter"] } "#; fn main() -> std::io::Result<()> { println!("# Example: Key Sequences (j j)"); + let config: DerivedConfig = toml::from_str(CONFIG).unwrap(); let mut last_key: Option = None; let mut last_time = Instant::now(); + print_config(&config.items); + run(move |key| { let ret = match config.get(&key) { Some(action) => match action { diff --git a/examples/simple.rs b/examples/simple.rs index e4c8446..ea2580c 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -35,6 +35,7 @@ esc = "Quit" fn main() -> std::io::Result<()> { let config: Config = toml::from_str(CONFIG).unwrap(); println!("# Example: Basic key mapping without derive macros"); + println!("\r↑ up | ↓ down | ← left | → right | ␣/^G jump | q/esc quit"); run(|key| { let Some((_, action)) = config.0.get_key_value(&key.to_keymap().unwrap()) else { diff --git a/examples/wasm/src/main.rs b/examples/wasm/src/main.rs index 2aa7b19..dcfec26 100644 --- a/examples/wasm/src/main.rs +++ b/examples/wasm/src/main.rs @@ -20,15 +20,15 @@ extern "C" { #[derive(Debug, Clone, keymap::KeyMap, Hash, PartialEq, Eq)] pub enum Action { /// Jump over obstacles - #[key("space", symbol = "↑", help = "jump")] // symbol gets overridden by toml config + #[key("space", symbol = "␣", help = "jump")] // symbol gets overridden by toml config Jump, /// Move leftward - #[key("left", help = "move left")] + #[key("left", symbol = "←", help = "move left")] Left, /// Move rightward - #[key("right", help = "move right")] + #[key("right", symbol = "→", help = "move right")] Right, /// Pause