mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-02-21 04:39:39 +08:00
Port fish_key_reader
This commit is contained in:
parent
90fde1a9cd
commit
7f110ed4c0
@ -85,6 +85,7 @@ fn main() {
|
||||
"fish-rust/src/ffi_init.rs",
|
||||
"fish-rust/src/ffi_tests.rs",
|
||||
"fish-rust/src/fish_indent.rs",
|
||||
"fish-rust/src/fish_key_reader.rs",
|
||||
"fish-rust/src/fish.rs",
|
||||
"fish-rust/src/function.rs",
|
||||
"fish-rust/src/future_feature_flags.rs",
|
||||
|
389
fish-rust/src/fish_key_reader.rs
Normal file
389
fish-rust/src/fish_key_reader.rs
Normal file
@ -0,0 +1,389 @@
|
||||
//! A small utility to print information related to pressing keys. This is similar to using tools
|
||||
//! like `xxd` and `od -tx1z` but provides more information such as the time delay between each
|
||||
//! character. It also allows pressing and interpreting keys that are normally special such as
|
||||
//! [ctrl-C] (interrupt the program) or [ctrl-D] (EOF to signal the program should exit).
|
||||
//! And unlike those other tools this one disables ICRNL mode so it can distinguish between
|
||||
//! carriage-return (\cM) and newline (\cJ).
|
||||
//!
|
||||
//! Type "exit" or "quit" to terminate the program.
|
||||
|
||||
use std::{
|
||||
os::unix::prelude::OsStrExt,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use libc::{STDIN_FILENO, TCSANOW, VEOF, VINTR};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::future::IsSomeAnd;
|
||||
use crate::{
|
||||
builtins::shared::BUILTIN_ERR_UNKNOWN,
|
||||
common::{scoped_push_replacer, shell_modes, str2wcstring, PROGRAM_NAME},
|
||||
env::env_init,
|
||||
fallback::fish_wcwidth,
|
||||
input::input_terminfo_get_name,
|
||||
input_common::{CharEvent, InputEventQueue, InputEventQueuer},
|
||||
parser::Parser,
|
||||
print_help::print_help,
|
||||
proc::set_interactive_session,
|
||||
reader::{
|
||||
check_exit_loop_maybe_warning, reader_init, reader_test_and_clear_interrupted,
|
||||
restore_term_mode,
|
||||
},
|
||||
signal::signal_set_handlers,
|
||||
threads,
|
||||
topic_monitor::topic_monitor_init,
|
||||
wchar::prelude::*,
|
||||
wgetopt::{wgetopter_t, wopt, woption, woption_argument_t},
|
||||
};
|
||||
|
||||
/// Return true if the recent sequence of characters indicates the user wants to exit the program.
|
||||
fn should_exit(recent_chars: &mut Vec<u8>, c: char) -> bool {
|
||||
let c = if c < '\u{80}' { c as u8 } else { 0 };
|
||||
|
||||
recent_chars.push(c);
|
||||
|
||||
for evt in [VINTR, VEOF] {
|
||||
if c == shell_modes().c_cc[evt] {
|
||||
if recent_chars.iter().rev().nth(1) == Some(&shell_modes().c_cc[evt]) {
|
||||
return true;
|
||||
}
|
||||
eprintf!(
|
||||
"Press [ctrl-%c] again to exit\n",
|
||||
char::from(shell_modes().c_cc[evt] + 0x40)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
recent_chars.ends_with(b"exit") || recent_chars.ends_with(b"quit")
|
||||
}
|
||||
|
||||
/// Return the name if the recent sequence of characters matches a known terminfo sequence.
|
||||
fn sequence_name(recent_chars: &mut Vec<u8>, c: char) -> Option<WString> {
|
||||
if c >= '\u{80}' {
|
||||
// Terminfo sequences are always ASCII.
|
||||
recent_chars.clear();
|
||||
return None;
|
||||
}
|
||||
|
||||
let c = c as u8;
|
||||
recent_chars.push(c);
|
||||
while recent_chars.len() > 8 {
|
||||
recent_chars.remove(0);
|
||||
}
|
||||
|
||||
// The entire sequence needs to match the sequence, or else we would output substrings.
|
||||
input_terminfo_get_name(&str2wcstring(recent_chars))
|
||||
}
|
||||
|
||||
/// Return true if the character must be escaped when used in the sequence of chars to be bound in
|
||||
/// a `bind` command.
|
||||
fn must_escape(c: char) -> bool {
|
||||
"[]()<>{}*\\?$#;&|'\"".contains(c)
|
||||
}
|
||||
|
||||
fn ctrl_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) {
|
||||
let ctrl_symbolic_names: [&wstr; 28] = {
|
||||
std::array::from_fn(|i| match i {
|
||||
8 => L!("\\b"),
|
||||
9 => L!("\\t"),
|
||||
10 => L!("\\n"),
|
||||
13 => L!("\\r"),
|
||||
27 => L!("\\e"),
|
||||
28 => L!("\\x1c"),
|
||||
_ => L!(""),
|
||||
})
|
||||
};
|
||||
|
||||
let c = u8::try_from(c).unwrap();
|
||||
let cu = usize::from(c);
|
||||
|
||||
if !ctrl_symbolic_names[cu].is_empty() {
|
||||
if bind_friendly {
|
||||
sprintf!(=> buf, "%s", ctrl_symbolic_names[cu]);
|
||||
} else {
|
||||
sprintf!(=> buf, "\\c%c (or %ls)", char::from(c + 0x40), ctrl_symbolic_names[cu]);
|
||||
}
|
||||
} else {
|
||||
sprintf!(=> buf, "\\c%c", char::from(c + 0x40));
|
||||
}
|
||||
}
|
||||
|
||||
fn space_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) {
|
||||
if bind_friendly {
|
||||
sprintf!(=> buf, "\\x%X", u32::from(c));
|
||||
} else {
|
||||
sprintf!(=> buf, "\\x%X (aka \"space\")", u32::from(c));
|
||||
}
|
||||
}
|
||||
|
||||
fn del_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) {
|
||||
if bind_friendly {
|
||||
sprintf!(=> buf, "\\x%X", u32::from(c));
|
||||
} else {
|
||||
sprintf!(=> buf, "\\x%X (aka \"del\")", u32::from(c));
|
||||
}
|
||||
}
|
||||
|
||||
fn ascii_printable_to_symbol(buf: &mut WString, c: char, bind_friendly: bool) {
|
||||
if bind_friendly && must_escape(c) {
|
||||
sprintf!(=> buf, "\\%c", c);
|
||||
} else {
|
||||
sprintf!(=> buf, "%c", c);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a wide-char to a symbol that can be used in our output.
|
||||
fn char_to_symbol(c: char, bind_friendly: bool) -> WString {
|
||||
let mut buff = WString::new();
|
||||
let buf = &mut buff;
|
||||
if c == '\x1b' {
|
||||
// Escape - this is *technically* also \c[
|
||||
buf.push_str("\\e");
|
||||
} else if c < ' ' {
|
||||
// ASCII control character
|
||||
ctrl_to_symbol(buf, c, bind_friendly);
|
||||
} else if c == ' ' {
|
||||
// the "space" character
|
||||
space_to_symbol(buf, c, bind_friendly);
|
||||
} else if c == '\x7F' {
|
||||
// the "del" character
|
||||
del_to_symbol(buf, c, bind_friendly);
|
||||
} else if c < '\u{80}' {
|
||||
// ASCII characters that are not control characters
|
||||
ascii_printable_to_symbol(buf, c, bind_friendly);
|
||||
} else if fish_wcwidth(c) > 0 {
|
||||
sprintf!(=> buf, "%lc", c);
|
||||
} else if c <= '\u{FFFF}' {
|
||||
// BMP Unicode chararacter
|
||||
sprintf!(=> buf, "\\u%04X", u32::from(c));
|
||||
} else {
|
||||
sprintf!(=> buf, "\\U%06X", u32::from(c));
|
||||
}
|
||||
buff
|
||||
}
|
||||
|
||||
fn add_char_to_bind_command(c: char, bind_chars: &mut Vec<char>) {
|
||||
bind_chars.push(c);
|
||||
}
|
||||
|
||||
fn output_bind_command(bind_chars: &mut Vec<char>) {
|
||||
if !bind_chars.is_empty() {
|
||||
printf!("bind ");
|
||||
for &bind_char in &*bind_chars {
|
||||
printf!("%s", char_to_symbol(bind_char, true));
|
||||
}
|
||||
printf!(" 'do something'\n");
|
||||
bind_chars.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn output_info_about_char(c: char) {
|
||||
eprintf!(
|
||||
"hex: %4X char: %ls\n",
|
||||
u32::from(c),
|
||||
char_to_symbol(c, false)
|
||||
);
|
||||
}
|
||||
|
||||
fn output_matching_key_name(recent_chars: &mut Vec<u8>, c: char) -> bool {
|
||||
if let Some(name) = sequence_name(recent_chars, c) {
|
||||
printf!("bind -k %ls 'do something'\n", name);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn output_elapsed_time(prev_timestamp: Instant, first_char_seen: bool, verbose: bool) -> Instant {
|
||||
// How much time has passed since the previous char was received in microseconds.
|
||||
let now = Instant::now();
|
||||
let delta = now - prev_timestamp;
|
||||
|
||||
if verbose {
|
||||
if delta >= Duration::from_millis(200) && first_char_seen {
|
||||
eprintf!("\n");
|
||||
}
|
||||
if delta >= Duration::from_millis(1000) {
|
||||
eprintf!(" ");
|
||||
} else {
|
||||
eprintf!(
|
||||
"(%3lld.%03lld ms) ",
|
||||
u64::try_from(delta.as_millis()).unwrap(),
|
||||
u64::try_from(delta.as_micros() % 1000).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
now
|
||||
}
|
||||
|
||||
/// Process the characters we receive as the user presses keys.
|
||||
fn process_input(continuous_mode: bool, verbose: bool) {
|
||||
let mut first_char_seen = false;
|
||||
let mut prev_timestamp = Instant::now()
|
||||
.checked_sub(Duration::from_millis(1000))
|
||||
.unwrap_or(Instant::now());
|
||||
let mut queue = InputEventQueue::new(STDIN_FILENO);
|
||||
let mut bind_chars = vec![];
|
||||
let mut recent_chars1 = vec![];
|
||||
let mut recent_chars2 = vec![];
|
||||
eprintf!("Press a key:\n");
|
||||
|
||||
while !check_exit_loop_maybe_warning(None) {
|
||||
let evt = if reader_test_and_clear_interrupted() != 0 {
|
||||
Some(CharEvent::from_char(char::from(shell_modes().c_cc[VINTR])))
|
||||
} else {
|
||||
queue.readch_timed_esc()
|
||||
};
|
||||
|
||||
if evt.as_ref().is_none_or(|evt| !evt.is_char()) {
|
||||
output_bind_command(&mut bind_chars);
|
||||
if first_char_seen && !continuous_mode {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let evt = evt.unwrap();
|
||||
|
||||
let c = evt.get_char();
|
||||
prev_timestamp = output_elapsed_time(prev_timestamp, 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
|
||||
// this key (nul) elsewhere.
|
||||
if c != '\0' {
|
||||
add_char_to_bind_command(c, &mut bind_chars);
|
||||
}
|
||||
if verbose {
|
||||
output_info_about_char(c);
|
||||
}
|
||||
if output_matching_key_name(&mut recent_chars1, c) {
|
||||
output_bind_command(&mut bind_chars);
|
||||
}
|
||||
|
||||
if continuous_mode && should_exit(&mut recent_chars2, c) {
|
||||
eprintf!("\nExiting at your request.\n");
|
||||
break;
|
||||
}
|
||||
|
||||
first_char_seen = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup our environment (e.g., tty modes), process key strokes, then reset the environment.
|
||||
fn setup_and_process_keys(continuous_mode: bool, verbose: bool) -> ! {
|
||||
set_interactive_session(true);
|
||||
topic_monitor_init();
|
||||
threads::init();
|
||||
env_init(None, true, false);
|
||||
reader_init();
|
||||
|
||||
let parser = Parser::principal_parser();
|
||||
let _interactive = scoped_push_replacer(
|
||||
|new_value| std::mem::replace(&mut parser.libdata_mut().pods.is_interactive, new_value),
|
||||
true,
|
||||
);
|
||||
|
||||
signal_set_handlers(true);
|
||||
// We need to set the shell-modes for ICRNL,
|
||||
// in fish-proper this is done once a command is run.
|
||||
unsafe { libc::tcsetattr(STDIN_FILENO, TCSANOW, shell_modes()) };
|
||||
|
||||
if continuous_mode {
|
||||
eprintf!("\n");
|
||||
eprintf!("To terminate this program type \"exit\" or \"quit\" in this window,\n");
|
||||
eprintf!(
|
||||
"or press [ctrl-%c] or [ctrl-%c] twice in a row.\n",
|
||||
char::from(shell_modes().c_cc[VINTR] + 0x40),
|
||||
char::from(shell_modes().c_cc[VEOF] + 0x40)
|
||||
);
|
||||
eprintf!("\n");
|
||||
}
|
||||
|
||||
process_input(continuous_mode, verbose);
|
||||
restore_term_mode();
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
fn parse_flags(continuous_mode: &mut bool, verbose: &mut bool) -> bool {
|
||||
const short_opts: &wstr = L!("+chvV");
|
||||
const long_opts: &[woption] = &[
|
||||
wopt(L!("continuous"), woption_argument_t::no_argument, 'c'),
|
||||
wopt(L!("help"), woption_argument_t::no_argument, 'h'),
|
||||
wopt(L!("version"), woption_argument_t::no_argument, 'v'),
|
||||
wopt(L!("verbose"), woption_argument_t::no_argument, 'V'),
|
||||
];
|
||||
|
||||
let args: Vec<WString> = std::env::args_os()
|
||||
.map(|osstr| str2wcstring(osstr.as_bytes()))
|
||||
.collect();
|
||||
let mut shim_args: Vec<&wstr> = args.iter().map(|s| s.as_ref()).collect();
|
||||
let mut w = wgetopter_t::new(short_opts, long_opts, &mut shim_args);
|
||||
while let Some(opt) = w.wgetopt_long() {
|
||||
match opt {
|
||||
'c' => {
|
||||
*continuous_mode = true;
|
||||
}
|
||||
'h' => {
|
||||
print_help("fish_key_reader");
|
||||
std::process::exit(0);
|
||||
}
|
||||
'v' => {
|
||||
printf!(
|
||||
"%s",
|
||||
wgettext_fmt!(
|
||||
"%ls, version %s\n",
|
||||
PROGRAM_NAME.get().unwrap(),
|
||||
crate::BUILD_VERSION
|
||||
)
|
||||
);
|
||||
}
|
||||
'V' => {
|
||||
*verbose = true;
|
||||
}
|
||||
'?' => {
|
||||
printf!(
|
||||
"%s",
|
||||
wgettext_fmt!(
|
||||
BUILTIN_ERR_UNKNOWN,
|
||||
"fish_key_reader",
|
||||
&w.argv[w.woptind - 1]
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
let argc = args.len() - w.woptind;
|
||||
if argc != 0 {
|
||||
eprintf!("Expected no arguments, got %d\n", argc);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn fish_key_reader_main() -> i32 {
|
||||
PROGRAM_NAME.set(L!("fish_key_reader")).unwrap();
|
||||
let mut continuous_mode = false;
|
||||
let mut verbose = false;
|
||||
|
||||
if !parse_flags(&mut continuous_mode, &mut verbose) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if unsafe { libc::isatty(STDIN_FILENO) } == 0 {
|
||||
eprintf!("Stdin must be attached to a tty.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
setup_and_process_keys(continuous_mode, verbose);
|
||||
}
|
||||
|
||||
#[cxx::bridge]
|
||||
mod fish_key_reader_ffi {
|
||||
extern "Rust" {
|
||||
fn fish_key_reader_main() -> i32;
|
||||
}
|
||||
}
|
@ -61,6 +61,7 @@ mod ffi_init;
|
||||
mod ffi_tests;
|
||||
mod fish;
|
||||
mod fish_indent;
|
||||
mod fish_key_reader;
|
||||
mod flog;
|
||||
mod fork_exec;
|
||||
mod function;
|
||||
|
@ -20,345 +20,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "common.h"
|
||||
#include "cxxgen.h"
|
||||
#include "env.h"
|
||||
#include "env/env_ffi.rs.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "ffi_baggage.h"
|
||||
#include "ffi_init.rs.h"
|
||||
#include "fish_version.h"
|
||||
#include "input_ffi.rs.h"
|
||||
#include "maybe.h"
|
||||
#include "parser.h"
|
||||
#include "print_help.rs.h"
|
||||
#include "proc.h"
|
||||
#include "reader.h"
|
||||
#include "signals.h"
|
||||
#include "wutil.h" // IWYU pragma: keep
|
||||
#include "fish_key_reader.rs.h"
|
||||
|
||||
static const wchar_t *ctrl_symbolic_names[] = {
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
L"\\b", L"\\t", L"\\n", nullptr, nullptr, L"\\r", nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, L"\\e", L"\\x1c", nullptr, nullptr, nullptr};
|
||||
|
||||
/// Return true if the recent sequence of characters indicates the user wants to exit the program.
|
||||
static bool should_exit(wchar_t wc) {
|
||||
unsigned char c = wc < 0x80 ? wc : 0;
|
||||
static unsigned char recent_chars[4] = {0};
|
||||
|
||||
recent_chars[0] = recent_chars[1];
|
||||
recent_chars[1] = recent_chars[2];
|
||||
recent_chars[2] = recent_chars[3];
|
||||
recent_chars[3] = c;
|
||||
if (c == shell_modes.c_cc[VINTR]) {
|
||||
if (recent_chars[2] == shell_modes.c_cc[VINTR]) return true;
|
||||
std::fwprintf(stderr, L"Press [ctrl-%c] again to exit\n", shell_modes.c_cc[VINTR] + 0x40);
|
||||
return false;
|
||||
}
|
||||
if (c == shell_modes.c_cc[VEOF]) {
|
||||
if (recent_chars[2] == shell_modes.c_cc[VEOF]) return true;
|
||||
std::fwprintf(stderr, L"Press [ctrl-%c] again to exit\n", shell_modes.c_cc[VEOF] + 0x40);
|
||||
return false;
|
||||
}
|
||||
return std::memcmp(recent_chars, "exit", const_strlen("exit")) == 0 ||
|
||||
std::memcmp(recent_chars, "quit", const_strlen("quit")) == 0;
|
||||
}
|
||||
|
||||
/// Return the name if the recent sequence of characters matches a known terminfo sequence.
|
||||
static maybe_t<wcstring> sequence_name(wchar_t wc) {
|
||||
static std::string recent_chars;
|
||||
if (wc >= 0x80) {
|
||||
// Terminfo sequences are always ASCII.
|
||||
recent_chars.clear();
|
||||
return none();
|
||||
}
|
||||
|
||||
unsigned char c = wc;
|
||||
recent_chars.push_back(c);
|
||||
while (recent_chars.size() > 8) {
|
||||
recent_chars.erase(recent_chars.begin());
|
||||
}
|
||||
|
||||
// The entire sequence needs to match the sequence, or else we would output substrings.
|
||||
wcstring out_name;
|
||||
if (input_terminfo_get_name(str2wcstring(recent_chars), out_name)) {
|
||||
return out_name;
|
||||
}
|
||||
return none();
|
||||
}
|
||||
|
||||
/// Return true if the character must be escaped when used in the sequence of chars to be bound in
|
||||
/// a `bind` command.
|
||||
static bool must_escape(wchar_t wc) { return std::wcschr(L"[]()<>{}*\\?$#;&|'\"", wc) != nullptr; }
|
||||
|
||||
static void ctrl_to_symbol(wchar_t *buf, int buf_len, wchar_t wc, bool bind_friendly) {
|
||||
if (ctrl_symbolic_names[wc]) {
|
||||
if (bind_friendly) {
|
||||
std::swprintf(buf, buf_len, L"%ls", ctrl_symbolic_names[wc]);
|
||||
} else {
|
||||
std::swprintf(buf, buf_len, L"\\c%c (or %ls)", wc + 0x40, ctrl_symbolic_names[wc]);
|
||||
}
|
||||
} else {
|
||||
std::swprintf(buf, buf_len, L"\\c%c", wc + 0x40);
|
||||
}
|
||||
}
|
||||
|
||||
static void space_to_symbol(wchar_t *buf, int buf_len, wchar_t wc, bool bind_friendly) {
|
||||
if (bind_friendly) {
|
||||
std::swprintf(buf, buf_len, L"\\x%X", wc);
|
||||
} else {
|
||||
std::swprintf(buf, buf_len, L"\\x%X (aka \"space\")", wc);
|
||||
}
|
||||
}
|
||||
|
||||
static void del_to_symbol(wchar_t *buf, int buf_len, wchar_t wc, bool bind_friendly) {
|
||||
if (bind_friendly) {
|
||||
std::swprintf(buf, buf_len, L"\\x%X", wc);
|
||||
} else {
|
||||
std::swprintf(buf, buf_len, L"\\x%X (aka \"del\")", wc);
|
||||
}
|
||||
}
|
||||
|
||||
static void ascii_printable_to_symbol(wchar_t *buf, int buf_len, wchar_t wc, bool bind_friendly) {
|
||||
if (bind_friendly && must_escape(wc)) {
|
||||
std::swprintf(buf, buf_len, L"\\%c", wc);
|
||||
} else {
|
||||
std::swprintf(buf, buf_len, L"%c", wc);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a wide-char to a symbol that can be used in our output. The use of a static buffer
|
||||
/// requires that the returned string be used before we are called again.
|
||||
static wchar_t *char_to_symbol(wchar_t wc, bool bind_friendly) {
|
||||
static wchar_t buf[64];
|
||||
|
||||
if (wc == '\x1b') {
|
||||
// Escape - this is *technically* also \c[
|
||||
std::swprintf(buf, sizeof(buf) / sizeof(*buf), L"\\e");
|
||||
} else if (wc < L' ') { // ASCII control character
|
||||
ctrl_to_symbol(buf, sizeof(buf) / sizeof(*buf), wc, bind_friendly);
|
||||
} else if (wc == L' ') { // the "space" character
|
||||
space_to_symbol(buf, sizeof(buf) / sizeof(*buf), wc, bind_friendly);
|
||||
} else if (wc == 0x7F) { // the "del" character
|
||||
del_to_symbol(buf, sizeof(buf) / sizeof(*buf), wc, bind_friendly);
|
||||
} else if (wc < 0x80) { // ASCII characters that are not control characters
|
||||
ascii_printable_to_symbol(buf, sizeof(buf) / sizeof(*buf), wc, bind_friendly);
|
||||
} else if (std::iswgraph(wc)) {
|
||||
std::swprintf(buf, sizeof(buf) / sizeof(*buf), L"%lc", wc);
|
||||
}
|
||||
// Conditional handling of BMP Unicode characters depends on the encoding. Assume width of wchar_t
|
||||
// corresponds to the encoding, i.e. WCHAR_T_BITS == 16 implies UTF-16 and WCHAR_T_BITS == 32
|
||||
// because there's no other sane way of handling the input.
|
||||
#if WCHAR_T_BITS == 16
|
||||
else if (wc <= 0xD7FF || (wc >= 0xE000 && wc <= 0xFFFD)) {
|
||||
// UTF-16 encoding of Unicode character in BMP range
|
||||
std::swprintf(buf, sizeof(buf) / sizeof(*buf), L"\\u%04X", wc);
|
||||
} else {
|
||||
// Our support for UTF-16 surrogate pairs is non-existent.
|
||||
// See https://github.com/fish-shell/fish-shell/issues/6585#issuecomment-783669903 for what
|
||||
// correct handling of surrogate pairs would look like - except it would need to be done
|
||||
// everywhere.
|
||||
|
||||
// 0xFFFD is the unicode codepoint for "symbol doesn't exist in codepage" and is the most
|
||||
// correct thing we can do given the byte-by-byte parsing without any support for surrogate
|
||||
// pairs.
|
||||
std::swprintf(buf, sizeof(buf) / sizeof(*buf), L"\\uFFFD");
|
||||
}
|
||||
#elif WCHAR_T_BITS == 32
|
||||
else if (wc <= 0xFFFF) { // BMP Unicode chararacter
|
||||
std::swprintf(buf, sizeof(buf) / sizeof(*buf), L"\\u%04X", wc);
|
||||
} else { // Non-BMP Unicode chararacter
|
||||
std::swprintf(buf, sizeof(buf) / sizeof(*buf), L"\\U%06X", wc);
|
||||
}
|
||||
#else
|
||||
static_assert(false, "Unsupported WCHAR_T size; unknown encoding!");
|
||||
#endif
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void add_char_to_bind_command(wchar_t wc, std::vector<wchar_t> &bind_chars) {
|
||||
bind_chars.push_back(wc);
|
||||
}
|
||||
|
||||
static void output_bind_command(std::vector<wchar_t> &bind_chars) {
|
||||
if (!bind_chars.empty()) {
|
||||
std::fputws(L"bind ", stdout);
|
||||
for (auto bind_char : bind_chars) {
|
||||
std::fputws(char_to_symbol(bind_char, true), stdout);
|
||||
}
|
||||
std::fputws(L" 'do something'\n", stdout);
|
||||
bind_chars.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static void output_info_about_char(wchar_t wc) {
|
||||
std::fwprintf(stderr, L"hex: %4X char: %ls\n", wc, char_to_symbol(wc, false));
|
||||
}
|
||||
|
||||
static bool output_matching_key_name(wchar_t wc) {
|
||||
if (maybe_t<wcstring> name = sequence_name(wc)) {
|
||||
std::fwprintf(stdout, L"bind -k %ls 'do something'\n", name->c_str());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static double output_elapsed_time(double prev_tstamp, bool first_char_seen, bool verbose) {
|
||||
// How much time has passed since the previous char was received in microseconds.
|
||||
double now = timef();
|
||||
long long int delta_tstamp_us = 1000000 * (now - prev_tstamp);
|
||||
|
||||
if (verbose) {
|
||||
if (delta_tstamp_us >= 200000 && first_char_seen) std::fputwc(L'\n', stderr);
|
||||
if (delta_tstamp_us >= 1000000) {
|
||||
std::fwprintf(stderr, L" ");
|
||||
} else {
|
||||
std::fwprintf(stderr, L"(%3lld.%03lld ms) ", delta_tstamp_us / 1000,
|
||||
delta_tstamp_us % 1000);
|
||||
}
|
||||
}
|
||||
return now;
|
||||
}
|
||||
|
||||
/// Process the characters we receive as the user presses keys.
|
||||
static void process_input(bool continuous_mode, bool verbose) {
|
||||
bool first_char_seen = false;
|
||||
double prev_tstamp = 0.0;
|
||||
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()) {
|
||||
maybe_t<rust::Box<CharEvent>> evt{};
|
||||
if (reader_test_and_clear_interrupted()) {
|
||||
evt = char_event_from_char(shell_modes.c_cc[VINTR]);
|
||||
} else {
|
||||
CharEvent *evt_raw = queue->readch_timed_esc();
|
||||
if (evt_raw) {
|
||||
evt = rust::Box<CharEvent>::from_raw(evt_raw);
|
||||
}
|
||||
}
|
||||
if (!evt || !(*evt)->is_char()) {
|
||||
output_bind_command(bind_chars);
|
||||
if (first_char_seen && !continuous_mode) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
// this key (nul) elsewhere.
|
||||
if (wc) {
|
||||
add_char_to_bind_command(wc, bind_chars);
|
||||
}
|
||||
if (verbose) {
|
||||
output_info_about_char(wc);
|
||||
}
|
||||
if (output_matching_key_name(wc)) {
|
||||
output_bind_command(bind_chars);
|
||||
}
|
||||
|
||||
if (continuous_mode && should_exit(wc)) {
|
||||
std::fwprintf(stderr, L"\nExiting at your request.\n");
|
||||
break;
|
||||
}
|
||||
|
||||
first_char_seen = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup our environment (e.g., tty modes), process key strokes, then reset the environment.
|
||||
[[noreturn]] static void setup_and_process_keys(bool continuous_mode, bool verbose) {
|
||||
set_interactive_session(true);
|
||||
rust_init();
|
||||
rust_env_init(true);
|
||||
reader_init();
|
||||
auto parser_box = parser_principal_parser();
|
||||
const parser_t &parser = parser_box->deref();
|
||||
scoped_push<bool> interactive{&parser.libdata_pods_mut().is_interactive, true};
|
||||
signal_set_handlers(true);
|
||||
// We need to set the shell-modes for ICRNL,
|
||||
// in fish-proper this is done once a command is run.
|
||||
tcsetattr(STDIN_FILENO, TCSANOW, &shell_modes);
|
||||
|
||||
if (continuous_mode) {
|
||||
std::fwprintf(stderr, L"\n");
|
||||
std::fwprintf(stderr,
|
||||
L"To terminate this program type \"exit\" or \"quit\" in this window,\n");
|
||||
std::fwprintf(stderr, L"or press [ctrl-%c] or [ctrl-%c] twice in a row.\n",
|
||||
shell_modes.c_cc[VINTR] + 0x40, shell_modes.c_cc[VEOF] + 0x40);
|
||||
std::fwprintf(stderr, L"\n");
|
||||
}
|
||||
|
||||
process_input(continuous_mode, verbose);
|
||||
restore_term_mode();
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
static bool parse_flags(int argc, char **argv, bool *continuous_mode, bool *verbose) {
|
||||
const char *short_opts = "+chvV";
|
||||
const struct option long_opts[] = {{"continuous", no_argument, nullptr, 'c'},
|
||||
{"help", no_argument, nullptr, 'h'},
|
||||
{"version", no_argument, nullptr, 'v'},
|
||||
{"verbose", no_argument, nullptr, 'V'},
|
||||
{}};
|
||||
int opt;
|
||||
bool error = false;
|
||||
while (!error && (opt = getopt_long(argc, argv, short_opts, long_opts, nullptr)) != -1) {
|
||||
switch (opt) {
|
||||
case 'c': {
|
||||
*continuous_mode = true;
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
unsafe_print_help("fish_key_reader");
|
||||
exit(0);
|
||||
}
|
||||
case 'v': {
|
||||
std::fwprintf(stdout, _(L"%ls, version %s\n"), program_name, get_fish_version());
|
||||
exit(0);
|
||||
}
|
||||
case 'V': {
|
||||
*verbose = true;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// We assume getopt_long() has already emitted a diagnostic msg.
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error) return false;
|
||||
|
||||
argc -= optind;
|
||||
if (argc != 0) {
|
||||
std::fwprintf(stderr, L"Expected no arguments, got %d\n", argc);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
program_name = L"fish_key_reader";
|
||||
bool continuous_mode = false;
|
||||
bool verbose = false;
|
||||
|
||||
if (!parse_flags(argc, argv, &continuous_mode, &verbose)) return 1;
|
||||
|
||||
if (!isatty(STDIN_FILENO)) {
|
||||
std::fwprintf(stderr, L"Stdin must be attached to a tty.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
setup_and_process_keys(continuous_mode, verbose);
|
||||
exit_without_destructors(0);
|
||||
return EXIT_FAILURE; // above should exit
|
||||
}
|
||||
int main() { return fish_key_reader_main(); }
|
||||
|
Loading…
x
Reference in New Issue
Block a user