Extended & human-friendly keys

See the changelog additions for user-visible changes.

Since we enable/disable terminal protocols whenever we pass terminal ownership,
tests can no longer run in parallel on the same terminal.

For the same reason, readline shortcuts in the gdb REPL will not work anymore.
As a remedy, use gdbserver, or lobby for CSI u support in libreadline.

Add sleep to some tests, otherwise they fall (both in CI and locally).

There are two weird failures on FreeBSD remaining, disable them for now
https://github.com/fish-shell/fish-shell/pull/10359/checks?check_run_id=23330096362

Design and implementation borrows heavily from Kakoune.

In future, we should try to implement more of the kitty progressive
enhancements.

Closes #10359
This commit is contained in:
Johannes Altmanninger
2024-03-30 16:10:12 +01:00
parent 8ada027f05
commit 8bf8b10f68
180 changed files with 2203 additions and 1304 deletions

View File

@@ -1,13 +1,25 @@
use crate::common::{fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked};
use libc::STDOUT_FILENO;
use once_cell::sync::OnceCell;
use crate::common::{
fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes, ScopeGuard,
ScopeGuarding,
};
use crate::env::{EnvStack, Environment};
use crate::fd_readable_set::FdReadableSet;
use crate::flog::FLOG;
use crate::reader::reader_current_data;
use crate::threads::{iothread_port, iothread_service_main};
use crate::key::{
self, alt, canonicalize_control_char, canonicalize_keyed_control_char, function_key, shift,
Key, Modifiers,
};
use crate::proc::is_interactive_session;
use crate::reader::{reader_current_data, reader_test_and_clear_interrupted};
use crate::threads::{iothread_port, iothread_service_main, MainThread};
use crate::universal_notifier::default_notifier;
use crate::wchar::{encode_byte_to_char, prelude::*};
use crate::wutil::encoding::{mbrtowc, zero_mbstate};
use crate::wutil::fish_wcstol;
use crate::wutil::encoding::{mbrtowc, mbstate_t, zero_mbstate};
use crate::wutil::{fish_wcstol, write_to_fd};
use std::cell::RefCell;
use std::collections::VecDeque;
use std::os::fd::RawFd;
use std::ptr;
@@ -111,6 +123,8 @@ pub enum ReadlineCmd {
EndUndoGroup,
RepeatJump,
DisableMouseTracking,
FocusIn,
FocusOut,
// ncurses uses the obvious name
ClearScreenAndRepaint,
// NOTE: This one has to be last.
@@ -121,7 +135,7 @@ pub enum ReadlineCmd {
#[derive(Debug, Clone)]
pub enum CharEventType {
/// A character was entered.
Char(char),
Char(Key),
/// A readline event.
Readline(ReadlineCmd),
@@ -147,9 +161,9 @@ pub struct ReadlineCmdEvent {
}
#[derive(Debug, Clone)]
pub struct PlainCharEvent {
pub struct KeyEvent {
// The key.
pub char: char,
pub key: Key,
// 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.
@@ -161,7 +175,7 @@ pub struct PlainCharEvent {
#[derive(Debug, Clone)]
pub enum CharEvent {
/// A character was entered.
Char(PlainCharEvent),
Key(KeyEvent),
/// A readline event.
Readline(ReadlineCmdEvent),
@@ -179,7 +193,7 @@ pub enum CharEvent {
impl CharEvent {
pub fn is_char(&self) -> bool {
matches!(self, CharEvent::Char(_))
matches!(self, CharEvent::Key(_))
}
pub fn is_eof(&self) -> bool {
@@ -198,6 +212,20 @@ 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<&KeyEvent> {
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");
@@ -213,19 +241,24 @@ pub fn get_command(&self) -> Option<&wstr> {
}
pub fn from_char(c: char) -> CharEvent {
Self::from_char_seq(c, WString::new())
Self::from_key(Key::from_raw(c))
}
pub fn get_char(&self) -> Option<char> {
match self {
CharEvent::Char(cevt) => Some(cevt.char),
_ => None,
}
pub fn from_key(key: Key) -> CharEvent {
Self::from_key_seq(key, WString::new())
}
pub fn from_key_seq(key: Key, seq: WString) -> CharEvent {
CharEvent::Key(KeyEvent {
key,
input_style: CharInputStyle::Normal,
seq,
})
}
pub fn from_char_seq(c: char, seq: WString) -> CharEvent {
CharEvent::Char(PlainCharEvent {
char: c,
CharEvent::Key(KeyEvent {
key: Key::from_raw(c),
input_style: CharInputStyle::Normal,
seq,
})
@@ -272,9 +305,11 @@ enum ReadbResult {
// Our ioport reported a change, so service main thread requests.
IOPortNotified,
NothingToRead,
}
fn readb(in_fd: RawFd) -> ReadbResult {
fn readb(in_fd: RawFd, blocking: bool) -> ReadbResult {
assert!(in_fd >= 0, "Invalid in fd");
let mut fdset = FdReadableSet::new();
loop {
@@ -293,7 +328,11 @@ fn readb(in_fd: RawFd) -> ReadbResult {
}
// Here's where we call select().
let select_res = fdset.check_readable(FdReadableSet::kNoTimeout);
let select_res = fdset.check_readable(if blocking {
FdReadableSet::kNoTimeout
} else {
0
});
if select_res < 0 {
let err = errno::errno().0;
if err == libc::EINTR || err == libc::EAGAIN {
@@ -305,12 +344,15 @@ fn readb(in_fd: RawFd) -> ReadbResult {
}
}
// 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 ReadbResult::UvarNotified;
if blocking {
// 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 ReadbResult::UvarNotified;
}
}
}
@@ -324,6 +366,9 @@ fn readb(in_fd: RawFd) -> ReadbResult {
// The common path is to return a u8.
return ReadbResult::Byte(arr[0]);
}
if !blocking {
return ReadbResult::NothingToRead;
}
// Check for iothread completions only if there is no data to be read from the stdin.
// This gives priority to the foreground.
@@ -383,6 +428,111 @@ pub fn update_wait_on_sequence_key_ms(vars: &EnvStack) {
}
}
pub static TERMINAL_PROTOCOLS: MainThread<RefCell<Option<TerminalProtocols>>> =
MainThread::new(RefCell::new(None));
pub fn terminal_protocols_enable() {
assert!(TERMINAL_PROTOCOLS.get().borrow().is_none());
TERMINAL_PROTOCOLS
.get()
.replace(Some(TerminalProtocols::new()));
}
pub fn terminal_protocols_disable() {
assert!(TERMINAL_PROTOCOLS.get().borrow().is_some());
TERMINAL_PROTOCOLS.get().replace(None);
}
pub fn terminal_protocols_enable_scoped() -> impl ScopeGuarding<Target = ()> {
terminal_protocols_enable();
ScopeGuard::new((), |()| terminal_protocols_disable())
}
pub fn terminal_protocols_disable_scoped() -> impl ScopeGuarding<Target = ()> {
terminal_protocols_disable();
ScopeGuard::new((), |()| {
// If a child is stopped, this will already be enabled.
if TERMINAL_PROTOCOLS.get().borrow().is_none() {
terminal_protocols_enable()
}
})
}
pub struct TerminalProtocols {}
impl TerminalProtocols {
fn new() -> Self {
terminal_protocols_enable_impl();
TerminalProtocols {}
}
}
impl Drop for TerminalProtocols {
fn drop(&mut self) {
terminal_protocols_disable_impl();
}
}
fn terminal_protocols_enable_impl() {
// Interactive or inside builtin read.
assert!(is_interactive_session() || reader_current_data().is_some());
let mut sequences = concat!(
"\x1b[?2004h", // Bracketed paste
"\x1b[>4;1m", // XTerm's modifyOtherKeys
"\x1b[>5u", // CSI u with kitty progressive enhancement
"\x1b=", // set application keypad mode, so the keypad keys send unique codes
"\x1b[?1004h", // enable focus notify
);
// Note: Don't call this initially because, even though we're in a fish_prompt event,
// tmux reacts sooo quickly that we'll still get a sequence before we're prepared for it.
// So this means that we won't get focus events until you've run at least one command,
// but that's preferable to always seeing "^[[I" when starting fish.
static FIRST_CALL_HACK: OnceCell<()> = OnceCell::new();
if FIRST_CALL_HACK.get().is_none() {
sequences = sequences.strip_suffix("\x1b[?1004h").unwrap();
}
FIRST_CALL_HACK.get_or_init(|| ());
FLOG!(
term_protocols,
format!(
"Enabling extended keys, bracketed paste and focus reporting: {:?}",
sequences
)
);
let _ = write_to_fd(sequences.as_bytes(), STDOUT_FILENO);
}
fn terminal_protocols_disable_impl() {
// Interactive or inside builtin read.
assert!(is_interactive_session() || reader_current_data().is_some());
let sequences = concat!(
"\x1b[?2004l",
"\x1b[>4;0m",
"\x1b[<1u", // Konsole breaks unless we pass an explicit number of entries to pop.
"\x1b>",
"\x1b[?1004l",
);
FLOG!(
term_protocols,
format!(
"Disabling extended keys, bracketed paste and focus reporting: {:?}",
sequences
)
);
let _ = write_to_fd(sequences.as_bytes(), STDOUT_FILENO);
}
fn parse_mask(mask: u32) -> Modifiers {
Modifiers {
ctrl: (mask & 4) != 0,
alt: (mask & 2) != 0,
shift: (mask & 1) != 0,
}
}
/// 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 {
@@ -395,10 +545,7 @@ fn try_pop(&mut self) -> Option<CharEvent> {
/// 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 {
let mut res: char = '\0';
let mut state = zero_mbstate();
let mut bytes = [0; 64 * 16];
let mut num_bytes = 0;
loop {
// Do we have something enqueued already?
// Note this may be initially true, or it may become true through calls to
@@ -413,14 +560,13 @@ fn readch(&mut self) -> CharEvent {
return mevt;
}
let rr = readb(self.get_in_fd());
let rr = readb(self.get_in_fd(), /*blocking=*/ true);
match rr {
ReadbResult::Eof => {
return CharEvent::Eof;
}
ReadbResult::Interrupted => {
// FIXME: here signals may break multibyte sequences.
self.select_interrupted();
}
@@ -433,58 +579,405 @@ fn readch(&mut self) -> CharEvent {
}
ReadbResult::Byte(read_byte) => {
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();
return CharEvent::from_char(res);
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)
};
if self.paste_is_buffering() {
if read_byte != 0x1b {
self.paste_push_char(read_byte);
}
continue;
}
let mut codepoint = u32::from(res);
let sz = unsafe {
mbrtowc(
std::ptr::addr_of_mut!(codepoint).cast(),
std::ptr::addr_of!(read_byte).cast(),
1,
let mut seq = WString::new();
let mut key = key_with_escape;
let mut consumed = 0;
for i in 0..buffer.len() {
self.parse_codepoint(
&mut state,
)
} as isize;
match sz {
-1 => {
FLOG!(reader, "Illegal input");
return CharEvent::from_check_exit();
}
-2 => {
// Sequence not yet complete.
bytes[num_bytes] = read_byte;
num_bytes += 1;
&mut key,
&mut seq,
&buffer,
i,
&mut consumed,
&mut have_escape_prefix,
);
}
return if let Some(key) = key {
CharEvent::from_key_seq(key, seq)
} else {
self.insert_front(seq.chars().skip(1).map(CharEvent::from_char));
let Some(c) = seq.chars().next() else {
continue;
}
0 => {
// Actual nul char.
return CharEvent::from_char('\0');
}
_ => (),
}
if let Some(res) = char::from_u32(codepoint) {
// Sequence complete.
if !fish_reserved_codepoint(res) {
return CharEvent::from_char(res);
}
}
bytes[num_bytes] = read_byte;
num_bytes += 1;
for &b in &bytes[1..num_bytes] {
let c = CharEvent::from_char(encode_byte_to_char(b));
self.push_back(c);
}
let res = CharEvent::from_char(encode_byte_to_char(bytes[0]));
return res;
};
CharEvent::from_key_seq(Key::from_raw(c), seq)
};
}
ReadbResult::NothingToRead => unreachable!(),
}
}
}
fn try_readb(&mut self, buffer: &mut Vec<u8>) -> Option<u8> {
let ReadbResult::Byte(next) = readb(self.get_in_fd(), /*blocking=*/ false) else {
return None;
};
buffer.push(next);
Some(next)
}
fn parse_escape_sequence(
&mut self,
buffer: &mut Vec<u8>,
have_escape_prefix: &mut bool,
) -> Option<Key> {
let Some(next) = self.try_readb(buffer) else {
if !self.paste_is_buffering() {
return Some(Key {
modifiers: Modifiers::default(),
codepoint: key::Escape,
});
}
return None;
};
if next == b'[' {
// potential CSI
return Some(self.parse_csi(buffer).unwrap_or(alt('[')));
}
if next == b'O' {
// potential SS3
return Some(self.parse_ss3(buffer).unwrap_or(alt('O')));
}
match canonicalize_control_char(next) {
Some(mut key) => {
key.modifiers.alt = true;
Some(key)
}
None => {
*have_escape_prefix = true;
None
}
}
}
fn parse_codepoint(
&mut self,
state: &mut mbstate_t,
out_key: &mut Option<Key>,
out_seq: &mut WString,
buffer: &[u8],
i: usize,
consumed: &mut usize,
have_escape_prefix: &mut bool,
) {
let mut res: char = '\0';
let read_byte = buffer[i];
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;
}
let mut codepoint = u32::from(res);
let sz = unsafe {
mbrtowc(
std::ptr::addr_of_mut!(codepoint).cast(),
std::ptr::addr_of!(read_byte).cast(),
1,
state,
)
} as isize;
match sz {
-1 => {
FLOG!(reader, "Illegal input");
*consumed += 1;
self.push_front(CharEvent::from_check_exit());
return;
}
-2 => {
// Sequence not yet complete.
return;
}
0 => {
// Actual nul char.
*consumed += 1;
out_seq.push('\0');
return;
}
_ => (),
}
if let Some(res) = char::from_u32(codepoint) {
// Sequence complete.
if !fish_reserved_codepoint(res) {
if *have_escape_prefix && i != 0 {
*have_escape_prefix = false;
*out_key = Some(alt(res));
}
*consumed += 1;
out_seq.push(res);
return;
}
}
for &b in &buffer[*consumed..i] {
out_seq.push(encode_byte_to_char(b));
*consumed += 1;
}
}
fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<Key> {
let mut next_char = |zelf: &mut Self| zelf.try_readb(buffer).unwrap_or(0xff);
let mut params = [[0_u32; 16]; 4];
let mut c = next_char(self);
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 && c >= 0x30 && c <= 0x3f {
if c.is_ascii_digit() {
params[count][subcount] = params[count][subcount] * 10 + u32::from(c - b'0');
} else if c == b':' && subcount < 3 {
subcount += 1;
} else if c == b';' {
count += 1;
subcount = 0;
} else {
return None;
}
c = next_char(self);
}
if c != b'$' && !(0x40..=0x7e).contains(&c) {
return None;
}
let masked_key = |mut codepoint, shifted_codepoint| {
let mask = params[1][0].saturating_sub(1);
let mut modifiers = parse_mask(mask);
if let Some(shifted_codepoint) = shifted_codepoint {
if shifted_codepoint != '\0' && modifiers.shift {
modifiers.shift = false;
codepoint = shifted_codepoint;
}
}
Key {
modifiers,
codepoint,
}
};
let key = match c {
b'$' => {
if private_mode == Some(b'?') && next_char(self) == b'y' {
// DECRPM
return None;
}
match params[0][0] {
23 | 24 => shift(
char::from_u32(u32::from(function_key(11)) + params[0][0] - 23).unwrap(), // rxvt style
),
_ => return None,
}
}
b'A' => masked_key(key::Up, None),
b'B' => masked_key(key::Down, None),
b'C' => masked_key(key::Right, None),
b'D' => masked_key(key::Left, None),
b'E' => masked_key('5', None), // Numeric keypad
b'F' => masked_key(key::End, None), // PC/xterm style
b'H' => masked_key(key::Home, None), // PC/xterm style
b'M' | b'm' => {
self.disable_mouse_tracking();
let sgr = private_mode == Some(b'<');
if !sgr && c == b'm' {
return None;
}
// 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.
if sgr {
return None;
}
// Generic X10 or modified VT200 sequence. It doesn't matter which, they're both 6
// chars (although in mode 1005, the characters may be unicode and not necessarily
// just one byte long) reporting the button that was clicked and its location.
let _ = next_char(self);
let _ = next_char(self);
let _ = next_char(self);
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..7 {
let _ = next_char(self);
}
return None;
}
b'P' => masked_key(function_key(1), None),
b'Q' => masked_key(function_key(2), None),
b'R' => masked_key(function_key(3), None),
b'S' => masked_key(function_key(4), None),
b'~' => match params[0][0] {
1 => masked_key(key::Home, None), // VT220/tmux style
2 => masked_key(key::Insert, None),
3 => masked_key(key::Delete, None),
4 => masked_key(key::End, None), // VT220/tmux style
5 => masked_key(key::PageUp, None),
6 => masked_key(key::PageDown, None),
7 => masked_key(key::Home, None), // rxvt style
8 => masked_key(key::End, None), // rxvt style
11..=15 => masked_key(
char::from_u32(u32::from(function_key(1)) + params[0][0] - 11).unwrap(),
None,
),
17..=21 => masked_key(
char::from_u32(u32::from(function_key(6)) + params[0][0] - 17).unwrap(),
None,
),
23 | 24 => masked_key(
char::from_u32(u32::from(function_key(11)) + params[0][0] - 23).unwrap(),
None,
),
25 | 26 => {
shift(char::from_u32(u32::from(function_key(3)) + params[0][0] - 25).unwrap())
} // rxvt style
28 | 29 => {
shift(char::from_u32(u32::from(function_key(5)) + params[0][0] - 28).unwrap())
} // rxvt style
31 | 32 => {
shift(char::from_u32(u32::from(function_key(7)) + params[0][0] - 31).unwrap())
} // rxvt style
33 | 34 => {
shift(char::from_u32(u32::from(function_key(9)) + params[0][0] - 33).unwrap())
} // rxvt style
200 => {
self.paste_start_buffering();
self.push_front(CharEvent::from_readline(ReadlineCmd::BeginUndoGroup));
return Some(Key::from_raw(key::Invalid));
}
201 => {
self.push_front(CharEvent::from_readline(ReadlineCmd::EndUndoGroup));
self.paste_commit();
return Some(Key::from_raw(key::Invalid));
}
_ => return None,
},
b'u' => {
// Treat numpad keys the same as their non-numpad counterparts. Could add a numpad modifier here.
let key = match params[0][0] {
57414 => key::Enter,
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 => canonicalize_keyed_control_char(char::from_u32(cp).unwrap()),
};
masked_key(
key,
Some(canonicalize_keyed_control_char(
char::from_u32(params[0][1]).unwrap(),
)),
)
}
b'Z' => shift(key::Tab),
b'I' => {
self.push_front(CharEvent::from_readline(ReadlineCmd::FocusIn));
return Some(Key::from_raw(key::Invalid));
}
b'O' => {
self.push_front(CharEvent::from_readline(ReadlineCmd::FocusOut));
return Some(Key::from_raw(key::Invalid));
}
_ => 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.
//
// Since this is only called when we detect an incoming mouse reporting payload, we know the
// terminal emulator supports mouse reporting, so no terminfo checks.
FLOG!(reader, "Disabling mouse tracking");
// We shouldn't directly manipulate stdout from here, so we ask the reader to do it.
// writembs(outputter_t::stdoutput(), "\x1B[?1000l");
self.push_front(CharEvent::from_readline(ReadlineCmd::DisableMouseTracking));
}
fn parse_ss3(&mut self, buffer: &mut Vec<u8>) -> Option<Key> {
let mut raw_mask = 0;
let mut code = b'0';
loop {
raw_mask = raw_mask * 10 + u32::from(code - b'0');
code = self.try_readb(buffer).unwrap_or(0xff);
if !(b'0'..=b'9').contains(&code) {
break;
}
}
let modifiers = parse_mask(raw_mask.saturating_sub(1));
#[rustfmt::skip]
let key = match code {
b' ' => Key{modifiers, codepoint: key::Space},
b'A' => Key{modifiers, codepoint: key::Up},
b'B' => Key{modifiers, codepoint: key::Down},
b'C' => Key{modifiers, codepoint: key::Right},
b'D' => Key{modifiers, codepoint: key::Left},
b'F' => Key{modifiers, codepoint: key::End},
b'H' => Key{modifiers, codepoint: key::Home},
b'I' => Key{modifiers, codepoint: key::Tab},
b'M' => Key{modifiers, codepoint: key::Enter},
b'P' => Key{modifiers, codepoint: function_key(1)},
b'Q' => Key{modifiers, codepoint: function_key(2)},
b'R' => Key{modifiers, codepoint: function_key(3)},
b'S' => Key{modifiers, codepoint: function_key(4)},
b'X' => Key{modifiers, codepoint: '='},
b'j' => Key{modifiers, codepoint: '*'},
b'k' => Key{modifiers, codepoint: '+'},
b'l' => Key{modifiers, codepoint: ','},
b'm' => Key{modifiers, codepoint: '-'},
b'n' => Key{modifiers, codepoint: '.'},
b'o' => Key{modifiers, codepoint: '/'},
b'p' => Key{modifiers, codepoint: '0'},
b'q' => Key{modifiers, codepoint: '1'},
b'r' => Key{modifiers, codepoint: '2'},
b's' => Key{modifiers, codepoint: '3'},
b't' => Key{modifiers, codepoint: '4'},
b'u' => Key{modifiers, codepoint: '5'},
b'v' => Key{modifiers, codepoint: '6'},
b'w' => Key{modifiers, codepoint: '7'},
b'x' => Key{modifiers, codepoint: '8'},
b'y' => Key{modifiers, codepoint: '9'},
_ => return None,
};
Some(key)
}
fn readch_timed_esc(&mut self) -> Option<CharEvent> {
self.readch_timed(WAIT_ON_ESCAPE_MS.load(Ordering::Relaxed))
}
@@ -556,6 +1049,24 @@ fn readch_timed(&mut self, wait_time_ms: usize) -> Option<CharEvent> {
/// Return the fd corresponding to stdin.
fn get_in_fd(&self) -> RawFd;
// 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);
fn paste_is_buffering(&self) -> bool;
fn paste_push_char(&mut self, _b: u8) {}
fn paste_commit(&mut self);
/// 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) {
@@ -622,8 +1133,8 @@ fn drop_leading_readline_events(&mut self) {
/// nothing.
fn prepare_to_select(&mut self) {}
/// Override point for when when select() is interrupted by a signal. The default does nothing.
fn select_interrupted(&mut self) {}
/// Called when select() is interrupted by a signal.
fn select_interrupted(&mut self);
/// Override point for when when select() is interrupted by the universal variable notifier.
/// The default does nothing.
@@ -639,6 +1150,7 @@ fn has_lookahead(&self) -> bool {
pub struct InputEventQueue {
queue: VecDeque<CharEvent>,
in_fd: RawFd,
is_in_bracketed_paste: bool,
}
impl InputEventQueue {
@@ -646,6 +1158,7 @@ pub fn new(in_fd: RawFd) -> InputEventQueue {
InputEventQueue {
queue: VecDeque::new(),
in_fd,
is_in_bracketed_paste: false,
}
}
}
@@ -662,4 +1175,23 @@ fn get_queue_mut(&mut self) -> &mut VecDeque<CharEvent> {
fn get_in_fd(&self) -> RawFd {
self.in_fd
}
fn select_interrupted(&mut self) {
if reader_test_and_clear_interrupted() != 0 {
let vintr = shell_modes().c_cc[libc::VINTR];
if vintr != 0 {
self.push_front(CharEvent::from_key(Key::from_single_byte(vintr)));
}
}
}
fn paste_start_buffering(&mut self) {
self.is_in_bracketed_paste = true;
}
fn paste_is_buffering(&self) -> bool {
self.is_in_bracketed_paste
}
fn paste_commit(&mut self) {
self.is_in_bracketed_paste = false;
}
}