2024-03-30 15:10:12 +00:00
|
|
|
use libc::VERASE;
|
|
|
|
|
|
|
|
use crate::{
|
2024-04-18 21:19:42 +00:00
|
|
|
common::{escape_string, EscapeFlags, EscapeStringStyle},
|
2024-04-08 22:33:02 +00:00
|
|
|
fallback::fish_wcwidth,
|
Move cursor on mouse click via kitty's OSC 133 click_events=1
When the user clicks somewhere in the prompt, kitty asks the shell
to move the cursor there (since there is not much else to do).
This is currently implemented by sending an array of
forward-char-passive commands. This has problems, for example it
is really slow on large command lines (probably because we repaint
everytime).
Implement kitty's `click_events=1` flag to set the
position directly. To convert from terminal-coordinates
to fish-coordinates, query [CSI 6 n Report Cursor
Position](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
and use it to compute the left prompt's terminal-coordinates (which
are (0, 0) in fish-coordinates).
Unfortunately this doesn't yet work correctly while the terminal
is scrolled. This is probably because the cursor position is wrong
if off-screen. To fix that we could probably record the cursor
position while not scrolled, but it doesn't seem terribly important
(the existing implementation also doesn't get it right).
We still turn off mouse reporting. If we turned it on, it
would be harder to select text in the terminal itself (not fish).
This would typically mean that mouse-drag will alter fish's
selection and shift+mouse-drag or alt+mouse-drag can be used.
To improve this, we could try to synchronize the selection: if parts
of the fish commandline are selected in the terminal's selection,
copy that to fish's selection and vice versa.
Or maybe there is an intuitive criteria, like: whenever we receive a
mouse event outside fish, turn off mouse reporting, and turn it back on
whenver we receive new keyboard input. One problem is that we lose
one event (though we could send it back to the terminal). Another
problem is we would turn it back on too late in some scenarios.
Closes #10932
2024-12-21 18:41:41 +00:00
|
|
|
flog::FloggableDebug,
|
2024-04-08 22:33:02 +00:00
|
|
|
reader::TERMINAL_MODE_ON_STARTUP,
|
2024-08-13 20:09:06 +00:00
|
|
|
wchar::{decode_byte_from_char, prelude::*},
|
2024-04-08 22:33:02 +00:00
|
|
|
wutil::{fish_is_pua, fish_wcstoi},
|
2024-03-30 15:10:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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-12-30 07:12:32 +00:00
|
|
|
pub(crate) const Invalid: char = '\u{F50f}';
|
2024-03-30 15:10:12 +00:00
|
|
|
pub(crate) fn function_key(n: u32) -> char {
|
|
|
|
assert!((1..=12).contains(&n));
|
|
|
|
char::from_u32(u32::from(Invalid) + n).unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
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")),
|
|
|
|
(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,
|
|
|
|
}
|
|
|
|
}
|
2024-04-08 15:35:24 +00:00
|
|
|
pub(crate) const ALT: Self = {
|
|
|
|
let mut m = Self::new();
|
|
|
|
m.alt = true;
|
|
|
|
m
|
|
|
|
};
|
2024-03-30 15:10:12 +00:00
|
|
|
pub(crate) fn is_some(&self) -> bool {
|
|
|
|
self.ctrl || self.alt || self.shift
|
|
|
|
}
|
|
|
|
pub(crate) fn is_none(&self) -> bool {
|
|
|
|
!self.is_some()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Move cursor on mouse click via kitty's OSC 133 click_events=1
When the user clicks somewhere in the prompt, kitty asks the shell
to move the cursor there (since there is not much else to do).
This is currently implemented by sending an array of
forward-char-passive commands. This has problems, for example it
is really slow on large command lines (probably because we repaint
everytime).
Implement kitty's `click_events=1` flag to set the
position directly. To convert from terminal-coordinates
to fish-coordinates, query [CSI 6 n Report Cursor
Position](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
and use it to compute the left prompt's terminal-coordinates (which
are (0, 0) in fish-coordinates).
Unfortunately this doesn't yet work correctly while the terminal
is scrolled. This is probably because the cursor position is wrong
if off-screen. To fix that we could probably record the cursor
position while not scrolled, but it doesn't seem terribly important
(the existing implementation also doesn't get it right).
We still turn off mouse reporting. If we turned it on, it
would be harder to select text in the terminal itself (not fish).
This would typically mean that mouse-drag will alter fish's
selection and shift+mouse-drag or alt+mouse-drag can be used.
To improve this, we could try to synchronize the selection: if parts
of the fish commandline are selected in the terminal's selection,
copy that to fish's selection and vice versa.
Or maybe there is an intuitive criteria, like: whenever we receive a
mouse event outside fish, turn off mouse reporting, and turn it back on
whenver we receive new keyboard input. One problem is that we lose
one event (though we could send it back to the terminal). Another
problem is we would turn it back on too late in some scenarios.
Closes #10932
2024-12-21 18:41:41 +00:00
|
|
|
/// Position in terminal coordinates, i.e. not starting from the prompt
|
|
|
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
|
|
pub struct ViewportPosition {
|
|
|
|
pub x: usize,
|
|
|
|
pub y: usize,
|
|
|
|
}
|
|
|
|
impl FloggableDebug for ViewportPosition {}
|
|
|
|
|
2024-03-30 15:10:12 +00:00
|
|
|
#[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 15:47:56 +00:00
|
|
|
if c == ascii_control('m') {
|
2024-03-30 15:10:12 +00: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();
|
2024-04-08 22:33:02 +00:00
|
|
|
} else if !fish_is_pua(key.codepoint) {
|
2024-03-30 15:10:12 +00:00
|
|
|
// 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 = ',';
|
|
|
|
|
2024-04-18 21:19:42 +00:00
|
|
|
fn escape_nonprintables(key_name: &wstr) -> WString {
|
|
|
|
escape_string(
|
|
|
|
key_name,
|
|
|
|
EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES | EscapeFlags::NO_QUOTED),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-05-27 18:07:02 +00:00
|
|
|
#[allow(clippy::nonminimal_bool)]
|
2024-03-30 15:10:12 +00:00
|
|
|
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());
|
2024-04-19 20:43:48 +00:00
|
|
|
} else if ((2..=3).contains(&value.len())
|
2024-03-30 15:10:12 +00:00
|
|
|
&& !value.contains('-')
|
|
|
|
&& !value.contains(KEY_SEPARATOR)
|
2024-04-15 07:39:40 +00:00
|
|
|
&& !KEY_NAMES.iter().any(|(_codepoint, name)| name == value)
|
2024-05-22 20:29:35 +00:00
|
|
|
&& value.as_char_slice()[0] != 'F'
|
|
|
|
&& !(value.as_char_slice()[0] == 'f' && value.char_at(1).is_ascii_digit()))
|
2024-04-18 20:26:14 +00:00
|
|
|
|| first < ' '
|
2024-03-30 15:10:12 +00:00
|
|
|
{
|
|
|
|
// 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.
|
2024-04-19 20:43:48 +00:00
|
|
|
// 2. it starts with an ASCII control character. This can be either a multi-key binding
|
|
|
|
// or a single-key that is sent as escape sequence (starting with \e).
|
2024-03-30 15:10:12 +00:00
|
|
|
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 {
|
2024-11-12 20:19:16 +00:00
|
|
|
_ if modifier == "ctrl" => modifiers.ctrl = true,
|
|
|
|
_ if modifier == "alt" => modifiers.alt = true,
|
2024-03-30 15:10:12 +00:00
|
|
|
_ if modifier == "shift" => modifiers.shift = true,
|
|
|
|
_ => {
|
|
|
|
return Err(wgettext_fmt!(
|
2024-04-08 21:03:15 +00:00
|
|
|
"unknown modifier '%s' in '%s'",
|
2024-03-30 15:10:12 +00:00
|
|
|
modifier,
|
2024-04-18 21:19:42 +00:00
|
|
|
escape_nonprintables(full_key_name)
|
2024-03-30 15:10:12 +00:00
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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,
|
|
|
|
})?
|
2024-05-22 20:29:35 +00:00
|
|
|
} else if codepoint.is_none() && key_name.starts_with('f') && key_name.len() <= 3 {
|
|
|
|
let num = key_name.strip_prefix('f').unwrap();
|
2024-03-30 15:10:12 +00:00
|
|
|
let codepoint = match fish_wcstoi(num) {
|
|
|
|
Ok(n) if (1..=12).contains(&n) => function_key(u32::try_from(n).unwrap()),
|
|
|
|
_ => {
|
|
|
|
return Err(wgettext_fmt!(
|
2024-05-22 20:29:35 +00:00
|
|
|
"only f1 through f12 are supported, not 'f%s'",
|
2024-03-30 15:10:12 +00:00
|
|
|
num,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Key {
|
|
|
|
modifiers,
|
|
|
|
codepoint,
|
|
|
|
}
|
|
|
|
} else {
|
2024-04-18 21:19:42 +00:00
|
|
|
return Err(wgettext_fmt!(
|
|
|
|
"cannot parse key '%s'",
|
|
|
|
escape_nonprintables(full_key_name)
|
|
|
|
));
|
2024-03-30 15:10:12 +00:00
|
|
|
};
|
|
|
|
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');
|
|
|
|
}
|
2024-04-08 22:33:02 +00:00
|
|
|
if fish_is_pua(c) || u32::from(c) <= 27 {
|
2024-03-30 15:10:12 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-09 19:29:09 +00:00
|
|
|
impl fish_printf::ToArg<'static> for Key {
|
|
|
|
fn to_arg(self) -> fish_printf::Arg<'static> {
|
|
|
|
fish_printf::Arg::WString(self.into())
|
2024-03-30 15:10:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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!(
|
2024-05-22 20:29:35 +00:00
|
|
|
"f%d",
|
2024-03-30 15:10:12 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-08 15:35:24 +00:00
|
|
|
fn ctrl_to_symbol(buf: &mut WString, c: char) {
|
|
|
|
// Most ascii control characters like \x01 are canonicalized as ctrl-a, except
|
|
|
|
// 1. if we are explicitly given a codepoint < 32 via CSI u.
|
|
|
|
// 2. key names that are given as raw escape sequence (\e123); those we want to display
|
|
|
|
// similar to how they are given.
|
|
|
|
|
|
|
|
let c = u8::try_from(c).unwrap();
|
2024-08-13 20:08:25 +00:00
|
|
|
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);
|
2024-04-08 15:35:24 +00:00
|
|
|
}
|
|
|
|
|
2024-03-30 15:10:12 +00:00
|
|
|
/// 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 {
|
2024-04-08 21:03:15 +00:00
|
|
|
"~[]()<>{}*\\?$#;&|'\"".contains(c)
|
2024-03-30 15:10:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2024-08-13 14:01:00 +00:00
|
|
|
pub fn char_to_symbol(c: char) -> WString {
|
2024-03-30 15:10:12 +00:00
|
|
|
let mut buff = WString::new();
|
|
|
|
let buf = &mut buff;
|
2024-08-13 20:08:25 +00:00
|
|
|
if c <= ' ' || c == '\x7F' {
|
2024-04-08 15:35:24 +00:00
|
|
|
ctrl_to_symbol(buf, c);
|
2024-04-07 06:26:45 +00:00
|
|
|
} else if c < '\u{80}' {
|
2024-03-30 15:10:12 +00:00
|
|
|
// ASCII characters that are not control characters
|
|
|
|
ascii_printable_to_symbol(buf, c);
|
2024-08-13 20:09:06 +00:00
|
|
|
} else if let Some(byte) = decode_byte_from_char(c) {
|
|
|
|
sprintf!(=> buf, "\\x%02x", byte);
|
2024-04-08 07:01:07 +00: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 15:10:12 +00: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
|
|
|
|
}
|