Expand description
A which-key popup widget for ratatui applications.
This crate provides a popup widget that displays available keybindings, similar to Neovim’s which-key plugin.
§How It Works
ratatui-which-key requires three data types be defined in your application.
§Scopes
The scope is what part of your application is currently “in focus”:
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Scope {
Global,
TextInputBox,
SearchPanel,
// ....
}
// When changing focus to another pane/window/etc:
app.which_key.set_scope(Scope::TextInputBox)§Actions
ratatui-which-key returns an Action when a keybind is triggered:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Action {
Quit,
ToggleHelp,
MoveUp,
MoveDown,
Save,
OpenFile,
SearchFiles,
SearchBuffers,
// ...
}
// Must implement Display to show descriptions in the which-key popup.
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Action::Quit => write!(f, "quit"),
Action::ToggleHelp => write!(f, "toggle help"),
Action::MoveUp => write!(f, ""),
Action::MoveDown => write!(f, "move down"),
Action::Save => write!(f, "save"),
Action::OpenFile => write!(f, "open file"),
Action::SearchFiles => write!(f, "search files"),
Action::SearchBuffers => write!(f, "search buffers"),
}
}
}
// In your input handler:
if let Some(action) = app.which_key.handle_event(key).into_action() {
match action {
Action::ToggleHelp => app.which_key.toggle(),
Action::Quit => (), // logic here
Action::MoveUp => (),
Action::MoveDown => (),
Action::Save => (),
Action::OpenFile => (),
Action::SearchFiles => (),
Action::SearchBuffers => (),
}
}§Categories
The ratatui-which-key popup displays keybinds sorted by category:
#[derive(derive_more::Display, Debug, Clone, Copy, PartialEq, Eq)]
enum Category {
General,
Navigation,
Search,
// ...
}§Keymap Configuration
You’ll need to put a WhichKeyState<KeyEvent, Scope, Action, Category> at the top-level of your application (like in App). Then at program start, configure your keybinds by creating a new Keymap. The code comments explain the different ways of performing keybindings.
struct App {
which_key: WhichKeyState<KeyEvent, Scope, Action, Category>,
}
let mut keymap = Keymap::new();
keymap
// Keys can be bound individually by specifying both the category and scope.
.bind("?", Action::ToggleHelp, Category::General, Scope::Global)
// Sequences are supported. This binds to sequence "sg".
.bind("sg", Action::SearchGrep, Category::General, Scope::Global)
// "describe_group" used to add a description to groups. Display will default to "..." if
// no group description is found.
.describe_group("<space>", "<leader>") // (key sequence, description)
.describe_group("<leader>g", "general")
// Bindings can be added to a specific group while also providing a description.
.group("s", "search", |g| {
// "sf" binding
g.bind("f", Action::SearchFiles, Category::General, Scope::SearchPanel)
// "sb" binding
.bind("b", Action::SearchBuffers, Category::General, Scope::SearchPanel);
})
// However, using `.scope` is recommended in most cases since scopes represent whatever is
// currently "in focus" for your app.
.scope(Scope::Global, |global| {
global
.bind("?", Action::ToggleHelp, Category::General)
.bind("j", Action::MoveDown, Category::Navigation)
// control keys supported
.bind("<c-c>", Action::Quit, Category::General)
// f-keys supported
.bind("<F1>", Action::ToggleHelp, Category::General)
// sequences supported
.bind("<leader>w", Action::Save, Category::General)
// sequences can start with any key
.bind("gof", Action::OpenFile, Category::General);
})
.scope(Scope::Insert, |insert| {
// While in the `Insert` scope, all keys will be routed to this handler.
insert.catch_all(|key| {
// You can filter the keys here
use crossterm::event::{KeyCode, KeyModifiers};
match key {
k if matches!(k, KeyEvent { code: KeyCode::Char(_), .. }) => {
if let KeyCode::Char(ch) = key.code {
Some(Action::InsertModePrintableChar(ch))
} else {
None
}
}
k if matches!(k, KeyEvent { code: KeyCode::Esc, .. }) => Some(Action::ToNormalMode),
_ => None
}
});
})
// Helper method if you want to bind based on category.
.category(Category::Navigation, |nav| {
nav
.bind("k", Action::MoveUp, Scope::Global)
.bind("j", Action::MoveDown, Scope::Global);
})
// Helper method if you want to bind based on both scope and category.
.scope_and_category(Scope::Global, Category::Navigation, |g| {
g.bind("<leader>gg", Action::MoveUp)
.bind("<leader>gd", Action::MoveDown);
});
let app = App { which_key: WhichKeyState::new(keymap, Scope::Global) };§Example
use crossterm::event::KeyEvent;
use ratatui_which_key::{Keymap, WhichKey, WhichKeyState};
// Define your action type
#[derive(Debug, Clone)]
enum Action { Quit, Save }
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Action::Quit => write!(f, "quit"),
Action::Save => write!(f, "save"),
}
}
}
// Define your scope type
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Scope { Global, Insert }
// Define keybind categories
#[derive(derive_more::Display, Debug, Clone, PartialEq)]
enum Category { General, Navigation }
// Build the keymap
let mut keymap: Keymap<KeyEvent, Scope, Action, Category> = Keymap::new();
keymap.bind("q", Action::Quit, Category::General, Scope::Global);
// Create state
let mut state = WhichKeyState::new(keymap, Scope::Global);
// In your event loop, handle keys:
if let Some(action) = state.handle_key(key) {
// dispatch action
}
// If handling more than keys (like mouse or terminal events):
use ratatui_which_key::CrosstermStateExt;
if let Some(action) = state.handle_event(event).into_action() {
// dispatch action
}
// When rendering
let widget = WhichKey::new();
widget.render(&mut buf, &mut state);§Feature Flags
crossterm(default): ProvidesKeyEventimplementation and event handlers
Structs§
- Binding
- A key binding with an associated description.
- Binding
Group - A group of related key bindings under a shared category.
- Category
Builder - Builder for creating keybindings within a specific category.
- Display
Binding - A key binding prepared for display, including category information.
- Group
Builder - A builder for binding key sequences under a shared prefix.
- KeyChild
- A key binding with its associated key and node.
- Keymap
- A hierarchical keymap that maps key sequences to actions with scope and category support.
- Leaf
Binding - A flattened key binding for display purposes.
- Leaf
Entry - A single action entry in a leaf node.
- Scope
AndCategory Builder - A builder for binding key sequences with a pre-configured scope and category.
- Scope
Builder - Builder for creating keybindings within a specific scope.
- Which
Key - Configuration for the which-key widget.
- Which
KeyState - State for the which-key widget.
Enums§
- Display
Mode - How bindings are organized in the popup.
- Event
Result - Result of handling a crossterm event.
- KeyNode
- A node in the keybinding tree, either a leaf with actions or a branch with children.
- Layout
Strategy - How the popup distributes content across columns.
- Node
Result - The result of traversing a node in the key binding tree.
- Popup
Position - Position of the which-key popup on screen.
Traits§
- Crossterm
Keymap Ext - Extension trait for Keymap to add crossterm event handlers.
- Crossterm
State Ext - Extension trait for WhichKeyState to handle crossterm events.
- Key
- A trait for abstracting keyboard key representation.
Functions§
- parse_
key_ sequence - Parses a key sequence string into a vector of keys.