diff --git a/src/builtins/bind.rs b/src/builtins/bind.rs index 812526c01..68a75922d 100644 --- a/src/builtins/bind.rs +++ b/src/builtins/bind.rs @@ -7,9 +7,9 @@ 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 @@ fn list_one( ) -> 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 @@ fn list_one( &mut ecmds, user, &mut sets_mode, - &mut terminfo_name, + &mut key_name_style, ) { return false; } @@ -115,21 +115,39 @@ fn list_one( } } - 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(' '); - // Append the name. - for (i, key) in seq.iter().enumerate() { - if i != 0 { - out.push(key::KEY_SEPARATOR); + out.push(' '); + match key_name_style { + KeyNameStyle::Plain => { + // Append the name. + for (i, key) in seq.iter().enumerate() { + if i != 0 { + out.push(key::KEY_SEPARATOR); + } + out.push_utfstr(&WString::from(*key)); + } + if seq.is_empty() { + out.push_str("''"); } - out.push_utfstr(&WString::from(*key)); } - if seq.is_empty() { - 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); } } @@ -236,22 +254,23 @@ fn add( cmds: &[&wstr], mode: WString, sets_mode: Option, - 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 @@ fn insert( &argv[optind + 1..], self.opts.bind_mode.to_owned(), self.opts.sets_bind_mode.to_owned(), - self.opts.use_terminfo, self.opts.user, streams, ) { diff --git a/src/input.rs b/src/input.rs index ade09f8ac..df65559f0 100644 --- a/src/input.rs +++ b/src/input.rs @@ -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, - /// Whether this sequence was specified via its terminfo name. - terminfo_name: Option, + /// Perhaps this binding was created using a raw escape sequence or terminfo. + key_name_style: KeyNameStyle, } impl InputMapping { @@ -62,7 +69,7 @@ fn new( commands: Vec, mode: WString, sets_mode: Option, - terminfo_name: Option, + 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 @@ fn new( specification_order, mode, sets_mode, - terminfo_name, + key_name_style, } } @@ -282,7 +289,7 @@ impl InputMappingSet { pub fn add( &mut self, sequence: Vec, - terminfo_name: Option, + key_name_style: KeyNameStyle, commands: Vec, mode: WString, sets_mode: Option, @@ -307,7 +314,7 @@ pub fn add( } // 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 @@ pub fn add( pub fn add1( &mut self, sequence: Vec, - terminfo_name: Option, + key_name_style: KeyNameStyle, command: WString, mode: WString, sets_mode: Option, @@ -323,7 +330,7 @@ pub fn add1( ) { 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, 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 @@ pub fn get<'a>( out_cmds: &mut &'a [WString], user: bool, out_sets_mode: &mut Option<&'a wstr>, - out_terminfo_name: &mut Option, + out_key_name_style: &mut KeyNameStyle, ) -> bool { let ml = if user { &self.mapping_list @@ -1019,7 +1030,7 @@ pub fn get<'a>( 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; } } diff --git a/src/key.rs b/src/key.rs index 30223aeb9..bdd413dc6 100644 --- a/src/key.rs +++ b/src/key.rs @@ -66,6 +66,11 @@ const fn new() -> Self { 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 @@ fn from(key: Key) -> Self { } } +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); diff --git a/src/tests/input.rs b/src/tests/input.rs index 4a5906dbe..0fadd4f39 100644 --- a/src/tests/input.rs +++ b/src/tests/input.rs @@ -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, diff --git a/tests/checks/bind.fish b/tests/checks/bind.fish index f48a9e544..129bbd52f 100644 --- a/tests/checks/bind.fish +++ b/tests/checks/bind.fish @@ -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