mod backend {
pub(crate) use crossterm::event::{KeyCode, KeyModifiers as KeyModifier};
}
use ratatui::{
style::{Color, Style},
text::Span,
};
use serde_derive::{Deserialize, Serialize};
use std::{fmt, rc::Rc, str::FromStr};
#[derive(Debug, Serialize, Deserialize)]
pub struct AppKeybinds {
pub select_up: KeyBinding,
pub select_down: KeyBinding,
pub collapse: KeyBinding,
pub expand: KeyBinding,
pub install: KeyBinding,
pub execute: KeyBinding,
pub add_alias: KeyBinding,
pub quit: KeyBinding,
}
impl Default for AppKeybinds {
fn default() -> Self {
AppKeybinds {
select_up: "k".parse().unwrap(),
select_down: "j".parse().unwrap(),
collapse: "h".parse().unwrap(),
expand: "l".parse().unwrap(),
install: "i".parse().unwrap(),
execute: "e".parse().unwrap(),
add_alias: "a".parse().unwrap(),
quit: "q".parse().unwrap(),
}
}
}
impl ratatui::widgets::StatefulWidget for &mut &'static AppKeybinds {
type State = Option<(Rc<cnf_lib::Query>, usize)>;
fn render(
self,
area: ratatui::layout::Rect,
buf: &mut ratatui::buffer::Buffer,
state: &mut Self::State,
) {
let key_style = Style::default().fg(Color::Red);
let separator_style = Style::default().add_modifier(ratatui::style::Modifier::BOLD);
let mut span_vec = vec![
Span::styled(self.quit.to_string(), key_style),
Span::raw(" quit"),
Span::styled(" | ", separator_style),
Span::styled(self.select_down.to_string(), key_style),
Span::raw("/"),
Span::styled(self.select_up.to_string(), key_style),
Span::raw(" select down/up"),
Span::styled(" | ", separator_style),
Span::styled(self.collapse.to_string(), key_style),
Span::raw("/"),
Span::styled(self.expand.to_string(), key_style),
Span::raw(" collapse/expand"),
];
if let Some((query, index)) = state {
if let Some(candidate) = query.results.as_ref().unwrap().get(*index)
&& candidate.actions.install.is_some()
{
span_vec.append(&mut vec![
Span::styled(" | ", separator_style),
Span::styled(self.install.to_string(), key_style),
Span::raw(" install"),
]);
};
span_vec.append(&mut vec![
Span::styled(" | ", separator_style),
Span::styled(self.execute.to_string(), key_style),
Span::raw(" execute"),
]);
span_vec.append(&mut vec![
Span::styled(" | ", separator_style),
Span::styled(self.add_alias.to_string(), key_style),
Span::raw(" add alias"),
]);
};
let spans = ratatui::text::Spans::from(span_vec);
let text = ratatui::widgets::Paragraph::new(ratatui::text::Text::from(spans))
.alignment(ratatui::layout::Alignment::Center)
.wrap(ratatui::widgets::Wrap { trim: true });
use ratatui::widgets::Widget;
text.render(area, buf)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KeyCode {
Char(u8),
Function(u8),
Space,
Enter,
Delete,
ArrowLeft,
ArrowRight,
ArrowDown,
ArrowUp,
Home,
End,
PageUp,
PageDown,
Tab,
Escape,
Plus,
Minus,
}
impl From<KeyCode> for backend::KeyCode {
fn from(value: KeyCode) -> Self {
use KeyCode as Cnf;
use backend::KeyCode as Backend;
match value {
Cnf::Char(val) => Backend::Char(val as char),
Cnf::Function(val) => Backend::F(val),
Cnf::Space => Backend::Char(' '),
Cnf::Enter => Backend::Enter,
Cnf::Delete => Backend::Delete,
Cnf::ArrowLeft => Backend::Left,
Cnf::ArrowDown => Backend::Down,
Cnf::ArrowUp => Backend::Up,
Cnf::ArrowRight => Backend::Right,
Cnf::Home => Backend::Home,
Cnf::End => Backend::End,
Cnf::PageUp => Backend::PageUp,
Cnf::PageDown => Backend::PageDown,
Cnf::Tab => Backend::Tab,
Cnf::Escape => Backend::Esc,
Cnf::Plus => Backend::Char('+'),
Cnf::Minus => Backend::Char('-'),
}
}
}
impl fmt::Display for KeyCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Char(val) => write!(f, "{}", *val as char),
Self::Function(val) => write!(f, "F{}", val),
Self::Space => write!(f, "space"),
Self::Enter => write!(f, "enter"),
Self::Delete => write!(f, "delete"),
Self::ArrowLeft => write!(f, "←"),
Self::ArrowDown => write!(f, "↓"),
Self::ArrowUp => write!(f, "↑"),
Self::ArrowRight => write!(f, "→"),
Self::Home => write!(f, "home"),
Self::End => write!(f, "end"),
Self::PageUp => write!(f, "pageup"),
Self::PageDown => write!(f, "pagedown"),
Self::Tab => write!(f, "tab"),
Self::Escape => write!(f, "esc"),
Self::Plus => write!(f, "+"),
Self::Minus => write!(f, "-"),
}
}
}
impl FromStr for KeyCode {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.to_ascii_lowercase();
let s = s.trim();
match s {
"space" => Ok(Self::Space),
"enter" => Ok(Self::Enter),
"del" | "delete" => Ok(Self::Delete),
"←" | "left" | "arrowleft" => Ok(Self::ArrowLeft),
"↓" | "down" | "arrowdown" => Ok(Self::ArrowDown),
"↑" | "up" | "arrowup" => Ok(Self::ArrowUp),
"→" | "right" | "arrowright" => Ok(Self::ArrowRight),
"home" | "pos1" => Ok(Self::Home),
"end" => Ok(Self::End),
"pagedown" | "pgdown" | "pgdn" => Ok(Self::PageDown),
"pageup" | "pgup" => Ok(Self::PageUp),
"tab" => Ok(Self::Tab),
"esc" | "escape" => Ok(Self::Escape),
"plus" => Ok(Self::Plus),
"minus" => Ok(Self::Minus),
"f1" | "f2" | "f3" | "f4" | "f5" | "f6" | "f7" | "f8" | "f9" | "f10" | "f11"
| "f12" => {
let num = (s[1..]).parse::<u8>().unwrap();
Ok(Self::Function(num))
}
_ => {
if s.is_ascii() && s.len() == 1 {
let char = s.chars().next().expect("tried parsing from empty string");
Ok(Self::Char(char as u8))
} else {
anyhow::bail!("failed to parse '{}' into a valid key code", s);
}
}
}
}
}
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy)]
pub enum KeyModifier {
Shift,
Control,
Alt,
Super,
Meta,
}
impl From<KeyModifier> for backend::KeyModifier {
fn from(value: KeyModifier) -> Self {
use KeyModifier as Cnf;
use backend::KeyModifier as Backend;
match value {
Cnf::Shift => Backend::SHIFT,
Cnf::Control => Backend::CONTROL,
Cnf::Alt => Backend::ALT,
Cnf::Super => Backend::SUPER,
Cnf::Meta => Backend::META,
}
}
}
impl fmt::Display for KeyModifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let output = match self {
Self::Shift => "shift",
Self::Control => "ctrl",
Self::Alt => "alt",
Self::Super => "super",
Self::Meta => "meta",
};
write!(f, "{}", output)
}
}
impl FromStr for KeyModifier {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.is_ascii() {
anyhow::bail!("cannot parse modifier keys from non-ascii input");
}
let s = s.to_ascii_lowercase();
let s = s.trim();
match s {
"shift" => Ok(Self::Shift),
"ctrl" | "control" => Ok(Self::Control),
"alt" => Ok(Self::Alt),
"super" => Ok(Self::Super),
"meta" => Ok(Self::Meta),
_ => anyhow::bail!("failed to parse '{}' into a valid modifier key", s),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[doc(hidden)]
#[serde(try_from = "String", into = "String")]
pub struct KeyBinding {
key: KeyCode,
modifiers: Vec<KeyModifier>,
}
impl PartialEq<crossterm::event::KeyEvent> for KeyBinding {
fn eq(&self, other: &crossterm::event::KeyEvent) -> bool {
let mut modifiers = backend::KeyModifier::NONE;
for m in self.modifiers.clone() {
modifiers |= m.into();
}
let keycode = if self.modifiers.contains(&KeyModifier::Shift) {
match self.key {
KeyCode::Char(val) => KeyCode::Char(val.to_ascii_uppercase()),
_ => self.key,
}
} else {
self.key
};
(backend::KeyCode::from(keycode) == other.code) && (modifiers == other.modifiers)
}
}
impl From<KeyBinding> for crossterm::event::KeyEvent {
fn from(value: KeyBinding) -> Self {
let mut modifiers = backend::KeyModifier::NONE;
for m in value.modifiers {
modifiers |= m.into();
}
crossterm::event::KeyEvent::new_with_kind(
value.key.into(),
modifiers,
crossterm::event::KeyEventKind::Press,
)
}
}
impl fmt::Display for KeyBinding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut modifiers = self.modifiers.clone();
modifiers.sort();
modifiers.dedup();
let key = if let KeyCode::Char(char) = self.key {
if self.modifiers.contains(&KeyModifier::Shift) && char.is_ascii_lowercase() {
modifiers.retain(|elem| elem != &KeyModifier::Shift);
(char as char).to_ascii_uppercase().to_string()
} else {
self.key.to_string()
}
} else {
self.key.to_string()
};
for m in modifiers {
write!(f, "{}+", m)?;
}
write!(f, "{}", key)
}
}
impl From<KeyBinding> for String {
fn from(value: KeyBinding) -> Self {
value.to_string()
}
}
impl FromStr for KeyBinding {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut key: Option<KeyCode> = None;
let mut modifiers: Vec<KeyModifier> = vec![];
for pat in s.split('+') {
if let Ok(current) = KeyCode::from_str(pat) {
if let Some(previous) = key {
anyhow::bail!(
"found multiple keys in binding: previous: '{}', current: '{}'",
previous,
current
);
} else {
key.replace(current);
}
continue;
}
if let Ok(modkey) = KeyModifier::from_str(pat) {
if modifiers.contains(&modkey) {
anyhow::bail!("modifier key '{}' exists multiple times in binding", modkey);
} else {
modifiers.push(modkey);
}
continue;
}
anyhow::bail!(
"invalid input '{}' is not recognized as bare key or modifier",
pat
);
}
if let Some(key) = key {
modifiers.sort();
Ok(KeyBinding { key, modifiers })
} else {
anyhow::bail!("valid key bindings must contain exactly one non-modifier key");
}
}
}
impl TryFrom<String> for KeyBinding {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::from_str(&value[..])
}
}
#[cfg(test)]
mod tests {
use super::*;
use KeyCode as Key;
use KeyModifier as Mod;
macro_rules! bind {
( $string:expr ) => {
KeyBinding::from_str($string)
};
}
mod assert {
#[macro_export]
macro_rules! key {
( $bind:ident, $key:expr ) => {
assert_eq!($bind.key, $key);
};
}
pub(super) use key;
#[macro_export]
macro_rules! mods {
( $bind:ident, $mod:expr ) => {
assert!($bind.modifiers.contains(&$mod));
};
}
pub(super) use mods;
#[macro_export]
macro_rules! num_mods {
( $bind:ident, $amount:expr ) => {
assert_eq!($bind.modifiers.len(), $amount);
};
}
pub(super) use num_mods;
#[macro_export]
macro_rules! err {
( $result:expr ) => {
assert!($result.is_err());
};
}
pub(super) use err;
}
#[test]
fn ascii_is_char() {
let bind = bind!("a").unwrap();
assert_eq!(bind.to_string(), "a".to_string());
}
#[test]
fn ascii_uppercase_is_ignored() {
let bind = bind!("A").unwrap();
assert::key!(bind, Key::Char(b'a'));
assert::num_mods!(bind, 0);
}
#[test]
fn deny_bogus() {
let bind = bind!("foo");
assert::err!(bind);
}
#[test]
fn deny_modifiers_only() {
let bind = bind!("ctrl");
assert::err!(bind);
}
#[test]
fn char_with_one_modifier() {
let bind = bind!("shift + a").unwrap();
assert::key!(bind, Key::Char(b'a'));
assert::num_mods!(bind, 1);
assert::mods!(bind, Mod::Shift);
}
#[test]
fn char_with_three_modifiers() {
let bind = bind!("alt + shift + ctrl + a").unwrap();
assert::key!(bind, Key::Char(b'a'));
assert::mods!(bind, Mod::Shift);
assert::mods!(bind, Mod::Alt);
assert::mods!(bind, Mod::Control);
}
#[test]
fn char_with_modifier_doubled() {
let bind = bind!("alt + alt + a");
assert::err!(bind);
}
#[test]
fn multiple_chars() {
let bind = bind!("a + b");
assert::err!(bind);
}
#[test]
fn whitespace_dont_matter() {
let bind = bind!(" a ").unwrap();
assert::key!(bind, Key::Char(b'a'));
assert::num_mods!(bind, 0);
}
#[test]
fn leading_stray_plus() {
let bind = bind!("+ a");
assert::err!(bind);
}
#[test]
fn trailing_stray_plus() {
let bind = bind!("a +");
assert::err!(bind);
}
#[test]
fn single_plus_char() {
let bind = bind!("+");
assert::err!(bind);
}
#[test]
fn order_is_irrelevant() {
let bind = bind!("a + ctrl").unwrap();
assert::key!(bind, Key::Char(b'a'));
assert::mods!(bind, Mod::Control);
assert::num_mods!(bind, 1);
}
#[test]
fn casing_is_irrelevant() {
let bind = bind!("Ctrl + SHiFT + A").unwrap();
assert::key!(bind, Key::Char(b'a'));
assert::mods!(bind, Mod::Control);
assert::mods!(bind, Mod::Shift);
assert::num_mods!(bind, 2);
}
#[test]
fn upper_char_requires_shift() {
let bind = bind!("shift + a").unwrap();
assert_eq!(bind.to_string(), "A".to_string());
assert::key!(bind, Key::Char(b'a'));
assert::mods!(bind, Mod::Shift);
assert::num_mods!(bind, 1);
}
}