mirror of
https://github.com/lotabout/skim.git
synced 2026-09-10 07:16:23 -04:00
424 lines
17 KiB
Rust
424 lines
17 KiB
Rust
//! Key binding configuration and parsing.
|
||
//!
|
||
//! This module provides utilities for parsing and managing keyboard shortcuts
|
||
//! and their associated actions in skim.
|
||
|
||
use std::collections::HashMap;
|
||
use std::ops::{Deref, DerefMut};
|
||
|
||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||
use eyre::{Result, eyre};
|
||
|
||
use crate::tui::actions::{self, Action};
|
||
|
||
/// Synthetic events that skim fires internally and that can be bound to actions
|
||
/// via the keymap, exactly like a real key press.
|
||
///
|
||
/// The keymap is keyed by crossterm's [`KeyEvent`], which cannot express
|
||
/// "the query changed" or "reading finished" directly. Each variant is
|
||
/// therefore represented *transparently* as a reserved function-key code in the
|
||
/// high-`F` range (`F(248)`–`F(255)`) that no real terminal ever emits.
|
||
/// Giving these reserved codes named variants keeps them in one place instead
|
||
/// of scattering magic function-key literals across the codebase, and lets
|
||
/// [`parse_key`] accept every friendly event name.
|
||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||
pub enum SkimEvent {
|
||
/// Fired once, when skim has started up and entered its event loop.
|
||
Start,
|
||
/// Fired when the reader finishes producing items (once per read; a
|
||
/// `reload` starts a new read and fires it again).
|
||
Load,
|
||
/// Fired whenever the query changes.
|
||
Change,
|
||
/// Fired when filtering for the current query completes and the result
|
||
/// list is ready.
|
||
Result,
|
||
/// Fired when the focused item changes (cursor movement or a result update).
|
||
Focus,
|
||
/// Fired when a completed search yields no matches.
|
||
Zero,
|
||
/// Fired when a completed search yields exactly one match.
|
||
One,
|
||
/// Fired after two left mouse-button presses no more than 500 ms apart.
|
||
DoubleClick,
|
||
}
|
||
|
||
impl SkimEvent {
|
||
/// The reserved [`KeyCode`] used to route this event through the keymap.
|
||
#[must_use]
|
||
pub const fn key_code(self) -> KeyCode {
|
||
match self {
|
||
SkimEvent::Change => KeyCode::F(255),
|
||
SkimEvent::Start => KeyCode::F(254),
|
||
SkimEvent::Load => KeyCode::F(253),
|
||
SkimEvent::Result => KeyCode::F(252),
|
||
SkimEvent::Focus => KeyCode::F(251),
|
||
SkimEvent::Zero => KeyCode::F(250),
|
||
SkimEvent::One => KeyCode::F(249),
|
||
SkimEvent::DoubleClick => KeyCode::F(248),
|
||
}
|
||
}
|
||
|
||
/// The reserved [`KeyEvent`] used to route this event through the keymap.
|
||
#[must_use]
|
||
pub const fn key_event(self) -> KeyEvent {
|
||
KeyEvent::new(self.key_code(), KeyModifiers::NONE)
|
||
}
|
||
|
||
/// Parses an event name (`start`, `load`, `change`) into a [`SkimEvent`].
|
||
///
|
||
/// Returns `None` if the name is not a recognised event.
|
||
#[must_use]
|
||
pub fn from_name(name: &str) -> Option<Self> {
|
||
match name {
|
||
"start" => Some(SkimEvent::Start),
|
||
"load" => Some(SkimEvent::Load),
|
||
"change" => Some(SkimEvent::Change),
|
||
"result" => Some(SkimEvent::Result),
|
||
"focus" => Some(SkimEvent::Focus),
|
||
"zero" => Some(SkimEvent::Zero),
|
||
"one" => Some(SkimEvent::One),
|
||
"double-click" => Some(SkimEvent::DoubleClick),
|
||
_ => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<SkimEvent> for KeyEvent {
|
||
fn from(event: SkimEvent) -> Self {
|
||
event.key_event()
|
||
}
|
||
}
|
||
|
||
/// A map of key events to their associated actions
|
||
#[derive(Clone, Debug)]
|
||
pub struct KeyMap(pub HashMap<KeyEvent, Vec<Action>>);
|
||
|
||
impl Deref for KeyMap {
|
||
type Target = HashMap<KeyEvent, Vec<Action>>;
|
||
|
||
fn deref(&self) -> &Self::Target {
|
||
&self.0
|
||
}
|
||
}
|
||
impl DerefMut for KeyMap {
|
||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||
&mut self.0
|
||
}
|
||
}
|
||
|
||
impl From<&str> for KeyMap {
|
||
fn from(value: &str) -> Self {
|
||
parse_keymaps(split_top_level(value, ',').into_iter())
|
||
}
|
||
}
|
||
|
||
impl Default for KeyMap {
|
||
fn default() -> Self {
|
||
get_default_key_map()
|
||
}
|
||
}
|
||
|
||
impl KeyMap {
|
||
/// Adds keymaps from a comma-separated string.
|
||
pub(crate) fn add_keymaps_str(&mut self, source: &str) {
|
||
self.add_keymaps(split_top_level(source, ',').into_iter());
|
||
}
|
||
|
||
/// Adds keymaps from the source, parsing them using `parse_keymap`
|
||
pub fn add_keymaps<'a, T>(&mut self, source: T)
|
||
where
|
||
T: Iterator<Item = &'a str>,
|
||
{
|
||
for map in source {
|
||
if let Ok((key, action_chain)) = parse_keymap(map) {
|
||
self.bind(key, action_chain)
|
||
.unwrap_or_else(|err| debug!("Failed to bind key {map}: {err}"));
|
||
} else {
|
||
debug!("Failed to parse key: {map}");
|
||
}
|
||
}
|
||
}
|
||
fn bind(&mut self, key: &str, action_chain: Vec<Action>) -> Result<()> {
|
||
let key = parse_key(key)?;
|
||
|
||
// remove the key for existing keymap;
|
||
let _ = self.remove(&key);
|
||
self.entry(key).or_insert(action_chain);
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Returns the default key bindings for skim
|
||
#[rustfmt::skip]
|
||
#[must_use]
|
||
pub fn get_default_key_map() -> KeyMap {
|
||
let mut ret = HashMap::new();
|
||
|
||
ret.insert(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE), vec![Action::Down(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE), vec![Action::Up(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE), vec![Action::PageUp(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE), vec![Action::PageDown(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::End, KeyModifiers::NONE), vec![Action::EndOfLine]);
|
||
ret.insert(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE), vec![Action::BeginningOfLine]);
|
||
ret.insert(KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE), vec![Action::DeleteChar]);
|
||
ret.insert(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), vec![Action::Toggle, Action::Down(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::BackTab, KeyModifiers::all()), vec![Action::Toggle, Action::Up(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), vec![Action::Abort]);
|
||
ret.insert(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), vec![Action::Accept(None)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE), vec![Action::BackwardChar]);
|
||
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE), vec![Action::ForwardChar]);
|
||
ret.insert(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), vec![Action::BackwardDeleteChar]);
|
||
ret.insert(SkimEvent::DoubleClick.key_event(), vec![Action::Accept(None)]);
|
||
|
||
|
||
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT), vec![Action::BackwardWord]);
|
||
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT), vec![Action::ForwardWord]);
|
||
ret.insert(KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT), vec![Action::PreviewUp(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT), vec![Action::PreviewDown(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT), vec![Action::Toggle, Action::Up(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT), vec![Action::Toggle, Action::Up(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Home, KeyModifiers::SHIFT), vec![Action::BeginningOfLine]);
|
||
|
||
|
||
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL), vec![Action::BackwardWord]);
|
||
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::CONTROL), vec![Action::ForwardWord]);
|
||
|
||
ret.insert(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL), vec![Action::BeginningOfLine]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL), vec![Action::BackwardChar]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), vec![Action::Abort]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL), vec![Action::Abort]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL), vec![Action::EndOfLine]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL), vec![Action::ForwardChar]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL), vec![Action::Abort]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL), vec![Action::BackwardDeleteChar]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('j'), KeyModifiers::CONTROL), vec![Action::Down(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL), vec![Action::Up(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL), vec![Action::ClearScreen]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL), vec![Action::Down(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL), vec![Action::Up(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL), vec![Action::ToggleInteractive]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL), vec![Action::RotateMode]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL), vec![Action::UnixLineDiscard]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL), vec![Action::UnixWordRubout]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL), vec![Action::Yank]);
|
||
|
||
|
||
ret.insert(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT), vec![Action::BackwardKillWord]);
|
||
|
||
ret.insert(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT), vec![Action::BackwardWord]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::ALT), vec![Action::KillWord]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::ALT), vec![Action::ForwardWord]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::ALT), vec![Action::ScrollLeft(1)]);
|
||
ret.insert(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::ALT), vec![Action::ScrollRight(1)]);
|
||
|
||
KeyMap(ret)
|
||
}
|
||
|
||
/// Parses a key str into a crossterm `KeyEvent`.
|
||
///
|
||
/// In addition to keyboard names, accepts all names recognized by
|
||
/// [`SkimEvent::from_name`], including `change`, `start`, and `double-click`.
|
||
///
|
||
/// # Errors
|
||
/// Returns an error if the key string is empty, contains an unknown modifier,
|
||
/// or does not correspond to a recognised key name.
|
||
pub fn parse_key(key: &str) -> Result<KeyEvent> {
|
||
if key.is_empty() {
|
||
return Err(eyre!("Cannot parse empty key"));
|
||
}
|
||
if let Some(event) = SkimEvent::from_name(key) {
|
||
return Ok(event.key_event());
|
||
}
|
||
let parts = key.split('-').collect::<Vec<&str>>();
|
||
let mut mods = KeyModifiers::NONE;
|
||
|
||
if parts.len() > 1 {
|
||
let mod_strs = &parts[..parts.len() - 1];
|
||
for mod_str in mod_strs {
|
||
mods |= match *mod_str {
|
||
"ctrl" => KeyModifiers::CONTROL,
|
||
"alt" => KeyModifiers::ALT,
|
||
"shift" => KeyModifiers::SHIFT,
|
||
s => return Err(eyre!("Failed to parse {} as key modifier", s)),
|
||
}
|
||
}
|
||
}
|
||
let key = parts.last().unwrap_or(&"").to_string();
|
||
|
||
let keycode: KeyCode;
|
||
if key.len() == 1 {
|
||
let char = key.chars().next().unwrap_or_default();
|
||
if char.is_uppercase() {
|
||
mods |= KeyModifiers::SHIFT;
|
||
keycode = KeyCode::Char(char.to_ascii_lowercase());
|
||
} else {
|
||
keycode = KeyCode::Char(char);
|
||
}
|
||
} else if let Some(f) = key.strip_prefix('f')
|
||
&& let Ok(f_index) = f.parse::<u8>()
|
||
{
|
||
// A function key like `f10`. If the suffix isn't numeric (e.g. `focus`,
|
||
// `first`), fall through to the named-key / event matching below.
|
||
keycode = KeyCode::F(f_index);
|
||
} else {
|
||
keycode = match key.as_str() {
|
||
"space" => KeyCode::Char(' '),
|
||
"enter" => KeyCode::Enter,
|
||
"bspace" | "bs" => KeyCode::Backspace,
|
||
"up" => KeyCode::Up,
|
||
"down" => KeyCode::Down,
|
||
"left" => KeyCode::Left,
|
||
"right" => KeyCode::Right,
|
||
"tab" => KeyCode::Tab,
|
||
"btab" => KeyCode::BackTab,
|
||
"esc" => KeyCode::Esc,
|
||
"home" => KeyCode::Home,
|
||
"end" => KeyCode::End,
|
||
"pgup" => KeyCode::PageUp,
|
||
"pgdown" => KeyCode::PageDown,
|
||
s => match SkimEvent::from_name(s) {
|
||
Some(event) => event.key_code(),
|
||
None => return Err(eyre!("Unknown key {}", s)),
|
||
},
|
||
}
|
||
}
|
||
|
||
debug!("parsed key {keycode:?} and mods {mods:?}");
|
||
|
||
Ok(KeyEvent::new(keycode, mods))
|
||
}
|
||
|
||
/// Parse an iterator of keymaps into a `KeyMap`
|
||
pub fn parse_keymaps<'a, T>(maps: T) -> KeyMap
|
||
where
|
||
T: Iterator<Item = &'a str>,
|
||
{
|
||
let mut res = KeyMap::default();
|
||
res.add_keymaps(maps);
|
||
res
|
||
}
|
||
|
||
pub(crate) fn split_top_level(value: &str, separator: char) -> Vec<&str> {
|
||
let mut depth = 0_u32;
|
||
let mut start = 0;
|
||
let mut parts = Vec::new();
|
||
|
||
for (index, ch) in value.char_indices() {
|
||
match ch {
|
||
'(' => depth += 1,
|
||
')' => depth = depth.saturating_sub(1),
|
||
_ if ch == separator && depth == 0 => {
|
||
parts.push(&value[start..index]);
|
||
start = index + ch.len_utf8();
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
parts.push(&value[start..]);
|
||
parts
|
||
}
|
||
|
||
/// Parses follow-up action bindings from raw `--bind` specs.
|
||
///
|
||
/// Any action can be bound as if it were an event: when the "key" of a bind is
|
||
/// not a real key but is a known action name, the bound chain becomes a
|
||
/// *follow-up* that runs right after that action. For example `reload:first`
|
||
/// queues `first` immediately after a `reload`. The returned map is keyed by the
|
||
/// action's canonical name (see [`Action::name`](crate::tui::actions::Action::name)),
|
||
/// so it can be looked up directly from the action that just ran.
|
||
///
|
||
/// Keys take precedence: if the "key" resolves to a real key it is left to the
|
||
/// key map, so a name shared by a key and an action (e.g. `up`) always binds the
|
||
/// key. To target the action in that case, prefix it with `act-` (`act-up`).
|
||
#[must_use]
|
||
pub fn parse_action_binds<'a, T>(maps: T) -> HashMap<String, Vec<Action>>
|
||
where
|
||
T: Iterator<Item = &'a str>,
|
||
{
|
||
let mut res = HashMap::new();
|
||
for map in maps {
|
||
let Some((key, chain)) = map.split_once(':') else {
|
||
continue;
|
||
};
|
||
// Keys win: anything that parses as a real key is not an action trigger.
|
||
if parse_key(key).is_ok() {
|
||
continue;
|
||
}
|
||
let Some(name) = action_trigger_name(key) else {
|
||
debug!("Ignoring bind `{map}`: `{key}` is neither a key nor an action");
|
||
continue;
|
||
};
|
||
match parse_action_chain(chain) {
|
||
Ok(actions) => {
|
||
res.insert(name.to_string(), actions);
|
||
}
|
||
Err(err) => debug!("Ignoring bind `{map}`: invalid action chain `{chain}`: {err}"),
|
||
}
|
||
}
|
||
res
|
||
}
|
||
|
||
/// Resolves a bind trigger to the canonical name of the action it targets
|
||
/// (see [`Action::name`](crate::tui::actions::Action::name)). `act-<name>`
|
||
/// explicitly targets the action `<name>`, even when `<name>` is also a key;
|
||
/// without the prefix, a bare action name works too. Returns `None` if the
|
||
/// name is not a known action.
|
||
///
|
||
/// This performs pure name resolution: callers that want "keys win" semantics
|
||
/// (e.g. [`parse_action_binds`]) must check [`parse_key`] first.
|
||
pub(crate) fn action_trigger_name(trigger: &str) -> Option<&'static str> {
|
||
let action_name = trigger.strip_prefix("act-").unwrap_or(trigger);
|
||
// Some actions require an argument when executed, but their canonical
|
||
// name is still valid as a trigger. `()` supplies the parser's empty
|
||
// placeholder solely for name validation.
|
||
let action = actions::parse_action(action_name).or_else(|| actions::parse_action(&format!("{action_name}()")))?;
|
||
Some(action.name())
|
||
}
|
||
|
||
/// Parses an action chain, separated by '+'s into the corresponding actions
|
||
///
|
||
/// # Errors
|
||
/// Returns an error if the action chain is empty or contains only unknown actions.
|
||
pub fn parse_action_chain(action_chain: &str) -> Result<Vec<Action>> {
|
||
let mut actions: Vec<Action> = vec![];
|
||
let mut split = split_top_level(action_chain, '+').into_iter();
|
||
|
||
while let Some(mut s) = split.next().map(String::from) {
|
||
if (s.starts_with("if-") || s.ends_with('{'))
|
||
&& let Some(otherwise) = split.next()
|
||
{
|
||
s += &(String::from("+") + otherwise);
|
||
}
|
||
if let Some(act) = actions::parse_action(&s) {
|
||
actions.push(act);
|
||
}
|
||
}
|
||
if actions.is_empty() {
|
||
Err(eyre!("Empty action chain or unknown action `{}`", action_chain))
|
||
} else {
|
||
Ok(actions)
|
||
}
|
||
}
|
||
|
||
/// Parse a single keymap and return the key and action(s)
|
||
///
|
||
/// # Errors
|
||
/// Returns an error if the string is empty, missing a colon separator, or the
|
||
/// action chain cannot be parsed.
|
||
pub fn parse_keymap(key_action: &str) -> Result<(&str, Vec<Action>)> {
|
||
if key_action.is_empty() {
|
||
return Err(eyre!("Got an empty keybind, skipping"));
|
||
}
|
||
debug!("got key_action: {key_action:?}");
|
||
let (key, action_chain) = key_action
|
||
.split_once(':')
|
||
.ok_or(eyre!("Failed to parse {} as key and action", key_action))?;
|
||
debug!("parsed key_action: {key:?}: {action_chain:?}");
|
||
Ok((key, parse_action_chain(action_chain)?))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "binds_tests.rs"]
|
||
mod tests;
|