feat(ui): implement which-key popup for command completions

Implement `WhichKey` popup to show available command completions.

- Added `format_key_label` to convert `KeyCode` to human-readable strings.
- Added `binding_hints` to `Keymap` to extract available commands for a
  given prefix.
- Added `src/ui/which_key.rs` for the widget implementation.
- Updated `src/ui/mod.rs` to export the new module.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
Edward Langley
2026-04-08 22:27:36 -07:00
parent bbc009b088
commit d49bcf0060
3 changed files with 120 additions and 0 deletions

View File

@ -8,6 +8,29 @@ use crate::ui::app::AppMode;
use crate::ui::effect::Effect;
use super::cmd::{self, CmdContext, CmdRegistry};
/// Format a KeyCode as a short human-readable label for which-key display.
fn format_key_label(code: &KeyCode) -> String {
match code {
KeyCode::Char(c) => c.to_string(),
KeyCode::Enter => "Enter".to_string(),
KeyCode::Esc => "Esc".to_string(),
KeyCode::Tab => "Tab".to_string(),
KeyCode::BackTab => "S-Tab".to_string(),
KeyCode::Backspace => "BS".to_string(),
KeyCode::Delete => "Del".to_string(),
KeyCode::Left => "".to_string(),
KeyCode::Right => "".to_string(),
KeyCode::Up => "".to_string(),
KeyCode::Down => "".to_string(),
KeyCode::Home => "Home".to_string(),
KeyCode::End => "End".to_string(),
KeyCode::PageUp => "PgUp".to_string(),
KeyCode::PageDown => "PgDn".to_string(),
KeyCode::F(n) => format!("F{n}"),
_ => format!("{code:?}"),
}
}
// `cmd` module imported for `default_registry()` in default_keymaps()
/// A key pattern that can be matched against a KeyEvent.
@ -146,6 +169,35 @@ impl Keymap {
.insert(KeyPattern::Any, Binding::Cmd { name, args: vec![] });
}
/// Return human-readable hints for all concrete bindings in this keymap.
/// Used by the which-key popup to show available completions after a prefix key.
pub fn binding_hints(&self) -> Vec<(String, &'static str)> {
let mut hints: Vec<(String, &'static str)> = self
.bindings
.iter()
.filter_map(|(pattern, binding)| {
let label = match pattern {
KeyPattern::Key(code, _) => format_key_label(code),
KeyPattern::AnyChar | KeyPattern::Any => return None,
};
let name = match binding {
Binding::Cmd { name, .. } => *name,
Binding::Prefix(_) => return None,
Binding::Sequence(steps) => {
if let Some((name, _)) = steps.first() {
*name
} else {
return None;
}
}
};
Some((label, name))
})
.collect();
hints.sort_by(|a, b| a.0.cmp(&b.0));
hints
}
/// Look up the binding for a key.
/// For Char keys, if exact (key, mods) match fails, retries with NONE
/// modifiers since terminals vary in whether they send SHIFT for