use libc::VERASE;
use crate::{
flog::FloggableDebug, localizable_string, reader::get_terminal_mode_on_startup, wgettext_fmt,
wutil::fish_wcstoul,
};
use fish_common::{escape_string, EscapeFlags, EscapeStringStyle};
use fish_fallback::fish_wcwidth;
use fish_feature_flags::{feature_test, FeatureFlag};
use fish_widestring::{
char_offset, decode_byte_from_char, wstr, WExt as _, WString, L, SPECIAL_KEY_ENCODE_BASE,
};
const fn special_key_char(offset: u8) -> char {
char_offset(SPECIAL_KEY_ENCODE_BASE, offset as u32)
}
macro_rules! define_special_keys {
($($key_name:ident: $offset:expr)*) => {
$(
pub(crate) const $key_name: char = special_key_char($offset);
)*
};
}
define_special_keys! {
BACKSPACE: 0
DELETE: 1
ESCAPE: 2
ENTER: 3
UP: 4
DOWN: 5
LEFT: 6
RIGHT: 7
PAGE_UP: 8
PAGE_DOWN: 9
HOME: 10
END: 11
INSERT: 12
TAB: 13
SPACE: 14
MENU: 15
PRINT_SCREEN: 16
INVALID: 255
}
pub(crate) const MAX_FUNCTION_KEY: u8 = 12;
pub(crate) fn function_key(n: u8) -> char {
assert!((1..=MAX_FUNCTION_KEY).contains(&n));
special_key_char(254 - MAX_FUNCTION_KEY + n)
}
pub(crate) const KEY_NAMES: &[(char, &wstr)] = &[
('-', 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")),
(PAGE_UP, L!("pageup")),
(PAGE_DOWN, L!("pagedown")),
(HOME, L!("home")),
(END, L!("end")),
(INSERT, L!("insert")),
(TAB, L!("tab")),
(SPACE, L!("space")),
(MENU, L!("menu")),
(PRINT_SCREEN, L!("printscreen")),
];
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Modifiers {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub sup: bool,
}
impl Modifiers {
const fn new() -> Self {
Modifiers {
ctrl: false,
alt: false,
shift: false,
sup: false,
}
}
#[cfg(test)]
pub(crate) const CTRL: Self = {
let mut m = Self::new();
m.ctrl = true;
m
};
pub(crate) const ALT: Self = {
let mut m = Self::new();
m.alt = true;
m
};
pub(crate) const SHIFT: Self = {
let mut m = Self::new();
m.shift = true;
m
};
pub(crate) fn is_some(&self) -> bool {
*self != Self::new()
}
pub(crate) fn is_none(&self) -> bool {
*self == Self::new()
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ViewportPosition {
pub x: usize,
pub y: usize,
}
impl FloggableDebug for ViewportPosition {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Key {
pub modifiers: Modifiers,
pub codepoint: char,
}
impl Key {
pub(crate) const fn new(modifiers: Modifiers, codepoint: char) -> Self {
Self {
modifiers,
codepoint,
}
}
pub(crate) fn from_raw(codepoint: char) -> Self {
Self::new(Modifiers::default(), codepoint)
}
}
pub(crate) const fn ctrl(codepoint: char) -> Key {
let mut modifiers = Modifiers::new();
modifiers.ctrl = true;
Key::new(modifiers, codepoint)
}
pub(crate) const fn alt(codepoint: char) -> Key {
let mut modifiers = Modifiers::new();
modifiers.alt = true;
Key::new(modifiers, codepoint)
}
pub(crate) const fn shift(codepoint: char) -> Key {
let mut modifiers = Modifiers::new();
modifiers.shift = true;
Key::new(modifiers, codepoint)
}
impl Key {
pub fn from_single_char(c: char) -> Self {
u8::try_from(c).map_or(Key::from_raw(c), Key::from_single_byte)
}
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::from_raw(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 let Some(tm) = get_terminal_mode_on_startup() {
if c == char::from(tm.c_cc[VERASE]) {
return BACKSPACE;
}
}
if c == char::from(127) {
return DELETE;
}
if c == '\x1b' {
return ESCAPE;
}
c
}
pub(crate) fn canonicalize_unkeyed_control_char(c: u8) -> char {
if c == 0 {
return SPACE;
}
if c < 27 {
return char::from(c - 1 + b'a');
}
assert!(c < 32);
char::from(c - 1 + b'A')
}
pub(crate) fn canonicalize_key(mut key: Key) -> Result<Key, WString> {
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;
}
}
Ok(key)
}
pub const KEY_SEPARATOR: char = ',';
fn escape_nonprintables(key_name: &wstr) -> WString {
escape_string(
key_name,
EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES | EscapeFlags::NO_QUOTED),
)
}
#[allow(clippy::nonminimal_bool)]
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 {
res.push(canonicalize_key(Key::from_raw(first)).unwrap());
} else if ((2..=3).contains(&value.len())
&& !value.contains('-')
&& !value.contains(KEY_SEPARATOR)
&& !KEY_NAMES.iter().any(|(_codepoint, name)| name == value)
&& value.as_char_slice()[0] != 'F'
&& !(value.as_char_slice()[0] == 'f' && value.char_at(1).is_ascii_digit()))
|| first < ' '
{
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 == "-" {
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,
_ if modifier == "super" => modifiers.sup = true,
_ => {
return Err(wgettext_fmt!(
"unknown modifier '%s' in '%s'",
modifier,
escape_nonprintables(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::new(modifiers, codepoint))?
} else if codepoint.is_none() && key_name.starts_with('f') && key_name.len() <= 3 {
let num = key_name.strip_prefix('f').unwrap();
let codepoint = match fish_wcstoul(num) {
Ok(n) if (1..=u64::from(MAX_FUNCTION_KEY)).contains(&n) => {
function_key(u8::try_from(n).unwrap())
}
_ => {
return Err(wgettext_fmt!(
"only f1 through f%d are supported, not 'f%s'",
MAX_FUNCTION_KEY,
num,
));
}
};
Key::new(modifiers, codepoint)
} else {
return Err(wgettext_fmt!(
"cannot parse key '%s'",
escape_nonprintables(full_key_name)
));
};
res.push(key);
}
}
Ok(canonicalize_raw_escapes(res))
}
pub(crate) fn canonicalize_raw_escapes(keys: Vec<Key>) -> Vec<Key> {
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 std::fmt::Display for Key {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
WString::from(*self).fmt(f)
}
}
impl fish_printf::ToArg<'static> for Key {
fn to_arg(self) -> fish_printf::Arg<'static> {
fish_printf::Arg::WString(self.into())
}
}
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(MAX_FUNCTION_KEY))
.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, key.modifiers.is_none()));
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-"));
}
if key.modifiers.sup {
res.insert_utfstr(0, L!("super-"));
}
res
}
}
fn ctrl_to_symbol(buf: &mut WString, c: char) {
let c = u8::try_from(c).unwrap();
let symbolic_name = match c {
9 => L!("\\t"),
13 => L!("\\r"),
27 => L!("\\e"),
_ => return sprintf!(=> buf, "\\x%02x", c),
};
buf.push_utfstr(symbolic_name);
}
fn must_escape(is_first_in_token: bool, c: char) -> bool {
"[]()<>{}*\\$;&|'\"".contains(c)
|| (is_first_in_token && "~#".contains(c))
|| (c == '?' && !feature_test(FeatureFlag::QuestionMarkNoGlob))
}
fn ascii_printable_to_symbol(buf: &mut WString, is_first_in_token: bool, c: char) {
if must_escape(is_first_in_token, c) {
sprintf!(=> buf, "\\%c", c);
} else {
sprintf!(=> buf, "%c", c);
}
}
pub fn char_to_symbol(c: char, is_first_in_token: bool) -> WString {
let mut buff = WString::new();
let buf = &mut buff;
if c <= ' ' || c == '\x7F' {
ctrl_to_symbol(buf, c);
} else if c < '\u{80}' {
ascii_printable_to_symbol(buf, is_first_in_token, c);
} else if let Some(byte) = decode_byte_from_char(c) {
sprintf!(=> buf, "\\x%02x", byte);
} else if ('\u{e000}'..='\u{f8ff}').contains(&c) {
sprintf!(=> buf, "\\u%04X", u32::from(c));
} else if fish_wcwidth(c).is_some_and(|w| w != 0) {
sprintf!(=> buf, "%c", c);
} else if c <= '\u{FFFF}' {
sprintf!(=> buf, "\\u%04X", u32::from(c));
} else {
sprintf!(=> buf, "\\U%06X", u32::from(c));
}
buff
}
#[cfg(test)]
mod tests {
use crate::key::{self, ctrl, function_key, parse_keys, Key};
use crate::prelude::*;
#[test]
fn test_parse_key() {
assert_eq!(
parse_keys(L!("escape")),
Ok(vec![Key::from_raw(key::ESCAPE)])
);
assert_eq!(parse_keys(L!("\x1b")), Ok(vec![Key::from_raw(key::ESCAPE)]));
assert_eq!(parse_keys(L!("ctrl-a")), Ok(vec![ctrl('a')]));
assert_eq!(parse_keys(L!("\x01")), Ok(vec![ctrl('a')]));
assert!(parse_keys(L!("f0")).is_err());
assert_eq!(
parse_keys(L!("f1")),
Ok(vec![Key::from_raw(function_key(1))])
);
assert!(parse_keys(L!("F1")).is_err());
}
}