use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use ratatui::style::Style;
use ratatui::text::Span;
#[cfg(target_os = "macos")]
const ALT_PREFIX: &str = "⌥ + ";
#[cfg(not(target_os = "macos"))]
const ALT_PREFIX: &str = "alt + ";
const CTRL_PREFIX: &str = "ctrl + ";
const SHIFT_PREFIX: &str = "shift + ";
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct KeyBinding {
key: KeyCode,
modifiers: KeyModifiers,
}
impl KeyBinding {
pub const fn new(key: KeyCode, modifiers: KeyModifiers) -> Self {
Self { key, modifiers }
}
pub fn from_event(event: KeyEvent) -> Self {
let (key, modifiers) = normalize_key_parts(event.code, event.modifiers);
Self { key, modifiers }
}
pub fn is_press(&self, event: KeyEvent) -> bool {
normalize_key_parts(self.key, self.modifiers)
== normalize_key_parts(event.code, event.modifiers)
&& (event.kind == KeyEventKind::Press || event.kind == KeyEventKind::Repeat)
}
pub const fn parts(&self) -> (KeyCode, KeyModifiers) {
(self.key, self.modifiers)
}
pub fn display_label(&self) -> String {
let modifiers = modifiers_to_string(self.modifiers);
let key = match self.key {
KeyCode::Enter => "enter".to_string(),
KeyCode::Char(' ') => "space".to_string(),
KeyCode::Up => "↑".to_string(),
KeyCode::Down => "↓".to_string(),
KeyCode::Left => "←".to_string(),
KeyCode::Right => "→".to_string(),
KeyCode::PageUp => "pgup".to_string(),
KeyCode::PageDown => "pgdn".to_string(),
_ => self.key.to_string().to_ascii_lowercase(),
};
format!("{modifiers}{key}")
}
}
pub fn normalize_key_parts(key: KeyCode, mut modifiers: KeyModifiers) -> (KeyCode, KeyModifiers) {
let KeyCode::Char(ch) = key else {
return (key, modifiers);
};
if modifiers.is_empty() {
if let Some(ctrl_char) = c0_control_char_to_ctrl_char(ch) {
return (KeyCode::Char(ctrl_char), KeyModifiers::CONTROL | modifiers);
}
}
if ch.is_ascii_uppercase() {
modifiers.insert(KeyModifiers::SHIFT);
return (KeyCode::Char(ch.to_ascii_lowercase()), modifiers);
}
(key, modifiers)
}
fn c0_control_char_to_ctrl_char(ch: char) -> Option<char> {
let code = u32::from(ch);
match code {
0x00 => Some(' '),
0x01..=0x1a => char::from_u32(code - 0x01 + u32::from('a')),
0x1c..=0x1f => char::from_u32(code - 0x1c + u32::from('4')),
_ => None,
}
}
pub trait KeyBindingListExt {
fn is_pressed(&self, event: KeyEvent) -> bool;
}
impl KeyBindingListExt for [KeyBinding] {
fn is_pressed(&self, event: KeyEvent) -> bool {
self.iter().any(|binding| binding.is_press(event))
}
}
pub fn is_plain_text_key_event(event: KeyEvent) -> bool {
matches!(
event,
KeyEvent {
code: KeyCode::Char(ch),
modifiers,
..
} if !ch.is_ascii_control()
&& !modifiers.contains(KeyModifiers::CONTROL)
&& !modifiers.contains(KeyModifiers::ALT)
)
}
pub const fn plain(key: KeyCode) -> KeyBinding {
KeyBinding::new(key, KeyModifiers::NONE)
}
pub const fn alt(key: KeyCode) -> KeyBinding {
KeyBinding::new(key, KeyModifiers::ALT)
}
pub const fn shift(key: KeyCode) -> KeyBinding {
KeyBinding::new(key, KeyModifiers::SHIFT)
}
pub const fn ctrl(key: KeyCode) -> KeyBinding {
KeyBinding::new(key, KeyModifiers::CONTROL)
}
pub const fn ctrl_alt(key: KeyCode) -> KeyBinding {
KeyBinding::new(key, KeyModifiers::CONTROL.union(KeyModifiers::ALT))
}
fn modifiers_to_string(modifiers: KeyModifiers) -> String {
let mut result = String::new();
if modifiers.contains(KeyModifiers::CONTROL) {
result.push_str(CTRL_PREFIX);
}
if modifiers.contains(KeyModifiers::SHIFT) {
result.push_str(SHIFT_PREFIX);
}
if modifiers.contains(KeyModifiers::ALT) {
result.push_str(ALT_PREFIX);
}
result
}
impl From<KeyBinding> for Span<'static> {
fn from(binding: KeyBinding) -> Self {
(&binding).into()
}
}
impl From<&KeyBinding> for Span<'static> {
fn from(binding: &KeyBinding) -> Self {
Span::styled(binding.display_label(), key_hint_style())
}
}
fn key_hint_style() -> Style {
Style::default().dim()
}
pub fn has_ctrl_or_alt(mods: KeyModifiers) -> bool {
(mods.contains(KeyModifiers::CONTROL) || mods.contains(KeyModifiers::ALT)) && !is_altgr(mods)
}
#[cfg(windows)]
#[inline]
pub fn is_altgr(mods: KeyModifiers) -> bool {
mods.contains(KeyModifiers::ALT) && mods.contains(KeyModifiers::CONTROL)
}
#[cfg(not(windows))]
#[inline]
pub fn is_altgr(_mods: KeyModifiers) -> bool {
false
}