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
39 changes: 18 additions & 21 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
81 changes: 63 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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<Action>`.
The `KeyMap` derive macro generates an associated `keymap_config()` method, which returns a `Config<Action>`.

```rust
// Retrieve the config
Expand All @@ -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`:**

Expand All @@ -116,30 +131,35 @@ 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<Action> = toml::from_str(config_str).unwrap();
let config: Config<Action> = toml::from_str(&data)?;
```

**Resulting keybindings:**

| Key | Action |
| ------------- | ------ |
| `"j"`, `"up"` | Jump |
| `@any` | Quit |

#### `DerivedConfig<T>`: 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<Action> = toml::from_str(config_str).unwrap();
let config: DerivedConfig<Action> = 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<T>`, keys from the config file take precedence over derive macro keys for the same action.

### 3. Compile-Time Validation

Expand Down Expand Up @@ -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.

---

Expand All @@ -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.

73 changes: 73 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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<T>**

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

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.
32 changes: 32 additions & 0 deletions examples/action.rs
Original file line number Diff line number Diff line change
@@ -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() {}
28 changes: 28 additions & 0 deletions examples/backend/crossterm.rs
Original file line number Diff line number Diff line change
@@ -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<F>(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() {}
25 changes: 25 additions & 0 deletions examples/backend/mock.rs
Original file line number Diff line number Diff line change
@@ -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<keymap::KeyMap, keymap::Error> {
todo!()
}
}

#[allow(dead_code)]
pub(crate) fn run<F>(mut f: F) -> io::Result<()>
where
F: FnMut(Key) -> bool,
{
// no-op
Ok(())
}

#[allow(unused)]
fn main() {}
Loading
Loading