use crate::common::{ fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes, str2wcstring, WSL, }; use crate::env::{EnvStack, Environment}; use crate::fd_readable_set::{FdReadableSet, Timeout}; use crate::flog::{FloggableDebug, FloggableDisplay, FLOG}; use crate::key::{ self, alt, canonicalize_control_char, canonicalize_keyed_control_char, char_to_symbol, function_key, shift, Key, Modifiers, ViewportPosition, }; use crate::reader::reader_test_and_clear_interrupted; use crate::threads::iothread_port; use crate::tty_handoff::{ maybe_set_kitty_keyboard_capability, maybe_set_scroll_content_up_capability, SCROLL_CONTENT_UP_TERMINFO_CODE, XTVERSION, }; use crate::universal_notifier::default_notifier; use crate::wchar::{encode_byte_to_char, prelude::*}; use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate}; use crate::wutil::{fish_is_pua, fish_wcstol}; use std::cell::{RefCell, RefMut}; use std::collections::VecDeque; use std::mem::MaybeUninit; use std::os::fd::RawFd; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; // The range of key codes for inputrc-style keyboard functions. pub const R_END_INPUT_FUNCTIONS: usize = (ReadlineCmd::ReverseRepeatJump as usize) + 1; /// 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(Copy, Clone, Debug, Eq, PartialEq)] pub enum CharInputStyle { // Insert characters normally. Normal, // Insert characters only if the cursor is not at the beginning. Otherwise, discard them. NotFirst, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[repr(u8)] pub enum ReadlineCmd { BeginningOfLine, EndOfLine, ForwardChar, BackwardChar, BackwardCharPassive, ForwardSingleChar, ForwardCharPassive, ForwardWord, BackwardWord, ForwardBigword, BackwardBigword, ForwardToken, BackwardToken, NextdOrForwardWord, PrevdOrBackwardWord, HistoryDelete, HistorySearchBackward, HistorySearchForward, HistoryPrefixSearchBackward, HistoryPrefixSearchForward, HistoryPager, #[deprecated] HistoryPagerDelete, DeleteChar, BackwardDeleteChar, KillLine, Yank, YankPop, Complete, CompleteAndSearch, PagerToggleSearch, BeginningOfHistory, EndOfHistory, BackwardKillLine, KillWholeLine, KillInnerLine, KillWord, KillBigword, KillToken, BackwardKillWord, BackwardKillPathComponent, BackwardKillBigword, BackwardKillToken, HistoryTokenSearchBackward, HistoryTokenSearchForward, HistoryLastTokenSearchBackward, HistoryLastTokenSearchForward, SelfInsert, SelfInsertNotFirst, TransposeChars, TransposeWords, UpcaseWord, DowncaseWord, CapitalizeWord, TogglecaseChar, UpcaseSelection, DowncaseSelection, TogglecaseSelection, Execute, BeginningOfBuffer, EndOfBuffer, RepaintMode, Repaint, ForceRepaint, UpLine, DownLine, SuppressAutosuggestion, AcceptAutosuggestion, BeginSelection, SwapSelectionStartStop, EndSelection, KillSelection, InsertLineUnder, InsertLineOver, ForwardJump, BackwardJump, ForwardJumpTill, BackwardJumpTill, JumpToMatchingBracket, JumpTillMatchingBracket, FuncAnd, FuncOr, ExpandAbbr, DeleteOrExit, Exit, ClearCommandline, CancelCommandline, Cancel, Undo, Redo, BeginUndoGroup, EndUndoGroup, RepeatJump, ClearScreenAndRepaint, ScrollbackPush, // NOTE: This one has to be last. ReverseRepeatJump, } #[derive(Clone, Copy, Debug)] pub struct KeyEvent { pub key: Key, pub shifted_codepoint: char, pub base_layout_codepoint: char, } impl KeyEvent { pub(crate) fn new(modifiers: Modifiers, codepoint: char) -> Self { Self::from(Key::new(modifiers, codepoint)) } pub(crate) fn new_with( modifiers: Modifiers, codepoint: char, shifted_key: Option, base_layout_key: Option, ) -> Self { Self { key: Key::new(modifiers, codepoint), shifted_codepoint: shifted_key.unwrap_or_default(), base_layout_codepoint: base_layout_key.unwrap_or_default(), } } pub(crate) fn from_raw(codepoint: char) -> Self { Self::from(Key::from_raw(codepoint)) } pub fn from_single_byte(c: u8) -> Self { Self::from(Key::from_single_byte(c)) } pub(crate) fn codepoint_text(&self) -> Option { let mut modifiers = self.modifiers; let mut c = self.codepoint; if self.shifted_codepoint != '\0' && modifiers.shift { modifiers.shift = false; c = self.shifted_codepoint; } if modifiers.is_some() { return None; } if c == key::Space { return Some(' '); } if c == key::Enter { return Some('\n'); } if c == key::Tab { return Some('\t'); } if fish_is_pua(c) || u32::from(c) <= 27 { return None; } Some(c) } } impl From for KeyEvent { fn from(key: Key) -> Self { Self::new_with(key.modifiers, key.codepoint, None, None) } } impl std::ops::Deref for KeyEvent { type Target = Key; fn deref(&self) -> &Self::Target { &self.key } } impl std::ops::DerefMut for KeyEvent { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.key } } fn apply_shift(mut key: Key, do_ascii: bool, shifted_codepoint: char) -> Option { if !key.modifiers.shift { return Some(key); } if shifted_codepoint != '\0' { key.codepoint = shifted_codepoint; } else if do_ascii && key.codepoint.is_ascii_lowercase() { // For backwards compatibility, we convert the "bind shift-a" notation to "bind A". // This enables us to match "A" events which are the legacy encoding for keys that // generate text -- until we request kitty's "Report all keys as escape codes". // We do not currently convert non-ASCII key notation such as "bind shift-ä". key.codepoint = key.codepoint.to_ascii_uppercase(); } else { return None; }; key.modifiers.shift = false; Some(key) } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub(crate) enum KeyMatchQuality { BaseLayoutModuloShift, BaseLayout, ModuloShift, Exact, } impl FloggableDebug for KeyMatchQuality {} pub(crate) fn match_key_event_to_key(event: &KeyEvent, key: &Key) -> Option { if &event.key == key { return Some(KeyMatchQuality::Exact); } let shifted_evt = apply_shift(event.key, false, event.shifted_codepoint); let shifted_key = apply_shift(*key, true, '\0'); if shifted_evt.is_some() && shifted_evt == shifted_key { return Some(KeyMatchQuality::ModuloShift); } if event.base_layout_codepoint != '\0' { let mut base_layout_key = event.key; base_layout_key.codepoint = event.base_layout_codepoint; if base_layout_key == *key { return Some(KeyMatchQuality::BaseLayout); } let shifted_base_layout_key = apply_shift(base_layout_key, true, '\0'); if shifted_base_layout_key.is_some() && shifted_base_layout_key == shifted_key { return Some(KeyMatchQuality::BaseLayoutModuloShift); } } None } #[test] fn test_match_key_event_to_key() { macro_rules! validate { ($evt:expr, $key:expr, $expected:expr) => { assert_eq!(match_key_event_to_key(&$evt, &$key), $expected); }; } let none = Modifiers::default(); let shift = Modifiers::SHIFT; let ctrl = Modifiers::CTRL; let ctrl_shift = Modifiers { ctrl: true, shift: true, ..Default::default() }; let exact = KeyMatchQuality::Exact; let modulo_shift = KeyMatchQuality::ModuloShift; let base_layout = KeyMatchQuality::BaseLayout; let base_layout_modulo_shift = KeyMatchQuality::BaseLayoutModuloShift; validate!(KeyEvent::new(none, 'a'), Key::new(none, 'a'), Some(exact)); validate!(KeyEvent::new(none, 'a'), Key::new(none, 'A'), None); validate!(KeyEvent::new(shift, 'a'), Key::new(shift, 'a'), Some(exact)); validate!(KeyEvent::new(shift, 'a'), Key::new(none, 'A'), None); validate!(KeyEvent::new(shift, 'ä'), Key::new(none, 'Ä'), None); // For historical reasons we canonicalize notation for ASCII keys like "shift-a" to "A", // but not "shift-a" events - those should send a shifted key. validate!( KeyEvent::new(none, 'A'), Key::new(shift, 'a'), Some(modulo_shift) ); validate!(KeyEvent::new(none, 'A'), Key::new(shift, 'A'), None); validate!(KeyEvent::new(none, 'Ä'), Key::new(none, 'Ä'), Some(exact)); validate!(KeyEvent::new(none, 'Ä'), Key::new(shift, 'ä'), None); // FYI: for codepoints that are not letters with uppercase/lowercase versions, we use // the shifted key in the canonical notation, because the unshifted one may depend on the // keyboard layout. let ctrl_shift_equals = KeyEvent::new_with(ctrl_shift, '=', Some('+'), None); validate!(ctrl_shift_equals, Key::new(ctrl_shift, '='), Some(exact)); validate!(ctrl_shift_equals, Key::new(ctrl, '+'), Some(modulo_shift)); // canonical notation validate!(ctrl_shift_equals, Key::new(ctrl_shift, '+'), None); validate!(ctrl_shift_equals, Key::new(ctrl, '='), None); // A event like capslock-shift-ä may or may not include a shifted codepoint. // // Without a shifted codepoint, we cannot easily match ctrl-Ä. let caps_ctrl_shift_ä = KeyEvent::new(ctrl_shift, 'ä'); validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'ä'), Some(exact)); // canonical notation validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'ä'), None); validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'Ä'), None); // can't match without shifted key validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'Ä'), None); // With a shifted codepoint, we can match the alternative notation too. let caps_ctrl_shift_ä = KeyEvent::new_with(ctrl_shift, 'ä', Some('Ä'), None); validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'ä'), Some(exact)); // canonical notation validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'ä'), None); validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'Ä'), Some(modulo_shift)); // matched via shifted key validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'Ä'), None); let ctrl_ц = KeyEvent::new_with(ctrl, 'ц', None, Some('w')); let ctrl_shift_ц = KeyEvent::new_with(ctrl_shift, 'ц', Some('Ц'), Some('w')); validate!(ctrl_ц, Key::new(ctrl, 'ц'), Some(exact)); validate!(ctrl_ц, Key::new(ctrl, 'w'), Some(base_layout)); validate!(ctrl_ц, Key::new(ctrl_shift, 'ц'), None); validate!(ctrl_ц, Key::new(ctrl_shift, 'w'), None); validate!( ctrl_shift_ц, Key::new(ctrl, 'W'), Some(base_layout_modulo_shift) ); validate!(ctrl_shift_ц, Key::new(ctrl, 'w'), None); // Note that "bind ctrl-Ц" will win over "bind ctrl-shift-w". // This is because we consider shift transformation to be less magic than base-key // transformation. validate!(ctrl_shift_ц, Key::new(ctrl, 'Ц'), Some(modulo_shift)); validate!(ctrl_shift_ц, Key::new(ctrl_shift, 'w'), Some(base_layout)); } /// Represents an event on the character input stream. #[derive(Debug, Clone)] pub enum CharEventType { /// A character was entered. Char(KeyInputEvent), /// A readline event. Readline(ReadlineCmd), /// A shell command. Command(WString), /// 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. CheckExit, } #[derive(Debug, Clone)] pub struct ReadlineCmdEvent { pub cmd: ReadlineCmd, /// 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. /// This is also empty for invalid Unicode code points, which produce multiple characters. pub seq: WString, } #[derive(Debug, Clone)] pub struct KeyInputEvent { // The key. pub key: KeyEvent, // The style to use when inserting characters into the command line. pub input_style: CharInputStyle, /// 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. /// This is also empty for invalid Unicode code points, which produce multiple characters. pub seq: WString, } #[derive(Debug, Clone)] pub enum ImplicitEvent { /// 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. CheckExit, /// A blocking terminal query was interrupterd with ctrl-c. QueryInterrupted, /// Our terminal window gained focus. FocusIn, /// Our terminal window lost focus. FocusOut, /// Request to disable mouse tracking. DisableMouseTracking, /// Mouse left click. MouseLeft(ViewportPosition), } #[derive(Debug, Clone)] pub enum QueryResponse { PrimaryDeviceAttribute, CursorPosition(ViewportPosition), } #[derive(Debug, Clone)] pub enum QueryResultEvent { Response(QueryResponse), Timeout, } #[derive(Debug, Clone)] pub enum CharEvent { /// A character was entered. Key(KeyInputEvent), /// A readline event. Readline(ReadlineCmdEvent), /// A shell command. Command(WString), /// Any event that has no user-visible representation. Implicit(ImplicitEvent), QueryResult(QueryResultEvent), } impl FloggableDebug for CharEvent {} impl CharEvent { pub fn is_char(&self) -> bool { matches!(self, CharEvent::Key(_)) } pub fn is_readline(&self) -> bool { matches!(self, CharEvent::Readline(_)) } pub fn is_readline_or_command(&self) -> bool { matches!(self, CharEvent::Readline(_) | CharEvent::Command(_)) } pub fn get_char(&self) -> char { let CharEvent::Key(kevt) = self else { panic!("Not a char type"); }; kevt.key.codepoint } pub fn get_key(&self) -> Option<&KeyInputEvent> { match self { CharEvent::Key(kevt) => Some(kevt), _ => None, } } pub fn get_readline(&self) -> ReadlineCmd { let CharEvent::Readline(c) = self else { panic!("Not a readline type"); }; c.cmd } pub fn get_command(&self) -> Option<&wstr> { match self { CharEvent::Command(c) => Some(c), _ => None, } } pub fn from_char(c: char) -> CharEvent { Self::from_key(KeyEvent::from_raw(c)) } pub fn from_key(key: KeyEvent) -> CharEvent { Self::from_key_seq(key, WString::new()) } pub fn from_key_seq(key: KeyEvent, seq: WString) -> CharEvent { CharEvent::Key(KeyInputEvent { key, input_style: CharInputStyle::Normal, seq, }) } pub fn from_readline(cmd: ReadlineCmd) -> CharEvent { Self::from_readline_seq(cmd, WString::new()) } pub fn from_readline_seq(cmd: ReadlineCmd, seq: WString) -> CharEvent { CharEvent::Readline(ReadlineCmdEvent { cmd, seq }) } pub fn from_check_exit() -> CharEvent { CharEvent::Implicit(ImplicitEvent::CheckExit) } } /// 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. const WAIT_ON_ESCAPE_DEFAULT: usize = 30; static WAIT_ON_ESCAPE_MS: AtomicUsize = AtomicUsize::new(WAIT_ON_ESCAPE_DEFAULT); const WAIT_ON_SEQUENCE_KEY_INFINITE: usize = usize::MAX; static WAIT_ON_SEQUENCE_KEY_MS: AtomicUsize = AtomicUsize::new(WAIT_ON_SEQUENCE_KEY_INFINITE); /// 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 InputEventTrigger { // A byte was successfully read. Byte(u8), // The in fd has been closed. Eof, // select() was interrupted by a signal. Interrupted, // Our uvar notifier reported a change (either through poll() or its fd). UvarNotified, // Our ioport reported a change, so service main thread requests. IOPortNotified, // No file descriptor was ready within the query timeout. TimeoutElapsed, } fn readb(in_fd: RawFd) -> Option { assert!(in_fd >= 0, "Invalid in fd"); let mut arr: [u8; 1] = [0]; if read_blocked(in_fd, &mut arr) != Ok(1) { // The terminal has been closed. return None; } let c = arr[0]; FLOG!(reader, "Read byte", char_to_symbol(char::from(c), true)); // The common path is to return a u8. Some(c) } fn next_input_event(in_fd: RawFd, timeout: Timeout) -> InputEventTrigger { let mut fdset = FdReadableSet::new(); loop { fdset.clear(); fdset.add(in_fd); // Add the completion ioport. let ioport_fd = iothread_port(); fdset.add(ioport_fd); // Get the uvar notifier fd (possibly none). let notifier = default_notifier(); let notifier_fd = notifier.notification_fd(); if let Some(notifier_fd) = notifier_fd { fdset.add(notifier_fd); } // Here's where we call select(). let select_res = fdset.check_readable(timeout); if select_res < 0 { let err = errno::errno().0; if err == libc::EINTR || err == libc::EAGAIN { // A signal. return InputEventTrigger::Interrupted; } else { // Some fd was invalid, so probably the tty has been closed. return InputEventTrigger::Eof; } } if select_res == 0 { assert!(!matches!(timeout, Timeout::Forever)); return InputEventTrigger::TimeoutElapsed; } // 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. if let Some(notifier_fd) = notifier_fd { if fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd) { return InputEventTrigger::UvarNotified; } } // Check stdin. if fdset.test(in_fd) { return readb(in_fd).map_or(InputEventTrigger::Eof, InputEventTrigger::Byte); } // 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 InputEventTrigger::IOPortNotified; } } } pub fn check_fd_readable(in_fd: RawFd, timeout: Duration) -> bool { use std::ptr; // 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(). let mut sigs = MaybeUninit::uninit(); let mut sigs = unsafe { libc::sigfillset(sigs.as_mut_ptr()); sigs.assume_init() }; // pselect expects timeouts in nanoseconds. const NSEC_PER_MSEC: u64 = 1000 * 1000; const NSEC_PER_SEC: u64 = NSEC_PER_MSEC * 1000; let wait_nsec: u64 = (timeout.as_millis() as u64) * NSEC_PER_MSEC; let timeout = libc::timespec { tv_sec: (wait_nsec / NSEC_PER_SEC).try_into().unwrap(), tv_nsec: (wait_nsec % NSEC_PER_SEC).try_into().unwrap(), }; // We have one fd of interest. let mut fdset = MaybeUninit::uninit(); let mut fdset = unsafe { libc::FD_ZERO(fdset.as_mut_ptr()); fdset.assume_init() }; unsafe { libc::FD_SET(in_fd, &mut fdset); } let res = unsafe { libc::pselect( in_fd + 1, &mut fdset, ptr::null_mut(), ptr::null_mut(), &timeout, &sigs, ) }; // Prevent signal starvation on WSL causing the `torn_escapes.py` test to fail if is_windows_subsystem_for_linux(WSL::V1) { // Merely querying the current thread's sigmask is sufficient to deliver a pending signal let _ = unsafe { libc::pthread_sigmask(0, ptr::null(), &mut sigs) }; } res > 0 } // Update the wait_on_escape_ms value in response to the fish_escape_delay_ms user variable being // set. pub fn update_wait_on_escape_ms(vars: &EnvStack) { let fish_escape_delay_ms = vars.get_unless_empty(L!("fish_escape_delay_ms")); let Some(fish_escape_delay_ms) = fish_escape_delay_ms else { WAIT_ON_ESCAPE_MS.store(WAIT_ON_ESCAPE_DEFAULT, Ordering::Relaxed); return; }; let fish_escape_delay_ms = fish_escape_delay_ms.as_string(); match fish_wcstol(&fish_escape_delay_ms) { Ok(val) if (10..5000).contains(&val) => { WAIT_ON_ESCAPE_MS.store(val.try_into().unwrap(), Ordering::Relaxed); } _ => { eprintf!( concat!( "ignoring fish_escape_delay_ms: value '%ls' ", "is not an integer or is < 10 or >= 5000 ms\n" ), fish_escape_delay_ms ) } } } // Update the wait_on_sequence_key_ms value in response to the fish_sequence_key_delay_ms user // variable being set. pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) { let sequence_key_time_ms = vars.get_unless_empty(L!("fish_sequence_key_delay_ms")); let Some(sequence_key_time_ms) = sequence_key_time_ms else { WAIT_ON_SEQUENCE_KEY_MS.store(WAIT_ON_SEQUENCE_KEY_INFINITE, Ordering::Relaxed); return; }; let sequence_key_time_ms = sequence_key_time_ms.as_string(); match fish_wcstol(&sequence_key_time_ms) { Ok(val) if (10..5000).contains(&val) => { WAIT_ON_SEQUENCE_KEY_MS.store(val.try_into().unwrap(), Ordering::Relaxed); } _ => { eprintf!( concat!( "ignoring fish_sequence_key_delay_ms: value '%ls' ", "is not an integer or is < 10 or >= 5000 ms\n" ), sequence_key_time_ms ) } } } fn parse_mask(mask: u32) -> (Modifiers, bool) { let modifiers = Modifiers { ctrl: (mask & 4) != 0, alt: (mask & 2) != 0, shift: (mask & 1) != 0, sup: (mask & 8) != 0, }; let caps_lock = (mask & 64) != 0; (modifiers, caps_lock) } // A data type used by the input machinery. #[derive(Default)] pub struct InputData { // The file descriptor from which we read input, often stdin. pub in_fd: RawFd, // Queue of unread characters. pub queue: VecDeque, // The current paste buffer, if any. pub paste_buffer: Option>, // The arguments to the most recently invoked input function. pub input_function_args: Vec, // The return status of the most recently invoked input function. pub function_status: bool, // Transient storage to avoid repeated allocations. pub event_storage: Vec, // How long to wait for responses for TTY queries. pub blocking_query_timeout: Option, // If set, events will be buffered until the query finishes. pub blocking_query: RefCell>, } impl InputData { /// Construct from the fd from which to read. pub fn new(in_fd: RawFd, blocking_query_timeout: Option) -> Self { Self { in_fd, queue: VecDeque::new(), paste_buffer: None, input_function_args: Vec::new(), function_status: false, event_storage: Vec::new(), blocking_query_timeout, blocking_query: RefCell::new(None), } } /// Enqueue a char event to the queue of unread characters that input_readch will return before /// actually reading from fd 0. pub fn queue_char(&mut self, ch: CharEvent) { self.queue.push_back(ch); } /// Sets the return status of the most recently executed input function. pub fn function_set_status(&mut self, status: bool) { self.function_status = status; } } #[derive(Clone, Eq, PartialEq)] pub enum CursorPositionQueryKind { MouseLeft(ViewportPosition), ScrollbackPush, } #[derive(Clone, Eq, PartialEq)] pub struct CursorPositionQuery { pub kind: CursorPositionQueryKind, pub result: Option, } impl CursorPositionQuery { pub fn new(kind: CursorPositionQueryKind) -> Self { Self { kind, result: None } } } #[derive(Eq, PartialEq)] pub enum TerminalQuery { Initial, CursorPosition(CursorPositionQuery), } /// A trait which knows how to produce a stream of input events. /// Note this is conceptually a "base class" with override points. pub trait InputEventQueuer { /// Return the next event in the queue, or none if the queue is empty. fn try_pop(&mut self) -> Option { if self.is_blocked_querying() { use ImplicitEvent::*; match self.get_input_data().queue.front()? { CharEvent::QueryResult(_) | CharEvent::Implicit(CheckExit | Eof | QueryInterrupted) => {} CharEvent::Key(_) | CharEvent::Readline(_) | CharEvent::Command(_) | CharEvent::Implicit(_) => { return None; // No code execution while blocked. } } } self.get_input_data_mut().queue.pop_front() } /// Function used by [`readch`](Self::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. fn readch(&mut self) -> CharEvent { loop { // 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 let Some(mevt) = self.try_pop() { return mevt; } // We are going to block; but first allow any override to inject events. self.prepare_to_select(); if let Some(mevt) = self.try_pop() { return mevt; } match next_input_event( self.get_in_fd(), if self.is_blocked_querying() { Timeout::Duration(self.get_input_data().blocking_query_timeout.unwrap()) } else { Timeout::Forever }, ) { InputEventTrigger::Eof => { return CharEvent::Implicit(ImplicitEvent::Eof); } InputEventTrigger::Interrupted => { self.select_interrupted(); } InputEventTrigger::UvarNotified => { self.uvar_change_notified(); } InputEventTrigger::IOPortNotified => { self.ioport_notified(); } InputEventTrigger::Byte(read_byte) => { let mut have_escape_prefix = false; let mut buffer = vec![read_byte]; let key_with_escape = if read_byte == 0x1b { self.parse_escape_sequence(&mut buffer, &mut have_escape_prefix) } else { canonicalize_control_char(read_byte).map(KeyEvent::from) }; if self.paste_is_buffering() { if read_byte != 0x1b { self.paste_push_char(read_byte); } continue; } let mut seq = WString::new(); let mut key = key_with_escape; if key.is_some_and(|key| key.key == Key::from_raw(key::Invalid)) { continue; } assert!(key.map_or(true, |key| key.codepoint != key::Invalid)); let mut consumed = 0; let mut state = zero_mbstate(); let mut i = 0; let ok = loop { if i == buffer.len() { buffer.push( match next_input_event(self.get_in_fd(), Timeout::Forever) { InputEventTrigger::Byte(b) => b, _ => 0, }, ); } match decode_input_byte( &mut seq, InvalidPolicy::Error, &mut state, &buffer[..i + 1], &mut consumed, ) { DecodeState::Incomplete => (), DecodeState::Complete => { if have_escape_prefix && i != 0 { have_escape_prefix = false; let c = seq.as_char_slice().last().unwrap(); key = Some(KeyEvent::from(alt(*c))); } if i + 1 == buffer.len() { break true; } } DecodeState::Error => { self.push_front(CharEvent::from_check_exit()); break false; } } i += 1; }; if !ok { continue; } let (key_evt, extra) = if let Some(key) = key { (CharEvent::from_key_seq(key, seq), None) } else { let Some(c) = seq.chars().next() else { continue; }; ( CharEvent::from_key_seq(KeyEvent::from_raw(c), seq.clone()), Some(seq.chars().skip(1).map(CharEvent::from_char)), ) }; if self.is_blocked_querying() { FLOG!( reader, "Still blocked on response from terminal, deferring key event", key_evt ); self.push_back(key_evt); extra.map(|extra| { for evt in extra { self.push_back(evt); } }); let vintr = shell_modes().c_cc[libc::VINTR]; if vintr != 0 && key.is_some_and(|key| { match_key_event_to_key(&key, &Key::from_single_byte(vintr)) .is_some() }) { FLOG!( reader, "Received interrupt key, giving up waiting for response from terminal" ); let ok = stop_query(self.blocking_query()); assert!(ok); self.get_input_data_mut().queue.clear(); self.push_front(CharEvent::Implicit(ImplicitEvent::QueryInterrupted)); } continue; } extra.map(|extra| self.insert_front(extra)); return key_evt; } InputEventTrigger::TimeoutElapsed => { return CharEvent::QueryResult(QueryResultEvent::Timeout); } } } } fn try_readb(&mut self, buffer: &mut Vec) -> Option { let fd = self.get_in_fd(); if !check_fd_readable( fd, Duration::from_millis(if self.paste_is_buffering() || self.is_blocked_querying() { 300 } else if buffer == b"\x1b" { 1 // distinguish legacy escape } else { 30 }), ) { FLOG!( reader, format!("Incomplete escape sequence: {}", DisplayBytes(buffer)) ); return None; } let next = readb(fd)?; buffer.push(next); Some(next) } fn parse_escape_sequence( &mut self, buffer: &mut Vec, have_escape_prefix: &mut bool, ) -> Option { assert!(buffer.len() <= 2); let recursive_invocation = buffer.len() == 2; let Some(next) = self.try_readb(buffer) else { return Some(KeyEvent::from_raw(key::Escape)); }; let invalid = KeyEvent::from_raw(key::Invalid); if recursive_invocation && next == b'\x1b' { return Some( match self.parse_escape_sequence(buffer, have_escape_prefix) { Some(mut nested_sequence) => { if nested_sequence.key == invalid.key { return Some(KeyEvent::from_raw(key::Escape)); } nested_sequence.modifiers.alt = true; nested_sequence } _ => invalid, }, ); } if next == b'[' { // potential CSI return Some(self.parse_csi(buffer).unwrap_or(invalid)); } if next == b'O' { // potential SS3 return Some(self.parse_ss3(buffer).unwrap_or(invalid)); } if !recursive_invocation && next == b'P' { // potential DCS return Some(self.parse_dcs(buffer).unwrap_or(invalid)); } match canonicalize_control_char(next) { Some(mut key) => { key.modifiers.alt = true; Some(KeyEvent::from(key)) } None => { *have_escape_prefix = true; None } } } fn parse_csi(&mut self, buffer: &mut Vec) -> Option { // The maximum number of CSI parameters is defined by NPAR, nominally 16. let mut params = [[0_u32; 4]; 16]; let Some(mut c) = self.try_readb(buffer) else { return Some(KeyEvent::from(alt('['))); }; let mut next_char = |zelf: &mut Self| zelf.try_readb(buffer).unwrap_or(0xff); let private_mode; if matches!(c, b'?' | b'<' | b'=' | b'>') { // private mode private_mode = Some(c); c = next_char(self); } else { private_mode = None; } let mut count = 0; let mut subcount = 0; while count < 16 && (0x30..=0x3f).contains(&c) { if c.is_ascii_digit() { // Return None on invalid ascii numeric CSI parameter exceeding u32 bounds match params[count][subcount] .checked_mul(10) .and_then(|result| result.checked_add(u32::from(c - b'0'))) { Some(c) => params[count][subcount] = c, None => return invalid_sequence(buffer), }; } else if c == b':' && subcount < 3 { subcount += 1; } else if c == b';' { count += 1; subcount = 0; } else { // Unexpected character or unrecognized CSI return None; } c = next_char(self); } if c != b'$' && !(0x40..=0x7e).contains(&c) { return None; } let kitty_key = |key: char, shifted_key: Option, base_layout_key: Option| { let mask = params[1][0].saturating_sub(1); let (mut modifiers, caps_lock) = parse_mask(mask); // An event like "capslock-shift-=" should have a shifted codepoint ("+") to enable // fish to match "bind +". // // With letters that are affected by capslock, capslock and shift cancel each // other out ("capslock-shift-ä"), unless there is another modifier to imply that // capslock should be ignored. // // So if shift is the only modifier, we should consume it, but not if the event is // something like "capslock-shift-delete" because delete is not affected by capslock. // // Normally, we could consume shift by translating to the shifted key. // While capslock is on however, we don't get a shifted key, see // https://github.com/kovidgoyal/kitty/issues/8493. // // Do it by trying to find out ourselves whether the key is affected by capslock. // // Alternatively, we could relax our exact matching semantics, and make "bind ä" // match the "shift-ä" event, as suggested in the kitty issue. if caps_lock && modifiers == Modifiers::SHIFT && !key.to_uppercase().eq(Some(key)) { modifiers.shift = false; } KeyEvent::new_with(modifiers, key, shifted_key, base_layout_key) }; let masked_key = |key: char| kitty_key(key, None, None); let key = match c { b'$' => { if next_char(self) == b'y' { // DECRPM/DECRQM return None; } match params[0][0] { 23 | 24 => KeyEvent::from(shift( char::from_u32(u32::from(function_key(11)) + params[0][0] - 23).unwrap(), // rxvt style )), _ => return None, } } b'A' => masked_key(key::Up), b'B' => masked_key(key::Down), b'C' => masked_key(key::Right), b'D' => masked_key(key::Left), b'E' => masked_key('5'), // Numeric keypad b'F' => masked_key(key::End), // PC/xterm style b'H' => masked_key(key::Home), // PC/xterm style b'M' | b'm' => { self.disable_mouse_tracking(); // Generic X10 or modified VT200 sequence, or 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. let sgr = private_mode == Some(b'<'); if !sgr && c == b'm' { return None; } let Some(button) = (if sgr { Some(params[0][0]) } else { u32::from(next_char(self)).checked_sub(32) }) else { return invalid_sequence(buffer); }; let mut convert = |param| { (if sgr { Some(param) } else { u32::from(next_char(self)).checked_sub(32) }) .and_then(|coord| coord.checked_sub(1)) .and_then(|coord| usize::try_from(coord).ok()) }; let Some(x) = convert(params[1][0]) else { return invalid_sequence(buffer); }; let Some(y) = convert(params[2][0]) else { return invalid_sequence(buffer); }; let position = ViewportPosition { x, y }; let (modifiers, _caps_lock) = parse_mask((button >> 2) & 0x07); let code = button & 0x43; if code != 0 || c != b'M' || modifiers.is_some() { return None; } self.push_front(CharEvent::Implicit(ImplicitEvent::MouseLeft(position))); return None; } b't' => { self.disable_mouse_tracking(); // VT200 button released in mouse highlighting mode at valid text location. 5 chars. let _ = next_char(self); let _ = next_char(self); return None; } b'T' => { self.disable_mouse_tracking(); // VT200 button released in mouse highlighting mode past end-of-line. 9 characters. for _ in 0..6 { let _ = next_char(self); } return None; } b'P' => masked_key(function_key(1)), b'Q' => masked_key(function_key(2)), b'R' => { let Some(y) = params[0][0] .checked_sub(1) .and_then(|y| usize::try_from(y).ok()) else { return invalid_sequence(buffer); }; let Some(x) = params[1][0] .checked_sub(1) .and_then(|x| usize::try_from(x).ok()) else { return invalid_sequence(buffer); }; FLOG!(reader, "Received cursor position report y:", y, "x:", x); let cursor_pos = ViewportPosition { x, y }; self.push_front(CharEvent::QueryResult(QueryResultEvent::Response( QueryResponse::CursorPosition(cursor_pos), ))); return None; } b'S' => masked_key(function_key(4)), b'~' => match params[0][0] { 1 => masked_key(key::Home), // VT220/tmux style 2 => masked_key(key::Insert), 3 => masked_key(key::Delete), 4 => masked_key(key::End), // VT220/tmux style 5 => masked_key(key::PageUp), 6 => masked_key(key::PageDown), 7 => masked_key(key::Home), // rxvt style 8 => masked_key(key::End), // rxvt style 11..=15 => masked_key( char::from_u32(u32::from(function_key(1)) + params[0][0] - 11).unwrap(), ), 17..=21 => masked_key( char::from_u32(u32::from(function_key(6)) + params[0][0] - 17).unwrap(), ), 23 | 24 => masked_key( char::from_u32(u32::from(function_key(11)) + params[0][0] - 23).unwrap(), ), 25 | 26 => KeyEvent::from(shift( char::from_u32(u32::from(function_key(3)) + params[0][0] - 25).unwrap(), )), // rxvt style 27 => { let Some(key) = char::from_u32(params[2][0]) else { return invalid_sequence(buffer); }; masked_key(canonicalize_keyed_control_char(key)) } 28 | 29 => KeyEvent::from(shift( char::from_u32(u32::from(function_key(5)) + params[0][0] - 28).unwrap(), )), // rxvt style 31 | 32 => KeyEvent::from(shift( char::from_u32(u32::from(function_key(7)) + params[0][0] - 31).unwrap(), )), // rxvt style 33 | 34 => KeyEvent::from(shift( char::from_u32(u32::from(function_key(9)) + params[0][0] - 33).unwrap(), )), // rxvt style 200 => { self.paste_start_buffering(); return None; } 201 => { self.paste_commit(); return None; } _ => return None, }, b'c' if private_mode == Some(b'?') => { FLOG!(reader, "Received Primary Device Attribute response"); self.push_front(CharEvent::QueryResult(QueryResultEvent::Response( QueryResponse::PrimaryDeviceAttribute, ))); return None; } b'u' => { if private_mode == Some(b'?') { maybe_set_kitty_keyboard_capability(); return None; } // Treat numpad keys the same as their non-numpad counterparts. Could add a numpad modifier here. let key = match params[0][0] { 57361 => key::PrintScreen, 57363 => key::Menu, 57399 => '0', 57400 => '1', 57401 => '2', 57402 => '3', 57403 => '4', 57404 => '5', 57405 => '6', 57406 => '7', 57407 => '8', 57408 => '9', 57409 => '.', 57410 => '/', 57411 => '*', 57412 => '-', 57413 => '+', 57414 => key::Enter, 57415 => '=', 57417 => key::Left, 57418 => key::Right, 57419 => key::Up, 57420 => key::Down, 57421 => key::PageUp, 57422 => key::PageDown, 57423 => key::Home, 57424 => key::End, 57425 => key::Insert, 57426 => key::Delete, cp => { let Some(key) = char::from_u32(cp) else { return invalid_sequence(buffer); }; canonicalize_keyed_control_char(key) } }; let Some(shifted_key) = char::from_u32(params[0][1]) else { return invalid_sequence(buffer); }; let Some(base_layout_key) = char::from_u32(params[0][2]) else { return invalid_sequence(buffer); }; kitty_key( key, Some(canonicalize_keyed_control_char(shifted_key)), Some(base_layout_key), ) } b'Z' => KeyEvent::from(shift(key::Tab)), b'I' => { self.push_front(CharEvent::Implicit(ImplicitEvent::FocusIn)); return None; } b'O' => { self.push_front(CharEvent::Implicit(ImplicitEvent::FocusOut)); return None; } _ => return None, }; Some(key) } fn disable_mouse_tracking(&mut self) { // 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 turn it off here to avoid wasting resources. FLOG!(reader, "Disabling mouse tracking"); // We shouldn't directly manipulate stdout from here, so we ask the reader to do it. self.push_front(CharEvent::Implicit(ImplicitEvent::DisableMouseTracking)); } fn parse_ss3(&mut self, buffer: &mut Vec) -> Option { let mut raw_mask = 0; let Some(mut code) = self.try_readb(buffer) else { return Some(KeyEvent::from(alt('O'))); }; while (b'0'..=b'9').contains(&code) { raw_mask = raw_mask * 10 + u32::from(code - b'0'); code = self.try_readb(buffer).unwrap_or(0xff); } let (modifiers, _caps_lock) = parse_mask(raw_mask.saturating_sub(1)); #[rustfmt::skip] let key = match code { b' ' => KeyEvent::new(modifiers, key::Space), b'A' => KeyEvent::new(modifiers, key::Up), b'B' => KeyEvent::new(modifiers, key::Down), b'C' => KeyEvent::new(modifiers, key::Right), b'D' => KeyEvent::new(modifiers, key::Left), b'F' => KeyEvent::new(modifiers, key::End), b'H' => KeyEvent::new(modifiers, key::Home), b'I' => KeyEvent::new(modifiers, key::Tab), b'M' => KeyEvent::new(modifiers, key::Enter), b'P' => KeyEvent::new(modifiers, function_key(1)), b'Q' => KeyEvent::new(modifiers, function_key(2)), b'R' => KeyEvent::new(modifiers, function_key(3)), b'S' => KeyEvent::new(modifiers, function_key(4)), b'X' => KeyEvent::new(modifiers, '='), b'j' => KeyEvent::new(modifiers, '*'), b'k' => KeyEvent::new(modifiers, '+'), b'l' => KeyEvent::new(modifiers, ','), b'm' => KeyEvent::new(modifiers, '-'), b'n' => KeyEvent::new(modifiers, '.'), b'o' => KeyEvent::new(modifiers, '/'), b'p' => KeyEvent::new(modifiers, '0'), b'q' => KeyEvent::new(modifiers, '1'), b'r' => KeyEvent::new(modifiers, '2'), b's' => KeyEvent::new(modifiers, '3'), b't' => KeyEvent::new(modifiers, '4'), b'u' => KeyEvent::new(modifiers, '5'), b'v' => KeyEvent::new(modifiers, '6'), b'w' => KeyEvent::new(modifiers, '7'), b'x' => KeyEvent::new(modifiers, '8'), b'y' => KeyEvent::new(modifiers, '9'), _ => return None, }; Some(key) } fn read_until_sequence_terminator(&mut self, buffer: &mut Vec) -> Option<()> { let mut escape = false; loop { let b = self.try_readb(buffer)?; if escape && b == b'\\' { break; } escape = b == b'\x1b'; } buffer.pop(); buffer.pop(); Some(()) } fn parse_xtversion(&mut self, buffer: &mut Vec) -> Option<()> { assert_eq!(buffer, b"\x1bP>"); self.read_until_sequence_terminator(buffer)?; if buffer.get(3)? != &b'|' { return None; } XTVERSION.get_or_init(|| { let xtversion = str2wcstring(&buffer[4..buffer.len()]); FLOG!( reader, format!("Received XTVERSION response: {}", xtversion) ); xtversion }); None } fn parse_dcs(&mut self, buffer: &mut Vec) -> Option { assert!(buffer.len() == 2); let Some(success) = self.try_readb(buffer) else { return Some(KeyEvent::from(alt('P'))); }; let success = match success { b'0' => false, b'1' => true, b'>' => { self.parse_xtversion(buffer); return None; } _ => return None, }; if self.try_readb(buffer)? != b'+' { return None; } if self.try_readb(buffer)? != b'r' { return None; } self.read_until_sequence_terminator(buffer)?; // \e P 1 r + Pn ST // \e P 0 r + msg ST let buffer = &buffer[5..]; if !success { FLOG!( reader, format!( "Received XTGETTCAP failure response: {}", str2wcstring(&parse_hex(buffer)?), ) ); return None; } let mut buffer = buffer.splitn(2, |&c| c == b'='); let key = buffer.next().unwrap(); let key = parse_hex(key)?; if let Some(value) = buffer.next() { let value = parse_hex(value)?; FLOG!( reader, format!( "Received XTGETTCAP response: {}={:?}", str2wcstring(&key), str2wcstring(&value) ) ); } else { FLOG!( reader, format!("Received XTGETTCAP response: {}", str2wcstring(&key)) ); } if key == SCROLL_CONTENT_UP_TERMINFO_CODE.as_bytes() { maybe_set_scroll_content_up_capability(); } return None; } fn readch_timed_esc(&mut self) -> Option { self.readch_timed(WAIT_ON_ESCAPE_MS.load(Ordering::Relaxed)) } fn readch_timed_sequence_key(&mut self) -> Option { let wait_on_sequence_key_ms = WAIT_ON_SEQUENCE_KEY_MS.load(Ordering::Relaxed); if wait_on_sequence_key_ms == WAIT_ON_SEQUENCE_KEY_INFINITE { return Some(self.readch()); } self.readch_timed(wait_on_sequence_key_ms) } /// Like readch(), except it will wait at most wait_time_ms milliseconds for a /// character to be available for reading. /// Return None on timeout, the event on success. fn readch_timed(&mut self, wait_time_ms: usize) -> Option { if let Some(evt) = self.try_pop() { return Some(evt); } check_fd_readable( self.get_in_fd(), Duration::from_millis(u64::try_from(wait_time_ms).unwrap()), ) .then(|| self.readch()) } /// Return the fd from which to read. fn get_in_fd(&self) -> RawFd { self.get_input_data().in_fd } /// Return the input data. This is to be implemented by the concrete type. fn get_input_data(&self) -> &InputData; fn get_input_data_mut(&mut self) -> &mut InputData; // Support for "bracketed paste" // The way it works is that we acknowledge our support by printing // \e\[?2004h // then the terminal will "bracket" every paste in // \e\[200~ and \e\[201~ // Every character in between those two will be part of the paste and should not cause a binding to execute (like \n executing commands). // // We enable it after every command and disable it before, see the terminal protocols logic. // // Support for this seems to be ubiquitous - emacs enables it unconditionally (!) since 25.1 // (though it only supports it since then, it seems to be the last term to gain support). // // See http://thejh.net/misc/website-terminal-copy-paste. fn paste_start_buffering(&mut self) { self.get_input_data_mut().paste_buffer = Some(Vec::new()); } fn paste_is_buffering(&self) -> bool { self.get_input_data().paste_buffer.is_some() } fn paste_push_char(&mut self, b: u8) { self.get_input_data_mut() .paste_buffer .as_mut() .unwrap() .push(b) } fn paste_commit(&mut self) { self.get_input_data_mut().paste_buffer = None; } /// Enqueue a character or a readline function to the queue of unread characters that /// readch will return before actually reading from fd 0. fn push_back(&mut self, ch: CharEvent) { self.get_input_data_mut().queue.push_back(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. fn push_front(&mut self, ch: CharEvent) { self.get_input_data_mut().queue.push_front(ch); } /// Find the first sequence of non-char events, and promote them to the front. fn promote_interruptions_to_front(&mut self) { // 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. let queue = &mut self.get_input_data_mut().queue; let is_char = |evt: &CharEvent| { evt.is_char() || matches!(evt, CharEvent::Implicit(ImplicitEvent::Eof)) }; // Find the index of the first non-char event. // If there's none, we're done. let Some(first): Option = queue.iter().position(|e| !is_char(e)) else { return; }; let last = queue .range(first..) .position(is_char) .map_or(queue.len(), |x| x + first); // Move the non-char events to the front, retaining their order. let elems: Vec = queue.drain(first..last).collect(); for elem in elems.into_iter().rev() { queue.push_front(elem); } } /// Add multiple 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. That is, the first element in evts will be the first element returned. fn insert_front(&mut self, evts: I) where I: IntoIterator, I::IntoIter: DoubleEndedIterator, { let queue = &mut self.get_input_data_mut().queue; let iter = evts.into_iter().rev(); queue.reserve(iter.size_hint().0); for evt in iter { queue.push_front(evt); } } /// Forget all enqueued readline events in the front of the queue. fn drop_leading_readline_events(&mut self) { let queue = &mut self.get_input_data_mut().queue; while let Some(evt) = queue.front() { if evt.is_readline_or_command() { queue.pop_front(); } else { break; } } } fn blocking_query(&self) -> RefMut<'_, Option> { self.get_input_data().blocking_query.borrow_mut() } fn is_blocked_querying(&self) -> bool { self.blocking_query().is_some() } /// Override point for when we are about to (potentially) block in select(). The default does /// nothing. fn prepare_to_select(&mut self) {} /// Called when select() is interrupted by a signal. fn select_interrupted(&mut self) {} fn enqueue_interrupt_key(&mut self) { let vintr = shell_modes().c_cc[libc::VINTR]; if vintr != 0 { let interrupt_evt = CharEvent::from_key(KeyEvent::from_single_byte(vintr)); if stop_query(self.blocking_query()) { FLOG!( reader, "Received interrupt, giving up on waiting for terminal response" ); self.get_input_data_mut().queue.clear(); self.push_front(CharEvent::Implicit(ImplicitEvent::QueryInterrupted)); } else { self.push_front(interrupt_evt); } } } /// Override point for when when select() is interrupted by the universal variable notifier. /// The default does nothing. fn uvar_change_notified(&mut self) {} /// Override point for when the ioport is ready. /// The default does nothing. fn ioport_notified(&mut self) {} /// Reset the function status. fn get_function_status(&self) -> bool { self.get_input_data().function_status } /// Return if we have any lookahead. fn has_lookahead(&self) -> bool { !self.get_input_data().queue.is_empty() } } pub(crate) enum DecodeState { Incomplete, Complete, Error, } #[derive(Eq, PartialEq)] pub(crate) enum InvalidPolicy { Error, Passthrough, } pub(crate) fn decode_input_byte( out_seq: &mut WString, invalid_policy: InvalidPolicy, state: &mut mbstate_t, buffer: &[u8], consumed: &mut usize, ) -> DecodeState { use DecodeState::*; let mut res: char = '\0'; let read_byte = *buffer.last().unwrap(); if crate::libc::MB_CUR_MAX() == 1 { // single-byte locale, all values are legal // FIXME: this looks wrong, this falsely assumes that // the single-byte locale is compatible with Unicode upper-ASCII. res = read_byte.into(); out_seq.push(res); return Complete; } let mut invalid = |out_seq: &mut WString, log_error: fn()| match invalid_policy { InvalidPolicy::Error => { (log_error)(); Error } InvalidPolicy::Passthrough => { for &b in &buffer[*consumed..] { out_seq.push(encode_byte_to_char(b)); } *consumed = buffer.len(); Complete } }; let mut codepoint = u32::from(res); match unsafe { mbrtowc( std::ptr::addr_of_mut!(codepoint), std::ptr::addr_of!(read_byte).cast(), 1, state, ) } as isize { -1 => { return invalid(out_seq, || FLOG!(reader, "Illegal input encoding")); } -2 => { // Sequence not yet complete. return Incomplete; } _ => (), } if let Some(res) = char::from_u32(codepoint) { // Sequence complete. if !fish_reserved_codepoint(res) { *consumed += 1; out_seq.push(res); return Complete; } } invalid(out_seq, || FLOG!(reader, "Illegal codepoint")) } pub(crate) fn stop_query(mut query: RefMut<'_, Option>) -> bool { query.take().is_some() } fn invalid_sequence(buffer: &[u8]) -> Option { FLOG!( reader, "Error: invalid escape sequence: ", DisplayBytes(buffer) ); None } struct DisplayBytes<'a>(&'a [u8]); impl<'a> std::fmt::Display for DisplayBytes<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for (i, &c) in self.0.iter().enumerate() { if i != 0 { write!(f, " ")?; } write!(f, "{}", char_to_symbol(char::from(c), i == 0))?; } Ok(()) } } impl<'a> FloggableDisplay for DisplayBytes<'a> {} /// A simple, concrete implementation of InputEventQueuer. pub struct InputEventQueue { data: InputData, } impl InputEventQueue { pub fn new(in_fd: RawFd, blocking_query_timeout: Option) -> Self { Self { data: InputData::new(in_fd, blocking_query_timeout), } } } impl InputEventQueuer for InputEventQueue { fn get_input_data(&self) -> &InputData { &self.data } fn get_input_data_mut(&mut self) -> &mut InputData { &mut self.data } fn select_interrupted(&mut self) { if reader_test_and_clear_interrupted() != 0 { self.enqueue_interrupt_key(); } } } fn parse_hex(hex: &[u8]) -> Option> { if hex.len() % 2 != 0 { return None; } let mut result = vec![0; hex.len() / 2]; let mut i = 0; while i < hex.len() { let d1 = char::from(hex[i]).to_digit(16)?; let d2 = char::from(hex[i + 1]).to_digit(16)?; let decoded = u8::try_from(16 * d1 + d2).unwrap(); result[i / 2] = decoded; i += 2; } Some(result) } #[test] fn test_parse_hex() { assert_eq!(parse_hex(b"3d"), Some(vec![61])); }