Add remaining input FFI bits and port builtin_bind

This implements input and input_common FFI pieces in input_ffi.rs, and
simultaneously ports bind.rs. This was done as a single commit because
builtin_bind would have required a substantial amount of work to use the input
ffi.
This commit is contained in:
ridiculousfish 2023-12-17 15:41:14 -08:00
parent 7ffb62d1d9
commit 8190e3419d
22 changed files with 1092 additions and 2645 deletions

View file

@ -99,7 +99,6 @@ endif()
# List of sources for builtin functions.
set(FISH_BUILTIN_SRCS
src/builtins/bind.cpp
src/builtins/commandline.cpp
)
# List of other sources.
@ -115,8 +114,6 @@ set(FISH_SRCS
src/fish_version.cpp
src/flog.cpp
src/highlight.cpp
src/input_common.cpp
src/input.cpp
src/output.cpp
src/parse_util.cpp
src/path.cpp

View file

@ -90,6 +90,7 @@ fn main() {
"fish-rust/src/future_feature_flags.rs",
"fish-rust/src/highlight.rs",
"fish-rust/src/history.rs",
"fish-rust/src/input_ffi.rs",
"fish-rust/src/io.rs",
"fish-rust/src/job_group.rs",
"fish-rust/src/kill.rs",

View file

@ -1,6 +1,19 @@
//! 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;
@ -19,10 +32,553 @@ struct Options {
have_preset: bool,
preset: bool,
mode: c_int,
bind_mode: &'static wstr,
sets_bind_mode: &'static wstr,
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 mode
/// if specified, only bindings from that mode will be erased. If not given
/// and @c all is @c false, @c DEFAULT_BIND_MODE will be used.
/// @param use_terminfo
/// Whether to look use terminfo -k name
///
fn erase(
&mut self,
seq: &[&wstr],
all: bool,
mode: Option<&wstr>,
use_terminfo: bool,
user: bool,
streams: &mut IoStreams,
) -> bool {
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 => {
// TODO: satisfy the borrow checker here.
let storage;
let bind_mode = if self.opts.bind_mode_given {
storage = self.opts.bind_mode.clone();
Some(storage.as_utfstr())
} else {
None
};
// If we get both, we erase both.
if self.opts.user {
if self.erase(
&argv[optind..],
self.opts.all,
bind_mode,
self.opts.use_terminfo,
true, /* user */
streams,
) {
return STATUS_CMD_ERROR;
}
}
if self.opts.preset {
if self.erase(
&argv[optind..],
self.opts.all,
bind_mode,
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> {
run_builtin_ffi(crate::ffi::builtin_bind, parser, streams, args)
BuiltinBind::new().bind(parser, streams, args)
}

View file

@ -13,6 +13,7 @@ use crate::event::Event;
use crate::ffi;
use crate::flog::FLOG;
use crate::global_safety::RelaxedAtomicBool;
use crate::input::init_input;
use crate::nix::{geteuid, getpid, isatty};
use crate::null_terminated_array::OwningNullTerminatedArray;
use crate::path::{
@ -722,7 +723,7 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
// Allow changes to variables to produce events.
env_dispatch_init(vars);
ffi::init_input();
init_input();
// Complain about invalid config paths.
// HACK: Assume the defaults are correct (in practice this is only --no-config anyway).

View file

@ -25,8 +25,6 @@ include_cpp! {
#include "flog.h"
#include "function.h"
#include "io.h"
#include "input_common.h"
#include "input.h"
#include "parse_constants.h"
#include "parser.h"
#include "parse_util.h"
@ -38,7 +36,6 @@ include_cpp! {
#include "tokenizer.h"
#include "wutil.h"
#include "builtins/bind.h"
#include "builtins/commandline.h"
safety!(unsafe_ffi)
@ -54,18 +51,14 @@ include_cpp! {
generate!("reader_read_ffi")
generate!("fish_is_unwinding_for_exit")
generate!("restore_term_mode")
generate!("update_wait_on_escape_ms_ffi")
generate!("read_generation_count")
generate!("set_flog_output_file_ffi")
generate!("flog_setlinebuf_ffi")
generate!("activate_flog_categories_by_pattern")
generate!("restore_term_foreground_process_group_for_exit")
generate!("builtin_bind")
generate!("builtin_commandline")
generate!("init_input")
generate!("shell_modes_ffi")
generate!("log_extra_to_flog_file")
@ -96,7 +89,6 @@ include_cpp! {
generate!("reader_change_history")
generate!("reader_change_cursor_selection_mode")
generate!("reader_set_autosuggestion_enabled_ffi")
generate!("update_wait_on_sequence_key_ms_ffi")
}
/// Allow wcharz_t to be "into" wstr.

View file

@ -12,96 +12,11 @@ use std::os::fd::RawFd;
use std::ptr;
use std::sync::atomic::{AtomicUsize, Ordering};
#[repr(u8)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ReadlineCmd {
BeginningOfLine,
EndOfLine,
ForwardChar,
BackwardChar,
ForwardSingleChar,
ForwardWord,
BackwardWord,
ForwardBigword,
BackwardBigword,
NextdOrForwardWord,
PrevdOrBackwardWord,
HistorySearchBackward,
HistorySearchForward,
HistoryPrefixSearchBackward,
HistoryPrefixSearchForward,
HistoryPager,
HistoryPagerDelete,
DeleteChar,
BackwardDeleteChar,
KillLine,
Yank,
YankPop,
Complete,
CompleteAndSearch,
PagerToggleSearch,
BeginningOfHistory,
EndOfHistory,
BackwardKillLine,
KillWholeLine,
KillInnerLine,
KillWord,
KillBigword,
BackwardKillWord,
BackwardKillPathComponent,
BackwardKillBigword,
HistoryTokenSearchBackward,
HistoryTokenSearchForward,
SelfInsert,
SelfInsertNotFirst,
TransposeChars,
TransposeWords,
UpcaseWord,
DowncaseWord,
CapitalizeWord,
TogglecaseChar,
TogglecaseSelection,
Execute,
BeginningOfBuffer,
EndOfBuffer,
RepaintMode,
Repaint,
ForceRepaint,
UpLine,
DownLine,
SuppressAutosuggestion,
AcceptAutosuggestion,
BeginSelection,
SwapSelectionStartStop,
EndSelection,
KillSelection,
InsertLineUnder,
InsertLineOver,
ForwardJump,
BackwardJump,
ForwardJumpTill,
BackwardJumpTill,
FuncAnd,
FuncOr,
ExpandAbbr,
DeleteOrExit,
Exit,
CancelCommandline,
Cancel,
Undo,
Redo,
BeginUndoGroup,
EndUndoGroup,
RepeatJump,
DisableMouseTracking,
// ncurses uses the obvious name
ClearScreenAndRepaint,
// NOTE: This one has to be last.
ReverseRepeatJump,
}
// The range of key codes for inputrc-style keyboard functions.
pub const R_END_INPUT_FUNCTIONS: usize = (ReadlineCmd::ReverseRepeatJump as usize) + 1;
pub const R_END_INPUT_FUNCTIONS: usize = (ReadlineCmd::ReverseRepeatJump.repr as usize) + 1;
// TODO: move CharInputStyle and ReadlineCmd here once they no longer must be exposed to C++.
pub use crate::input_ffi::{CharInputStyle, ReadlineCmd};
/// Represents an event on the character input stream.
#[derive(Debug, Copy, Clone)]
@ -120,17 +35,6 @@ pub enum CharEventType {
CheckExit,
}
/// Hackish: the input style, which describes how char events (only) are applied to the command
/// line. Note this is set only after applying bindings; it is not set from readb().
#[derive(Debug, Copy, Clone)]
pub enum CharInputStyle {
// Insert characters normally.
Normal,
// Insert characters only if the cursor is not at the beginning. Otherwise, discard them.
NotFirst,
}
#[derive(Debug, Clone)]
pub struct CharEvent {
pub evt: CharEventType,

267
fish-rust/src/input_ffi.rs Normal file
View file

@ -0,0 +1,267 @@
use crate::ffi::wcstring_list_ffi_t;
use crate::input::*;
use crate::input_common::*;
use crate::parser::ParserRefFFI;
use crate::threads::CppCallback;
use crate::wchar::prelude::*;
use crate::wchar_ffi::AsWstr;
use crate::wchar_ffi::WCharToFFI;
use cxx::CxxWString;
pub use ffi::{CharInputStyle, ReadlineCmd};
use std::pin::Pin;
// Returns the code, or -1 on failure.
fn input_function_get_code_ffi(name: &CxxWString) -> i32 {
if let Some(code) = input_function_get_code(name.as_wstr()) {
code.repr as i32
} else {
-1
}
}
fn char_event_from_readline_ffi(cmd: ReadlineCmd) -> Box<CharEvent> {
Box::new(CharEvent::from_readline(cmd))
}
fn char_event_from_char_ffi(c: u8) -> Box<CharEvent> {
Box::new(CharEvent::from_char(c.into()))
}
fn make_inputter_ffi(parser: &ParserRefFFI, in_fd: i32) -> Box<Inputter> {
Box::new(Inputter::new(parser.0.clone(), in_fd))
}
fn make_input_event_queue_ffi(in_fd: i32) -> Box<InputEventQueue> {
Box::new(InputEventQueue::new(in_fd))
}
fn input_terminfo_get_name_ffi(seq: &CxxWString, out: Pin<&mut CxxWString>) -> bool {
let Some(name) = input_terminfo_get_name(seq.as_wstr()) else {
return false;
};
out.push_chars(name.as_char_slice());
true
}
impl Inputter {
#[allow(clippy::boxed_local)]
fn queue_char_ffi(&mut self, ch: Box<CharEvent>) {
self.queue_char(*ch);
}
fn read_char_ffi(&mut self, command_handler: &cxx::SharedPtr<CppCallback>) -> Box<CharEvent> {
let mut rust_handler = |cmds: &[WString]| {
let ffi_cmds = cmds.to_ffi();
command_handler.invoke_with_param(ffi_cmds.as_ref().unwrap() as *const _ as *const u8);
};
let mhandler = if !command_handler.is_null() {
Some(&mut rust_handler as &mut CommandHandler)
} else {
None
};
Box::new(self.read_char(mhandler))
}
fn function_pop_arg_ffi(&mut self) -> u32 {
self.function_pop_arg().into()
}
}
impl CharEvent {
fn get_char_ffi(&self) -> u32 {
self.get_char().into()
}
fn get_input_style_ffi(&self) -> CharInputStyle {
self.input_style
}
}
impl InputEventQueue {
// Returns Box<CharEvent>::into_raw(), or nullptr if None.
fn readch_timed_esc_ffi(&mut self) -> *mut CharEvent {
match self.readch_timed_esc() {
Some(ch) => Box::into_raw(Box::new(ch)),
None => std::ptr::null_mut(),
}
}
}
#[cxx::bridge]
mod ffi {
/// Hackish: the input style, which describes how char events (only) are applied to the command
/// line. Note this is set only after applying bindings; it is not set from readb().
#[cxx_name = "char_input_style_t"]
#[repr(u8)]
#[derive(Debug, Copy, Clone)]
pub enum CharInputStyle {
// Insert characters normally.
Normal,
// Insert characters only if the cursor is not at the beginning. Otherwise, discard them.
NotFirst,
}
#[cxx_name = "readline_cmd_t"]
#[repr(u8)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ReadlineCmd {
BeginningOfLine,
EndOfLine,
ForwardChar,
BackwardChar,
ForwardSingleChar,
ForwardWord,
BackwardWord,
ForwardBigword,
BackwardBigword,
NextdOrForwardWord,
PrevdOrBackwardWord,
HistorySearchBackward,
HistorySearchForward,
HistoryPrefixSearchBackward,
HistoryPrefixSearchForward,
HistoryPager,
HistoryPagerDelete,
DeleteChar,
BackwardDeleteChar,
KillLine,
Yank,
YankPop,
Complete,
CompleteAndSearch,
PagerToggleSearch,
BeginningOfHistory,
EndOfHistory,
BackwardKillLine,
KillWholeLine,
KillInnerLine,
KillWord,
KillBigword,
BackwardKillWord,
BackwardKillPathComponent,
BackwardKillBigword,
HistoryTokenSearchBackward,
HistoryTokenSearchForward,
SelfInsert,
SelfInsertNotFirst,
TransposeChars,
TransposeWords,
UpcaseWord,
DowncaseWord,
CapitalizeWord,
TogglecaseChar,
TogglecaseSelection,
Execute,
BeginningOfBuffer,
EndOfBuffer,
RepaintMode,
Repaint,
ForceRepaint,
UpLine,
DownLine,
SuppressAutosuggestion,
AcceptAutosuggestion,
BeginSelection,
SwapSelectionStartStop,
EndSelection,
KillSelection,
InsertLineUnder,
InsertLineOver,
ForwardJump,
BackwardJump,
ForwardJumpTill,
BackwardJumpTill,
FuncAnd,
FuncOr,
ExpandAbbr,
DeleteOrExit,
Exit,
CancelCommandline,
Cancel,
Undo,
Redo,
BeginUndoGroup,
EndUndoGroup,
RepeatJump,
DisableMouseTracking,
// ncurses uses the obvious name
ClearScreenAndRepaint,
// NOTE: This one has to be last.
ReverseRepeatJump,
}
extern "C++" {
include!("parser.h");
include!("reader.h");
include!("callback.h");
type wcstring_list_ffi_t = super::wcstring_list_ffi_t;
type ParserRef = crate::parser::ParserRefFFI;
#[rust_name = "CppCallback"]
type callback_t = crate::threads::CppCallback;
}
extern "Rust" {
fn init_input();
#[cxx_name = "input_function_get_code"]
fn input_function_get_code_ffi(name: &CxxWString) -> i32;
#[cxx_name = "input_terminfo_get_name"]
fn input_terminfo_get_name_ffi(seq: &CxxWString, out: Pin<&mut CxxWString>) -> bool;
}
extern "Rust" {
type CharEvent;
#[cxx_name = "char_event_from_readline"]
fn char_event_from_readline_ffi(cmd: ReadlineCmd) -> Box<CharEvent>;
#[cxx_name = "char_event_from_char"]
fn char_event_from_char_ffi(c: u8) -> Box<CharEvent>;
fn is_char(&self) -> bool;
fn is_readline(&self) -> bool;
fn is_check_exit(&self) -> bool;
fn is_eof(&self) -> bool;
#[cxx_name = "get_char"]
fn get_char_ffi(&self) -> u32;
#[cxx_name = "get_input_style"]
fn get_input_style_ffi(&self) -> CharInputStyle;
fn get_readline(&self) -> ReadlineCmd;
}
extern "Rust" {
type Inputter;
#[cxx_name = "make_inputter"]
fn make_inputter_ffi(parser: &ParserRef, in_fd: i32) -> Box<Inputter>;
#[cxx_name = "queue_char"]
fn queue_char_ffi(&mut self, ch: Box<CharEvent>);
fn queue_readline(&mut self, cmd: ReadlineCmd);
#[cxx_name = "read_char"]
fn read_char_ffi(&mut self, command_handler: &SharedPtr<CppCallback>) -> Box<CharEvent>;
fn function_set_status(&mut self, status: bool);
#[cxx_name = "function_pop_arg"]
fn function_pop_arg_ffi(&mut self) -> u32;
}
extern "Rust" {
type InputEventQueue;
#[cxx_name = "make_input_event_queue"]
fn make_input_event_queue_ffi(in_fd: i32) -> Box<InputEventQueue>;
#[cxx_name = "readch_timed_esc"]
fn readch_timed_esc_ffi(&mut self) -> *mut CharEvent;
}
}

View file

@ -71,6 +71,7 @@ mod highlight;
mod history;
mod input;
mod input_common;
mod input_ffi;
mod io;
mod job_group;
mod kill;

View file

@ -102,6 +102,7 @@ mod ffi {
}
}
pub use ffi::CppCallback;
unsafe impl Send for ffi::CppCallback {}
unsafe impl Sync for ffi::CppCallback {}

View file

@ -1,521 +0,0 @@
// Implementation of the bind builtin.
#include "config.h" // IWYU pragma: keep
#include "bind.h"
#include <unistd.h>
#include <cerrno>
#include <set>
#include <string>
#include <vector>
#include "../builtin.h"
#include "../common.h"
#include "../env.h"
#include "../fallback.h" // IWYU pragma: keep
#include "../highlight.h"
#include "../input.h"
#include "../io.h"
#include "../maybe.h"
#include "../parser.h"
#include "../wgetopt.h"
#include "../wutil.h" // IWYU pragma: keep
#include "builtins/shared.rs.h"
enum { BIND_INSERT, BIND_ERASE, BIND_KEY_NAMES, BIND_FUNCTION_NAMES };
struct bind_cmd_opts_t {
bool all = false;
bool bind_mode_given = false;
bool list_modes = false;
bool print_help = false;
bool silent = false;
bool use_terminfo = false;
bool have_user = false;
bool user = false;
bool have_preset = false;
bool preset = false;
int mode = BIND_INSERT;
const wchar_t *bind_mode = DEFAULT_BIND_MODE;
const wchar_t *sets_bind_mode = L"";
};
namespace {
class builtin_bind_t {
public:
maybe_t<int> builtin_bind(const parser_t &parser, io_streams_t &streams, const wchar_t **argv);
builtin_bind_t() : input_mappings_(input_mappings()) {}
private:
bind_cmd_opts_t *opts;
/// Note that builtin_bind_t holds the singleton lock.
/// It must not call out to anything which can execute fish shell code or attempt to acquire the
/// lock again.
acquired_lock<input_mapping_set_t> input_mappings_;
void list(const wchar_t *bind_mode, bool user, const parser_t &parser, io_streams_t &streams);
void key_names(bool all, io_streams_t &streams);
void function_names(io_streams_t &streams);
bool add(const wcstring &seq, const wchar_t *const *cmds, size_t cmds_len, const wchar_t *mode,
const wchar_t *sets_mode, bool terminfo, bool user, io_streams_t &streams);
bool erase(const wchar_t *const *seq, bool all, const wchar_t *mode, bool use_terminfo,
bool user, io_streams_t &streams);
bool get_terminfo_sequence(const wcstring &seq, wcstring *out_seq, io_streams_t &streams) const;
bool insert(int optind, int argc, const wchar_t **argv, const parser_t &parser,
io_streams_t &streams);
void list_modes(io_streams_t &streams);
bool list_one(const wcstring &seq, const wcstring &bind_mode, bool user, const parser_t &parser,
io_streams_t &streams);
bool list_one(const wcstring &seq, const wcstring &bind_mode, bool user, bool preset,
const parser_t &parser, io_streams_t &streams);
};
/// List a single key binding.
/// Returns false if no binding with that sequence and mode exists.
bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bool user,
const parser_t &parser, io_streams_t &streams) {
std::vector<wcstring> ecmds;
wcstring sets_mode, out;
if (!input_mappings_->get(seq, bind_mode, &ecmds, user, &sets_mode)) {
return false;
}
out.append(L"bind");
// Append the mode flags if applicable.
if (!user) {
out.append(L" --preset");
}
if (bind_mode != DEFAULT_BIND_MODE) {
out.append(L" -M ");
out.append(escape_string(bind_mode));
}
if (!sets_mode.empty() && sets_mode != bind_mode) {
out.append(L" -m ");
out.append(escape_string(sets_mode));
}
// Append the name.
wcstring tname;
if (input_terminfo_get_name(seq, &tname)) {
// Note that we show -k here because we have an input key name.
out.append(L" -k ");
out.append(tname);
} else {
// No key name, so no -k; we show the escape sequence directly.
const wcstring eseq = escape_string(seq);
out.append(L" ");
out.append(eseq);
}
// Now show the list of commands.
for (const auto &ecmd : ecmds) {
out.push_back(' ');
out.append(escape_string(ecmd));
}
out.push_back(L'\n');
if (!streams.out_is_redirected() && isatty(STDOUT_FILENO)) {
auto ffi_colors = highlight_shell_ffi(out, *parser_context(parser), false, {});
auto ffi_colored = colorize(out, *ffi_colors, parser.vars());
std::string colored{ffi_colored.begin(), ffi_colored.end()};
streams.out()->append(str2wcstring(std::move(colored)));
} else {
streams.out()->append(out);
}
return true;
}
// Overload with both kinds of bindings.
// Returns false only if neither exists.
bool builtin_bind_t::list_one(const wcstring &seq, const wcstring &bind_mode, bool user,
bool preset, const parser_t &parser, io_streams_t &streams) {
bool retval = false;
if (preset) {
retval |= list_one(seq, bind_mode, false, parser, streams);
}
if (user) {
retval |= list_one(seq, bind_mode, true, parser, streams);
}
return retval;
}
/// List all current key bindings.
void builtin_bind_t::list(const wchar_t *bind_mode, bool user, const parser_t &parser,
io_streams_t &streams) {
const std::vector<input_mapping_name_t> lst = input_mappings_->get_names(user);
for (const input_mapping_name_t &binding : lst) {
if (bind_mode && bind_mode != binding.mode) {
continue;
}
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.
void builtin_bind_t::key_names(bool all, io_streams_t &streams) {
const std::vector<wcstring> names = input_terminfo_get_names(!all);
for (const wcstring &name : names) {
streams.out()->append(name);
streams.out()->push(L'\n');
}
}
/// Print all the special key binding functions to string buffer used for standard output.
void builtin_bind_t::function_names(io_streams_t &streams) {
std::vector<wcstring> names = input_function_get_names();
for (const auto &name : names) {
auto seq = name.c_str();
streams.out()->append(format_string(L"%ls\n", seq));
}
}
/// Wraps input_terminfo_get_sequence(), appending the correct error messages as needed.
bool builtin_bind_t::get_terminfo_sequence(const wcstring &seq, wcstring *out_seq,
io_streams_t &streams) const {
if (input_terminfo_get_sequence(seq, out_seq)) {
return true;
}
wcstring eseq = escape_string(seq, ESCAPE_NO_PRINTABLES);
if (!opts->silent) {
if (errno == ENOENT) {
streams.err()->append(
format_string(_(L"%ls: No key with name '%ls' found\n"), L"bind", eseq.c_str()));
} else if (errno == EILSEQ) {
streams.err()->append(format_string(
_(L"%ls: Key with name '%ls' does not have any mapping\n"), L"bind", eseq.c_str()));
} else {
streams.err()->append(
format_string(_(L"%ls: Unknown error trying to bind to key named '%ls'\n"), L"bind",
eseq.c_str()));
}
}
return false;
}
/// Add specified key binding.
bool builtin_bind_t::add(const wcstring &seq, const wchar_t *const *cmds, size_t cmds_len,
const wchar_t *mode, const wchar_t *sets_mode, bool terminfo, bool user,
io_streams_t &streams) {
if (terminfo) {
wcstring seq2;
if (get_terminfo_sequence(seq, &seq2, streams)) {
input_mappings_->add(seq2, cmds, cmds_len, mode, sets_mode, user);
} else {
return true;
}
} else {
input_mappings_->add(seq, cmds, cmds_len, mode, sets_mode, user);
}
return 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 mode
/// if specified, only bindings from that mode will be erased. If not given
/// and @c all is @c false, @c DEFAULT_BIND_MODE will be used.
/// @param use_terminfo
/// Whether to look use terminfo -k name
///
bool builtin_bind_t::erase(const wchar_t *const *seq, bool all, const wchar_t *mode,
bool use_terminfo, bool user, io_streams_t &streams) {
if (all) {
input_mappings_->clear(mode, user);
return false;
}
bool res = false;
if (mode == nullptr) mode = DEFAULT_BIND_MODE; //!OCLINT(parameter reassignment)
while (*seq) {
if (use_terminfo) {
wcstring seq2;
if (get_terminfo_sequence(*seq++, &seq2, streams)) {
input_mappings_->erase(seq2, mode, user);
} else {
res = true;
}
} else {
input_mappings_->erase(*seq++, mode, user);
}
}
return res;
}
bool builtin_bind_t::insert(int optind, int argc, const wchar_t **argv, const parser_t &parser,
io_streams_t &streams) {
const wchar_t *cmd = argv[0];
int arg_count = argc - optind;
if (arg_count < 2) {
// If we get both or neither preset/user, we list both.
if (!opts->have_preset && !opts->have_user) {
opts->preset = true;
opts->user = true;
}
} else {
// Inserting both on the other hand makes no sense.
if (opts->have_preset && opts->have_user) {
streams.err()->append(
format_string(BUILTIN_ERR_COMBO2_EXCLUSIVE, cmd, L"--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).
if (opts->preset) {
list(opts->bind_mode_given ? opts->bind_mode : nullptr, false, parser, streams);
}
if (opts->user) {
list(opts->bind_mode_given ? opts->bind_mode : nullptr, true, parser, streams);
}
} else if (arg_count == 1) {
wcstring seq;
if (opts->use_terminfo) {
if (!get_terminfo_sequence(argv[optind], &seq, streams)) {
// get_terminfo_sequence already printed the error.
return true;
}
} else {
seq = argv[optind];
}
if (!list_one(seq, opts->bind_mode, opts->user, opts->preset, parser, streams)) {
wcstring eseq = escape_string(argv[optind], ESCAPE_NO_PRINTABLES);
if (!opts->silent) {
if (opts->use_terminfo) {
streams.err()->append(format_string(_(L"%ls: No binding found for key '%ls'\n"),
cmd, eseq.c_str()));
} else {
streams.err()->append(format_string(
_(L"%ls: No binding found for sequence '%ls'\n"), cmd, eseq.c_str()));
}
}
return true;
}
} else {
// Actually insert!
if (add(argv[optind], argv + (optind + 1), argc - (optind + 1), opts->bind_mode,
opts->sets_bind_mode, opts->use_terminfo, opts->user, streams)) {
return true;
}
}
return false;
}
/// List all current bind modes.
void builtin_bind_t::list_modes(io_streams_t &streams) {
// List all known modes, even if they are only in preset bindings.
const std::vector<input_mapping_name_t> lst = input_mappings_->get_names(true);
const std::vector<input_mapping_name_t> preset_lst = input_mappings_->get_names(false);
// A set accomplishes two things for us here:
// - It removes duplicates (no twenty "default" entries).
// - It sorts it, which makes it nicer on the user.
std::set<wcstring> modes;
for (const input_mapping_name_t &binding : lst) {
modes.insert(binding.mode);
}
for (const input_mapping_name_t &binding : preset_lst) {
modes.insert(binding.mode);
}
for (const auto &mode : modes) {
streams.out()->append(format_string(L"%ls\n", mode.c_str()));
}
}
static int parse_cmd_opts(bind_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method)
int argc, const wchar_t **argv, const parser_t &parser,
io_streams_t &streams) {
const wchar_t *cmd = argv[0];
static const wchar_t *const short_options = L":aehkKfM:Lm:s";
static const struct woption long_options[] = {{L"all", no_argument, 'a'},
{L"erase", no_argument, 'e'},
{L"function-names", no_argument, 'f'},
{L"help", no_argument, 'h'},
{L"key", no_argument, 'k'},
{L"key-names", no_argument, 'K'},
{L"list-modes", no_argument, 'L'},
{L"mode", required_argument, 'M'},
{L"preset", no_argument, 'p'},
{L"sets-mode", required_argument, 'm'},
{L"silent", no_argument, 's'},
{L"user", no_argument, 'u'},
{}};
int opt;
wgetopter_t w;
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
switch (opt) {
case L'a': {
opts.all = true;
break;
}
case L'e': {
opts.mode = BIND_ERASE;
break;
}
case L'f': {
opts.mode = BIND_FUNCTION_NAMES;
break;
}
case L'h': {
opts.print_help = true;
break;
}
case L'k': {
opts.use_terminfo = true;
break;
}
case L'K': {
opts.mode = BIND_KEY_NAMES;
break;
}
case L'L': {
opts.list_modes = true;
return STATUS_CMD_OK;
}
case L'M': {
if (!valid_var_name(w.woptarg)) {
streams.err()->append(format_string(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg));
return STATUS_INVALID_ARGS;
}
opts.bind_mode = w.woptarg;
opts.bind_mode_given = true;
break;
}
case L'm': {
if (!valid_var_name(w.woptarg)) {
streams.err()->append(format_string(BUILTIN_ERR_BIND_MODE, cmd, w.woptarg));
return STATUS_INVALID_ARGS;
}
opts.sets_bind_mode = w.woptarg;
break;
}
case L'p': {
opts.have_preset = true;
opts.preset = true;
break;
}
case L's': {
opts.silent = true;
break;
}
case L'u': {
opts.have_user = true;
opts.user = true;
break;
}
case ':': {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], true);
return STATUS_INVALID_ARGS;
}
case L'?': {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], true);
return STATUS_INVALID_ARGS;
}
default: {
DIE("unexpected retval from wgetopt_long");
}
}
}
*optind = w.woptind;
return STATUS_CMD_OK;
}
} // namespace
/// The bind builtin, used for setting character sequences.
maybe_t<int> builtin_bind_t::builtin_bind(const parser_t &parser, io_streams_t &streams,
const wchar_t **argv) {
const wchar_t *cmd = argv[0];
int argc = builtin_count_args(argv);
bind_cmd_opts_t opts;
this->opts = &opts;
int optind;
int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams);
if (retval != STATUS_CMD_OK) return retval;
if (opts.list_modes) {
list_modes(streams);
return STATUS_CMD_OK;
}
if (opts.print_help) {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
// Default to user mode
if (!opts.have_preset && !opts.have_user) opts.user = true;
switch (opts.mode) {
case BIND_ERASE: {
const wchar_t *bind_mode = opts.bind_mode_given ? opts.bind_mode : nullptr;
// If we get both, we erase both.
if (opts.user) {
if (erase(&argv[optind], opts.all, bind_mode, opts.use_terminfo, /* user */ true,
streams)) {
return STATUS_CMD_ERROR;
}
}
if (opts.preset) {
if (erase(&argv[optind], opts.all, bind_mode, opts.use_terminfo, /* user */ false,
streams)) {
return STATUS_CMD_ERROR;
}
}
break;
}
case BIND_INSERT: {
if (insert(optind, argc, argv, parser, streams)) {
return STATUS_CMD_ERROR;
}
break;
}
case BIND_KEY_NAMES: {
key_names(opts.all, streams);
break;
}
case BIND_FUNCTION_NAMES: {
function_names(streams);
break;
}
default: {
streams.err()->append(format_string(_(L"%ls: Invalid state\n"), cmd));
return STATUS_CMD_ERROR;
}
}
return STATUS_CMD_OK;
}
int builtin_bind(const void *_parser, void *_streams, void *_argv) {
const auto &parser = *static_cast<const parser_t *>(_parser);
auto &streams = *static_cast<io_streams_t *>(_streams);
auto argv = static_cast<const wchar_t **>(_argv);
builtin_bind_t bind;
return *bind.builtin_bind(parser, streams, argv);
}

View file

@ -1,14 +0,0 @@
// Prototypes for executing builtin_bind function.
#ifndef FISH_BUILTIN_BIND_H
#define FISH_BUILTIN_BIND_H
#include "../maybe.h"
struct Parser;
struct IoStreams;
using parser_t = Parser;
using io_streams_t = IoStreams;
int builtin_bind(const void *parser, void *streams, void *argv);
#endif

View file

@ -11,8 +11,6 @@
#include "../builtin.h"
#include "../common.h"
#include "../fallback.h" // IWYU pragma: keep
#include "../input.h"
#include "../input_common.h"
#include "../io.h"
#include "../maybe.h"
#include "../parse_constants.h"
@ -24,6 +22,7 @@
#include "../wgetopt.h"
#include "../wutil.h" // IWYU pragma: keep
#include "builtins/shared.rs.h"
#include "input_ffi.rs.h"
/// Which part of the comandbuffer are we operating on.
enum {
@ -305,10 +304,12 @@ int builtin_commandline(const void *_parser, void *_streams, void *_argv) {
using rl = readline_cmd_t;
for (i = w.woptind; i < argc; i++) {
if (auto mc = input_function_get_code(argv[i])) {
int mci = input_function_get_code(argv[i]);
if (mci >= 0) {
readline_cmd_t mc = static_cast<readline_cmd_t>(mci);
// Don't enqueue a repaint if we're currently in the middle of one,
// because that's an infinite loop.
if (mc == rl::repaint_mode || mc == rl::force_repaint || mc == rl::repaint) {
if (mc == rl::RepaintMode || mc == rl::ForceRepaint || mc == rl::Repaint) {
if (ld.is_repaint()) continue;
}
@ -317,11 +318,11 @@ int builtin_commandline(const void *_parser, void *_streams, void *_argv) {
// insert/replace operations into readline functions with associated data, so that
// all queued `commandline` operations - including buffer modifications - are
// executed in order
if (mc == rl::begin_undo_group || mc == rl::end_undo_group) {
reader_handle_command(*mc);
if (mc == rl::BeginUndoGroup || mc == rl::EndUndoGroup) {
reader_handle_command(mc);
} else {
// Inserts the readline function at the back of the queue.
reader_queue_ch(*mc);
reader_queue_ch(char_event_from_readline(mc));
}
} else {
streams.err()->append(

View file

@ -1,10 +1,8 @@
#include "builtin.h"
#include "builtins/bind.h"
#include "builtins/commandline.h"
#include "event.h"
#include "fds.h"
#include "highlight.h"
#include "input.h"
#include "parse_util.h"
#include "reader.h"
#include "screen.h"
@ -20,7 +18,6 @@ void mark_as_used(const parser_t& parser, env_stack_t& env_stack) {
expand_tilde(s, env_stack);
get_history_variable_text_ffi({});
highlight_spec_t{};
init_input();
reader_change_cursor_selection_mode(cursor_selection_mode_t::exclusive);
reader_change_history({});
reader_read_ffi({}, {}, {});
@ -34,6 +31,5 @@ void mark_as_used(const parser_t& parser, env_stack_t& env_stack) {
term_copy_modes();
unsetenv_lock({});
builtin_bind({}, {}, {});
builtin_commandline({}, {}, {});
}

View file

@ -27,8 +27,7 @@
#include "ffi_baggage.h"
#include "ffi_init.rs.h"
#include "fish_version.h"
#include "input.h"
#include "input_common.h"
#include "input_ffi.rs.h"
#include "maybe.h"
#include "parser.h"
#include "print_help.rs.h"
@ -85,7 +84,7 @@ static maybe_t<wcstring> sequence_name(wchar_t wc) {
for (size_t i = 0; i < recent_chars.size(); i++) {
wcstring out_name;
wcstring seq = str2wcstring(recent_chars.substr(i));
if (input_terminfo_get_name(seq, &out_name)) {
if (input_terminfo_get_name(seq, out_name)) {
return out_name;
}
}
@ -230,18 +229,21 @@ static double output_elapsed_time(double prev_tstamp, bool first_char_seen, bool
static void process_input(bool continuous_mode, bool verbose) {
bool first_char_seen = false;
double prev_tstamp = 0.0;
input_event_queue_t queue;
auto queue = make_input_event_queue(STDIN_FILENO);
std::vector<wchar_t> bind_chars;
std::fwprintf(stderr, L"Press a key:\n");
while (!check_exit_loop_maybe_warning(nullptr)) {
maybe_t<char_event_t> evt{};
maybe_t<rust::Box<char_event_t>> evt{};
if (reader_test_and_clear_interrupted()) {
evt = char_event_t{shell_modes.c_cc[VINTR]};
evt = char_event_from_char(shell_modes.c_cc[VINTR]);
} else {
evt = queue.readch_timed_esc();
char_event_t *evt_raw = queue->readch_timed_esc();
if (evt_raw) {
evt = rust::Box<char_event_t>::from_raw(evt_raw);
}
}
if (!evt || !evt->is_char()) {
if (!evt || !(*evt)->is_char()) {
output_bind_command(bind_chars);
if (first_char_seen && !continuous_mode) {
return;
@ -249,7 +251,7 @@ static void process_input(bool continuous_mode, bool verbose) {
continue;
}
wchar_t wc = evt->get_char();
wchar_t wc = (*evt)->get_char();
prev_tstamp = output_elapsed_time(prev_tstamp, first_char_seen, verbose);
// Hack for #3189. Do not suggest \c@ as the binding for nul, because a string containing
// nul cannot be passed to builtin_bind since it uses C strings. We'll output the name of

View file

@ -73,8 +73,7 @@
#include "global_safety.h"
#include "highlight.h"
#include "history.h"
#include "input.h"
#include "input_common.h"
#include "input_ffi.rs.h"
#include "io.h"
#include "iothread.h"
#include "kill.rs.h"

View file

@ -1,973 +0,0 @@
// Functions for reading a character of input from stdin.
#include "config.h"
#include <errno.h>
#if HAVE_TERM_H
#include <curses.h> // IWYU pragma: keep
#include <term.h>
#elif HAVE_NCURSES_TERM_H
#include <ncurses/term.h>
#endif
#include <termios.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "common.h"
#include "env.h"
#include "event.h"
#include "fallback.h" // IWYU pragma: keep
#include "flog.h"
#include "global_safety.h"
#include "input.h"
#include "input_common.h"
#include "parser.h"
#include "proc.h"
#include "reader.h"
#include "signals.h" // IWYU pragma: keep
#include "threads.rs.h"
#include "wutil.h" // IWYU pragma: keep
/// A name for our own key mapping for nul.
static const wchar_t *k_nul_mapping_name = L"nul";
/// Struct representing a keybinding. Returned by input_get_mappings.
struct input_mapping_t {
/// Character sequence which generates this event.
wcstring seq;
/// Commands that should be evaluated by this mapping.
std::vector<wcstring> commands;
/// We wish to preserve the user-specified order. This is just an incrementing value.
unsigned int specification_order;
/// Mode in which this command should be evaluated.
wcstring mode;
/// New mode that should be switched to after command evaluation.
wcstring sets_mode;
input_mapping_t(wcstring s, std::vector<wcstring> c, wcstring m, wcstring sm)
: seq(std::move(s)), commands(std::move(c)), mode(std::move(m)), sets_mode(std::move(sm)) {
static unsigned int s_last_input_map_spec_order = 0;
specification_order = ++s_last_input_map_spec_order;
}
/// \return true if this is a generic mapping, i.e. acts as a fallback.
bool is_generic() const { return seq.empty(); }
};
/// A struct representing the mapping from a terminfo key name to a terminfo character sequence.
struct terminfo_mapping_t {
// name of key
const wchar_t *name;
// character sequence generated on keypress, or none if there was no mapping.
maybe_t<std::string> seq;
terminfo_mapping_t(const wchar_t *name, const char *s) : name(name) {
if (s) seq.emplace(s);
}
terminfo_mapping_t(const wchar_t *name, std::string s) : name(name), seq(std::move(s)) {}
};
static constexpr size_t input_function_count = R_END_INPUT_FUNCTIONS;
/// Input function metadata. This list should be kept in sync with the key code list in
/// input_common.h.
struct input_function_metadata_t {
const wchar_t *name;
readline_cmd_t code;
};
/// A static mapping of all readline commands as strings to their readline_cmd_t equivalent.
/// Keep this list sorted alphabetically!
static constexpr const input_function_metadata_t input_function_metadata[] = {
// NULL makes it unusable - this is specially inserted when we detect mouse input
{L"", readline_cmd_t::disable_mouse_tracking},
{L"accept-autosuggestion", readline_cmd_t::accept_autosuggestion},
{L"and", readline_cmd_t::func_and},
{L"backward-bigword", readline_cmd_t::backward_bigword},
{L"backward-char", readline_cmd_t::backward_char},
{L"backward-delete-char", readline_cmd_t::backward_delete_char},
{L"backward-jump", readline_cmd_t::backward_jump},
{L"backward-jump-till", readline_cmd_t::backward_jump_till},
{L"backward-kill-bigword", readline_cmd_t::backward_kill_bigword},
{L"backward-kill-line", readline_cmd_t::backward_kill_line},
{L"backward-kill-path-component", readline_cmd_t::backward_kill_path_component},
{L"backward-kill-word", readline_cmd_t::backward_kill_word},
{L"backward-word", readline_cmd_t::backward_word},
{L"begin-selection", readline_cmd_t::begin_selection},
{L"begin-undo-group", readline_cmd_t::begin_undo_group},
{L"beginning-of-buffer", readline_cmd_t::beginning_of_buffer},
{L"beginning-of-history", readline_cmd_t::beginning_of_history},
{L"beginning-of-line", readline_cmd_t::beginning_of_line},
{L"cancel", readline_cmd_t::cancel},
{L"cancel-commandline", readline_cmd_t::cancel_commandline},
{L"capitalize-word", readline_cmd_t::capitalize_word},
{L"clear-screen", readline_cmd_t::clear_screen_and_repaint},
{L"complete", readline_cmd_t::complete},
{L"complete-and-search", readline_cmd_t::complete_and_search},
{L"delete-char", readline_cmd_t::delete_char},
{L"delete-or-exit", readline_cmd_t::delete_or_exit},
{L"down-line", readline_cmd_t::down_line},
{L"downcase-word", readline_cmd_t::downcase_word},
{L"end-of-buffer", readline_cmd_t::end_of_buffer},
{L"end-of-history", readline_cmd_t::end_of_history},
{L"end-of-line", readline_cmd_t::end_of_line},
{L"end-selection", readline_cmd_t::end_selection},
{L"end-undo-group", readline_cmd_t::end_undo_group},
{L"execute", readline_cmd_t::execute},
{L"exit", readline_cmd_t::exit},
{L"expand-abbr", readline_cmd_t::expand_abbr},
{L"force-repaint", readline_cmd_t::force_repaint},
{L"forward-bigword", readline_cmd_t::forward_bigword},
{L"forward-char", readline_cmd_t::forward_char},
{L"forward-jump", readline_cmd_t::forward_jump},
{L"forward-jump-till", readline_cmd_t::forward_jump_till},
{L"forward-single-char", readline_cmd_t::forward_single_char},
{L"forward-word", readline_cmd_t::forward_word},
{L"history-pager", readline_cmd_t::history_pager},
{L"history-pager-delete", readline_cmd_t::history_pager_delete},
{L"history-prefix-search-backward", readline_cmd_t::history_prefix_search_backward},
{L"history-prefix-search-forward", readline_cmd_t::history_prefix_search_forward},
{L"history-search-backward", readline_cmd_t::history_search_backward},
{L"history-search-forward", readline_cmd_t::history_search_forward},
{L"history-token-search-backward", readline_cmd_t::history_token_search_backward},
{L"history-token-search-forward", readline_cmd_t::history_token_search_forward},
{L"insert-line-over", readline_cmd_t::insert_line_over},
{L"insert-line-under", readline_cmd_t::insert_line_under},
{L"kill-bigword", readline_cmd_t::kill_bigword},
{L"kill-inner-line", readline_cmd_t::kill_inner_line},
{L"kill-line", readline_cmd_t::kill_line},
{L"kill-selection", readline_cmd_t::kill_selection},
{L"kill-whole-line", readline_cmd_t::kill_whole_line},
{L"kill-word", readline_cmd_t::kill_word},
{L"nextd-or-forward-word", readline_cmd_t::nextd_or_forward_word},
{L"or", readline_cmd_t::func_or},
{L"pager-toggle-search", readline_cmd_t::pager_toggle_search},
{L"prevd-or-backward-word", readline_cmd_t::prevd_or_backward_word},
{L"redo", readline_cmd_t::redo},
{L"repaint", readline_cmd_t::repaint},
{L"repaint-mode", readline_cmd_t::repaint_mode},
{L"repeat-jump", readline_cmd_t::repeat_jump},
{L"repeat-jump-reverse", readline_cmd_t::reverse_repeat_jump},
{L"self-insert", readline_cmd_t::self_insert},
{L"self-insert-notfirst", readline_cmd_t::self_insert_notfirst},
{L"suppress-autosuggestion", readline_cmd_t::suppress_autosuggestion},
{L"swap-selection-start-stop", readline_cmd_t::swap_selection_start_stop},
{L"togglecase-char", readline_cmd_t::togglecase_char},
{L"togglecase-selection", readline_cmd_t::togglecase_selection},
{L"transpose-chars", readline_cmd_t::transpose_chars},
{L"transpose-words", readline_cmd_t::transpose_words},
{L"undo", readline_cmd_t::undo},
{L"up-line", readline_cmd_t::up_line},
{L"upcase-word", readline_cmd_t::upcase_word},
{L"yank", readline_cmd_t::yank},
{L"yank-pop", readline_cmd_t::yank_pop},
};
ASSERT_SORTED_BY_NAME(input_function_metadata);
static_assert(sizeof(input_function_metadata) / sizeof(input_function_metadata[0]) ==
input_function_count,
"input_function_metadata size mismatch with input_common. Did you forget to update "
"input_function_metadata?");
// Keep this function for debug purposes
// See 031b265
wcstring describe_char(wint_t c) {
if (c < R_END_INPUT_FUNCTIONS) {
return format_string(L"%02x (%ls)", c, input_function_metadata[c].name);
}
return format_string(L"%02x", c);
}
using mapping_list_t = std::vector<input_mapping_t>;
input_mapping_set_t::input_mapping_set_t() = default;
input_mapping_set_t::~input_mapping_set_t() = default;
acquired_lock<input_mapping_set_t> input_mappings() {
static owning_lock<input_mapping_set_t> s_mappings{input_mapping_set_t()};
return s_mappings.acquire();
}
/// Terminfo map list.
static latch_t<std::vector<terminfo_mapping_t>> s_terminfo_mappings;
/// \return the input terminfo.
static std::vector<terminfo_mapping_t> create_input_terminfo();
/// Return the current bind mode.
static wcstring input_get_bind_mode(const environment_t &vars) {
auto mode = vars.get(FISH_BIND_MODE_VAR);
return mode ? mode->as_string() : DEFAULT_BIND_MODE;
}
/// Set the current bind mode.
static void input_set_bind_mode(const parser_t &parser, const wcstring &bm) {
// Only set this if it differs to not execute variable handlers all the time.
// modes may not be empty - empty is a sentinel value meaning to not change the mode
assert(!bm.empty());
if (input_get_bind_mode(env_stack_t{parser.vars_boxed()}) != bm) {
// Must send events here - see #6653.
parser.set_var_and_fire(FISH_BIND_MODE_VAR, ENV_GLOBAL, wcstring_list_ffi_t{{bm}});
}
}
/// Returns the arity of a given input function.
static int input_function_arity(readline_cmd_t function) {
switch (function) {
case readline_cmd_t::forward_jump:
case readline_cmd_t::backward_jump:
case readline_cmd_t::forward_jump_till:
case readline_cmd_t::backward_jump_till:
return 1;
default:
return 0;
}
}
/// Helper function to compare the lengths of sequences.
static bool length_is_greater_than(const input_mapping_t &m1, const input_mapping_t &m2) {
return m1.seq.size() > m2.seq.size();
}
static bool specification_order_is_less_than(const input_mapping_t &m1, const input_mapping_t &m2) {
return m1.specification_order < m2.specification_order;
}
/// Inserts an input mapping at the correct position. We sort them in descending order by length, so
/// that we test longer sequences first.
static void input_mapping_insert_sorted(mapping_list_t &ml, input_mapping_t new_mapping) {
auto loc = std::lower_bound(ml.begin(), ml.end(), new_mapping, length_is_greater_than);
ml.insert(loc, std::move(new_mapping));
}
/// Adds an input mapping.
void input_mapping_set_t::add(wcstring sequence, const wchar_t *const *commands,
size_t commands_len, const wchar_t *mode, const wchar_t *sets_mode,
bool user) {
assert(commands && mode && sets_mode && "Null parameter");
// Clear cached mappings.
all_mappings_cache_.reset();
// Remove existing mappings with this sequence.
const std::vector<wcstring> commands_vector(commands, commands + commands_len);
mapping_list_t &ml = user ? mapping_list_ : preset_mapping_list_;
for (input_mapping_t &m : ml) {
if (m.seq == sequence && m.mode == mode) {
m.commands = commands_vector;
m.sets_mode = sets_mode;
return;
}
}
// Add a new mapping, using the next order.
input_mapping_t new_mapping =
input_mapping_t(std::move(sequence), commands_vector, mode, sets_mode);
input_mapping_insert_sorted(ml, std::move(new_mapping));
}
void input_mapping_set_t::add(wcstring sequence, const wchar_t *command, const wchar_t *mode,
const wchar_t *sets_mode, bool user) {
input_mapping_set_t::add(std::move(sequence), &command, 1, mode, sets_mode, user);
}
/// Set up arrays used by readch to detect escape sequences for special keys and perform related
/// initializations for our input subsystem.
void init_input() {
ASSERT_IS_MAIN_THREAD();
if (s_terminfo_mappings.is_set()) return;
s_terminfo_mappings = create_input_terminfo();
auto input_mapping = input_mappings();
// If we have no keybindings, add a few simple defaults.
if (input_mapping->preset_mapping_list_.empty()) {
input_mapping->add(L"", L"self-insert", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\n", L"execute", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\r", L"execute", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\t", L"complete", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\x3", L"cancel-commandline", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE,
false);
input_mapping->add(L"\x4", L"exit", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\x5", L"bind", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
// ctrl-s
input_mapping->add(L"\x13", L"pager-toggle-search", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE,
false);
// ctrl-u
input_mapping->add(L"\x15", L"backward-kill-line", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE,
false);
// del/backspace
input_mapping->add(L"\x7f", L"backward-delete-char", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE,
false);
// Arrows - can't have functions, so *-or-search isn't available.
input_mapping->add(L"\x1B[A", L"up-line", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\x1B[B", L"down-line", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\x1B[C", L"forward-char", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\x1B[D", L"backward-char", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE,
false);
// emacs-style ctrl-p/n/b/f
input_mapping->add(L"\x10", L"up-line", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\x0e", L"down-line", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\x02", L"backward-char", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
input_mapping->add(L"\x06", L"forward-char", DEFAULT_BIND_MODE, DEFAULT_BIND_MODE, false);
}
}
inputter_t::inputter_t(const parser_t &parser, int in)
: input_event_queue_t(in), parser_(parser.shared()) {}
void inputter_t::prepare_to_select() /* override */ {
// Fire any pending events and reap stray processes, including printing exit status messages.
auto &parser = this->parser_->deref();
event_fire_delayed(parser);
if (job_reap(parser, true)) reader_schedule_prompt_repaint();
}
void inputter_t::select_interrupted() /* override */ {
// Readline commands may be bound to \cc which also sets the cancel flag.
// See #6937, #8125.
signal_clear_cancel();
// Fire any pending events and reap stray processes, including printing exit status messages.
auto &parser = this->parser_->deref();
event_fire_delayed(parser);
if (job_reap(parser, true)) reader_schedule_prompt_repaint();
// Tell the reader an event occurred.
if (reader_reading_interrupted()) {
auto vintr = shell_modes.c_cc[VINTR];
if (vintr != 0) {
this->push_front(char_event_t{vintr});
}
return;
}
this->push_front(char_event_t{char_event_type_t::check_exit});
}
void inputter_t::uvar_change_notified() /* override */ {
this->parser_->deref().sync_uvars_and_fire(true /* always */);
}
void inputter_t::function_push_arg(wchar_t arg) { input_function_args_.push_back(arg); }
wchar_t inputter_t::function_pop_arg() {
assert(!input_function_args_.empty() && "function_pop_arg underflow");
auto result = input_function_args_.back();
input_function_args_.pop_back();
return result;
}
void inputter_t::function_push_args(readline_cmd_t code) {
int arity = input_function_arity(code);
assert(event_storage_.empty() && "event_storage_ should be empty");
auto &skipped = event_storage_;
for (int i = 0; i < arity; i++) {
// Skip and queue up any function codes. See issue #2357.
wchar_t arg{};
for (;;) {
auto evt = this->readch();
if (evt.is_char()) {
arg = evt.get_char();
break;
}
skipped.push_back(evt);
}
function_push_arg(arg);
}
// Push the function codes back into the input stream.
this->insert_front(skipped.begin(), skipped.end());
event_storage_.clear();
}
/// Perform the action of the specified binding. allow_commands controls whether fish commands
/// should be executed, or should be deferred until later.
void inputter_t::mapping_execute(const input_mapping_t &m,
const command_handler_t &command_handler) {
// has_functions: there are functions that need to be put on the input queue
// has_commands: there are shell commands that need to be evaluated
bool has_commands = false, has_functions = false;
for (const wcstring &cmd : m.commands) {
if (input_function_get_code(cmd)) {
has_functions = true;
} else {
has_commands = true;
}
if (has_functions && has_commands) {
break;
}
}
// !has_functions && !has_commands: only set bind mode
if (!has_commands && !has_functions) {
if (!m.sets_mode.empty()) input_set_bind_mode(parser_->deref(), m.sets_mode);
return;
}
if (has_commands && !command_handler) {
// We don't want to run commands yet. Put the characters back and return check_exit.
this->insert_front(m.seq.cbegin(), m.seq.cend());
this->push_front(char_event_type_t::check_exit);
return; // skip the input_set_bind_mode
} else if (has_functions && !has_commands) {
// Functions are added at the head of the input queue.
for (auto it = m.commands.rbegin(), end = m.commands.rend(); it != end; ++it) {
readline_cmd_t code = input_function_get_code(*it).value();
function_push_args(code);
this->push_front(char_event_t(code, m.seq));
}
} else if (has_commands && !has_functions) {
// Execute all commands.
//
// FIXME(snnw): if commands add stuff to input queue (e.g. commandline -f execute), we won't
// see that until all other commands have also been run.
command_handler(m.commands);
this->push_front(char_event_type_t::check_exit);
} else {
// Invalid binding, mixed commands and functions. We would need to execute these one by
// one.
this->push_front(char_event_type_t::check_exit);
}
// Empty bind mode indicates to not reset the mode (#2871)
if (!m.sets_mode.empty()) input_set_bind_mode(parser_->deref(), m.sets_mode);
}
void inputter_t::queue_char(const char_event_t &ch) {
if (ch.is_readline()) {
function_push_args(ch.get_readline());
}
this->push_back(ch);
}
/// A class which allows accumulating input events, or returns them to the queue.
/// This contains a list of events which have been dequeued, and a current index into that list.
class event_queue_peeker_t {
public:
explicit event_queue_peeker_t(input_event_queue_t &event_queue) : event_queue_(event_queue) {}
/// \return the next event.
char_event_t next() {
assert(idx_ <= peeked_.size() && "Index must not be larger than dequeued event count");
if (idx_ == peeked_.size()) {
auto event = event_queue_.readch();
peeked_.push_back(event);
}
return peeked_.at(idx_++);
}
/// Check if the next event is the given character. This advances the index on success only.
/// If \p timed is set, then return false if this (or any other) character had a timeout.
bool next_is_char(wchar_t c, bool escaped = false) {
assert(idx_ <= peeked_.size() && "Index must not be larger than dequeued event count");
// See if we had a timeout already.
if (escaped && had_timeout_) {
return false;
}
// Grab a new event if we have exhausted what we have already peeked.
// Use either readch or readch_timed, per our param.
if (idx_ == peeked_.size()) {
char_event_t newevt{L'\0'};
if (!escaped) {
if (auto mevt = event_queue_.readch_timed_sequence_key()) {
newevt = mevt.acquire();
} else {
had_timeout_ = true;
return false;
}
} else if (auto mevt = event_queue_.readch_timed_esc()) {
newevt = mevt.acquire();
} else {
had_timeout_ = true;
return false;
}
peeked_.push_back(newevt);
}
// Now we have peeked far enough; check the event.
// If it matches the char, then increment the index.
if (peeked_.at(idx_).maybe_char() == c) {
idx_++;
return true;
}
return false;
}
/// \return the current index.
size_t len() const { return idx_; }
/// Consume all events up to the current index.
/// Remaining events are returned to the queue.
void consume() {
event_queue_.insert_front(peeked_.cbegin() + idx_, peeked_.cend());
peeked_.clear();
idx_ = 0;
}
/// Test if any of our peeked events are readline or check_exit.
bool char_sequence_interrupted() const {
for (const auto &evt : peeked_) {
if (evt.is_readline() || evt.is_check_exit()) return true;
}
return false;
}
/// Reset our index back to 0.
void restart() { idx_ = 0; }
~event_queue_peeker_t() {
assert(idx_ == 0 && "Events left on the queue - missing restart or consume?");
consume();
}
private:
/// The list of events which have been dequeued.
std::vector<char_event_t> peeked_{};
/// If set, then some previous timed event timed out.
bool had_timeout_{false};
/// The current index. This never exceeds peeked_.size().
size_t idx_{0};
/// The queue from which to read more events.
input_event_queue_t &event_queue_;
};
/// Try reading a mouse-tracking CSI sequence, using the given \p peeker.
/// Events are left on the peeker and the caller must restart or consume it.
/// \return true if matched, false if not.
static bool have_mouse_tracking_csi(event_queue_peeker_t *peeker) {
// Maximum length of any CSI is NPAR (which is nominally 16), although this does not account for
// user input intermixed with pseudo input generated by the tty emulator.
// Check for the CSI first.
if (!peeker->next_is_char(L'\x1b') || !peeker->next_is_char(L'[', true /* escaped */)) {
return false;
}
auto next = peeker->next().maybe_char();
size_t length = 0;
if (next == L'M') {
// Generic X10 or modified VT200 sequence. It doesn't matter which, they're both 6 chars
// (although in mode 1005, the characters may be unicode and not necessarily just one byte
// long) reporting the button that was clicked and its location.
length = 6;
} else if (next == L'<') {
// Extended (SGR/1006) mouse reporting mode, with semicolon-separated parameters for button
// code, Px, and Py, ending with 'M' for button press or 'm' for button release.
while (true) {
next = peeker->next().maybe_char();
if (next == L'M' || next == L'm') {
// However much we've read, we've consumed the CSI in its entirety.
length = peeker->len();
break;
}
if (peeker->len() >= 16) {
// This is likely a malformed mouse-reporting CSI but we can't do anything about it.
return false;
}
}
} else if (next == L't') {
// VT200 button released in mouse highlighting mode at valid text location. 5 chars.
length = 5;
} else if (next == L'T') {
// VT200 button released in mouse highlighting mode past end-of-line. 9 characters.
length = 9;
} else {
return false;
}
// Consume however many characters it takes to prevent the mouse tracking sequence from reaching
// the prompt, dependent on the class of mouse reporting as detected above.
while (peeker->len() < length) {
(void)peeker->next();
}
return true;
}
/// \return true if a given \p peeker matches a given sequence of char events given by \p str.
static bool try_peek_sequence(event_queue_peeker_t *peeker, const wcstring &str) {
assert(!str.empty() && "Empty string passed to try_peek_sequence");
wchar_t prev = L'\0';
for (wchar_t c : str) {
// If we just read an escape, we need to add a timeout for the next char,
// to distinguish between the actual escape key and an "alt"-modifier.
bool escaped = prev == L'\x1B';
if (!peeker->next_is_char(c, escaped)) {
return false;
}
prev = c;
}
return true;
}
/// \return the first mapping that matches, walking first over the user's mapping list, then the
/// preset list.
/// \return none if nothing matches, or if we may have matched a longer sequence but it was
/// interrupted by a readline event.
maybe_t<input_mapping_t> inputter_t::find_mapping(event_queue_peeker_t *peeker) {
const input_mapping_t *generic = nullptr;
env_stack_t vars{parser_->deref().vars_boxed()};
const wcstring bind_mode = input_get_bind_mode(vars);
const input_mapping_t *escape = nullptr;
auto ml = input_mappings()->all_mappings();
for (const auto &m : *ml) {
if (m.mode != bind_mode) {
continue;
}
// Defer generic mappings until the end.
if (m.is_generic()) {
if (!generic) generic = &m;
continue;
}
if (try_peek_sequence(peeker, m.seq)) {
// A binding for just escape should also be deferred
// so escape sequences take precedence.
if (m.seq == L"\x1B") {
if (!escape) {
escape = &m;
}
} else {
return m;
}
}
peeker->restart();
}
if (peeker->char_sequence_interrupted()) {
// We might have matched a longer sequence, but we were interrupted, e.g. by a signal.
FLOG(reader, "torn sequence, rearranging events");
return none();
}
if (escape) {
// We need to reconsume the escape.
peeker->next();
return *escape;
}
return generic ? maybe_t<input_mapping_t>(*generic) : none();
}
void inputter_t::mapping_execute_matching_or_generic(const command_handler_t &command_handler) {
event_queue_peeker_t peeker(*this);
// Check for mouse-tracking CSI before mappings to prevent the generic mapping handler from
// taking over.
if (have_mouse_tracking_csi(&peeker)) {
// fish recognizes but does not actually support mouse reporting. We never turn it on, and
// it's only ever enabled if a program we spawned enabled it and crashed or forgot to turn
// it off before exiting. We swallow the events to prevent garbage from piling up at the
// prompt, but don't do anything further with the received codes. To prevent this from
// breaking user interaction with the tty emulator, wasting CPU, and adding latency to the
// event queue, we turn off mouse reporting here.
//
// Since this is only called when we detect an incoming mouse reporting payload, we know the
// terminal emulator supports the xterm ANSI extensions for mouse reporting and can safely
// issue this without worrying about termcap.
FLOGF(reader, "Disabling mouse tracking");
// We can't/shouldn't directly manipulate stdout from `input.cpp`, so request the execution
// of a helper function to disable mouse tracking.
// writembs(outputter_t::stdoutput(), "\x1B[?1000l");
peeker.consume();
this->push_front(char_event_t(readline_cmd_t::disable_mouse_tracking, L""));
return;
}
peeker.restart();
// Check for ordinary mappings.
if (auto mapping = find_mapping(&peeker)) {
peeker.consume();
mapping_execute(*mapping, command_handler);
return;
}
peeker.restart();
if (peeker.char_sequence_interrupted()) {
// This may happen if we received a signal in the middle of an escape sequence or other
// multi-char binding. Move these non-char events to the front of the queue, handle them
// first, and then later we'll return and try the sequence again. See #8628.
peeker.consume();
this->promote_interruptions_to_front();
return;
}
FLOGF(reader, L"no generic found, ignoring char...");
auto evt = peeker.next();
peeker.consume();
}
/// Helper function. Picks through the queue of incoming characters until we get to one that's not a
/// readline function.
char_event_t inputter_t::read_characters_no_readline() {
assert(event_storage_.empty() && "saved_events_storage should be empty");
auto &saved_events = event_storage_;
char_event_t evt_to_return{0};
for (;;) {
auto evt = this->readch();
if (evt.is_readline()) {
saved_events.push_back(evt);
} else {
evt_to_return = evt;
break;
}
}
// Restore any readline functions
this->insert_front(saved_events.cbegin(), saved_events.cend());
event_storage_.clear();
return evt_to_return;
}
char_event_t inputter_t::read_char(const command_handler_t &command_handler) {
// Clear the interrupted flag.
reader_reset_interrupted();
// Search for sequence in mapping tables.
while (true) {
auto evt = this->readch();
if (evt.is_readline()) {
switch (evt.get_readline()) {
case readline_cmd_t::self_insert:
case readline_cmd_t::self_insert_notfirst: {
// Typically self-insert is generated by the generic (empty) binding.
// However if it is generated by a real sequence, then insert that sequence.
this->insert_front(evt.seq.cbegin(), evt.seq.cend());
// Issue #1595: ensure we only insert characters, not readline functions. The
// common case is that this will be empty.
char_event_t res = read_characters_no_readline();
// Hackish: mark the input style.
res.input_style = evt.get_readline() == readline_cmd_t::self_insert_notfirst
? char_input_style_t::notfirst
: char_input_style_t::normal;
return res;
}
case readline_cmd_t::func_and:
case readline_cmd_t::func_or: {
// If previous function has bad status, we want to skip all functions that
// follow us.
if ((evt.get_readline() == readline_cmd_t::func_and) != function_status_) {
drop_leading_readline_events();
}
continue;
}
default: {
return evt;
}
}
} else if (evt.is_eof()) {
// If we have EOF, we need to immediately quit.
// There's no need to go through the input functions.
return evt;
} else if (evt.is_check_exit()) {
// Allow the reader to check for exit conditions.
return evt;
} else {
assert(evt.is_char() && "Should be char event");
this->push_front(evt);
mapping_execute_matching_or_generic(command_handler);
// Regarding allow_commands, we're in a loop, but if a fish command is executed,
// check_exit is unread, so the next pass through the loop we'll break out and return
// it.
}
}
}
std::vector<input_mapping_name_t> input_mapping_set_t::get_names(bool user) const {
// Sort the mappings by the user specification order, so we can return them in the same order
// that the user specified them in.
std::vector<input_mapping_t> local_list = user ? mapping_list_ : preset_mapping_list_;
std::sort(local_list.begin(), local_list.end(), specification_order_is_less_than);
std::vector<input_mapping_name_t> result;
result.reserve(local_list.size());
for (const auto &m : local_list) {
result.push_back((input_mapping_name_t){m.seq, m.mode});
}
return result;
}
void input_mapping_set_t::clear(const wchar_t *mode, bool user) {
all_mappings_cache_.reset();
mapping_list_t &ml = user ? mapping_list_ : preset_mapping_list_;
auto should_erase = [=](const input_mapping_t &m) { return mode == nullptr || mode == m.mode; };
ml.erase(std::remove_if(ml.begin(), ml.end(), should_erase), ml.end());
}
bool input_mapping_set_t::erase(const wcstring &sequence, const wcstring &mode, bool user) {
// Clear cached mappings.
all_mappings_cache_.reset();
bool result = false;
mapping_list_t &ml = user ? mapping_list_ : preset_mapping_list_;
for (auto it = ml.begin(), end = ml.end(); it != end; ++it) {
if (sequence == it->seq && mode == it->mode) {
ml.erase(it);
result = true;
break;
}
}
return result;
}
bool input_mapping_set_t::get(const wcstring &sequence, const wcstring &mode,
std::vector<wcstring> *out_cmds, bool user,
wcstring *out_sets_mode) const {
bool result = false;
const auto &ml = user ? mapping_list_ : preset_mapping_list_;
for (const input_mapping_t &m : ml) {
if (sequence == m.seq && mode == m.mode) {
*out_cmds = m.commands;
*out_sets_mode = m.sets_mode;
result = true;
break;
}
}
return result;
}
std::shared_ptr<const mapping_list_t> input_mapping_set_t::all_mappings() {
// Populate the cache if needed.
if (!all_mappings_cache_) {
mapping_list_t all_mappings = mapping_list_;
all_mappings.insert(all_mappings.end(), preset_mapping_list_.begin(),
preset_mapping_list_.end());
all_mappings_cache_ = std::make_shared<const mapping_list_t>(std::move(all_mappings));
}
return all_mappings_cache_;
}
/// Create a list of terminfo mappings.
static std::vector<terminfo_mapping_t> create_input_terminfo() {
assert(CURSES_INITIALIZED);
if (!cur_term) return {}; // setupterm() failed so we can't referency any key definitions
#define TERMINFO_ADD(key) \
{ (L## #key) + 4, key }
return {
TERMINFO_ADD(key_a1), TERMINFO_ADD(key_a3), TERMINFO_ADD(key_b2),
TERMINFO_ADD(key_backspace), TERMINFO_ADD(key_beg), TERMINFO_ADD(key_btab),
TERMINFO_ADD(key_c1), TERMINFO_ADD(key_c3), TERMINFO_ADD(key_cancel),
TERMINFO_ADD(key_catab), TERMINFO_ADD(key_clear), TERMINFO_ADD(key_close),
TERMINFO_ADD(key_command), TERMINFO_ADD(key_copy), TERMINFO_ADD(key_create),
TERMINFO_ADD(key_ctab), TERMINFO_ADD(key_dc), TERMINFO_ADD(key_dl), TERMINFO_ADD(key_down),
TERMINFO_ADD(key_eic), TERMINFO_ADD(key_end), TERMINFO_ADD(key_enter),
TERMINFO_ADD(key_eol), TERMINFO_ADD(key_eos), TERMINFO_ADD(key_exit), TERMINFO_ADD(key_f0),
TERMINFO_ADD(key_f1), TERMINFO_ADD(key_f2), TERMINFO_ADD(key_f3), TERMINFO_ADD(key_f4),
TERMINFO_ADD(key_f5), TERMINFO_ADD(key_f6), TERMINFO_ADD(key_f7), TERMINFO_ADD(key_f8),
TERMINFO_ADD(key_f9), TERMINFO_ADD(key_f10), TERMINFO_ADD(key_f11), TERMINFO_ADD(key_f12),
TERMINFO_ADD(key_f13), TERMINFO_ADD(key_f14), TERMINFO_ADD(key_f15), TERMINFO_ADD(key_f16),
TERMINFO_ADD(key_f17), TERMINFO_ADD(key_f18), TERMINFO_ADD(key_f19), TERMINFO_ADD(key_f20),
// Note key_f21 through key_f63 are available but no actual keyboard supports them.
TERMINFO_ADD(key_find), TERMINFO_ADD(key_help), TERMINFO_ADD(key_home),
TERMINFO_ADD(key_ic), TERMINFO_ADD(key_il), TERMINFO_ADD(key_left), TERMINFO_ADD(key_ll),
TERMINFO_ADD(key_mark), TERMINFO_ADD(key_message), TERMINFO_ADD(key_move),
TERMINFO_ADD(key_next), TERMINFO_ADD(key_npage), TERMINFO_ADD(key_open),
TERMINFO_ADD(key_options), TERMINFO_ADD(key_ppage), TERMINFO_ADD(key_previous),
TERMINFO_ADD(key_print), TERMINFO_ADD(key_redo), TERMINFO_ADD(key_reference),
TERMINFO_ADD(key_refresh), TERMINFO_ADD(key_replace), TERMINFO_ADD(key_restart),
TERMINFO_ADD(key_resume), TERMINFO_ADD(key_right), TERMINFO_ADD(key_save),
TERMINFO_ADD(key_sbeg), TERMINFO_ADD(key_scancel), TERMINFO_ADD(key_scommand),
TERMINFO_ADD(key_scopy), TERMINFO_ADD(key_screate), TERMINFO_ADD(key_sdc),
TERMINFO_ADD(key_sdl), TERMINFO_ADD(key_select), TERMINFO_ADD(key_send),
TERMINFO_ADD(key_seol), TERMINFO_ADD(key_sexit), TERMINFO_ADD(key_sf),
TERMINFO_ADD(key_sfind), TERMINFO_ADD(key_shelp), TERMINFO_ADD(key_shome),
TERMINFO_ADD(key_sic), TERMINFO_ADD(key_sleft), TERMINFO_ADD(key_smessage),
TERMINFO_ADD(key_smove), TERMINFO_ADD(key_snext), TERMINFO_ADD(key_soptions),
TERMINFO_ADD(key_sprevious), TERMINFO_ADD(key_sprint), TERMINFO_ADD(key_sr),
TERMINFO_ADD(key_sredo), TERMINFO_ADD(key_sreplace), TERMINFO_ADD(key_sright),
TERMINFO_ADD(key_srsume), TERMINFO_ADD(key_ssave), TERMINFO_ADD(key_ssuspend),
TERMINFO_ADD(key_stab), TERMINFO_ADD(key_sundo), TERMINFO_ADD(key_suspend),
TERMINFO_ADD(key_undo), TERMINFO_ADD(key_up),
// We introduce our own name for the string containing only the nul character - see
// #3189. This can typically be generated via control-space.
terminfo_mapping_t(k_nul_mapping_name, std::string{'\0'})};
#undef TERMINFO_ADD
}
bool input_terminfo_get_sequence(const wcstring &name, wcstring *out_seq) {
assert(s_terminfo_mappings.is_set());
for (const terminfo_mapping_t &m : *s_terminfo_mappings) {
if (name == m.name) {
// Found the mapping.
if (!m.seq) {
errno = EILSEQ;
return false;
} else {
*out_seq = str2wcstring(*m.seq);
return true;
}
}
}
errno = ENOENT;
return false;
}
bool input_terminfo_get_name(const wcstring &seq, wcstring *out_name) {
assert(s_terminfo_mappings.is_set());
for (const terminfo_mapping_t &m : *s_terminfo_mappings) {
if (m.seq && seq == str2wcstring(*m.seq)) {
out_name->assign(m.name);
return true;
}
}
return false;
}
std::vector<wcstring> input_terminfo_get_names(bool skip_null) {
assert(s_terminfo_mappings.is_set());
std::vector<wcstring> result;
const auto &mappings = *s_terminfo_mappings;
result.reserve(mappings.size());
for (const terminfo_mapping_t &m : mappings) {
if (skip_null && !m.seq) {
continue;
}
result.emplace_back(m.name);
}
return result;
}
const std::vector<wcstring> &input_function_get_names() {
// The list and names of input functions are hard-coded and never change
static std::vector<wcstring> result = ([&]() {
std::vector<wcstring> result;
result.reserve(input_function_count);
for (const auto &md : input_function_metadata) {
if (md.name[0]) {
result.push_back(md.name);
}
}
return result;
})();
return result;
}
maybe_t<readline_cmd_t> input_function_get_code(const wcstring &name) {
// `input_function_metadata` is required to be kept in asciibetical order, making it OK to do
// a binary search for the matching name.
if (const input_function_metadata_t *md = get_by_sorted_name(name, input_function_metadata)) {
return md->code;
}
return none();
}

View file

@ -1,162 +0,0 @@
// Functions for reading a character of input from stdin, using the inputrc information for key
// bindings.
#ifndef FISH_INPUT_H
#define FISH_INPUT_H
#include <stddef.h>
#include <unistd.h>
#include <functional>
#include <memory>
#include <vector>
#include "common.h"
#include "input_common.h"
#include "maybe.h"
#include "parser.h"
#define FISH_BIND_MODE_VAR L"fish_bind_mode"
#define DEFAULT_BIND_MODE L"default"
class event_queue_peeker_t;
wcstring describe_char(wint_t c);
/// Set up arrays used by readch to detect escape sequences for special keys and perform related
/// initializations for our input subsystem.
void init_input();
struct input_mapping_t;
class inputter_t final : private input_event_queue_t {
public:
/// Construct from a parser, and the fd from which to read.
explicit inputter_t(const parser_t &parser, int in = STDIN_FILENO);
/// Read a character from stdin. Try to convert some escape sequences into character constants,
/// but do not permanently block the escape character.
///
/// This is performed in the same way vim does it, i.e. if an escape character is read, wait for
/// more input for a short time (a few milliseconds). If more input is available, it is assumed
/// to be an escape sequence for a special character (such as an arrow key), and readch attempts
/// to parse it. If no more input follows after the escape key, it is assumed to be an actual
/// escape key press, and is returned as such.
///
/// \p command_handler is used to run commands. If empty (in the std::function sense), when a
/// character is encountered that would invoke a fish command, it is unread and
/// char_event_type_t::check_exit is returned. Note the handler is not stored.
using command_handler_t = std::function<void(const std::vector<wcstring> &)>;
char_event_t read_char(const command_handler_t &command_handler = {});
/// Enqueue a char event to the queue of unread characters that input_readch will return before
/// actually reading from fd 0.
void queue_char(const char_event_t &ch);
/// Sets the return status of the most recently executed input function.
void function_set_status(bool status) { function_status_ = status; }
/// Pop an argument from the function argument stack.
wchar_t function_pop_arg();
private:
// Called right before potentially blocking in select().
void prepare_to_select() override;
// Called when select() is interrupted by a signal.
void select_interrupted() override;
// Called when we are notified of a uvar change.
void uvar_change_notified() override;
void function_push_arg(wchar_t arg);
void function_push_args(readline_cmd_t code);
void mapping_execute(const input_mapping_t &m, const command_handler_t &command_handler);
void mapping_execute_matching_or_generic(const command_handler_t &command_handler);
maybe_t<input_mapping_t> find_mapping(event_queue_peeker_t *peeker);
char_event_t read_characters_no_readline();
#if INCLUDE_RUST_HEADERS
// We need a parser to evaluate bindings.
const rust::Box<ParserRef> parser_;
#endif
std::vector<wchar_t> input_function_args_{};
bool function_status_{false};
// Transient storage to avoid repeated allocations.
std::vector<char_event_t> event_storage_{};
};
struct input_mapping_name_t {
wcstring seq;
wcstring mode;
};
/// The input mapping set is the set of mappings from character sequences to commands.
class input_mapping_set_t {
friend acquired_lock<input_mapping_set_t> input_mappings();
friend void init_input();
using mapping_list_t = std::vector<input_mapping_t>;
mapping_list_t mapping_list_;
mapping_list_t preset_mapping_list_;
std::shared_ptr<const mapping_list_t> all_mappings_cache_;
input_mapping_set_t();
public:
~input_mapping_set_t();
/// Erase all bindings.
void clear(const wchar_t *mode = nullptr, bool user = true);
/// Erase binding for specified key sequence.
bool erase(const wcstring &sequence, const wcstring &mode = DEFAULT_BIND_MODE,
bool user = true);
/// Gets the command bound to the specified key sequence in the specified mode. Returns true if
/// it exists, false if not.
bool get(const wcstring &sequence, const wcstring &mode, std::vector<wcstring> *out_cmds,
bool user, wcstring *out_sets_mode) const;
/// Returns all mapping names and modes.
std::vector<input_mapping_name_t> get_names(bool user = true) const;
/// Add a key mapping from the specified sequence to the specified command.
///
/// \param sequence the sequence to bind
/// \param command an input function that will be run whenever the key sequence occurs
void add(wcstring sequence, const wchar_t *command, const wchar_t *mode = DEFAULT_BIND_MODE,
const wchar_t *sets_mode = DEFAULT_BIND_MODE, bool user = true);
void add(wcstring sequence, const wchar_t *const *commands, size_t commands_len,
const wchar_t *mode = DEFAULT_BIND_MODE, const wchar_t *sets_mode = DEFAULT_BIND_MODE,
bool user = true);
/// \return a snapshot of the list of input mappings.
std::shared_ptr<const mapping_list_t> all_mappings();
};
/// Access the singleton input mapping set.
acquired_lock<input_mapping_set_t> input_mappings();
/// Return the sequence for the terminfo variable of the specified name.
///
/// If no terminfo variable of the specified name could be found, return false and set errno to
/// ENOENT. If the terminfo variable does not have a value, return false and set errno to EILSEQ.
bool input_terminfo_get_sequence(const wcstring &name, wcstring *out_seq);
/// Return the name of the terminfo variable with the specified sequence, in out_name. Returns true
/// if found, false if not found.
bool input_terminfo_get_name(const wcstring &seq, wcstring *out_name);
/// Return a list of all known terminfo names.
std::vector<wcstring> input_terminfo_get_names(bool skip_null);
/// Returns the input function code for the given input function name.
maybe_t<readline_cmd_t> input_function_get_code(const wcstring &name);
/// Returns a list of all existing input function names.
const std::vector<wcstring> &input_function_get_names(void);
#endif

View file

@ -1,341 +0,0 @@
// Implementation file for the low level input library.
#include "config.h"
#include <errno.h>
#include <pthread.h> // IWYU pragma: keep
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
#include <algorithm>
#include <climits>
#include <cstdlib>
#include <cstring>
#include <cwchar>
#include <deque>
#include <utility>
#include "common.h"
#include "env.h"
#include "env_universal_common.h"
#include "fallback.h" // IWYU pragma: keep
#include "fd_readable_set.rs.h"
#include "fds.h"
#include "flog.h"
#include "input_common.h"
#include "iothread.h"
#include "wutil.h"
/// Time in milliseconds to wait for another byte to be available for reading
/// after \x1B is read before assuming that escape key was pressed, and not an
/// escape sequence.
#define WAIT_ON_ESCAPE_DEFAULT 30
static int wait_on_escape_ms = WAIT_ON_ESCAPE_DEFAULT;
#define WAIT_ON_SEQUENCE_KEY_INFINITE (-1)
static int wait_on_sequence_key_ms = WAIT_ON_SEQUENCE_KEY_INFINITE;
input_event_queue_t::input_event_queue_t(int in) : in_(in) {}
/// Internal function used by readch to read one byte.
/// This calls select() on three fds: input (e.g. stdin), the ioport notifier fd (for main thread
/// requests), and the uvar notifier. This returns either the byte which was read, or one of the
/// special values below.
enum {
// The in fd has been closed.
readb_eof = -1,
// select() was interrupted by a signal.
readb_interrupted = -2,
// Our uvar notifier reported a change (either through poll() or its fd).
readb_uvar_notified = -3,
// Our ioport reported a change, so service main thread requests.
readb_ioport_notified = -4,
};
using readb_result_t = int;
static readb_result_t readb(int in_fd) {
assert(in_fd >= 0 && "Invalid in fd");
auto notifier_box = default_notifier();
const auto& notifier = *notifier_box;
auto fdset_box = new_fd_readable_set();
fd_readable_set_t& fdset = *fdset_box;
for (;;) {
fdset.clear();
fdset.add(in_fd);
// Add the completion ioport.
int ioport_fd = iothread_port();
fdset.add(ioport_fd);
// Get the uvar notifier fd (possibly none).
int notifier_fd = notifier.notification_fd();
fdset.add(notifier_fd);
// Here's where we call select().
int select_res = fdset.check_readable(kNoTimeout);
if (select_res < 0) {
if (errno == EINTR || errno == EAGAIN) {
// A signal.
return readb_interrupted;
} else {
// Some fd was invalid, so probably the tty has been closed.
return readb_eof;
}
}
// select() did not return an error, so we may have a readable fd.
// The priority order is: uvars, stdin, ioport.
// Check to see if we want a universal variable barrier.
// This may come about through readability, or through a call to poll().
if (fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd)) {
return readb_uvar_notified;
}
// Check stdin.
if (fdset.test(in_fd)) {
unsigned char arr[1];
if (read_blocked(in_fd, arr, 1) != 1) {
// The terminal has been closed.
return readb_eof;
}
// The common path is to return a (non-negative) char.
return static_cast<int>(arr[0]);
}
// Check for iothread completions only if there is no data to be read from the stdin.
// This gives priority to the foreground.
if (fdset.test(ioport_fd)) {
return readb_ioport_notified;
}
}
}
// Update the wait_on_escape_ms value in response to the fish_escape_delay_ms user variable being
// set.
void update_wait_on_escape_ms(const environment_t& vars) {
auto escape_time_ms = vars.get_unless_empty(L"fish_escape_delay_ms");
if (!escape_time_ms) {
wait_on_escape_ms = WAIT_ON_ESCAPE_DEFAULT;
return;
}
long tmp = fish_wcstol(escape_time_ms->as_string().c_str());
if (errno || tmp < 10 || tmp >= 5000) {
std::fwprintf(stderr,
L"ignoring fish_escape_delay_ms: value '%ls' "
L"is not an integer or is < 10 or >= 5000 ms\n",
escape_time_ms->as_string().c_str());
} else {
wait_on_escape_ms = static_cast<int>(tmp);
}
}
void update_wait_on_escape_ms_ffi(bool empty, const wcstring& fish_escape_delay_ms) {
if (empty) {
wait_on_escape_ms = WAIT_ON_ESCAPE_DEFAULT;
return;
}
long tmp = fish_wcstol(fish_escape_delay_ms.c_str());
if (errno || tmp < 10 || tmp >= 5000) {
std::fwprintf(stderr,
L"ignoring fish_escape_delay_ms: value '%ls' "
L"is not an integer or is < 10 or >= 5000 ms\n",
fish_escape_delay_ms.c_str());
} else {
wait_on_escape_ms = static_cast<int>(tmp);
}
}
// Update the wait_on_sequence_key_ms value in response to the fish_sequence_key_delay_ms user
// variable being set.
void update_wait_on_sequence_key_ms(const environment_t& vars) {
auto sequence_key_time_ms = vars.get_unless_empty(L"fish_sequence_key_delay_ms");
if (!sequence_key_time_ms) {
wait_on_sequence_key_ms = WAIT_ON_SEQUENCE_KEY_INFINITE;
return;
}
long tmp = fish_wcstol(sequence_key_time_ms->as_string().c_str());
if (errno || tmp < 10 || tmp >= 5000) {
std::fwprintf(stderr,
L"ignoring fish_sequence_key_delay_ms: value '%ls' "
L"is not an integer or is < 10 or >= 5000 ms\n",
sequence_key_time_ms->as_string().c_str());
} else {
wait_on_sequence_key_ms = static_cast<int>(tmp);
}
}
void update_wait_on_sequence_key_ms_ffi(bool empty, const wcstring& fish_sequence_key_delay_ms) {
if (empty) {
wait_on_sequence_key_ms = WAIT_ON_SEQUENCE_KEY_INFINITE;
return;
}
long tmp = fish_wcstol(fish_sequence_key_delay_ms.c_str());
if (errno || tmp < 10 || tmp >= 5000) {
std::fwprintf(stderr,
L"ignoring fish_sequence_key_delay_ms: value '%ls' "
L"is not an integer or is < 10 or >= 5000 ms\n",
fish_sequence_key_delay_ms.c_str());
} else {
wait_on_sequence_key_ms = static_cast<int>(tmp);
}
}
maybe_t<char_event_t> input_event_queue_t::try_pop() {
if (queue_.empty()) {
return none();
}
auto result = std::move(queue_.front());
queue_.pop_front();
return result;
}
char_event_t input_event_queue_t::readch() {
wchar_t res{};
mbstate_t state = {};
for (;;) {
// Do we have something enqueued already?
// Note this may be initially true, or it may become true through calls to
// iothread_service_main() or env_universal_barrier() below.
if (auto mevt = try_pop()) {
return mevt.acquire();
}
// We are going to block; but first allow any override to inject events.
this->prepare_to_select();
if (auto mevt = try_pop()) {
return mevt.acquire();
}
readb_result_t rr = readb(in_);
switch (rr) {
case readb_eof:
return char_event_type_t::eof;
case readb_interrupted:
// FIXME: here signals may break multibyte sequences.
this->select_interrupted();
break;
case readb_uvar_notified:
this->uvar_change_notified();
break;
case readb_ioport_notified:
iothread_service_main();
break;
default: {
assert(rr >= 0 && rr <= UCHAR_MAX &&
"Read byte out of bounds - missing error case?");
char read_byte = static_cast<char>(static_cast<unsigned char>(rr));
if (MB_CUR_MAX == 1) {
// single-byte locale, all values are legal
res = read_byte;
return res;
}
size_t sz = std::mbrtowc(&res, &read_byte, 1, &state);
switch (sz) {
case static_cast<size_t>(-1):
std::memset(&state, '\0', sizeof(state));
FLOG(reader, L"Illegal input");
return char_event_type_t::check_exit;
case static_cast<size_t>(-2):
// Sequence not yet complete.
break;
case 0:
// Actual nul char.
return 0;
default:
// Sequence complete.
return res;
}
break;
}
}
}
}
maybe_t<char_event_t> input_event_queue_t::readch_timed_esc() {
return readch_timed(wait_on_escape_ms);
}
maybe_t<char_event_t> input_event_queue_t::readch_timed_sequence_key() {
if (wait_on_sequence_key_ms == WAIT_ON_SEQUENCE_KEY_INFINITE) {
return readch();
}
return readch_timed(wait_on_sequence_key_ms);
}
maybe_t<char_event_t> input_event_queue_t::readch_timed(const int wait_time_ms) {
if (auto evt = try_pop()) {
return evt;
}
// We are not prepared to handle a signal immediately; we only want to know if we get input on
// our fd before the timeout. Use pselect to block all signals; we will handle signals
// before the next call to readch().
sigset_t sigs;
sigfillset(&sigs);
// pselect expects timeouts in nanoseconds.
const uint64_t nsec_per_msec = 1000 * 1000;
const uint64_t nsec_per_sec = nsec_per_msec * 1000;
const uint64_t wait_nsec = wait_time_ms * nsec_per_msec;
struct timespec timeout;
timeout.tv_sec = (wait_nsec) / nsec_per_sec;
timeout.tv_nsec = (wait_nsec) % nsec_per_sec;
// We have one fd of interest.
fd_set fdset;
FD_ZERO(&fdset);
FD_SET(in_, &fdset);
int res = pselect(in_ + 1, &fdset, nullptr, nullptr, &timeout, &sigs);
// Prevent signal starvation on WSL causing the `torn_escapes.py` test to fail
if (is_windows_subsystem_for_linux()) {
// Merely querying the current thread's sigmask is sufficient to deliver a pending signal
pthread_sigmask(0, nullptr, &sigs);
}
if (res > 0) {
return readch();
}
return none();
}
void input_event_queue_t::push_back(const char_event_t& ch) { queue_.push_back(ch); }
void input_event_queue_t::push_front(const char_event_t& ch) { queue_.push_front(ch); }
void input_event_queue_t::promote_interruptions_to_front() {
// Find the first sequence of non-char events.
// EOF is considered a char: we don't want to pull EOF in front of real chars.
auto is_char = [](const char_event_t& ch) { return ch.is_char() || ch.is_eof(); };
auto first = std::find_if_not(queue_.begin(), queue_.end(), is_char);
auto last = std::find_if(first, queue_.end(), is_char);
std::rotate(queue_.begin(), first, last);
}
void input_event_queue_t::drop_leading_readline_events() {
queue_.erase(queue_.begin(),
std::find_if(queue_.begin(), queue_.end(),
[](const char_event_t& evt) { return !evt.is_readline(); }));
}
void input_event_queue_t::prepare_to_select() {}
void input_event_queue_t::select_interrupted() {}
void input_event_queue_t::uvar_change_notified() {}
input_event_queue_t::~input_event_queue_t() = default;

View file

@ -1,263 +0,0 @@
// Header file for the low level input library.
#ifndef INPUT_COMMON_H
#define INPUT_COMMON_H
#include <unistd.h>
#include <cstdint>
#include <deque>
#include <string>
#include <utility>
#include "common.h"
#include "env.h"
#include "maybe.h"
enum class readline_cmd_t {
beginning_of_line,
end_of_line,
forward_char,
backward_char,
forward_single_char,
forward_word,
backward_word,
forward_bigword,
backward_bigword,
nextd_or_forward_word,
prevd_or_backward_word,
history_search_backward,
history_search_forward,
history_prefix_search_backward,
history_prefix_search_forward,
history_pager,
history_pager_delete,
delete_char,
backward_delete_char,
kill_line,
yank,
yank_pop,
complete,
complete_and_search,
pager_toggle_search,
beginning_of_history,
end_of_history,
backward_kill_line,
kill_whole_line,
kill_inner_line,
kill_word,
kill_bigword,
backward_kill_word,
backward_kill_path_component,
backward_kill_bigword,
history_token_search_backward,
history_token_search_forward,
self_insert,
self_insert_notfirst,
transpose_chars,
transpose_words,
upcase_word,
downcase_word,
capitalize_word,
togglecase_char,
togglecase_selection,
execute,
beginning_of_buffer,
end_of_buffer,
repaint_mode,
repaint,
force_repaint,
up_line,
down_line,
suppress_autosuggestion,
accept_autosuggestion,
begin_selection,
swap_selection_start_stop,
end_selection,
kill_selection,
insert_line_under,
insert_line_over,
forward_jump,
backward_jump,
forward_jump_till,
backward_jump_till,
func_and,
func_or,
expand_abbr,
delete_or_exit,
exit,
cancel_commandline,
cancel,
undo,
redo,
begin_undo_group,
end_undo_group,
repeat_jump,
disable_mouse_tracking,
// ncurses uses the obvious name
clear_screen_and_repaint,
// NOTE: This one has to be last.
reverse_repeat_jump
};
// The range of key codes for inputrc-style keyboard functions.
enum { R_END_INPUT_FUNCTIONS = static_cast<int>(readline_cmd_t::reverse_repeat_jump) + 1 };
/// Represents an event on the character input stream.
enum class char_event_type_t : uint8_t {
/// A character was entered.
charc,
/// A readline event.
readline,
/// end-of-file was reached.
eof,
/// An event was handled internally, or an interrupt was received. Check to see if the reader
/// loop should exit.
check_exit,
};
/// Hackish: the input style, which describes how char events (only) are applied to the command
/// line. Note this is set only after applying bindings; it is not set from readb().
enum class char_input_style_t : uint8_t {
// Insert characters normally.
normal,
// Insert characters only if the cursor is not at the beginning. Otherwise, discard them.
notfirst,
};
class char_event_t {
union {
/// Set if the type is charc.
wchar_t c;
/// Set if the type is readline.
readline_cmd_t rl;
} v_{};
public:
/// The type of event.
char_event_type_t type;
/// The style to use when inserting characters into the command line.
char_input_style_t input_style{char_input_style_t::normal};
/// The sequence of characters in the input mapping which generated this event.
/// Note that the generic self-insert case does not have any characters, so this would be empty.
wcstring seq{};
bool is_char() const { return type == char_event_type_t::charc; }
bool is_eof() const { return type == char_event_type_t::eof; }
bool is_check_exit() const { return type == char_event_type_t::check_exit; }
bool is_readline() const { return type == char_event_type_t::readline; }
wchar_t get_char() const {
assert(type == char_event_type_t::charc && "Not a char type");
return v_.c;
}
maybe_t<wchar_t> maybe_char() const {
if (type == char_event_type_t::charc) {
return v_.c;
} else {
return none();
}
}
readline_cmd_t get_readline() const {
assert(type == char_event_type_t::readline && "Not a readline type");
return v_.rl;
}
/* implicit */ char_event_t(wchar_t c) : type(char_event_type_t::charc) { v_.c = c; }
/* implicit */ char_event_t(readline_cmd_t rl, wcstring seq = {})
: type(char_event_type_t::readline), seq(std::move(seq)) {
v_.rl = rl;
}
/* implicit */ char_event_t(char_event_type_t type) : type(type) {
assert(type != char_event_type_t::charc && type != char_event_type_t::readline &&
"Cannot create a char event with this constructor");
}
};
/// Adjust the escape timeout.
void update_wait_on_escape_ms(const environment_t &vars);
void update_wait_on_escape_ms_ffi(bool empty, const wcstring &fish_escape_delay_ms);
void update_wait_on_sequence_key_ms(const environment_t &vars);
void update_wait_on_sequence_key_ms_ffi(bool empty, const wcstring &fish_sequence_key_delay_ms);
/// A class which knows how to produce a stream of input events.
/// This is a base class; you may subclass it for its override points.
class input_event_queue_t {
public:
/// Construct from a file descriptor \p in, and an interrupt handler \p handler.
explicit input_event_queue_t(int in = STDIN_FILENO);
/// Function used by input_readch to read bytes from stdin until enough bytes have been read to
/// convert them to a wchar_t. Conversion is done using mbrtowc. If a character has previously
/// been read and then 'unread' using \c input_common_unreadch, that character is returned.
char_event_t readch();
/// Like readch(), except it will wait at most WAIT_ON_ESCAPE milliseconds for a
/// character to be available for reading.
/// \return none on timeout, the event on success.
maybe_t<char_event_t> readch_timed(const int wait_time_ms);
maybe_t<char_event_t> readch_timed_esc();
maybe_t<char_event_t> readch_timed_sequence_key();
/// Enqueue a character or a readline function to the queue of unread characters that
/// readch will return before actually reading from fd 0.
void push_back(const char_event_t &ch);
/// Add a character or a readline function to the front of the queue of unread characters. This
/// will be the next character returned by readch.
void push_front(const char_event_t &ch);
/// Find the first sequence of non-char events, and promote them to the front.
void promote_interruptions_to_front();
/// Add multiple characters or readline events to the front of the queue of unread characters.
/// The order of the provided events is not changed, i.e. they are not inserted in reverse
/// order.
template <typename Iterator>
void insert_front(const Iterator begin, const Iterator end) {
queue_.insert(queue_.begin(), begin, end);
}
/// Forget all enqueued readline events in the front of the queue.
void drop_leading_readline_events();
/// Override point for when we are about to (potentially) block in select(). The default does
/// nothing.
virtual void prepare_to_select();
/// Override point for when when select() is interrupted by a signal. The default does nothing.
virtual void select_interrupted();
/// Override point for when when select() is interrupted by the universal variable notifier.
/// The default does nothing.
virtual void uvar_change_notified();
virtual ~input_event_queue_t();
private:
/// \return if we have any lookahead.
bool has_lookahead() const { return !queue_.empty(); }
/// \return the next event in the queue, or none if the queue is empty.
maybe_t<char_event_t> try_pop();
int in_{0};
std::deque<char_event_t> queue_;
};
#endif

View file

@ -16,6 +16,7 @@
struct Parser;
using parser_t = Parser;
struct ParserRef;
#if INCLUDE_RUST_HEADERS
#include "parser.rs.h"

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@
#include "maybe.h"
#include "parse_constants.h"
#include "parser.h"
#include "wutil.h"
#if INCLUDE_RUST_HEADERS
#include "reader.rs.h"
@ -79,8 +80,9 @@ void reader_write_title_ffi(const wcstring &cmd, const void *parser, bool reset_
void reader_schedule_prompt_repaint();
/// Enqueue an event to the back of the reader's input queue.
class char_event_t;
void reader_queue_ch(const char_event_t &ch);
struct CharEvent;
using char_event_t = CharEvent;
void reader_queue_ch(rust::Box<char_event_t> ch);
/// Return the value of the interrupted flag, which is set by the sigint handler, and clear it if it
/// was set. If the current reader is interruptible, call \c reader_exit().