use std::collections::BTreeMap;
use std::time::{Duration, Instant};
use hjkl_keymap::{Chord, KeyEvent};
use crate::app::keymap::HjklMode;
pub struct Entry {
pub key: String,
pub desc: String,
}
pub fn format_key(ev: KeyEvent, leader: char) -> String {
Chord(vec![ev]).to_notation(leader)
}
pub fn entries_for(
km: &hjkl_keymap::Keymap<crate::keymap_actions::AppAction, HjklMode>,
mode: HjklMode,
prefix: &[KeyEvent],
leader: char,
) -> Vec<Entry> {
let vim_mode: hjkl_vim::Mode = mode;
let mut by_key: BTreeMap<String, Entry> = BTreeMap::new();
for d in hjkl_vim::descriptors::children_for(vim_mode, prefix) {
let key = format_key(d.key, leader);
let desc = d.desc.unwrap_or("\u{2026}").to_string();
by_key.insert(key.clone(), Entry { key, desc });
}
let chord = Chord(prefix.to_vec());
for (ev, binding) in km.children_all(mode, &chord) {
let key = format_key(ev, leader);
let desc = match binding {
Some(b) => b.desc.clone(),
None => "\u{2026}".to_string(), };
by_key.insert(key.clone(), Entry { key, desc });
}
by_key.into_values().collect()
}
pub fn should_show(
pending_at: Option<Instant>,
delay: Duration,
enabled: bool,
now: Instant,
) -> bool {
if !enabled {
return false;
}
match pending_at {
Some(at) => now.duration_since(at) >= delay,
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn empty_keymap() -> hjkl_keymap::Keymap<crate::keymap_actions::AppAction, HjklMode> {
hjkl_keymap::Keymap::new(' ')
}
#[test]
fn should_show_returns_false_when_disabled() {
let at = Instant::now() - Duration::from_secs(2);
assert!(!should_show(
Some(at),
Duration::from_millis(500),
false,
Instant::now()
));
}
#[test]
fn should_show_returns_false_when_no_prefix() {
assert!(!should_show(
None,
Duration::from_millis(500),
true,
Instant::now()
));
}
#[test]
fn should_show_returns_false_before_delay() {
let at = Instant::now();
assert!(!should_show(Some(at), Duration::from_millis(500), true, at));
}
#[test]
fn should_show_returns_true_after_delay() {
let at = Instant::now() - Duration::from_secs(2);
assert!(should_show(
Some(at),
Duration::from_millis(500),
true,
Instant::now()
));
}
#[test]
fn entries_include_engine_descriptors_at_root() {
let km = empty_keymap();
let entries = entries_for(&km, HjklMode::Normal, &[], ' ');
let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
for k in ["h", "j", "k", "l", "i", "a", "w", "b"] {
assert!(keys.contains(&k), "entries_for missing engine key '{k}'");
}
}
#[test]
fn entries_include_g_prefix_engine_children() {
let km = empty_keymap();
let entries = entries_for(&km, HjklMode::Normal, &[KeyEvent::char('g')], ' ');
let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
assert!(!entries.is_empty(), "g-prefix popup should be non-empty");
assert!(keys.contains(&"g"), "g-prefix missing 'gg' entry");
assert!(keys.contains(&"j"), "g-prefix missing 'gj' entry");
}
#[test]
fn entries_include_z_prefix_engine_children() {
let km = empty_keymap();
let entries = entries_for(&km, HjklMode::Normal, &[KeyEvent::char('z')], ' ');
let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
assert!(!entries.is_empty(), "z-prefix popup should be non-empty");
assert!(keys.contains(&"z"), "z-prefix missing 'zz' entry");
}
#[test]
fn app_entry_shadows_engine_entry() {
let mut km: hjkl_keymap::Keymap<crate::keymap_actions::AppAction, HjklMode> =
hjkl_keymap::Keymap::new(' ');
km.add(
HjklMode::Normal,
"i",
crate::keymap_actions::AppAction::OpenFilePicker,
"custom insert desc",
)
.expect("add failed");
let entries = entries_for(&km, HjklMode::Normal, &[], ' ');
let i_entry = entries.iter().find(|e| e.key == "i").expect("missing 'i'");
assert_eq!(
i_entry.desc, "custom insert desc",
"app desc should override engine desc for 'i'"
);
}
#[test]
fn entries_sorted_by_key() {
let km = empty_keymap();
let entries = entries_for(&km, HjklMode::Normal, &[], ' ');
let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(keys, sorted, "entries should be sorted by key string");
}
}