fish-shell/src/builtins/bind.rs
2024-01-13 03:58:33 +01:00

577 lines
18 KiB
Rust

//! Implementation of the bind builtin.
use super::prelude::*;
use crate::common::{
escape, escape_string, str2wcstring, valid_var_name, EscapeFlags, EscapeStringStyle,
};
use crate::highlight::{colorize, highlight_shell};
use crate::input::{
input_function_get_names, input_mappings, input_terminfo_get_name, input_terminfo_get_names,
input_terminfo_get_sequence, InputMappingSet,
};
use crate::nix::isatty;
use nix::errno::Errno;
use std::sync::MutexGuard;
const DEFAULT_BIND_MODE: &wstr = L!("default");
const BIND_INSERT: c_int = 0;
const BIND_ERASE: c_int = 1;
const BIND_KEY_NAMES: c_int = 2;
const BIND_FUNCTION_NAMES: c_int = 3;
struct Options {
all: bool,
bind_mode_given: bool,
list_modes: bool,
print_help: bool,
silent: bool,
use_terminfo: bool,
have_user: bool,
user: bool,
have_preset: bool,
preset: bool,
mode: c_int,
bind_mode: WString,
sets_bind_mode: WString,
}
impl Options {
fn new() -> Options {
Options {
all: false,
bind_mode_given: false,
list_modes: false,
print_help: false,
silent: false,
use_terminfo: false,
have_user: false,
user: false,
have_preset: false,
preset: false,
mode: BIND_INSERT,
bind_mode: DEFAULT_BIND_MODE.to_owned(),
sets_bind_mode: WString::new(),
}
}
}
struct BuiltinBind {
/// Note that BuiltinBind holds the singleton lock.
/// It must not call out to anything which can execute fish shell code or attempt to acquire the
/// lock again.
input_mappings: MutexGuard<'static, InputMappingSet>,
opts: Options,
}
impl BuiltinBind {
fn new() -> BuiltinBind {
BuiltinBind {
input_mappings: input_mappings(),
opts: Options::new(),
}
}
/// List a single key binding.
/// Returns false if no binding with that sequence and mode exists.
fn list_one(
&self,
seq: &wstr,
bind_mode: &wstr,
user: bool,
parser: &Parser,
streams: &mut IoStreams,
) -> bool {
let mut ecmds = Vec::new();
let mut sets_mode = WString::new();
let mut out = WString::new();
if !self
.input_mappings
.get(seq, bind_mode, &mut ecmds, user, &mut sets_mode)
{
return false;
}
out.push_str("bind");
// Append the mode flags if applicable.
if !user {
out.push_str(" --preset");
}
if bind_mode != DEFAULT_BIND_MODE {
out.push_str(" -M ");
out.push_utfstr(&escape(bind_mode));
}
if !sets_mode.is_empty() && sets_mode != bind_mode {
out.push_str(" -m ");
out.push_utfstr(&escape(&sets_mode));
}
// Append the name.
if let Some(tname) = input_terminfo_get_name(seq) {
// Note that we show -k here because we have an input key name.
out.push_str(" -k ");
out.push_utfstr(&tname);
} else {
// No key name, so no -k; we show the escape sequence directly.
let eseq = escape(seq);
out.push(' ');
out.push_utfstr(&eseq);
}
// Now show the list of commands.
for ecmd in ecmds {
out.push(' ');
out.push_utfstr(&escape(&ecmd));
}
out.push('\n');
if !streams.out_is_redirected && isatty(libc::STDOUT_FILENO) {
let mut colors = Vec::new();
highlight_shell(&out, &mut colors, &parser.context(), false, None);
let colored = colorize(&out, &colors, parser.vars());
streams.out.append(str2wcstring(&colored));
} else {
streams.out.append(out);
}
true
}
// Overload with both kinds of bindings.
// Returns false only if neither exists.
fn list_one_user_andor_preset(
&self,
seq: &wstr,
bind_mode: &wstr,
user: bool,
preset: bool,
parser: &Parser,
streams: &mut IoStreams,
) -> bool {
let mut retval = false;
if preset {
retval |= self.list_one(seq, bind_mode, false, parser, streams);
}
if user {
retval |= self.list_one(seq, bind_mode, true, parser, streams);
}
retval
}
/// List all current key bindings.
fn list(&self, bind_mode: Option<&wstr>, user: bool, parser: &Parser, streams: &mut IoStreams) {
let lst = self.input_mappings.get_names(user);
for binding in lst {
if bind_mode.is_some() && bind_mode.unwrap() != binding.mode {
continue;
}
self.list_one(&binding.seq, &binding.mode, user, parser, streams);
}
}
/// Print terminfo key binding names to string buffer used for standard output.
///
/// \param all if set, all terminfo key binding names will be printed. If not set, only ones that
/// are defined for this terminal are printed.
fn key_names(&self, all: bool, streams: &mut IoStreams) {
let names = input_terminfo_get_names(!all);
for name in names {
streams.out.appendln(name);
}
}
/// Print all the special key binding functions to string buffer used for standard output.
fn function_names(&self, streams: &mut IoStreams) {
let names = input_function_get_names();
for name in names {
streams.out.appendln(name);
}
}
/// Wraps input_terminfo_get_sequence(), appending the correct error messages as needed.
fn get_terminfo_sequence(&self, seq: &wstr, streams: &mut IoStreams) -> Option<WString> {
let mut tseq = WString::new();
if input_terminfo_get_sequence(seq, &mut tseq) {
return Some(tseq);
}
let err = Errno::last();
if !self.opts.silent {
let eseq = escape_string(seq, EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES));
if err == Errno::ENOENT {
streams.err.append(wgettext_fmt!(
"%ls: No key with name '%ls' found\n",
"bind",
eseq
));
} else if err == Errno::EILSEQ {
streams.err.append(wgettext_fmt!(
"%ls: Key with name '%ls' does not have any mapping\n",
"bind",
eseq
));
} else {
streams.err.append(wgettext_fmt!(
"%ls: Unknown error trying to bind to key named '%ls'\n",
"bind",
eseq
));
}
}
None
}
/// Add specified key binding.
fn add(
&mut self,
seq: &wstr,
cmds: &[&wstr],
mode: WString,
sets_mode: WString,
terminfo: bool,
user: bool,
streams: &mut IoStreams,
) -> bool {
let cmds = cmds.iter().map(|&s| s.to_owned()).collect();
if terminfo {
if let Some(seq2) = self.get_terminfo_sequence(seq, streams) {
self.input_mappings.add(seq2, cmds, mode, sets_mode, user);
} else {
return true;
}
} else {
self.input_mappings
.add(seq.to_owned(), cmds, mode, sets_mode, user)
}
false
}
/// Erase specified key bindings
///
/// @param seq
/// an array of all key bindings to erase
/// @param all
/// if specified, _all_ key bindings will be erased
/// @param use_terminfo
/// Whether to look use terminfo -k name
///
fn erase(
&mut self,
seq: &[&wstr],
all: bool,
use_terminfo: bool,
user: bool,
streams: &mut IoStreams,
) -> bool {
let mode = if self.opts.bind_mode_given {
Some(self.opts.bind_mode.as_utfstr())
} else {
None
};
if all {
self.input_mappings.clear(mode, user);
return false;
}
let mut res = false;
let mode = mode.unwrap_or(DEFAULT_BIND_MODE);
for s in seq {
if use_terminfo {
if let Some(seq2) = self.get_terminfo_sequence(s, streams) {
self.input_mappings.erase(&seq2, mode, user);
} else {
res = true;
}
} else {
self.input_mappings.erase(s, mode, user);
}
}
res
}
fn insert(
&mut self,
optind: usize,
argv: &[&wstr],
parser: &Parser,
streams: &mut IoStreams,
) -> bool {
let argc = argv.len();
let cmd = argv[0];
let arg_count = argc - optind;
if arg_count < 2 {
// If we get both or neither preset/user, we list both.
if !self.opts.have_preset && !self.opts.have_user {
self.opts.preset = true;
self.opts.user = true;
}
} else {
// Inserting both on the other hand makes no sense.
if self.opts.have_preset && self.opts.have_user {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_COMBO2_EXCLUSIVE,
cmd,
"--preset",
"--user"
));
return true;
}
}
if arg_count == 0 {
// We don't overload this with user and def because we want them to be grouped.
// First the presets, then the users (because of scrolling).
let bind_mode = if self.opts.bind_mode_given {
Some(self.opts.bind_mode.as_utfstr())
} else {
None
};
if self.opts.preset {
self.list(bind_mode, false, parser, streams);
}
if self.opts.user {
self.list(bind_mode, true, parser, streams);
}
} else if arg_count == 1 {
let seq = if self.opts.use_terminfo {
let Some(seq2) = self.get_terminfo_sequence(argv[optind], streams) else {
// get_terminfo_sequence already printed the error.
return true;
};
seq2
} else {
argv[optind].to_owned()
};
if !self.list_one_user_andor_preset(
&seq,
&self.opts.bind_mode,
self.opts.user,
self.opts.preset,
parser,
streams,
) {
let eseq = escape_string(
argv[optind],
EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES),
);
if !self.opts.silent {
if self.opts.use_terminfo {
streams.err.append(wgettext_fmt!(
"%ls: No binding found for key '%ls'\n",
cmd,
eseq
));
} else {
streams.err.append(wgettext_fmt!(
"%ls: No binding found for sequence '%ls'\n",
cmd,
eseq
));
}
}
return true;
}
} else {
// Actually insert!
if self.add(
argv[optind],
&argv[optind + 1..],
self.opts.bind_mode.to_owned(),
self.opts.sets_bind_mode.to_owned(),
self.opts.use_terminfo,
self.opts.user,
streams,
) {
return true;
}
}
false
}
/// List all current bind modes.
fn list_modes(&mut self, streams: &mut IoStreams) {
// List all known modes, even if they are only in preset bindings.
let lst = self.input_mappings.get_names(true);
let preset_lst = self.input_mappings.get_names(false);
// Extract the bind modes, uniqueize, and sort.
let mut modes: Vec<WString> = lst.into_iter().chain(preset_lst).map(|m| m.mode).collect();
modes.sort_unstable();
modes.dedup();
for mode in modes {
streams.out.appendln(mode);
}
}
}
fn parse_cmd_opts(
opts: &mut Options,
optind: &mut usize,
argv: &mut [&wstr],
parser: &Parser,
streams: &mut IoStreams,
) -> Option<i32> {
let cmd = argv[0];
let short_options = L!(":aehkKfM:Lm:s");
const long_options: &[woption] = &[
wopt(L!("all"), no_argument, 'a'),
wopt(L!("erase"), no_argument, 'e'),
wopt(L!("function-names"), no_argument, 'f'),
wopt(L!("help"), no_argument, 'h'),
wopt(L!("key"), no_argument, 'k'),
wopt(L!("key-names"), no_argument, 'K'),
wopt(L!("list-modes"), no_argument, 'L'),
wopt(L!("mode"), required_argument, 'M'),
wopt(L!("preset"), no_argument, 'p'),
wopt(L!("sets-mode"), required_argument, 'm'),
wopt(L!("silent"), no_argument, 's'),
wopt(L!("user"), no_argument, 'u'),
];
let mut w = wgetopter_t::new(short_options, long_options, argv);
while let Some(c) = w.wgetopt_long() {
match c {
'a' => opts.all = true,
'e' => opts.mode = BIND_ERASE,
'f' => opts.mode = BIND_FUNCTION_NAMES,
'h' => opts.print_help = true,
'k' => opts.use_terminfo = true,
'K' => opts.mode = BIND_KEY_NAMES,
'L' => {
opts.list_modes = true;
return STATUS_CMD_OK;
}
'M' => {
if !valid_var_name(w.woptarg.unwrap()) {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_BIND_MODE,
cmd,
w.woptarg.unwrap()
));
return STATUS_INVALID_ARGS;
}
opts.bind_mode = w.woptarg.unwrap().to_owned();
opts.bind_mode_given = true;
}
'm' => {
if !valid_var_name(w.woptarg.unwrap()) {
streams.err.append(wgettext_fmt!(
BUILTIN_ERR_BIND_MODE,
cmd,
w.woptarg.unwrap()
));
return STATUS_INVALID_ARGS;
}
opts.sets_bind_mode = w.woptarg.unwrap().to_owned();
}
'p' => {
opts.have_preset = true;
opts.preset = true;
}
's' => opts.silent = true,
'u' => {
opts.have_user = true;
opts.user = true;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true);
return STATUS_INVALID_ARGS;
}
'?' => {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true);
return STATUS_INVALID_ARGS;
}
_ => {
panic!("unexpected retval from wgetopt_long")
}
}
}
*optind = w.woptind;
return STATUS_CMD_OK;
}
impl BuiltinBind {
/// The bind builtin, used for setting character sequences.
pub fn bind(
&mut self,
parser: &Parser,
streams: &mut IoStreams,
argv: &mut [&wstr],
) -> Option<c_int> {
let cmd = argv[0];
let mut optind = 0;
let retval = parse_cmd_opts(&mut self.opts, &mut optind, argv, parser, streams);
if retval != STATUS_CMD_OK {
return retval;
}
if self.opts.list_modes {
self.list_modes(streams);
return STATUS_CMD_OK;
}
if self.opts.print_help {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
// Default to user mode
if !self.opts.have_preset && !self.opts.have_user {
self.opts.user = true;
}
match self.opts.mode {
BIND_ERASE => {
// If we get both, we erase both.
if self.opts.user {
if self.erase(
&argv[optind..],
self.opts.all,
self.opts.use_terminfo,
true, /* user */
streams,
) {
return STATUS_CMD_ERROR;
}
}
if self.opts.preset {
if self.erase(
&argv[optind..],
self.opts.all,
self.opts.use_terminfo,
false, /* user */
streams,
) {
return STATUS_CMD_ERROR;
}
}
}
BIND_INSERT => {
if self.insert(optind, argv, parser, streams) {
return STATUS_CMD_ERROR;
}
}
BIND_KEY_NAMES => self.key_names(self.opts.all, streams),
BIND_FUNCTION_NAMES => self.function_names(streams),
_ => {
streams
.err
.append(wgettext_fmt!("%ls: Invalid state\n", cmd));
return STATUS_CMD_ERROR;
}
}
STATUS_CMD_OK
}
}
pub fn bind(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Option<c_int> {
BuiltinBind::new().bind(parser, streams, args)
}