1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
// Copyright (C) 2026 Jayson Lennon
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program; if not, see <https://opensource.org/license/lgpl-3-0>.
//! 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":
//!
//! ```
//! # use crossterm::event::KeyEvent;
//! # use ratatui_which_key::{Keymap, WhichKey, WhichKeyState};
//! # #[derive(Debug, Clone)]
//! # enum Action {
//! # Quit,
//! # Save,
//! # ToggleHelp,
//! # MoveDown,
//! # MoveUp,
//! # OpenFile,
//! # SearchFiles,
//! # SearchBuffers,
//! # }
//! #
//! # 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"),
//! # Action::ToggleHelp => write!(f, "ToggleHelp"),
//! # Action::MoveUp => write!(f, "MoveUp"),
//! # Action::MoveDown => write!(f, "MoveDown"),
//! # Action::OpenFile => write!(f, "OpenFile"),
//! # Action::SearchFiles => write!(f, "SearchFiles"),
//! # Action::SearchBuffers => write!(f, "SearchBuffers"),
//! # }
//! # }
//! # }
//! # #[derive(derive_more::Display, Debug, Clone, PartialEq)]
//! # enum Category { General, Navigation, SearchPanel }
//! # struct App {
//! # which_key: WhichKeyState<KeyEvent, Scope, Action, Category>,
//! # }
//! # let mut app = App { which_key: WhichKeyState::new(Keymap::default(), Scope::Global) };
//! #[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:
//!
//! ```
//! # use crossterm::event::KeyEvent;
//! # use ratatui_which_key::{Keymap, WhichKey, WhichKeyState, CrosstermStateExt};
//! # #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
//! # enum Scope { Global, Insert, SearchPanel }
//! # #[derive(derive_more::Display, Debug, Clone, PartialEq)]
//! # enum Category { General, Navigation, SearchPanel }
//! # struct App {
//! # which_key: WhichKeyState<KeyEvent, Scope, Action, Category>,
//! # }
//! # let mut app = App { which_key: WhichKeyState::new(Keymap::default(), Scope::Global) };
//! #[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:
//! # use crossterm::event::{KeyCode, KeyModifiers};
//! # let key = crossterm::event::Event::Key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()));
//! 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.
//!
//! ```
//! # use crossterm::event::KeyEvent;
//! # use ratatui_which_key::{Keymap, WhichKey, WhichKeyState};
//! # // Define your action type
//! # #[derive(Debug, Clone)]
//! # enum Action {
//! # Quit,
//! # Save,
//! # ToggleHelp,
//! # MoveDown,
//! # MoveUp,
//! # OpenFile,
//! # SearchFiles,
//! # SearchBuffers,
//! # SearchGrep,
//! # InsertModePrintableChar(char),
//! # ToNormalMode
//! # }
//! #
//! # 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"),
//! # Action::ToggleHelp => write!(f, "ToggleHelp"),
//! # Action::MoveUp => write!(f, "MoveUp"),
//! # Action::MoveDown => write!(f, "MoveDown"),
//! # Action::OpenFile => write!(f, "OpenFile"),
//! # Action::SearchFiles => write!(f, "SearchFiles"),
//! # Action::SearchBuffers => write!(f, "SearchBuffers"),
//! # Action::SearchGrep => write!(f, "SearchGrep"),
//! # Action::InsertModePrintableChar(k) => write!(f, "key: {k}"),
//! # Action::ToNormalMode => write!(f, "normal mode"),
//! # }
//! # }
//! # }
//! # #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
//! # enum Scope { Global, Insert, SearchPanel }
//! # #[derive(derive_more::Display, Debug, Clone, PartialEq)]
//! # enum Category { General, Navigation, SearchPanel }
//! 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:
//! # use crossterm::event::{KeyCode, KeyModifiers};
//! # let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty());
//! if let Some(action) = state.handle_key(key) {
//! // dispatch action
//! }
//!
//! // If handling more than keys (like mouse or terminal events):
//! # let event = crossterm::event::Event::Key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::empty()));
//! use ratatui_which_key::CrosstermStateExt;
//! if let Some(action) = state.handle_event(event).into_action() {
//! // dispatch action
//! }
//!
//! // When rendering
//! let widget = WhichKey::new();
//! # // (buffer from ratatui)
//! # let mut buf = ratatui::buffer::Buffer::default();
//! widget.render(&mut buf, &mut state);
//! ```
//!
//! # Feature Flags
//!
//! - `crossterm` (default): Provides `KeyEvent` implementation and event handlers
pub use CategoryBuilder;
pub use GroupBuilder;
pub use Key;
pub use parse_key_sequence;
pub use Keymap;
pub use ;
pub use ScopeAndCategoryBuilder;
pub use ScopeBuilder;
pub use WhichKeyState;
pub use ;
pub use ;
pub use ;