2024-03-30 23:10:12 +08:00
|
|
|
use std::ops;
|
|
|
|
use std::rc::Rc;
|
|
|
|
|
|
|
|
use libc::VERASE;
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
fallback::fish_wcwidth, reader::TERMINAL_MODE_ON_STARTUP, wchar::prelude::*, wutil::fish_wcstoi,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub(crate) const Backspace: char = '\u{F500}'; // below ENCODE_DIRECT_BASE
|
|
|
|
pub(crate) const Delete: char = '\u{F501}';
|
|
|
|
pub(crate) const Escape: char = '\u{F502}';
|
|
|
|
pub(crate) const Enter: char = '\u{F503}';
|
|
|
|
pub(crate) const Up: char = '\u{F504}';
|
|
|
|
pub(crate) const Down: char = '\u{F505}';
|
|
|
|
pub(crate) const Left: char = '\u{F506}';
|
|
|
|
pub(crate) const Right: char = '\u{F507}';
|
|
|
|
pub(crate) const PageUp: char = '\u{F508}';
|
|
|
|
pub(crate) const PageDown: char = '\u{F509}';
|
|
|
|
pub(crate) const Home: char = '\u{F50a}';
|
|
|
|
pub(crate) const End: char = '\u{F50b}';
|
|
|
|
pub(crate) const Insert: char = '\u{F50c}';
|
|
|
|
pub(crate) const Tab: char = '\u{F50d}';
|
|
|
|
pub(crate) const Space: char = '\u{F50e}';
|
2024-04-02 22:24:37 +08:00
|
|
|
pub const Invalid: char = '\u{F50f}';
|
2024-03-30 23:10:12 +08:00
|
|
|
pub(crate) fn function_key(n: u32) -> char {
|
|
|
|
assert!((1..=12).contains(&n));
|
|
|
|
char::from_u32(u32::from(Invalid) + n).unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
const NAMED_KEYS_RANGE: ops::Range<u32> = 0xF500..(Invalid as u32 + 12);
|
|
|
|
|
|
|
|
const KEY_NAMES: &[(char, &wstr)] = &[
|
|
|
|
('+', L!("plus")),
|
|
|
|
('-', L!("minus")),
|
|
|
|
(',', L!("comma")),
|
|
|
|
(Backspace, L!("backspace")),
|
|
|
|
(Delete, L!("delete")),
|
|
|
|
(Escape, L!("escape")),
|
|
|
|
(Enter, L!("enter")),
|
|
|
|
(Up, L!("up")),
|
|
|
|
(Down, L!("down")),
|
|
|
|
(Left, L!("left")),
|
|
|
|
(Right, L!("right")),
|
|
|
|
(PageUp, L!("pageup")),
|
|
|
|
(PageDown, L!("pagedown")),
|
|
|
|
(Home, L!("home")),
|
|
|
|
(End, L!("end")),
|
|
|
|
(Insert, L!("insert")),
|
|
|
|
(Tab, L!("tab")),
|
|
|
|
(Space, L!("space")),
|
|
|
|
];
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
|
|
pub struct Modifiers {
|
|
|
|
pub ctrl: bool,
|
|
|
|
pub alt: bool,
|
|
|
|
pub shift: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Modifiers {
|
|
|
|
const fn new() -> Self {
|
|
|
|
Modifiers {
|
|
|
|
ctrl: false,
|
|
|
|
alt: false,
|
|
|
|
shift: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
pub(crate) fn is_some(&self) -> bool {
|
|
|
|
self.ctrl || self.alt || self.shift
|
|
|
|
}
|
|
|
|
pub(crate) fn is_none(&self) -> bool {
|
|
|
|
!self.is_some()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
pub struct Key {
|
|
|
|
pub modifiers: Modifiers,
|
|
|
|
pub codepoint: char,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Key {
|
|
|
|
pub(crate) fn from_raw(codepoint: char) -> Self {
|
|
|
|
Self {
|
|
|
|
modifiers: Modifiers::default(),
|
|
|
|
codepoint,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) const fn ctrl(codepoint: char) -> Key {
|
|
|
|
let mut modifiers = Modifiers::new();
|
|
|
|
modifiers.ctrl = true;
|
|
|
|
Key {
|
|
|
|
modifiers,
|
|
|
|
codepoint,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) const fn alt(codepoint: char) -> Key {
|
|
|
|
let mut modifiers = Modifiers::new();
|
|
|
|
modifiers.alt = true;
|
|
|
|
Key {
|
|
|
|
modifiers,
|
|
|
|
codepoint,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) const fn shift(codepoint: char) -> Key {
|
|
|
|
let mut modifiers = Modifiers::new();
|
|
|
|
modifiers.shift = true;
|
|
|
|
Key {
|
|
|
|
modifiers,
|
|
|
|
codepoint,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Key {
|
|
|
|
pub fn from_single_char(c: char) -> Self {
|
|
|
|
u8::try_from(c)
|
|
|
|
.map(Key::from_single_byte)
|
|
|
|
.unwrap_or(Key::from_raw(c))
|
|
|
|
}
|
|
|
|
pub fn from_single_byte(c: u8) -> Self {
|
|
|
|
canonicalize_control_char(c).unwrap_or(Key::from_raw(char::from(c)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn canonicalize_control_char(c: u8) -> Option<Key> {
|
|
|
|
let codepoint = canonicalize_keyed_control_char(char::from(c));
|
|
|
|
if u32::from(codepoint) > 255 {
|
|
|
|
return Some(Key {
|
|
|
|
modifiers: Modifiers::default(),
|
|
|
|
codepoint,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if c < 32 {
|
|
|
|
return Some(ctrl(canonicalize_unkeyed_control_char(c)));
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
fn ascii_control(c: char) -> char {
|
|
|
|
char::from_u32(u32::from(c) & 0o37).unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn canonicalize_keyed_control_char(c: char) -> char {
|
2024-04-02 23:47:56 +08:00
|
|
|
if c == ascii_control('m') {
|
2024-03-30 23:10:12 +08:00
|
|
|
return Enter;
|
|
|
|
}
|
|
|
|
if c == ascii_control('i') {
|
|
|
|
return Tab;
|
|
|
|
}
|
|
|
|
if c == ' ' {
|
|
|
|
return Space;
|
|
|
|
}
|
|
|
|
if c == char::from(TERMINAL_MODE_ON_STARTUP.lock().unwrap().c_cc[VERASE]) {
|
|
|
|
return Backspace;
|
|
|
|
}
|
|
|
|
if c == char::from(127) {
|
|
|
|
// when it's not backspace
|
|
|
|
return Delete;
|
|
|
|
}
|
|
|
|
if c == '\x1b' {
|
|
|
|
return Escape;
|
|
|
|
}
|
|
|
|
c
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn canonicalize_unkeyed_control_char(c: u8) -> char {
|
|
|
|
if c == 0 {
|
|
|
|
// For legacy terminals we have to make a decision here; they send NUL on Ctrl-2,
|
|
|
|
// Ctrl-Shift-2 or Ctrl-Backtick, but the most straightforward way is Ctrl-Space.
|
|
|
|
return Space;
|
|
|
|
}
|
|
|
|
// Represent Ctrl-letter combinations in lower-case, to be clear
|
|
|
|
// that Shift is not involved.
|
|
|
|
if c < 27 {
|
|
|
|
return char::from(c - 1 + b'a');
|
|
|
|
}
|
|
|
|
// Represent Ctrl-symbol combinations in "upper-case", as they are
|
|
|
|
// traditionally-rendered.
|
|
|
|
assert!(c < 32);
|
|
|
|
return char::from(c - 1 + b'A');
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn canonicalize_key(mut key: Key) -> Result<Key, WString> {
|
|
|
|
// Leave raw escapes to disambiguate from named escape.
|
|
|
|
if key.codepoint != '\x1b' {
|
|
|
|
key.codepoint = canonicalize_keyed_control_char(key.codepoint);
|
|
|
|
if key.codepoint < ' ' {
|
|
|
|
key.codepoint = canonicalize_unkeyed_control_char(u8::try_from(key.codepoint).unwrap());
|
|
|
|
if key.modifiers.ctrl {
|
|
|
|
return Err(wgettext_fmt!(
|
|
|
|
"Cannot add control modifier to control character '%s'",
|
|
|
|
key
|
|
|
|
));
|
|
|
|
}
|
|
|
|
key.modifiers.ctrl = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if key.modifiers.shift {
|
|
|
|
if key.codepoint.is_ascii_alphabetic() {
|
|
|
|
// Shift + ASCII letters is just the uppercase letter.
|
|
|
|
key.modifiers.shift = false;
|
|
|
|
key.codepoint = key.codepoint.to_ascii_uppercase();
|
|
|
|
} else if !NAMED_KEYS_RANGE.contains(&u32::from(key.codepoint)) {
|
|
|
|
// Shift + any other printable character is not allowed.
|
|
|
|
return Err(wgettext_fmt!(
|
|
|
|
"Shift modifier is only supported on special keys and lowercase ASCII, not '%s'",
|
|
|
|
key,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(key)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub const KEY_SEPARATOR: char = ',';
|
|
|
|
|
|
|
|
pub(crate) fn parse_keys(value: &wstr) -> Result<Vec<Key>, WString> {
|
|
|
|
let mut res = vec![];
|
|
|
|
if value.is_empty() {
|
|
|
|
return Ok(res);
|
|
|
|
}
|
|
|
|
let first = value.as_char_slice()[0];
|
|
|
|
if value.len() == 1 {
|
|
|
|
// Hack: allow singular comma.
|
|
|
|
res.push(canonicalize_key(Key::from_raw(first)).unwrap());
|
|
|
|
} else if (value.len() == 2
|
|
|
|
&& !value.contains('-')
|
|
|
|
&& !value.contains(KEY_SEPARATOR)
|
|
|
|
&& !KEY_NAMES.iter().any(|(_codepoint, name)| name == value))
|
|
|
|
|| (first == '\x1b' || first == ascii_control(first))
|
|
|
|
{
|
|
|
|
// Hack: treat as legacy syntax (meaning: not comma separated) if
|
|
|
|
// 1. it doesn't contain '-' or ',' and is short enough to probably not be a key name.
|
|
|
|
// 2. it starts with raw escape (\e) or a raw ASCII control character (\c).
|
|
|
|
for c in value.chars() {
|
|
|
|
res.push(canonicalize_key(Key::from_raw(c)).unwrap());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for full_key_name in value.split(KEY_SEPARATOR) {
|
|
|
|
if full_key_name == "-" {
|
|
|
|
// Hack: allow singular minus.
|
|
|
|
res.push(canonicalize_key(Key::from_raw('-')).unwrap());
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let mut modifiers = Modifiers::default();
|
|
|
|
let num_keys = full_key_name.split('-').count();
|
|
|
|
let mut components = full_key_name.split('-');
|
|
|
|
for _i in 0..num_keys.checked_sub(1).unwrap() {
|
|
|
|
let modifier = components.next().unwrap();
|
|
|
|
match modifier {
|
|
|
|
_ if modifier == "ctrl" => modifiers.ctrl = true,
|
|
|
|
_ if modifier == "alt" => modifiers.alt = true,
|
|
|
|
_ if modifier == "shift" => modifiers.shift = true,
|
|
|
|
_ => {
|
|
|
|
return Err(wgettext_fmt!(
|
|
|
|
"unknown modififer '%s' in '%s'",
|
|
|
|
modifier,
|
|
|
|
full_key_name
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let key_name = components.next().unwrap();
|
|
|
|
let codepoint = KEY_NAMES
|
|
|
|
.iter()
|
|
|
|
.find_map(|(codepoint, name)| (name == key_name).then_some(*codepoint))
|
|
|
|
.or_else(|| (key_name.len() == 1).then(|| key_name.as_char_slice()[0]));
|
|
|
|
let key = if let Some(codepoint) = codepoint {
|
|
|
|
canonicalize_key(Key {
|
|
|
|
modifiers,
|
|
|
|
codepoint,
|
|
|
|
})?
|
|
|
|
} else if codepoint.is_none() && key_name.starts_with('F') && key_name.len() <= 3 {
|
|
|
|
let num = key_name.strip_prefix('F');
|
|
|
|
let codepoint = match fish_wcstoi(num) {
|
|
|
|
Ok(n) if (1..=12).contains(&n) => function_key(u32::try_from(n).unwrap()),
|
|
|
|
_ => {
|
|
|
|
return Err(wgettext_fmt!(
|
|
|
|
"only F1 through F12 are supported, not '%s'",
|
|
|
|
num,
|
|
|
|
full_key_name
|
|
|
|
));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Key {
|
|
|
|
modifiers,
|
|
|
|
codepoint,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Err(wgettext_fmt!("cannot parse key '%s'", full_key_name));
|
|
|
|
};
|
|
|
|
res.push(key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(canonicalize_raw_escapes(res))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn canonicalize_raw_escapes(keys: Vec<Key>) -> Vec<Key> {
|
|
|
|
// Historical bindings use \ek to mean alt-k. Canonicalize them.
|
|
|
|
if !keys.iter().any(|key| key.codepoint == '\x1b') {
|
|
|
|
return keys;
|
|
|
|
}
|
|
|
|
let mut canonical = vec![];
|
|
|
|
let mut had_literal_escape = false;
|
|
|
|
for mut key in keys {
|
|
|
|
if had_literal_escape {
|
|
|
|
had_literal_escape = false;
|
|
|
|
if key.modifiers.alt {
|
|
|
|
canonical.push(Key::from_raw(Escape));
|
|
|
|
} else {
|
|
|
|
key.modifiers.alt = true;
|
|
|
|
if key.codepoint == '\x1b' {
|
|
|
|
key.codepoint = Escape;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if key.codepoint == '\x1b' {
|
|
|
|
had_literal_escape = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
canonical.push(key);
|
|
|
|
}
|
|
|
|
if had_literal_escape {
|
|
|
|
canonical.push(Key::from_raw(Escape));
|
|
|
|
}
|
|
|
|
canonical
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Key {
|
|
|
|
pub(crate) fn codepoint_text(&self) -> Option<char> {
|
|
|
|
if self.modifiers.is_some() {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
let c = self.codepoint;
|
|
|
|
if c == Space {
|
|
|
|
return Some(' ');
|
|
|
|
}
|
|
|
|
if c == Enter {
|
|
|
|
return Some('\n');
|
|
|
|
}
|
|
|
|
if c == Tab {
|
|
|
|
return Some('\t');
|
|
|
|
}
|
|
|
|
if NAMED_KEYS_RANGE.contains(&u32::from(c)) || u32::from(c) <= 27 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
Some(c)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Display for Key {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
WString::from(*self).fmt(f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl printf_compat::args::ToArg<'static> for Key {
|
|
|
|
fn to_arg(self) -> printf_compat::args::Arg<'static> {
|
|
|
|
printf_compat::args::Arg::BoxedStr(Rc::new(WString::from(self).into_boxed_utfstr()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<Key> for WString {
|
|
|
|
fn from(key: Key) -> Self {
|
|
|
|
let name = KEY_NAMES
|
|
|
|
.iter()
|
|
|
|
.find_map(|&(codepoint, name)| (codepoint == key.codepoint).then(|| name.to_owned()))
|
|
|
|
.or_else(|| {
|
|
|
|
(function_key(1)..=function_key(12))
|
|
|
|
.contains(&key.codepoint)
|
|
|
|
.then(|| {
|
|
|
|
sprintf!(
|
|
|
|
"F%d",
|
|
|
|
u32::from(key.codepoint) - u32::from(function_key(1)) + 1
|
|
|
|
)
|
|
|
|
})
|
|
|
|
});
|
|
|
|
let mut res = name.unwrap_or_else(|| char_to_symbol(key.codepoint));
|
|
|
|
|
|
|
|
if key.modifiers.shift {
|
|
|
|
res.insert_utfstr(0, L!("shift-"));
|
|
|
|
}
|
|
|
|
if key.modifiers.alt {
|
|
|
|
res.insert_utfstr(0, L!("alt-"));
|
|
|
|
}
|
|
|
|
if key.modifiers.ctrl {
|
|
|
|
res.insert_utfstr(0, L!("ctrl-"));
|
|
|
|
}
|
|
|
|
|
|
|
|
res
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return true if the character must be escaped when used in the sequence of chars to be bound in
|
|
|
|
/// a `bind` command.
|
|
|
|
fn must_escape(c: char) -> bool {
|
|
|
|
"[]()<>{}*\\?$#;&|'\"".contains(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn ascii_printable_to_symbol(buf: &mut WString, c: char) {
|
|
|
|
if must_escape(c) {
|
|
|
|
sprintf!(=> buf, "\\%c", c);
|
|
|
|
} else {
|
|
|
|
sprintf!(=> buf, "%c", c);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Convert a wide-char to a symbol that can be used in our output.
|
|
|
|
fn char_to_symbol(c: char) -> WString {
|
|
|
|
let mut buff = WString::new();
|
|
|
|
let buf = &mut buff;
|
2024-04-07 14:26:45 +08:00
|
|
|
if c <= ' ' {
|
|
|
|
// Most ascii control characters like \x01 are canonicalized like ctrl-a, except if we
|
|
|
|
// are given the control character directly with CSI u.
|
|
|
|
sprintf!(=> buf, "\\x%02x", u8::try_from(c).unwrap());
|
|
|
|
} else if c < '\u{80}' {
|
2024-03-30 23:10:12 +08:00
|
|
|
// ASCII characters that are not control characters
|
|
|
|
ascii_printable_to_symbol(buf, c);
|
2024-04-08 15:01:07 +08:00
|
|
|
} else if ('\u{e000}'..='\u{f8ff}').contains(&c) {
|
|
|
|
// Unmapped key from https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions
|
|
|
|
sprintf!(=> buf, "\\u%04X", u32::from(c));
|
2024-03-30 23:10:12 +08:00
|
|
|
} else if fish_wcwidth(c) > 0 {
|
|
|
|
sprintf!(=> buf, "%lc", c);
|
|
|
|
} else if c <= '\u{FFFF}' {
|
|
|
|
// BMP Unicode chararacter
|
|
|
|
sprintf!(=> buf, "\\u%04X", u32::from(c));
|
|
|
|
} else {
|
|
|
|
sprintf!(=> buf, "\\U%06X", u32::from(c));
|
|
|
|
}
|
|
|
|
buff
|
|
|
|
}
|