diff --git a/Cargo.toml b/Cargo.toml index 08ae8c7..d9015a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,34 +47,31 @@ toml = "0.8" wasm-bindgen-test = "0.3" [[example]] -name = "crossterm" -required-features = ["crossterm"] +name = "simple" +path = "examples/simple.rs" +required-features = ["derive"] [[example]] -name = "crossterm_derive" -path = "examples/crossterm/derive.rs" -required-features = ["crossterm", "derive"] +name = "derive" +path = "examples/derive.rs" +required-features = ["derive"] [[example]] -name = "crossterm_derived_config" -path = "examples/crossterm/derived_config.rs" -required-features = ["crossterm", "derive"] +name = "config" +path = "examples/config.rs" +required-features = ["derive"] [[example]] -name = "termion" -required-features = ["termion"] - -[[example]] -name = "termion_derive" -path = "examples/termion/derive.rs" -required-features = ["termion", "derive"] - -[[example]] -name = "termion_derived_config" -path = "examples/termion/derived_config.rs" -required-features = ["termion", "derive"] +name = "derived_config" +path = "examples/derived_config.rs" +required-features = ["derive"] [[example]] name = "modes" path = "examples/modes.rs" -required-features = ["crossterm", "derive"] +required-features = ["derive"] + +[[example]] +name = "sequences" +path = "examples/sequences.rs" +required-features = ["derive"] diff --git a/README.md b/README.md index 8b39392..278a25b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,21 @@ **keymap-rs** is a lightweight and extensible key mapping library for Rust that simplifies input processing for terminal user interfaces (TUIs), WebAssembly (WASM) applications, and more. It parses keymaps from derive macros or configuration files and maps them to actions from various input backends, including [`crossterm`](https://crates.io/crates/crossterm), [`termion`](https://docs.rs/termion/latest/termion/), and [`wasm`](https://webassembly.org/). +## 📋 Table of Contents + +- [Features](#-features) +- [Demo](#️-demo) +- [Installation](#-installation) +- [Usage](#-usage) + - [1. Deriving `KeyMap`](#1-deriving-keymap) + - [2. Using External Configuration](#2-using-external-configuration) + - [3. Compile-Time Validation](#3-compile-time-validation) + - [4. Direct Key Parsing](#4-direct-key-parsing) +- [Key Syntax Reference](#-key-syntax-reference) +- [Examples](#-examples) +- [License](#-license) +- [Contributions](#-contributions) + --- ## 🔧 Features @@ -47,7 +62,7 @@ cargo add keymap --feature {crossterm | termion | wasm} ## 🚀 Usage -### 1. Deriving Keymaps +### 1. Deriving `KeyMap` The easiest way to get started is with the `keymap::KeyMap` derive macro. @@ -79,7 +94,7 @@ pub enum Action { **Use the generated keymap:** -The `KeyMap` derive macro generates an associated `keymap_config()` method that returns a `Config`. +The `KeyMap` derive macro generates an associated `keymap_config()` method, which returns a `Config`. ```rust // Retrieve the config @@ -98,7 +113,7 @@ match config.get(&key) { ### 2. Using External Configuration -Keymaps can also be loaded from external files (e.g., `config.toml`). This is useful for user-configurable keybindings. +`keymap-rs` also supports loading keymaps from external files (e.g., `config.toml`). This is useful for user-configurable keybindings. **Example `config.toml`:** @@ -116,9 +131,11 @@ This deserializes **only** the keybindings from the configuration file, ignoring ```rust // This config will only contain 'Jump' and 'Quit' from the TOML file. -let config: Config = toml::from_str(config_str).unwrap(); +let config: Config = toml::from_str(&data)?; ``` +**Resulting keybindings:** + | Key | Action | | ------------- | ------ | | `"j"`, `"up"` | Jump | @@ -126,20 +143,23 @@ let config: Config = toml::from_str(config_str).unwrap(); #### `DerivedConfig`: Merge Derived and File Configs -This **merges** the keybindings from the `#[key("...")]` attributes with the ones from the configuration file. Keys from the external file will override any conflicting keys defined in the enum. +This **merges** keybindings from the `#[key("...")]` attributes with those from the configuration file. Keys from the external file will override any conflicting keys defined in the enum. ```rust // This config contains keys from both the derive macro and the TOML file. -let config: DerivedConfig = toml::from_str(config_str).unwrap(); +let config: DerivedConfig = toml::from_str(&data)?; ``` -| Key | Action | -| ------------------------ | ------ | -| `"j"`, `"up"` | Jump | -| `"h"`, `"left"` | Left | -| `"l"`, `"right"` | Right | -| `@any` | Quit | -| *`"q"`, `"esc"`, `"space"` are ignored* | +**Resulting keybindings:** + +| Key | Action | Source | +| ------------------------ | ------ | ------ | +| `"j"`, `"up"` | Jump | Config file (overrides `"space"`) | +| `"h"`, `"left"` | Left | Derive macro | +| `"l"`, `"right"` | Right | Derive macro | +| `@any` | Quit | Config file (overrides `"q"`, `"esc"`) | + +> **Note**: When using `DerivedConfig`, keys from the config file take precedence over derive macro keys for the same action. ### 3. Compile-Time Validation @@ -191,12 +211,38 @@ assert_eq!( --- +## 📝 Key Syntax Reference + +| Type | Description | Example | +|---|---|---| +| **Single Keys** | Individual characters, special keys, arrow keys, and function keys. | `a`, `enter`, `up`, `f1` | +| **Key Combinations** | Keys pressed simultaneously with modifiers (Ctrl, Alt, Shift). | `ctrl-c`, `alt-f4`, `ctrl-alt-shift-f1` | +| **Key Sequences** | Multiple keys pressed in order. | `g g` (press `g` twice), `ctrl-b n` (Ctrl+B, then N), `ctrl-b c` (tmux-style new window) | +| **Key Groups** | Predefined patterns matching sets of keys. | `@upper` (A-Z), `@alpha` (A-Z, a-z), `@any` (any key) | + +**Examples in Configuration:** +```toml +# Single keys +Quit = { keys = ["q", "esc"] } + +# Key combinations +Save = { keys = ["ctrl-s"] } +ForceQuit = { keys = ["ctrl-alt-f4"] } + +# Key sequences +ShowGitStatus = { keys = ["g s"] } +NewTmuxWindow = { keys = ["ctrl-b c"] } + +# Key groups +AnyLetter = { keys = ["@alpha"] } +AnyKey = { keys = ["@any"] } +``` + +--- + ## 📖 Examples -For complete, runnable examples, check out the [`/examples`](https://github.com/rezigned/keymap-rs/tree/main/examples) directory, which includes demos for: -- `crossterm` -- `termion` -- `wasm` +For complete, runnable examples, check out the [`/examples`](https://github.com/rezigned/keymap-rs/tree/main/examples) directory. --- @@ -209,4 +255,3 @@ This project is licensed under the [MIT License](https://github.com/rezigned/key ## 🙌 Contributions Contributions, issues, and feature requests are welcome! Feel free to open an issue or submit a pull request. - diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..d93428b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,73 @@ +# Examples + +This directory contains examples demonstrating various features and use cases of the `keymap-rs` library. Each example showcases different aspects of key mapping, from basic usage to advanced configurations. + +> [!NOTE] +> +> All examples below work with any backend by simply passing the backend feature (e.g., `crossterm`, `termion`, `wasm`) to the `--features` argument. +> +> ``` +> cargo run --example {example} --features {backend} +> ``` + +### [`simple.rs`](./simple.rs) +**Basic key mapping without derive macros** + +Illustrates how to use the library without the `#[derive(KeyMap)]` macro, including manual TOML configuration parsing and basic action handling. + + +### [`derive.rs`](./derive.rs) +**Using the KeyMap derive macro** + +Presents the most common and recommended approach using the `#[derive(KeyMap)]` macro, showcasing automatic keymap generation from enum attributes and clean, declarative key mapping. + + +### [`config.rs`](./config.rs) +**External configuration with Config** + +Shows how to load key mappings exclusively from external configuration files, ignoring derive macro definitions, and highlights file-based key overrides and custom key descriptions. + +### [`derived_config.rs`](./derived_config.rs) +**Merging derive macros with external config using DerivedConfig** + +Explores combining derive macro defaults with external configuration overrides, covering configuration precedence and key group patterns like `@digit`. + + +### [`modes.rs`](./modes.rs) +**Multi-mode application with different key mappings** + +Illustrates building applications with multiple modes (like `vim`), where different key mappings are active depending on the current mode, including mode-based key mapping switching and dynamic mode transitions. + +### [`sequences.rs`](./sequences.rs) +**Key sequences and timing** + +Explains how to handle multi-key sequences (like `j j` for double-tap actions), including sequence detection, timing-based handling, and sequence timeout management. + +--- + +## WebAssembly Example + +### [`wasm/`](./wasm/) +**Complete WebAssembly game implementation** + +A fully functional browser-based game demonstrating keymap-rs in WebAssembly. + +**Try it live:** [https://rezigned.com/keymap-rs/](https://rezigned.com/keymap-rs/) + + +The WASM example requires additional setup: + +```bash +cd examples/wasm + +# Install trunk for WASM building +cargo install trunk + +# Build and serve the WASM example +trunk serve + +# Or build for production +trunk build --release +``` + +Then open your browser to `http://localhost:8080` to play the game. diff --git a/examples/action.rs b/examples/action.rs new file mode 100644 index 0000000..8ba7ca0 --- /dev/null +++ b/examples/action.rs @@ -0,0 +1,32 @@ +use serde::Deserialize; + +#[cfg(feature = "derive")] +#[derive(Debug, keymap::KeyMap, Deserialize, Hash, PartialEq, Eq)] +pub(crate) enum Action { + /// Jump over obstacles + #[key("space", "@digit")] + Jump, + + /// Climb or move up + #[key("up")] + Up, + + /// Drop or crouch down + #[key("down")] + Down, + + /// Move leftward + #[key("left")] + Left, + + /// Move rightward + #[key("right")] + Right, + + /// Exit or pause game + #[key("q", "esc")] + Quit, +} + +#[allow(dead_code)] +fn main() {} diff --git a/examples/backend/crossterm.rs b/examples/backend/crossterm.rs new file mode 100644 index 0000000..5d196f7 --- /dev/null +++ b/examples/backend/crossterm.rs @@ -0,0 +1,28 @@ +use std::io; + +use crossterm::{ + event::{read, Event, KeyEvent}, + terminal::{disable_raw_mode, enable_raw_mode}, +}; + +#[allow(dead_code)] +pub(crate) fn run(mut f: F) -> io::Result<()> +where + F: FnMut(KeyEvent) -> bool, +{ + enable_raw_mode()?; + + loop { + if let Event::Key(key) = read()? { + let quit = f(key); + if quit { + break; + } + } + } + + disable_raw_mode() +} + +#[allow(unused)] +fn main() {} diff --git a/examples/backend/mock.rs b/examples/backend/mock.rs new file mode 100644 index 0000000..5447c94 --- /dev/null +++ b/examples/backend/mock.rs @@ -0,0 +1,25 @@ +use std::io; + +use keymap::ToKeyMap; +use serde::Deserialize; + +#[derive(Clone, Copy, Debug, Deserialize)] +pub(crate) enum Key {} + +impl ToKeyMap for Key { + fn to_keymap(&self) -> Result { + todo!() + } +} + +#[allow(dead_code)] +pub(crate) fn run(mut f: F) -> io::Result<()> +where + F: FnMut(Key) -> bool, +{ + // no-op + Ok(()) +} + +#[allow(unused)] +fn main() {} diff --git a/examples/backend/mod.rs b/examples/backend/mod.rs new file mode 100644 index 0000000..932f061 --- /dev/null +++ b/examples/backend/mod.rs @@ -0,0 +1,59 @@ +use keymap::Item; + +#[cfg(feature = "crossterm")] +#[path = "./crossterm.rs"] +mod crossterm; + +#[cfg(feature = "crossterm")] +pub(crate) use crossterm::run; + +#[cfg(feature = "crossterm")] +#[allow(unused)] +pub(crate) use ::crossterm::event::KeyEvent as Key; + +#[cfg(feature = "termion")] +#[path = "./termion.rs"] +mod termion; + +#[cfg(feature = "termion")] +pub(crate) use termion::run; + +#[cfg(feature = "termion")] +#[allow(unused)] +pub(crate) use ::termion::event::Key; + +#[cfg(not(any(feature = "crossterm", feature = "termion", feature = "wasm")))] +#[path = "./mock.rs"] +mod mock; + +#[allow(unused)] +#[cfg(not(any(feature = "crossterm", feature = "termion", feature = "wasm")))] +pub(crate) use mock::{run, Key}; + +#[allow(dead_code)] +pub(crate) fn print(s: &str) -> bool { + println!("{s}\r"); + false +} + +#[allow(dead_code)] +pub(crate) fn quit(s: &str) -> bool { + println!("{s}\r"); + true +} + +#[allow(dead_code)] +pub(crate) fn print_config(items: &[(T, Item)]) { + println!("--- keymap ---"); + + items.iter().for_each(|(action, v)| { + println!( + "{action:?} = keys: {:?}, description: {}", + v.keys, v.description + ) + }); + + println!("--------------"); +} +#[allow(unused)] +fn main() {} diff --git a/examples/backend/termion.rs b/examples/backend/termion.rs new file mode 100644 index 0000000..b041e87 --- /dev/null +++ b/examples/backend/termion.rs @@ -0,0 +1,28 @@ +use std::io::{stdin, stdout, Result, Write}; + +use termion::event::Key; +use termion::input::TermRead; +use termion::raw::IntoRawMode; + +#[allow(dead_code)] +pub(crate) fn run(mut f: F) -> Result<()> +where + F: FnMut(Key) -> bool, +{ + let stdin = stdin(); + let mut stdout = stdout().into_raw_mode()?; + + for key in stdin.keys() { + let quit = f(key.unwrap()); + if quit { + break; + } + + stdout.flush().unwrap(); + } + + write!(stdout, "{}", termion::cursor::Show) +} + +#[allow(unused)] +fn main() {} diff --git a/examples/config.rs b/examples/config.rs index 3b9d7b9..ca158f7 100644 --- a/examples/config.rs +++ b/examples/config.rs @@ -1,37 +1,31 @@ -use std::collections::HashMap; -use keymap::KeyMap; -use serde::Deserialize; +#[path = "./backend/mod.rs"] +mod backend; -#[derive(Debug, Deserialize, PartialEq)] -pub(crate) enum Action { - Jump, - Up, - Down, - Left, - Right, - Quit, -} +#[path = "./action.rs"] +mod action; -#[allow(unused)] -#[derive(Debug, Deserialize)] -pub(crate) struct Config(pub HashMap); +use crate::backend::{print, quit, run}; +use action::Action; +use keymap::{Config, KeyMapConfig}; -#[allow(unused)] -pub(crate) const CONFIG_DATA: &str = r#" -up = "Up" -down = "Down" -left = "Left" -right = "Right" -ctrl-g = "Jump" -space = "Jump" -q = "Quit" -esc = "Quit" +// Override default key mapping defined via #[derive(KeyMap)] in Action. +pub(crate) const CONFIG: &str = r#" +Jump = { keys = ["j"], description = "Jump Jump!" } +Quit = { keys = ["esc"], description = "Quit with ESC only!" } "#; -#[allow(unused)] -pub(crate) fn parse_config() -> Config { - toml::from_str(CONFIG_DATA).unwrap() -} +fn main() -> std::io::Result<()> { + println!("# Example: External configuration with Config"); -#[allow(unused)] -fn main() {} + let config: Config = toml::from_str(CONFIG).unwrap(); + + 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), + ), + }, + None => print(&format!("Unknown key {key:?}")), + }) +} diff --git a/examples/config_derive.rs b/examples/config_derive.rs deleted file mode 100644 index 9f35540..0000000 --- a/examples/config_derive.rs +++ /dev/null @@ -1,74 +0,0 @@ -use keymap::Config; -#[cfg(feature = "derive")] -use keymap::{DerivedConfig, Item}; - -#[cfg(feature = "derive")] -use serde::Deserialize; - -#[cfg(feature = "derive")] -#[derive(Debug, keymap::KeyMap, Deserialize, Hash, PartialEq, Eq)] -pub(crate) enum Action { - /// Jump over obstacles - #[key("space", "@digit")] - Jump, - - /// Climb or move up - #[key("up")] - Up, - - /// Drop or crouch down - #[key("down")] - Down, - - /// Move leftward - #[key("left")] - Left, - - /// Move rightward - #[key("right")] - Right, - - /// Exit or pause game - #[key("q", "esc")] - Quit, -} - -/// Overrides the default keymap -#[allow(unused)] -pub(crate) const DERIVED_CONFIG: &str = r#" -Jump = { keys = ["j"], description = "Jump Jump!" } -Up = { keys = ["u", "g g"], description = "Fly!" } -Quit = { keys = ["@digit"], description = "Quit!" } -"#; - -#[cfg(feature = "derive")] -#[allow(unused)] -pub(crate) fn derived_config() -> DerivedConfig { - toml::from_str(DERIVED_CONFIG).unwrap() -} - -#[allow(unused)] -pub(crate) fn config() -> Config { - toml::from_str(DERIVED_CONFIG).unwrap() -} - -#[cfg(feature = "derive")] -#[allow(unused)] -pub(crate) fn print_config(items: &[(Action, Item)]) { - println!("--- keymap ---"); - - items - .iter() - .map(|(action, v)| { - println!( - "{action:?} = keys: {:?}, description: {}", - v.keys, v.description - ) - }) - .collect::>(); - - println!("--------------"); -} - -#[allow(unused)] -fn main() {} diff --git a/examples/crossterm/derive.rs b/examples/crossterm/derive.rs deleted file mode 100644 index 274fbef..0000000 --- a/examples/crossterm/derive.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[path = "../config_derive.rs"] -mod config_derive; - -#[path = "./utils.rs"] -mod crossterm_utils; - -use config_derive::Action; -use crossterm::{ - event::{read, Event}, - terminal::{disable_raw_mode, enable_raw_mode}, -}; -use crossterm_utils::output; -use keymap::KeyMapConfig; -use std::io; - -fn main() -> io::Result<()> { - let config = Action::keymap_config(); - config_derive::print_config(&config.items); - - enable_raw_mode()?; - - let mut send = output(); - - loop { - let event = read()?; - - if let Event::Key(key) = event { - match config.get(&key) { - Some(action) => match action { - Action::Quit => break, - Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => { - send(format!("{action:?}"))? - } - }, - None => send(format!("Unknown key {key:?}"))?, - } - } - } - - disable_raw_mode() -} diff --git a/examples/crossterm/derived_config.rs b/examples/crossterm/derived_config.rs deleted file mode 100644 index 7d5ed8f..0000000 --- a/examples/crossterm/derived_config.rs +++ /dev/null @@ -1,98 +0,0 @@ -#[path = "../config_derive.rs"] -mod config_derive; - -#[path = "./utils.rs"] -mod crossterm_utils; - -use config_derive::Action; -use crossterm::{ - event::{self, read, Event, KeyEvent}, - terminal::{disable_raw_mode, enable_raw_mode}, -}; -use crossterm_utils::output; -use keymap::DerivedConfig; -use std::{ - io, - time::{Duration, Instant}, -}; - -fn main() -> io::Result<()> { - let config = config_derive::derived_config(); - config_derive::print_config(&config.items); - - enable_raw_mode()?; - - handle_key_sequence(config)?; - - disable_raw_mode() -} - -#[allow(dead_code)] -fn handle_key(config: DerivedConfig) -> io::Result<()> { - let mut send = output(); - - loop { - let event = read()?; - - if let Event::Key(key) = event { - // Or using config.get(key) if we don't need the item - match config.get_item(&key) { - Some((action, item)) => match action { - Action::Quit => break, - Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => { - send(format!("{action:?} -> {}", item.description))? - } - }, - None => send(format!("Unknown key [{:?}]", key.code))?, - } - } - } - - Ok(()) -} - -#[allow(dead_code)] -fn handle_key_sequence(config: DerivedConfig) -> io::Result<()> { - let mut send = output(); - - let mut last_key: Option = None; - let mut last_time = Instant::now(); - - loop { - if event::poll(Duration::from_millis(300))? { - if let Event::Key(key) = event::read()? { - match config.get(&key) { - Some(action) => match action { - Action::Quit => break, - Action::Up | Action::Down | Action::Left | Action::Right | Action::Jump => { - send(format!("{action:?}"))? - } - }, - None => { - // Handle key sequence - if let Some(last) = last_key { - match config.get_seq(&[last, key]) { - Some(action) => send(format!( - "Match key sequence: [{} {}] = {action:?}", - last.code, key.code - ))?, - None => send(format!("Unknown key [{:?}]", key.code))?, - } - } - - // Store last key - last_key = Some(key); - last_time = Instant::now(); - } - } - - // timeout for sequence (e.g., 1 sec) - if last_time.elapsed() > Duration::from_secs(1) { - last_key = None; - } - } - } - } - - Ok(()) -} diff --git a/examples/crossterm/main.rs b/examples/crossterm/main.rs deleted file mode 100644 index 50235e6..0000000 --- a/examples/crossterm/main.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::io; - -#[path = "../config.rs"] -mod config; - -#[path = "./utils.rs"] -mod crossterm_utils; - -use config::{parse_config, Action}; -use crossterm::{ - event::{read, Event}, - terminal::{disable_raw_mode, enable_raw_mode}, -}; -use crossterm_utils::output; -use keymap::ToKeyMap; - -fn main() -> io::Result<()> { - enable_raw_mode()?; - - let mut send = output(); - let config = parse_config(); - - loop { - let event = read()?; - - if let Event::Key(key) = event { - if let Some((_, action)) = config.0.get_key_value(&key.to_keymap().unwrap()) { - match action { - Action::Up => send("Up!")?, - Action::Down => send("Down!")?, - Action::Jump => send("Jump!")?, - Action::Left => send("Left!")?, - Action::Right => send("Right!")?, - Action::Quit => break, - } - } - } - } - - disable_raw_mode() -} diff --git a/examples/crossterm/utils.rs b/examples/crossterm/utils.rs deleted file mode 100644 index b6fbbf4..0000000 --- a/examples/crossterm/utils.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::io; - -#[cfg(feature = "crossterm")] -use crossterm::{cursor, execute, style::Print}; -use crossterm::{ - event::{read, Event, KeyEvent}, - terminal::{disable_raw_mode, enable_raw_mode}, -}; - -#[allow(dead_code)] -#[cfg(feature = "crossterm")] -pub(crate) fn output() -> impl FnMut(T) -> std::io::Result<()> { - use crossterm::terminal::{Clear, ClearType}; - - let mut stdout = std::io::stdout(); - move |s| { - execute!( - stdout, - cursor::MoveToNextLine(0), - Clear(ClearType::CurrentLine), - Print(s), - ) - } -} - -#[allow(dead_code)] -pub(crate) fn print(s: &str) -> bool { - println!("{s}\r"); - false -} - -#[allow(dead_code)] -pub(crate) fn quit(s: &str) -> bool { - println!("{s}\r"); - true -} - -#[allow(dead_code)] -pub(crate) fn run(mut f: F) -> io::Result<()> -where - F: FnMut(KeyEvent) -> bool, -{ - enable_raw_mode()?; - - loop { - if let Event::Key(key) = read()? { - let quit = f(key); - if quit { - break; - } - } - } - - disable_raw_mode() -} - -#[allow(unused)] -fn main() {} diff --git a/examples/derive.rs b/examples/derive.rs new file mode 100644 index 0000000..b06ce4e --- /dev/null +++ b/examples/derive.rs @@ -0,0 +1,24 @@ +#[path = "./backend/mod.rs"] +mod backend; + +#[path = "./action.rs"] +mod action; + +use crate::backend::{print, quit, run}; +use keymap::KeyMapConfig; +use action::Action; + +fn main() -> std::io::Result<()> { + println!("# Example: Using the KeyMap derive macro"); + let config = Action::keymap_config(); + + 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:?}")) + } + }, + None => print(&format!("Unknown key {key:?}")), + }) +} diff --git a/examples/derived_config.rs b/examples/derived_config.rs new file mode 100644 index 0000000..6ae1a1c --- /dev/null +++ b/examples/derived_config.rs @@ -0,0 +1,32 @@ +#[path = "./backend/mod.rs"] +mod backend; + +#[path = "./action.rs"] +mod action; + +use crate::backend::{print, quit, run}; +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"], description = "Jump Jump!" } +Up = { keys = ["u"], description = "Fly!" } +Quit = { keys = ["@digit"], description = "Quit!" } +"#; + +fn main() -> std::io::Result<()> { + println!("# Example: Merging derive macros with external config using DerivedConfig"); + + let config: DerivedConfig = toml::from_str(CONFIG).unwrap(); + + 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)) + } + }, + None => print(&format!("Unknown key {key:?}")), + }) +} diff --git a/examples/modes.rs b/examples/modes.rs index 8ee2951..b90c56e 100644 --- a/examples/modes.rs +++ b/examples/modes.rs @@ -1,13 +1,12 @@ use std::collections::HashMap; +#[path = "./backend/mod.rs"] +mod backend; + +use crate::backend::{print, quit, run}; use keymap::DerivedConfig; use serde::Deserialize; -use crate::crossterm_utils::{print, quit, run}; - -#[path = "./crossterm/utils.rs"] -mod crossterm_utils; - #[derive(keymap::KeyMap, Deserialize, Debug, Hash, Eq, PartialEq)] enum HomeAction { #[key("esc")] @@ -45,6 +44,7 @@ fn main() -> std::io::Result<()> { let modes: Modes = toml::from_str(CONFIG).unwrap(); let mut mode = "home"; + println!("# Example: Multi-mode application with different key mappings"); println!("mode: {mode}\r"); run(move |key| match modes.get(mode).unwrap() { @@ -56,7 +56,7 @@ fn main() -> std::io::Result<()> { print("enter edit mode!") } }, - None => print(&format!("{}", key.code)), + None => print(&format!("{key:?}")), }, Actions::Edit(config) => match config.get(&key) { Some(action) => match action { @@ -65,7 +65,7 @@ fn main() -> std::io::Result<()> { print("exit edit mode!") } }, - None => print(&format!("{}", key.code)), + None => print(&format!("{key:?}")), }, }) } diff --git a/examples/sequences.rs b/examples/sequences.rs new file mode 100644 index 0000000..79d7515 --- /dev/null +++ b/examples/sequences.rs @@ -0,0 +1,57 @@ +#[path = "./backend/mod.rs"] +mod backend; + +#[path = "./action.rs"] +mod action; + +use std::time::{Duration, Instant}; + +use crate::backend::{print, quit, run, Key}; +use action::Action; +use keymap::DerivedConfig; + +// Override default key mapping defined via #[derive(KeyMap)] in Action. +pub(crate) const CONFIG: &str = r#" +Jump = { keys = ["j j"], description = "Jump Jump!" } +"#; + +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(); + + run(move |key| { + 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:?}")) + } + }, + None => { + // Handle key sequence + let Some(last) = last_key else { + // Store last key + last_key = Some(key); + last_time = Instant::now(); + + return false; + }; + + match config.get_seq(&[last, key]) { + Some(action) => print(&format!("Key sequence: {action:?}")), + None => print(&format!("Unknown key [{key:?}]")), + } + } + }; + + // timeout for sequence (e.g., 1 sec) + if last_time.elapsed() > Duration::from_secs(1) { + last_key = None; + } + + ret + }) +} diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..e4c8446 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,54 @@ +use keymap::{KeyMap, ToKeyMap}; +use serde::Deserialize; +use std::collections::HashMap; + +#[path = "./backend/mod.rs"] +mod backend; +use crate::backend::{print, quit, run}; + +#[derive(Debug, Deserialize, PartialEq)] +enum Action { + Jump, + Up, + Down, + Left, + Right, + Quit, +} + +#[allow(unused)] +#[derive(Debug, Deserialize)] +struct Config(pub HashMap); + +#[allow(unused)] +const CONFIG: &str = r#" +up = "Up" +down = "Down" +left = "Left" +right = "Right" +ctrl-g = "Jump" +space = "Jump" +q = "Quit" +esc = "Quit" +"#; + +fn main() -> std::io::Result<()> { + let config: Config = toml::from_str(CONFIG).unwrap(); + println!("# Example: Basic key mapping without derive macros"); + + run(|key| { + let Some((_, action)) = config.0.get_key_value(&key.to_keymap().unwrap()) else { + print(&format!("{key:?}")); + return false; + }; + + match action { + Action::Up => print("Up!"), + Action::Down => print("Down!"), + Action::Jump => print("Jump!"), + Action::Left => print("Left!"), + Action::Right => print("Right!"), + Action::Quit => quit("quit"), + } + }) +} diff --git a/examples/termion/derive.rs b/examples/termion/derive.rs deleted file mode 100644 index d49e213..0000000 --- a/examples/termion/derive.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[path = "../config_derive.rs"] -mod config_derive; -#[path = "./utils.rs"] -mod termion_utils; - -use config_derive::Action; -use keymap::KeyMapConfig; -use std::io::{stdin, Write}; -use termion::event::Event; -use termion::input::TermRead; -use termion_utils::{output, print, Result}; - -fn main() -> Result { - let config = Action::keymap_config(); - config_derive::print_config(&config.items); - - let stdin = stdin(); - let mut stdout = output(); - - for event in stdin.events() { - let mut send = |s: &str| print(&mut stdout, s); - - if let Event::Key(key) = event? { - match config.get(&key) { - Some(action) => match action { - Action::Up => send("Up!"), - Action::Down => send("Down!"), - Action::Jump => send("Jump!"), - Action::Left => send("Left!"), - Action::Right => send("Right!"), - Action::Quit => break, - }, - None => send(&format!("Unknown key {key:?}")), - }; - } - - stdout.flush().unwrap(); - } - - Ok(()) -} diff --git a/examples/termion/derived_config.rs b/examples/termion/derived_config.rs deleted file mode 100644 index 91b1dc2..0000000 --- a/examples/termion/derived_config.rs +++ /dev/null @@ -1,40 +0,0 @@ -#[path = "../config_derive.rs"] -mod config_derive; -#[path = "./utils.rs"] -mod termion_utils; - -use config_derive::Action; -use std::io::{stdin, Write}; -use termion::event::Event; -use termion::input::TermRead; -use termion_utils::{output, print, Result}; - -fn main() -> Result { - let config = config_derive::derived_config(); - config_derive::print_config(&config.items); - - let stdin = stdin(); - let mut stdout = output(); - - for event in stdin.events() { - let mut send = |s: &str| print(&mut stdout, s); - - if let Event::Key(key) = event? { - match config.get(&key) { - Some(action) => match action { - Action::Up => send("Up!"), - Action::Down => send("Down!"), - Action::Jump => send("Jump!"), - Action::Left => send("Left!"), - Action::Right => send("Right!"), - Action::Quit => break, - }, - None => send("Unknown key"), - }; - } - - stdout.flush().unwrap(); - } - - Ok(()) -} diff --git a/examples/termion/main.rs b/examples/termion/main.rs deleted file mode 100644 index 29defdf..0000000 --- a/examples/termion/main.rs +++ /dev/null @@ -1,38 +0,0 @@ -#[path = "../config.rs"] -mod config; -#[path = "./utils.rs"] -mod termion_utils; - -use std::io::{stdin, Write}; - -use config::{parse_config, Action}; -use keymap::ToKeyMap; -use termion::event::Event; -use termion::input::TermRead; -use termion_utils::{output, print, Result}; - -fn main() -> Result { - let stdin = stdin(); - let mut stdout = output(); - let bindings = parse_config(); - - for event in stdin.events() { - if let Event::Key(key) = event? { - let mut send = |s: &str| print(&mut stdout, s); - - match bindings.0.get_key_value(&key.to_keymap().unwrap()) { - Some((key, action)) => { - if *action == Action::Quit { - break; - } - send(&format!("{key}")) - } - None => send(&format!("{key:?}")), - } - } - - stdout.flush().unwrap(); - } - - Ok(()) -} diff --git a/examples/termion/utils.rs b/examples/termion/utils.rs deleted file mode 100644 index e0eb3ec..0000000 --- a/examples/termion/utils.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[cfg(feature = "termion")] -use termion::input::MouseTerminal; -#[cfg(feature = "termion")] -use termion::raw::{IntoRawMode, RawTerminal}; -#[cfg(feature = "termion")] -use std::io::{self, Write, stdout, Stdout}; - -#[cfg(feature = "termion")] -#[allow(unused)] -pub(crate) type Result = io::Result<()>; - -#[cfg(feature = "termion")] -#[allow(unused)] -pub(crate) fn print<'a>(stdout: &'a mut MouseTerminal>, s: &'a str) { - write!(stdout, "{}{}KEY: {}", termion::clear::All, termion::cursor::Goto(1, 1), s).unwrap(); -} - -#[cfg(feature = "termion")] -#[allow(unused)] -pub(crate) fn output() -> MouseTerminal> { - MouseTerminal::from(stdout().into_raw_mode().unwrap()) -} - -#[allow(unused)] -fn main() {}