Display raw escape sequences the old way again

If a binding was input starting with "\e", it's usually a raw control sequence.
Today we display the canonical version like:

    bind --preset alt-\[,1,\;,5,C foo

even if the input is

    bind --preset \e\[1\;5C foo

Make it look like the input again.  This looks more familiar and less
surprising (especially since we canonicalize CSI to "alt-[").

Except that we use the \x01 representation instead of \ca because the
"control" part can be confusing. We're inside an escape sequence so it seems
highly unlikely that an ASCII control character actually comes from the user
holding the control key.

The downside is that this hides the canonical version; it might be surprising
that a raw-escape-sequence binding can be erased using the new syntax and
vice versa.
This commit is contained in:
Johannes Altmanninger 2024-04-08 17:35:24 +02:00
parent d025b245f6
commit f61ef2c63d
5 changed files with 119 additions and 60 deletions

View File

@ -7,9 +7,9 @@ use crate::common::{
use crate::highlight::{colorize, highlight_shell};
use crate::input::{
input_function_get_names, input_mappings, input_terminfo_get_names,
input_terminfo_get_sequence, GetSequenceError, InputMappingSet,
input_terminfo_get_sequence, GetSequenceError, InputMappingSet, KeyNameStyle,
};
use crate::key::{self, canonicalize_raw_escapes, parse_keys, Key};
use crate::key::{self, canonicalize_raw_escapes, char_to_symbol, parse_keys, Key, Modifiers};
use crate::nix::isatty;
use std::sync::MutexGuard;
@ -84,7 +84,7 @@ impl BuiltinBind {
) -> bool {
let mut ecmds: &[_] = &[];
let mut sets_mode = None;
let mut terminfo_name = None;
let mut key_name_style = KeyNameStyle::Plain;
let mut out = WString::new();
if !self.input_mappings.get(
seq,
@ -92,7 +92,7 @@ impl BuiltinBind {
&mut ecmds,
user,
&mut sets_mode,
&mut terminfo_name,
&mut key_name_style,
) {
return false;
}
@ -115,12 +115,9 @@ impl BuiltinBind {
}
}
if let Some(tname) = terminfo_name {
// Note that we show -k here because we have an input key name.
out.push_str(" -k ");
out.push_utfstr(&tname);
} else {
out.push(' ');
match key_name_style {
KeyNameStyle::Plain => {
// Append the name.
for (i, key) in seq.iter().enumerate() {
if i != 0 {
@ -132,6 +129,27 @@ impl BuiltinBind {
out.push_str("''");
}
}
KeyNameStyle::RawEscapeSequence => {
for key in seq {
if key.modifiers == Modifiers::ALT {
out.push_utfstr(&char_to_symbol('\x1b'));
out.push_utfstr(&char_to_symbol(if key.codepoint == key::Escape {
'\x1b'
} else {
key.codepoint
}));
} else {
assert!(key.modifiers.is_none());
out.push_utfstr(&char_to_symbol(key.codepoint));
}
}
}
KeyNameStyle::Terminfo(tname) => {
// Note that we show -k here because we have an input key name.
out.push_str("-k ");
out.push_utfstr(&tname);
}
}
// Now show the list of commands.
for ecmd in ecmds {
@ -236,22 +254,23 @@ impl BuiltinBind {
cmds: &[&wstr],
mode: WString,
sets_mode: Option<WString>,
is_terminfo_key: bool,
user: bool,
streams: &mut IoStreams,
) -> bool {
let cmds = cmds.iter().map(|&s| s.to_owned()).collect();
let is_raw_escape_sequence = seq.len() > 2 && seq.char_at(0) == '\x1b';
let Some(key_seq) = self.compute_seq(streams, seq) else {
return true;
};
self.input_mappings.add(
key_seq,
is_terminfo_key.then(|| seq.to_owned()),
cmds,
mode,
sets_mode,
user,
);
let key_name_style = if self.opts.use_terminfo {
KeyNameStyle::Terminfo(seq.to_owned())
} else if is_raw_escape_sequence {
KeyNameStyle::RawEscapeSequence
} else {
KeyNameStyle::Plain
};
self.input_mappings
.add(key_seq, key_name_style, cmds, mode, sets_mode, user);
false
}
@ -396,7 +415,6 @@ impl BuiltinBind {
&argv[optind + 1..],
self.opts.bind_mode.to_owned(),
self.opts.sets_bind_mode.to_owned(),
self.opts.use_terminfo,
self.opts.user,
streams,
) {

View File

@ -38,6 +38,13 @@ pub struct InputMappingName {
pub mode: WString,
}
#[derive(Clone, Debug)]
pub enum KeyNameStyle {
Plain,
RawEscapeSequence,
Terminfo(WString),
}
/// Struct representing a keybinding. Returned by input_get_mappings.
#[derive(Debug, Clone)]
struct InputMapping {
@ -51,8 +58,8 @@ struct InputMapping {
mode: WString,
/// New mode that should be switched to after command evaluation, or None to leave the mode unchanged.
sets_mode: Option<WString>,
/// Whether this sequence was specified via its terminfo name.
terminfo_name: Option<WString>,
/// Perhaps this binding was created using a raw escape sequence or terminfo.
key_name_style: KeyNameStyle,
}
impl InputMapping {
@ -62,7 +69,7 @@ impl InputMapping {
commands: Vec<WString>,
mode: WString,
sets_mode: Option<WString>,
terminfo_name: Option<WString>,
key_name_style: KeyNameStyle,
) -> InputMapping {
static LAST_INPUT_MAP_SPEC_ORDER: AtomicU32 = AtomicU32::new(0);
let specification_order = 1 + LAST_INPUT_MAP_SPEC_ORDER.fetch_add(1, Ordering::Relaxed);
@ -76,7 +83,7 @@ impl InputMapping {
specification_order,
mode,
sets_mode,
terminfo_name,
key_name_style,
}
}
@ -282,7 +289,7 @@ impl InputMappingSet {
pub fn add(
&mut self,
sequence: Vec<Key>,
terminfo_name: Option<WString>,
key_name_style: KeyNameStyle,
commands: Vec<WString>,
mode: WString,
sets_mode: Option<WString>,
@ -307,7 +314,7 @@ impl InputMappingSet {
}
// Add a new mapping, using the next order.
let new_mapping = InputMapping::new(sequence, commands, mode, sets_mode, terminfo_name);
let new_mapping = InputMapping::new(sequence, commands, mode, sets_mode, key_name_style);
input_mapping_insert_sorted(ml, new_mapping);
}
@ -315,7 +322,7 @@ impl InputMappingSet {
pub fn add1(
&mut self,
sequence: Vec<Key>,
terminfo_name: Option<WString>,
key_name_style: KeyNameStyle,
command: WString,
mode: WString,
sets_mode: Option<WString>,
@ -323,7 +330,7 @@ impl InputMappingSet {
) {
self.add(
sequence,
terminfo_name,
key_name_style,
vec![command],
mode,
sets_mode,
@ -349,7 +356,7 @@ pub fn init_input() {
let mut add = |key: Vec<Key>, cmd: &str| {
let mode = DEFAULT_BIND_MODE.to_owned();
let sets_mode = Some(DEFAULT_BIND_MODE.to_owned());
input_mapping.add1(key, None, cmd.into(), mode, sets_mode, false);
input_mapping.add1(key, KeyNameStyle::Plain, cmd.into(), mode, sets_mode, false);
};
add(vec![], "self-insert");
@ -372,18 +379,22 @@ pub fn init_input() {
add(vec![ctrl('b')], "backward-char");
add(vec![ctrl('f')], "forward-char");
let mut add_legacy = |escape_sequence: &str, cmd: &str| {
add(
canonicalize_raw_escapes(
escape_sequence.chars().map(Key::from_single_char).collect(),
),
cmd,
let mut add_raw = |escape_sequence: &str, cmd: &str| {
let mode = DEFAULT_BIND_MODE.to_owned();
let sets_mode = Some(DEFAULT_BIND_MODE.to_owned());
input_mapping.add1(
canonicalize_raw_escapes(escape_sequence.chars().map(Key::from_raw).collect()),
KeyNameStyle::RawEscapeSequence,
cmd.into(),
mode,
sets_mode,
false,
);
};
add_legacy("\x1B[A", "up-line");
add_legacy("\x1B[B", "down-line");
add_legacy("\x1B[C", "forward-char");
add_legacy("\x1B[D", "backward-char");
add_raw("\x1B[A", "up-line");
add_raw("\x1B[B", "down-line");
add_raw("\x1B[C", "forward-char");
add_raw("\x1B[D", "backward-char");
}
}
@ -1008,7 +1019,7 @@ impl InputMappingSet {
out_cmds: &mut &'a [WString],
user: bool,
out_sets_mode: &mut Option<&'a wstr>,
out_terminfo_name: &mut Option<WString>,
out_key_name_style: &mut KeyNameStyle,
) -> bool {
let ml = if user {
&self.mapping_list
@ -1019,7 +1030,7 @@ impl InputMappingSet {
if m.seq == sequence && m.mode == mode {
*out_cmds = &m.commands;
*out_sets_mode = m.sets_mode.as_deref();
*out_terminfo_name = m.terminfo_name.clone();
*out_key_name_style = m.key_name_style.clone();
return true;
}
}

View File

@ -66,6 +66,11 @@ impl Modifiers {
shift: false,
}
}
pub(crate) const ALT: Self = {
let mut m = Self::new();
m.alt = true;
m
};
pub(crate) fn is_some(&self) -> bool {
self.ctrl || self.alt || self.shift
}
@ -396,6 +401,33 @@ impl From<Key> for WString {
}
}
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 ctrl_symbolic_names: [&wstr; 28] = {
std::array::from_fn(|i| match i {
8 => L!("\\b"),
9 => L!("\\t"),
10 => L!("\\n"),
13 => L!("\\r"),
27 => L!("\\e"),
_ => L!(""),
})
};
let c = u8::try_from(c).unwrap();
let cu = usize::from(c);
if !ctrl_symbolic_names[cu].is_empty() {
sprintf!(=> buf, "%s", ctrl_symbolic_names[cu]);
} else {
sprintf!(=> buf, "\\x%02x", c);
}
}
/// 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 {
@ -411,13 +443,11 @@ fn ascii_printable_to_symbol(buf: &mut WString, c: char) {
}
/// Convert a wide-char to a symbol that can be used in our output.
fn char_to_symbol(c: char) -> WString {
pub(crate) 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());
ctrl_to_symbol(buf, c);
} else if c < '\u{80}' {
// ASCII characters that are not control characters
ascii_printable_to_symbol(buf, c);

View File

@ -1,4 +1,4 @@
use crate::input::{input_mappings, Inputter, DEFAULT_BIND_MODE};
use crate::input::{input_mappings, Inputter, KeyNameStyle, DEFAULT_BIND_MODE};
use crate::input_common::{CharEvent, ReadlineCmd};
use crate::key::Key;
use crate::parser::Parser;
@ -26,7 +26,7 @@ fn test_input() {
let mut input_mapping = input_mappings();
input_mapping.add1(
prefix_binding,
None,
KeyNameStyle::Plain,
WString::from_str("up-line"),
default_mode(),
None,
@ -34,7 +34,7 @@ fn test_input() {
);
input_mapping.add1(
desired_binding.clone(),
None,
KeyNameStyle::Plain,
WString::from_str("down-line"),
default_mode(),
None,

View File

@ -15,8 +15,8 @@ bind -M bind-mode \cX true
bind -M bind_mode \cX true
# Listing bindings
bind | string match -v '*escape,\\[*' # Hide legacy bindings.
bind --user --preset | string match -v '*escape,\\[*'
bind | string match -v '*\e\\[*' # Hide raw bindings.
bind --user --preset | string match -v '*\e\\[*'
# CHECK: bind --preset '' self-insert
# CHECK: bind --preset enter execute
# CHECK: bind --preset tab complete
@ -55,7 +55,7 @@ bind --user --preset | string match -v '*escape,\\[*'
# CHECK: bind -M bind_mode ctrl-x true
# Preset only
bind --preset | string match -v '*escape,\\[*'
bind --preset | string match -v '*\e\\[*'
# CHECK: bind --preset '' self-insert
# CHECK: bind --preset enter execute
# CHECK: bind --preset tab complete
@ -75,12 +75,12 @@ bind --preset | string match -v '*escape,\\[*'
# CHECK: bind --preset ctrl-f forward-char
# User only
bind --user | string match -v '*escape,\\[*'
bind --user | string match -v '*\e\\[*'
# CHECK: bind -M bind_mode ctrl-x true
# Adding bindings
bind tab 'echo banana'
bind | string match -v '*escape,\\[*'
bind | string match -v '*\e\\[*'
# CHECK: bind --preset '' self-insert
# CHECK: bind --preset enter execute
# CHECK: bind --preset tab complete