use super::table::{self, TableKey};
use super::{ArmedBy, LeaderStep, LeaderTiming, PressFacts, decide};
use crate::app::window_state::WindowState;
use std::time::Instant;
use winit::keyboard::{Key, NamedKey, PhysicalKey};
#[derive(Debug, Clone, Copy)]
pub(crate) struct LeaderPress<'a> {
pub(crate) logical: &'a Key,
pub(crate) physical: PhysicalKey,
pub(crate) pressed: bool,
pub(crate) os_repeat: bool,
}
impl<'a> LeaderPress<'a> {
pub(crate) fn from_event(event: &'a winit::event::KeyEvent) -> Self {
Self {
logical: &event.logical_key,
physical: event.physical_key,
pressed: event.state == winit::event::ElementState::Pressed,
os_repeat: event.repeat,
}
}
#[cfg(test)]
pub(crate) fn pressed(logical: &'a Key) -> Self {
Self {
logical,
physical: PhysicalKey::Unidentified(winit::keyboard::NativeKeyCode::Unidentified),
pressed: true,
os_repeat: false,
}
}
}
impl WindowState {
pub(crate) fn handle_leader_press(&mut self, press: LeaderPress<'_>) -> Option<LeaderStep> {
let now = Instant::now();
if self.leader.expired(now) {
self.disarm_leader();
}
let config = self.config.load();
let modifiers = self.input_handler.modifiers;
let tmux_tab = self.leader_tmux_tab();
let modifier_only = matches!(
press.logical,
Key::Named(
NamedKey::Shift
| NamedKey::Control
| NamedKey::Alt
| NamedKey::Super
| NamedKey::Meta
)
);
let is_leader = !modifier_only
&& self
.leader
.combo_for(&config.input.leader_key)
.is_some_and(|combo| {
par_term_keybindings::KeybindingMatcher::from_key_fields_with_remapping(
press.logical,
press.physical,
&modifiers,
&config.input.modifier_remapping,
)
.matches_with_physical_preference(combo, config.input.use_physical_keys)
});
let is_tmux_prefix = tmux_tab
&& !modifier_only
&& self
.tmux_state
.tmux_prefix_key
.as_ref()
.is_some_and(|prefix| prefix.matches(press.logical, modifiers.state()));
let facts = PressFacts {
pressed: press.pressed,
os_repeat: press.os_repeat,
modifier_only,
escape: matches!(press.logical, Key::Named(NamedKey::Escape)),
is_leader,
is_tmux_prefix,
table_key: TableKey::from_key(press.logical, modifiers.state()),
};
let vim_keys = config.input.leader_vim_keys;
let timing = LeaderTiming::from_config(&config.input);
drop(config);
let step = decide(self.leader.is_armed(), &facts, vim_keys, tmux_tab)?;
self.apply_leader_step(step, press, &facts, now, timing);
Some(step)
}
pub(crate) fn leader_tmux_tab(&self) -> bool {
self.config.load().tmux.tmux_enabled
&& self.is_tmux_connected()
&& self
.tab_manager
.active_tab()
.is_some_and(|tab| tab.tmux.tmux_gateway_active || tab.tmux.tmux_pane_id.is_some())
}
fn apply_leader_step(
&mut self,
step: LeaderStep,
press: LeaderPress<'_>,
facts: &PressFacts,
now: Instant,
timing: LeaderTiming,
) {
match step {
LeaderStep::Arm(by) => {
crate::debug_info!("LEADER", "armed by {by:?}");
self.leader.arm(now, by, timing);
self.overlay_state.which_key = None;
self.focus_state.needs_redraw = true;
self.request_redraw();
}
LeaderStep::Swallow => {}
LeaderStep::Cancel => self.disarm_leader(),
LeaderStep::Literal => {
self.disarm_leader();
self.send_leader_literal(press);
}
LeaderStep::Unbound => {
self.disarm_leader();
let what = facts
.table_key
.map_or_else(|| "that key".to_string(), TableKey::label);
self.show_toast(format!("Leader: {what} is not bound"));
}
LeaderStep::Run { action, repeat } => {
self.after_leader_key(repeat, now, timing);
crate::debug_info!("LEADER", "running {action}");
self.execute_keybinding_action(action);
}
LeaderStep::Tmux { key, repeat } => {
self.after_leader_key(repeat, now, timing);
let vim_keys = self.config.load().input.leader_vim_keys;
if let Some(tmux_key) = table::tmux_key(key, vim_keys) {
self.run_tmux_prefix_key(&tmux_key);
}
}
}
}
fn after_leader_key(&mut self, repeat: bool, now: Instant, timing: LeaderTiming) {
if repeat {
self.leader.refresh(now, timing);
} else {
self.disarm_leader();
}
}
pub(crate) fn disarm_leader(&mut self) {
if !self.leader.is_armed() && self.overlay_state.which_key.is_none() {
return;
}
self.leader.disarm();
self.overlay_state.which_key = None;
self.focus_state.needs_redraw = true;
self.request_redraw();
}
fn send_leader_literal(&mut self, press: LeaderPress<'_>) {
let (modify_other_keys, application_cursor) = self.focused_key_modes();
let input = par_term_input::KeyInput {
logical_key: press.logical.clone(),
physical_key: press.physical,
state: winit::event::ElementState::Pressed,
};
match self.input_handler.handle_key_input_with_mode(
&input,
modify_other_keys,
application_cursor,
) {
Some(bytes) => self.send_typed_bytes(bytes),
None => log::debug!("Leader literal: the chord has no terminal encoding"),
}
}
fn focused_key_modes(&self) -> (u8, bool) {
let Some(tab) = self.tab_manager.active_tab() else {
return (0, false);
};
if let Some(pane) = tab.pane_manager().and_then(|pm| pm.focused_pane())
&& let Ok(term) = pane.terminal.try_read()
{
return (term.modify_other_keys_mode(), term.application_cursor());
}
let (modify_other_keys, application_cursor, _) = tab.read_or_cached_modes();
(modify_other_keys, application_cursor)
}
pub(crate) fn tick_leader(&mut self, now: Instant) -> Option<Instant> {
if self.leader.expired(now) {
self.disarm_leader();
return None;
}
if self.leader.overlay_due(now) && self.overlay_state.which_key.is_none() {
self.overlay_state.which_key = Some(self.build_which_key());
self.focus_state.needs_redraw = true;
}
self.leader.next_wake(now)
}
pub(crate) fn build_which_key(&self) -> super::WhichKey {
let catalog = crate::command_palette::catalog::build_catalog();
let label_of = |action: &str| {
catalog
.iter()
.find(|entry| entry.action_id == action)
.map_or_else(
|| crate::command_palette::catalog::humanize(action),
|entry| entry.label.clone(),
)
};
let leader_chord = self
.keybinding_leader_chord()
.unwrap_or_else(|| "leader".to_string());
super::which_key::build(
&super::which_key::WhichKeyInputs {
vim_keys: self.config.load().input.leader_vim_keys,
tmux_tab: self.leader_tmux_tab(),
armed_by: self.leader.armed_by().unwrap_or(ArmedBy::Leader),
leader_chord,
},
|action| self.live_chord_hint(action),
label_of,
)
}
fn keybinding_leader_chord(&self) -> Option<String> {
let source = self.config.load().input.leader_key.clone();
par_term_keybindings::parser::parse_key_combo(source.trim())
.ok()
.map(|combo| {
crate::command_palette::catalog::chord_display(&combo.platform_normalized())
})
}
}