From 7f110ed4c064ae593d436178a5932c768ad67457 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Thu, 28 Dec 2023 18:18:56 +0100 Subject: [PATCH] Port fish_key_reader --- fish-rust/build.rs | 1 + fish-rust/src/fish_key_reader.rs | 389 +++++++++++++++++++++++++++++++ fish-rust/src/lib.rs | 1 + src/fish_key_reader.cpp | 342 +-------------------------- 4 files changed, 393 insertions(+), 340 deletions(-) create mode 100644 fish-rust/src/fish_key_reader.rs diff --git a/fish-rust/build.rs b/fish-rust/build.rs index d98cab3ba..7940d01fd 100644 --- a/fish-rust/build.rs +++ b/fish-rust/build.rs @@ -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", diff --git a/fish-rust/src/fish_key_reader.rs b/fish-rust/src/fish_key_reader.rs new file mode 100644 index 000000000..943346aa5 --- /dev/null +++ b/fish-rust/src/fish_key_reader.rs @@ -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, 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, c: char) -> Option { + 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) { + bind_chars.push(c); +} + +fn output_bind_command(bind_chars: &mut Vec) { + 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, 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 = 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; + } +} diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index 2bb831b0e..409f1f029 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -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; diff --git a/src/fish_key_reader.cpp b/src/fish_key_reader.cpp index ace48f7d9..fd11b4229 100644 --- a/src/fish_key_reader.cpp +++ b/src/fish_key_reader.cpp @@ -20,345 +20,7 @@ #include #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 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 &bind_chars) { - bind_chars.push_back(wc); -} - -static void output_bind_command(std::vector &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 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 bind_chars; - - std::fwprintf(stderr, L"Press a key:\n"); - while (!check_exit_loop_maybe_warning()) { - maybe_t> 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::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 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(); }