use std::collections::HashMap;
use std::fmt;
use std::sync::RwLock;
use crate::event::Key;
pub fn keys(names: &str) -> Vec<Key> {
try_keys(names).unwrap_or_else(|name| panic!("unknown key {name:?}"))
}
pub fn try_keys(names: &str) -> Result<Vec<Key>, String> {
let names = key_names(names)?;
names
.iter()
.map(|name| Key::parse(name).ok_or_else(|| name.clone()))
.collect()
}
fn key_names(text: &str) -> Result<Vec<String>, String> {
let mut names = Vec::new();
let mut name = String::new();
let mut chars = text.chars();
while let Some(c) = chars.next() {
match c {
'"' | '\'' if name.is_empty() => {
let mut quoted = String::new();
let mut closed = false;
for d in chars.by_ref() {
if d == c {
closed = true;
break;
}
quoted.push(d);
}
if !closed || quoted.is_empty() {
return Err(text.trim().to_string());
}
names.push(quoted);
}
c if c.is_whitespace() || c == ',' => {
if !name.is_empty() {
names.push(std::mem::take(&mut name));
}
}
c => name.push(c),
}
}
if !name.is_empty() {
names.push(name);
}
if names.is_empty() && !text.trim().is_empty() {
return Err(text.trim().to_string());
}
Ok(names)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Binding {
pub context: String,
pub action: String,
pub keys: Vec<Key>,
pub description: String,
pub sequence: bool,
pub unavailable: Option<String>,
}
impl Binding {
pub fn new(
context: impl Into<String>,
action: impl Into<String>,
keys: impl IntoIterator<Item = Key>,
description: impl Into<String>,
) -> Binding {
Binding {
context: context.into(),
action: action.into(),
keys: keys.into_iter().collect(),
description: description.into(),
sequence: false,
unavailable: None,
}
}
pub fn sequence(
context: impl Into<String>,
action: impl Into<String>,
keys: impl IntoIterator<Item = Key>,
description: impl Into<String>,
) -> Binding {
Binding {
sequence: true,
..Binding::new(context, action, keys, description)
}
}
#[must_use]
pub fn unavailable(mut self, reason: impl Into<String>) -> Binding {
self.unavailable = Some(reason.into());
self
}
pub fn id(&self) -> String {
format!("{}.{}", self.context, self.action)
}
pub fn keys_label(&self) -> String {
self.keys
.iter()
.map(Key::to_string)
.collect::<Vec<_>>()
.join(if self.sequence { " " } else { "/" })
}
pub fn hint(&self) -> String {
crate::chrome::keys_hint_of(&self.keys, self.sequence)
}
}
impl fmt::Display for Binding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.keys_label(), self.description)
}
}
#[derive(Clone, Debug, Default)]
pub struct Keymap {
context: String,
bindings: Vec<Binding>,
rebound: HashMap<String, Vec<Key>>,
}
impl Keymap {
pub fn new(context: impl Into<String>) -> Keymap {
Keymap {
context: context.into(),
..Keymap::default()
}
}
pub fn context(&self) -> &str {
&self.context
}
#[must_use]
pub fn bind(
mut self,
action: impl Into<String>,
keys: impl IntoIterator<Item = Key>,
description: impl Into<String>,
) -> Keymap {
let binding = Binding::new(self.context.clone(), action, keys, description);
self.add(binding);
self
}
pub fn add(&mut self, binding: Binding) {
match self
.bindings
.iter_mut()
.find(|b| b.context == binding.context && b.action == binding.action)
{
Some(existing) => *existing = binding,
None => self.bindings.push(binding),
}
}
pub fn extend(&mut self, other: Keymap) {
for (id, keys) in other.rebound {
self.rebound.entry(id).or_insert(keys);
}
for binding in other.bindings {
if !self
.bindings
.iter()
.any(|b| b.context == binding.context && b.action == binding.action)
{
self.bindings.push(binding);
}
}
}
pub fn rebind(&mut self, action: &str, keys: impl IntoIterator<Item = Key>) {
let id = format!("{}.{action}", self.context);
self.rebound.insert(id, keys.into_iter().collect());
}
#[must_use]
pub fn rebound(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Keymap {
self.rebind(action, keys);
self
}
pub fn apply(&mut self, overrides: &Overrides) {
for (id, keys) in &overrides.keys {
self.rebound.insert(id.clone(), keys.clone());
}
}
fn source(&self, binding: &Binding) -> (u8, Vec<Key>) {
if self.plain() {
return (0, binding.keys.clone());
}
let id = binding.id();
if let Some(keys) = self.rebound.get(&id) {
return (2, keys.clone());
}
if let Some(keys) = installed_keys(&id) {
return (1, keys);
}
(0, binding.keys.clone())
}
fn plain(&self) -> bool {
self.rebound.is_empty() && !any_installed()
}
fn effective(&self, binding: &Binding) -> Vec<Key> {
if self.plain() {
return binding.keys.clone();
}
let (level, mut keys) = self.source(binding);
if level < 2 {
for other in &self.bindings {
if other.context != binding.context || other.action == binding.action {
continue;
}
let (other_level, taken) = self.source(other);
if other_level > level {
keys.retain(|key| !taken.contains(key));
}
}
}
keys
}
fn find(&self, key: Key) -> Option<usize> {
self.find_by(|keys| keys.contains(&key))
.or_else(|| self.find_by(|keys| key.matches_any(keys)))
}
fn find_by(&self, fires: impl Fn(&[Key]) -> bool) -> Option<usize> {
if self.plain() {
return (self.bindings.iter())
.position(|binding| !binding.sequence && fires(&binding.keys));
}
let mut found: Option<(u8, usize)> = None;
for (index, binding) in self.bindings.iter().enumerate() {
if binding.sequence {
continue;
}
let (level, _) = self.source(binding);
if found.is_some_and(|(best, _)| best >= level) {
continue;
}
if fires(&self.effective(binding)) {
found = Some((level, index));
}
}
found.map(|(_, index)| index)
}
pub fn lookup(&self, key: Key) -> Option<Binding> {
self.find(key).map(|index| {
let binding = &self.bindings[index];
Binding {
keys: self.effective(binding),
..binding.clone()
}
})
}
pub fn action(&self, key: Key) -> Option<&str> {
self.find(key)
.map(|index| self.bindings[index].action.as_str())
}
pub fn is(&self, key: Key, action: &str) -> bool {
key.matches_any(&self.keys(action))
}
pub fn keys(&self, action: &str) -> Vec<Key> {
self.bindings
.iter()
.find(|binding| binding.action == action)
.map(|binding| self.effective(binding))
.unwrap_or_default()
}
pub fn key(&self, action: &str) -> Option<Key> {
self.keys(action).first().copied()
}
pub fn bindings(&self) -> Vec<Binding> {
self.bindings
.iter()
.map(|binding| Binding {
keys: self.effective(binding),
..binding.clone()
})
.collect()
}
pub fn declared(&self) -> &[Binding] {
&self.bindings
}
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
pub fn len(&self) -> usize {
self.bindings.len()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Overrides {
keys: HashMap<String, Vec<Key>>,
}
impl Overrides {
pub fn new() -> Overrides {
Overrides::default()
}
pub fn set(
&mut self,
context: &str,
action: &str,
keys: impl IntoIterator<Item = Key>,
) -> &mut Self {
self.keys
.insert(format!("{context}.{action}"), keys.into_iter().collect());
self
}
pub fn get(&self, id: &str) -> Option<&[Key]> {
self.keys.get(id).map(Vec::as_slice)
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn from_pairs<I, A, K>(pairs: I) -> Result<Overrides, String>
where
I: IntoIterator<Item = (A, K)>,
A: AsRef<str>,
K: AsRef<str>,
{
let mut overrides = Overrides::new();
for (id, names) in pairs {
let id = id.as_ref().trim();
let Some((context, action)) = id.rsplit_once('.') else {
return Err(format!("{id:?} is not context.action"));
};
if context.is_empty() || action.is_empty() {
return Err(format!("{id:?} is not context.action"));
}
let keys = try_keys(names.as_ref()).map_err(|name| {
if name.contains([',', '"', '\'']) {
format!("{id}: no key in {name:?} (the comma key is `comma`, or quoted: \",\")")
} else {
format!("{id}: unknown key {name:?}")
}
})?;
overrides.set(context, action, keys);
}
Ok(overrides)
}
pub fn parse(text: &str) -> Result<Overrides, String> {
let mut pairs = Vec::new();
for (number, line) in text.lines().enumerate() {
let line = strip_comment(line).trim();
if line.is_empty() {
continue;
}
let Some((id, names)) = line.split_once('=') else {
return Err(format!(
"line {}: expected context.action = keys",
number + 1
));
};
let (id, names) = (id.trim(), names.trim());
if names.is_empty() {
return Err(format!(
"line {}: {id} has no keys (quote a \"#\" key; `none` unbinds)",
number + 1
));
}
let names = if names.eq_ignore_ascii_case("none") {
""
} else {
names
};
pairs.push((id.to_string(), names.to_string()));
}
Overrides::from_pairs(pairs)
}
}
fn strip_comment(line: &str) -> &str {
let mut quote = None;
let mut previous = ' ';
for (index, c) in line.char_indices() {
match quote {
Some(open) if c == open => quote = None,
Some(_) => {}
None if c == '"' || c == '\'' => quote = Some(c),
None if c == '#' && previous.is_whitespace() => return &line[..index],
None => {}
}
previous = c;
}
line
}
static INSTALLED: RwLock<Option<Overrides>> = RwLock::new(None);
pub fn install(overrides: Overrides) {
let mut installed = INSTALLED.write().unwrap_or_else(|e| e.into_inner());
*installed = (!overrides.is_empty()).then_some(overrides);
}
pub fn installed() -> Overrides {
INSTALLED
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
.unwrap_or_default()
}
fn any_installed() -> bool {
let installed = INSTALLED.read().unwrap_or_else(|e| e.into_inner());
installed.as_ref().is_some_and(|keys| !keys.is_empty())
}
fn installed_keys(id: &str) -> Option<Vec<Key>> {
let installed = INSTALLED.read().unwrap_or_else(|e| e.into_inner());
installed.as_ref()?.get(id).map(<[Key]>::to_vec)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_overrides() {
let overrides = Overrides::parse(
"# mine\nselect.down = ctrl+n, down\n\ntabs.next = alt+l # comment\nx.y = none\n",
)
.unwrap();
assert_eq!(
overrides.get("select.down").unwrap(),
[Key::ctrl('n'), Key::new(crate::KeyCode::Down)]
);
assert_eq!(overrides.get("x.y").unwrap(), []);
assert!(Overrides::parse("nodot = up").is_err());
assert!(Overrides::parse("a.b = nokey").is_err());
assert!(Overrides::parse("a.b up").is_err());
}
#[test]
fn installed_overrides_apply_below_local_ones() {
let keymap = Keymap::new("keymap-test-context").bind("go", keys("g"), "go");
let mut overrides = Overrides::new();
overrides.set("keymap-test-context", "go", keys("h"));
install(overrides);
assert_eq!(keymap.action(Key::char('h')), Some("go"));
assert_eq!(keymap.action(Key::char('g')), None);
let local = keymap.clone().rebound("go", keys("j"));
assert_eq!(local.action(Key::char('j')), Some("go"));
install(Overrides::new());
assert_eq!(keymap.action(Key::char('g')), Some("go"));
}
#[test]
fn extends_and_lists() {
let mut outer = Keymap::new("outer").bind("quit", keys("q"), "quit");
let inner = Keymap::new("inner")
.bind("down", keys("down j"), "move down")
.rebound("down", keys("n"));
outer.extend(inner);
let listed: Vec<String> = outer.bindings().iter().map(ToString::to_string).collect();
assert_eq!(listed, ["q quit", "n move down"]);
assert_eq!(outer.lookup(Key::char('n')).unwrap().id(), "inner.down");
}
#[test]
fn a_legacy_key_does_the_action_of_any_key_it_could_be() {
use crate::KeyCode;
let keymap = Keymap::new("legacy-test")
.bind("indent", keys("ctrl+i"), "indent")
.bind("submit", keys("ctrl+m"), "submit");
let tab = Key::new(KeyCode::Tab);
assert_eq!(keymap.action(tab), Some("indent"));
assert!(keymap.is(tab, "indent"));
assert_eq!(keymap.action(Key::new(KeyCode::Enter)), Some("submit"));
assert_eq!(keymap.action(tab.exact()), None);
assert!(!keymap.is(tab.exact(), "indent"));
assert_eq!(keymap.action(Key::ctrl('i').exact()), Some("indent"));
let keymap = keymap.bind("next", keys("tab"), "next field");
assert_eq!(keymap.action(tab), Some("next"));
assert_eq!(keymap.action(Key::ctrl('i')), Some("indent"));
}
#[test]
fn a_rebound_key_moves_from_an_earlier_action() {
let keymap = Keymap::new("move-test")
.bind("next", keys("n"), "next match")
.bind("down", keys("down j"), "scroll down")
.rebound("down", keys("n"));
assert_eq!(keymap.action(Key::char('n')), Some("down"));
assert_eq!(keymap.lookup(Key::char('n')).unwrap().action, "down");
assert_eq!(keymap.keys("next"), []);
assert!(!keymap.is(Key::char('n'), "next"));
let keymap = keymap.rebound("next", keys("m"));
assert_eq!(keymap.action(Key::char('m')), Some("next"));
}
}