mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-26 03:35:17 +00:00
ece4ebaf72
We don't yet support all keys from https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions Instead of displaying a private-use character, show the character code; this can be used to map the key even if we don't know a name for it. bind \uE011 'echo print screen' bind ctrl-\uE011 'echo do control + print screen' Note that it's also possible to mape the raw CSI u sequence, like bind \e\[57361u 'echo print screen' but we should not encourage that syntax because it does not allow adding the modifiers like ctrl. Of course leaking the PUA character code is not ideal.
436 lines
13 KiB
Rust
436 lines
13 KiB
Rust
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}';
|
|
pub const Invalid: char = '\u{F50f}';
|
|
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 {
|
|
if c == ascii_control('m') {
|
|
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;
|
|
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}' {
|
|
// ASCII characters that are not control characters
|
|
ascii_printable_to_symbol(buf, c);
|
|
} 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));
|
|
} 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
|
|
}
|