use super::{KeySequence, KeyToken, action::VimAction};
pub const DEFAULT_LEADER_DELAY_MS: u64 = 150;
#[derive(Clone, Debug, Default, Eq, PartialEq, bevy::prelude::Resource)]
pub struct LeaderState {
state: LeaderInteraction,
}
impl LeaderState {
pub const fn start(&mut self) {
self.state = LeaderInteraction::Pending { elapsed_ms: 0 };
}
pub const fn cancel(&mut self) {
self.state = LeaderInteraction::Inactive;
}
#[must_use]
pub const fn is_pending(&self) -> bool {
matches!(self.state, LeaderInteraction::Pending { .. })
}
#[must_use]
pub const fn is_menu_visible(&self) -> bool {
matches!(self.state, LeaderInteraction::MenuVisible)
}
pub const fn tick(&mut self, delta_ms: u64, delay_ms: u64) {
if let LeaderInteraction::Pending { elapsed_ms } = &mut self.state {
*elapsed_ms = elapsed_ms.saturating_add(delta_ms);
if *elapsed_ms >= delay_ms {
self.state = LeaderInteraction::MenuVisible;
}
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
enum LeaderInteraction {
#[default]
Inactive,
Pending {
elapsed_ms: u64,
},
MenuVisible,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LeaderBinding {
pub sequence: KeySequence,
pub label: String,
pub action: VimAction,
}
impl LeaderBinding {
#[must_use]
pub fn new(sequence: KeySequence, label: impl Into<String>, action: VimAction) -> Self {
Self {
sequence,
label: label.into(),
action,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LeaderConfig {
pub key: KeyToken,
pub delay_ms: u64,
pub bindings: Vec<LeaderBinding>,
}
impl LeaderConfig {
#[must_use]
pub fn binding_for(&self, sequence: &[KeyToken]) -> Option<&LeaderBinding> {
self.bindings
.iter()
.find(|binding| binding.sequence.as_slice() == sequence)
}
}
impl Default for LeaderConfig {
fn default() -> Self {
use super::{PageDirection, SearchDirection};
Self {
key: KeyToken::Char(' '),
delay_ms: DEFAULT_LEADER_DELAY_MS,
bindings: vec![
LeaderBinding::new(
KeySequence::new([KeyToken::Char('w')]),
"w write",
VimAction::ExCommand(String::from("w")),
),
LeaderBinding::new(
KeySequence::new([KeyToken::Char('q')]),
"q quit",
VimAction::ExCommand(String::from("q")),
),
LeaderBinding::new(
KeySequence::new([KeyToken::Char('x')]),
"x write-quit",
VimAction::ExCommand(String::from("wq")),
),
LeaderBinding::new(
KeySequence::new([KeyToken::Char('n')]),
"n next search",
VimAction::RepeatSearch(SearchDirection::Forward),
),
LeaderBinding::new(
KeySequence::new([KeyToken::Char('N')]),
"N previous search",
VimAction::RepeatSearch(SearchDirection::Backward),
),
LeaderBinding::new(
KeySequence::new([KeyToken::Char('f')]),
"f page down",
VimAction::ViewportPage(PageDirection::Forward),
),
LeaderBinding::new(
KeySequence::new([KeyToken::Char('b')]),
"b page up",
VimAction::ViewportPage(PageDirection::Backward),
),
LeaderBinding::new(
KeySequence::new([KeyToken::Char('h')]),
"h help",
VimAction::NoOp,
),
],
}
}
}
#[cfg(test)]
mod tests {
use super::{DEFAULT_LEADER_DELAY_MS, LeaderState};
#[test]
fn leader_menu_appears_after_delay() {
let mut state = LeaderState::default();
state.start();
assert!(state.is_pending());
state.tick(DEFAULT_LEADER_DELAY_MS - 1, DEFAULT_LEADER_DELAY_MS);
assert!(!state.is_menu_visible());
state.tick(1, DEFAULT_LEADER_DELAY_MS);
assert!(state.is_menu_visible());
}
#[test]
fn leader_cancel_clears_pending_or_visible_state() {
let mut state = LeaderState::default();
state.start();
state.cancel();
assert!(!state.is_pending());
state.start();
state.tick(DEFAULT_LEADER_DELAY_MS, DEFAULT_LEADER_DELAY_MS);
state.cancel();
assert!(!state.is_menu_visible());
}
}