pub(crate) mod table;
pub(crate) mod which_key;
mod window;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod window_tests;
pub(crate) use table::TableKey;
pub(crate) use which_key::WhichKey;
pub(crate) use window::LeaderPress;
use par_term_keybindings::KeyCombo;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ArmedBy {
Leader,
TmuxPrefix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LeaderTiming {
pub(crate) timeout: Duration,
pub(crate) overlay_delay: Duration,
}
impl LeaderTiming {
pub(crate) fn from_config(input: &par_term_config::InputConfig) -> Self {
Self {
timeout: Duration::from_millis(input.leader_timeout_ms),
overlay_delay: Duration::from_millis(input.leader_overlay_delay_ms),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
Idle,
Armed {
by: ArmedBy,
deadline: Instant,
overlay_at: Instant,
},
}
#[derive(Debug)]
pub(crate) struct LeaderState {
phase: Phase,
combo: Option<(String, Option<KeyCombo>)>,
}
impl Default for LeaderState {
fn default() -> Self {
Self {
phase: Phase::Idle,
combo: None,
}
}
}
impl LeaderState {
pub(crate) fn is_armed(&self) -> bool {
matches!(self.phase, Phase::Armed { .. })
}
pub(crate) fn armed_by(&self) -> Option<ArmedBy> {
match self.phase {
Phase::Armed { by, .. } => Some(by),
Phase::Idle => None,
}
}
pub(crate) fn arm(&mut self, now: Instant, by: ArmedBy, timing: LeaderTiming) {
self.phase = Phase::Armed {
by,
deadline: now + timing.timeout,
overlay_at: now + timing.overlay_delay,
};
}
pub(crate) fn disarm(&mut self) {
self.phase = Phase::Idle;
}
pub(crate) fn refresh(&mut self, now: Instant, timing: LeaderTiming) {
if let Phase::Armed { deadline, .. } = &mut self.phase {
*deadline = now + timing.timeout;
}
}
pub(crate) fn expired(&self, now: Instant) -> bool {
matches!(self.phase, Phase::Armed { deadline, .. } if now >= deadline)
}
pub(crate) fn overlay_due(&self, now: Instant) -> bool {
matches!(self.phase, Phase::Armed { overlay_at, .. } if now >= overlay_at)
}
pub(crate) fn next_wake(&self, now: Instant) -> Option<Instant> {
match self.phase {
Phase::Armed {
deadline,
overlay_at,
..
} if now < overlay_at => Some(overlay_at.min(deadline)),
Phase::Armed { deadline, .. } => Some(deadline),
Phase::Idle => None,
}
}
pub(crate) fn combo_for(&mut self, source: &str) -> Option<&KeyCombo> {
if self
.combo
.as_ref()
.is_none_or(|(cached, _)| cached != source)
{
let trimmed = source.trim();
let parsed = if trimmed.is_empty() {
None
} else {
match par_term_keybindings::parser::parse_key_combo(trimmed) {
Ok(combo) => Some(combo),
Err(e) => {
log::warn!("Invalid leader_key '{source}': {e}; the leader is off");
None
}
}
};
self.combo = Some((source.to_string(), parsed));
}
self.combo.as_ref().and_then(|(_, combo)| combo.as_ref())
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct PressFacts {
pub(crate) pressed: bool,
pub(crate) os_repeat: bool,
pub(crate) modifier_only: bool,
pub(crate) escape: bool,
pub(crate) is_leader: bool,
pub(crate) is_tmux_prefix: bool,
pub(crate) table_key: Option<TableKey>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LeaderStep {
Arm(ArmedBy),
Swallow,
Literal,
Cancel,
Unbound,
Run {
action: &'static str,
repeat: bool,
},
Tmux {
key: TableKey,
repeat: bool,
},
}
pub(crate) fn decide(
armed: bool,
press: &PressFacts,
vim_keys: bool,
tmux_tab: bool,
) -> Option<LeaderStep> {
if !armed {
if !press.pressed || press.os_repeat {
return None;
}
if press.is_leader {
return Some(LeaderStep::Arm(ArmedBy::Leader));
}
if press.is_tmux_prefix {
return Some(LeaderStep::Arm(ArmedBy::TmuxPrefix));
}
return None;
}
if !press.pressed || press.modifier_only || press.os_repeat {
return Some(LeaderStep::Swallow);
}
if press.is_leader || press.is_tmux_prefix {
return Some(LeaderStep::Literal);
}
if press.escape {
return Some(LeaderStep::Cancel);
}
let Some(key) = press.table_key else {
return Some(LeaderStep::Unbound);
};
let binding = table::lookup(key, vim_keys);
if tmux_tab && table::tmux_key(key, vim_keys).is_some() {
return Some(LeaderStep::Tmux {
key,
repeat: binding.is_some_and(|b| b.repeat),
});
}
Some(match binding {
Some(b) => LeaderStep::Run {
action: b.action,
repeat: b.repeat,
},
None => LeaderStep::Unbound,
})
}