mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-04-30 09:31:15 -03:00
As reported in https://github.com/fish-shell/fish-shell/discussions/11868, some terminals advertise support for the kitty keyboard protocol despite it not necessarily being enabled. We use this flag in30ff3710a0(Increase timeout when reading escape sequences inside paste/kitty kbd, 2025-07-24), to support the AutoHotKey scenario on terminals that support the kitty keyboard protocols. Let's move towards the more comprehensive fix mentioned inabd23d2a1b(Increase escape sequence timeout while waiting for query response, 2025-09-30), i.e. only apply a low timeout when necessary to actually distinguish legacy escape. Let's pick 30ms for now (which has been used successfully for similar things historically, see30ff3710a0); a higher timeout let alone a warning on incomplete sequence seems risky for a patch relase, and it's also not 100% clear if this is actually a degraded state because in theory the user might legitimately type "escape [ 1" (while kitty keyboard protocol is turned off, e.g. before the shell regains control). This obsoletes and hence reverts commit623c14aed0(Kitty keyboard protocol is non-functional on old versions of Zellij, 2025-10-04). (cherry picked from commit6accc475c9)
1827 lines
63 KiB
Rust
1827 lines
63 KiB
Rust
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<char>,
|
|
base_layout_key: Option<char>,
|
|
) -> 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<char> {
|
|
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<Key> 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<Key> {
|
|
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<KeyMatchQuality> {
|
|
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<u8> {
|
|
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<CharEvent>,
|
|
|
|
// The current paste buffer, if any.
|
|
pub paste_buffer: Option<Vec<u8>>,
|
|
|
|
// The arguments to the most recently invoked input function.
|
|
pub input_function_args: Vec<char>,
|
|
|
|
// The return status of the most recently invoked input function.
|
|
pub function_status: bool,
|
|
|
|
// Transient storage to avoid repeated allocations.
|
|
pub event_storage: Vec<CharEvent>,
|
|
|
|
// How long to wait for responses for TTY queries.
|
|
pub blocking_query_timeout: Option<Duration>,
|
|
|
|
// If set, events will be buffered until the query finishes.
|
|
pub blocking_query: RefCell<Option<TerminalQuery>>,
|
|
}
|
|
|
|
impl InputData {
|
|
/// Construct from the fd from which to read.
|
|
pub fn new(in_fd: RawFd, blocking_query_timeout: Option<Duration>) -> 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<ViewportPosition>,
|
|
}
|
|
|
|
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<CharEvent> {
|
|
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<u8>) -> Option<u8> {
|
|
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<u8>,
|
|
have_escape_prefix: &mut bool,
|
|
) -> Option<KeyEvent> {
|
|
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<u8>) -> Option<KeyEvent> {
|
|
// 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<char>, base_layout_key: Option<char>| {
|
|
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<u8>) -> Option<KeyEvent> {
|
|
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<u8>) -> 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<u8>) -> 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<u8>) -> Option<KeyEvent> {
|
|
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<CharEvent> {
|
|
self.readch_timed(WAIT_ON_ESCAPE_MS.load(Ordering::Relaxed))
|
|
}
|
|
|
|
fn readch_timed_sequence_key(&mut self) -> Option<CharEvent> {
|
|
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<CharEvent> {
|
|
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<usize> = 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<CharEvent> = 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<I>(&mut self, evts: I)
|
|
where
|
|
I: IntoIterator<Item = CharEvent>,
|
|
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<TerminalQuery>> {
|
|
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<TerminalQuery>>) -> bool {
|
|
query.take().is_some()
|
|
}
|
|
|
|
fn invalid_sequence(buffer: &[u8]) -> Option<KeyEvent> {
|
|
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<Duration>) -> 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<Vec<u8>> {
|
|
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]));
|
|
}
|