diff --git a/examples/example.rs b/examples/example.rs index 7b3ccc86b..f937b8f7c 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -113,11 +113,11 @@ fn main() -> rustyline::Result<()> { println!("Line: {}", line); } Err(ReadlineError::Interrupted) => { - println!("CTRL-C"); + println!("Interrupted"); break; } Err(ReadlineError::Eof) => { - println!("CTRL-D"); + println!("Encountered Eof"); break; } Err(err) => { diff --git a/src/error.rs b/src/error.rs index b4d365f5d..3ef771a43 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,9 +14,9 @@ use std::io; pub enum ReadlineError { /// I/O Error Io(io::Error), - /// EOF (Ctrl-D) + /// EOF (VEOF / Ctrl-D) Eof, - /// Ctrl-C + /// Interrupt signal (VINTR / VQUIT / Ctrl-C) Interrupted, /// Chars Error #[cfg(unix)] diff --git a/src/keymap.rs b/src/keymap.rs index a2eba2546..428b0dfe9 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -434,6 +434,7 @@ impl InputState { } } + /// Application customized binding fn custom_binding( &self, wrt: &mut dyn Refresher, @@ -456,6 +457,20 @@ impl InputState { } } + /// Terminal peculiar binding + fn term_binding( + rdr: &mut R, + wrt: &mut dyn Refresher, + key: &KeyEvent, + ) -> Option { + let cmd = rdr.find_binding(key); + if cmd == Some(Cmd::EndOfFile) && !wrt.line().is_empty() { + None // ReadlineError::Eof only if line is empty + } else { + cmd + } + } + fn custom_seq_binding( &self, rdr: &mut R, @@ -550,6 +565,8 @@ impl InputState { } else { cmd }); + } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) { + return Ok(cmd); } let cmd = match key { E(K::Char(c), M::NONE) => { @@ -725,6 +742,8 @@ impl InputState { } else { cmd }); + } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) { + return Ok(cmd); } let cmd = match key { E(K::Char('$'), M::NONE) | E(K::End, M::NONE) => Cmd::Move(Movement::EndOfLine), @@ -896,6 +915,8 @@ impl InputState { } else { cmd }); + } else if let Some(cmd) = InputState::term_binding(rdr, wrt, &key) { + return Ok(cmd); } let cmd = match key { E(K::Char(c), M::NONE) => { @@ -1037,6 +1058,7 @@ impl InputState { } else { Movement::ForwardChar(n) }), + #[cfg(any(windows, test))] E(K::Char('C'), M::CTRL) => Cmd::Interrupt, E(K::Char('D'), M::CTRL) => { if self.is_emacs_mode() && !wrt.line().is_empty() { @@ -1045,8 +1067,10 @@ impl InputState { } else { Movement::BackwardChar(n) }) - } else { + } else if cfg!(window) || cfg!(test) || !wrt.line().is_empty() { Cmd::EndOfFile + } else { + Cmd::Unknown } } E(K::Delete, M::NONE) => Cmd::Kill(if positive { @@ -1092,7 +1116,6 @@ impl InputState { Cmd::Unknown // TODO Validate } } - E(K::Char('Z'), M::CTRL) => Cmd::Suspend, E(K::Char('_'), M::CTRL) => Cmd::Undo(n), E(K::UnknownEscSeq, M::NONE) => Cmd::Noop, E(K::BracketedPasteStart, M::NONE) => { diff --git a/src/lib.rs b/src/lib.rs index 44ebbff63..a19bf542e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -443,6 +443,7 @@ fn readline_edit( initial: Option<(&str, &str)>, editor: &mut Editor, original_mode: &tty::Mode, + term_key_map: tty::KeyMap, ) -> Result { let mut stdout = editor.term.create_writer(); @@ -460,7 +461,7 @@ fn readline_edit( .update((left.to_owned() + right).as_ref(), left.len()); } - let mut rdr = editor.term.create_reader(&editor.config)?; + let mut rdr = editor.term.create_reader(&editor.config, term_key_map)?; if editor.term.is_output_tty() && editor.config.check_cursor_position() { if let Err(e) = s.move_cursor_at_leftmost(&mut rdr) { if s.out.sigwinch() { @@ -569,9 +570,9 @@ fn readline_raw( initial: Option<(&str, &str)>, editor: &mut Editor, ) -> Result { - let original_mode = editor.term.enable_raw_mode()?; + let (original_mode, term_key_map) = editor.term.enable_raw_mode()?; let guard = Guard(&original_mode); - let user_input = readline_edit(prompt, initial, editor, &original_mode); + let user_input = readline_edit(prompt, initial, editor, &original_mode, term_key_map); if editor.config.auto_add_history() { if let Ok(ref line) = user_input { editor.add_history_entry(line.as_str()); diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 3b511bb84..96bcf8f84 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -7,7 +7,7 @@ use crate::highlight::Highlighter; use crate::keys::KeyEvent; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::Result; +use crate::{Cmd, Result}; /// Terminal state pub trait RawMode: Sized { @@ -24,6 +24,8 @@ pub trait RawReader { fn next_char(&mut self) -> Result; /// Bracketed paste fn read_pasted_text(&mut self) -> Result; + /// Check if `key` is bound to a peculiar command + fn find_binding(&self, key: &KeyEvent) -> Option; } /// Display prompt, line and cursor in terminal output @@ -199,6 +201,7 @@ fn width(s: &str, esc_seq: &mut u8) -> usize { /// Terminal contract pub trait Term { + type KeyMap; type Reader: RawReader; // rl_instream type Writer: Renderer; // rl_outstream type Mode: RawMode; @@ -218,9 +221,9 @@ pub trait Term { /// check if output stream is connected to a terminal. fn is_output_tty(&self) -> bool; /// Enable RAW mode for the terminal. - fn enable_raw_mode(&mut self) -> Result; + fn enable_raw_mode(&mut self) -> Result<(Self::Mode, Self::KeyMap)>; /// Create a RAW reader - fn create_reader(&self, config: &Config) -> Result; + fn create_reader(&self, config: &Config, key_map: Self::KeyMap) -> Result; /// Create a writer fn create_writer(&self) -> Self::Writer; } diff --git a/src/tty/test.rs b/src/tty/test.rs index d8bed9111..0b362f4dd 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -10,8 +10,9 @@ use crate::highlight::Highlighter; use crate::keys::KeyEvent; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::Result; +use crate::{Cmd, Result}; +pub type KeyMap = (); pub type Mode = (); impl RawMode for Mode { @@ -36,6 +37,10 @@ impl<'a> RawReader for Iter<'a, KeyEvent> { fn read_pasted_text(&mut self) -> Result { unimplemented!() } + + fn find_binding(&self, _: &KeyEvent) -> Option { + None + } } impl RawReader for IntoIter { @@ -59,6 +64,10 @@ impl RawReader for IntoIter { fn read_pasted_text(&mut self) -> Result { unimplemented!() } + + fn find_binding(&self, _: &KeyEvent) -> Option { + None + } } pub struct Sink {} @@ -140,6 +149,7 @@ pub struct DummyTerminal { } impl Term for DummyTerminal { + type KeyMap = KeyMap; type Mode = Mode; type Reader = IntoIter; type Writer = Sink; @@ -181,11 +191,11 @@ impl Term for DummyTerminal { // Interactive loop: - fn enable_raw_mode(&mut self) -> Result { - Ok(()) + fn enable_raw_mode(&mut self) -> Result<(Mode, KeyMap)> { + Ok(((), ())) } - fn create_reader(&self, _: &Config) -> Result> { + fn create_reader(&self, _: &Config, _: KeyMap) -> Result { Ok(self.keys.clone().into_iter()) } diff --git a/src/tty/unix.rs b/src/tty/unix.rs index b86a5ce99..e1c31ae78 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -9,18 +9,18 @@ use log::{debug, warn}; use nix::poll::{self, PollFlags}; use nix::sys::signal; use nix::sys::termios; -use nix::sys::termios::SetArg; +use nix::sys::termios::{SetArg, SpecialCharacterIndices as SCI, Termios}; use unicode_segmentation::UnicodeSegmentation; use utf8parse::{Parser, Receiver}; use super::{width, RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; -use crate::error; use crate::highlight::Highlighter; use crate::keys::{KeyCode as K, KeyEvent, KeyEvent as E, Modifiers as M}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::Result; +use crate::{error, Cmd, Result}; +use std::collections::HashMap; const STDIN_FILENO: RawFd = libc::STDIN_FILENO; @@ -95,6 +95,10 @@ fn is_a_tty(fd: RawFd) -> bool { unsafe { libc::isatty(fd) != 0 } } +pub type PosixKeyMap = HashMap; +#[cfg(not(test))] +pub type KeyMap = PosixKeyMap; + #[must_use = "You must restore default mode (disable_raw_mode)"] pub struct PosixMode { termios: termios::Termios, @@ -150,6 +154,7 @@ pub struct PosixRawReader { buf: [u8; 1], parser: Parser, receiver: Utf8, + key_map: PosixKeyMap, } struct Utf8 { @@ -184,7 +189,7 @@ const RXVT_CTRL: char = '\x1e'; const RXVT_CTRL_SHIFT: char = '@'; impl PosixRawReader { - fn new(config: &Config) -> Self { + fn new(config: &Config, key_map: PosixKeyMap) -> Self { Self { stdin: StdinRaw {}, timeout_ms: config.keyseq_timeout(), @@ -194,6 +199,7 @@ impl PosixRawReader { c: None, valid: true, }, + key_map, } } @@ -716,6 +722,14 @@ impl RawReader for PosixRawReader { let buffer = buffer.replace("\r", "\n"); Ok(buffer) } + + fn find_binding(&self, key: &KeyEvent) -> Option { + let cmd = self.key_map.get(key).cloned(); + if let Some(ref cmd) = cmd { + debug!(target: "rustyline", "terminal key binding: {:?} => {:?}", key, cmd); + } + cmd + } } impl Receiver for Utf8 { @@ -1029,6 +1043,13 @@ extern "C" fn sigwinch_handler(_: libc::c_int) { debug!(target: "rustyline", "SIGWINCH"); } +fn map_key(key_map: &mut HashMap, raw: &Termios, index: SCI, name: &str, cmd: Cmd) { + let cc = char::from(raw.control_chars[index as usize]); + let key = KeyEvent::new(cc, M::NONE); + debug!(target: "rustyline", "{}: {:?}", name, key); + key_map.insert(key, cmd); +} + #[cfg(not(test))] pub type Terminal = PosixTerminal; @@ -1055,6 +1076,7 @@ impl PosixTerminal { } impl Term for PosixTerminal { + type KeyMap = PosixKeyMap; type Mode = PosixMode; type Reader = PosixRawReader; type Writer = PosixRenderer; @@ -1101,9 +1123,9 @@ impl Term for PosixTerminal { // Interactive loop: - fn enable_raw_mode(&mut self) -> Result { + fn enable_raw_mode(&mut self) -> Result<(Self::Mode, PosixKeyMap)> { use nix::errno::Errno::ENOTTY; - use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, SpecialCharacterIndices}; + use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags}; if !self.stdin_isatty { return Err(nix::Error::from_errno(ENOTTY).into()); } @@ -1125,8 +1147,15 @@ impl Term for PosixTerminal { // disable echoing, canonical mode, extended input processing and signals raw.local_flags &= !(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG); - raw.control_chars[SpecialCharacterIndices::VMIN as usize] = 1; // One character-at-a-time input - raw.control_chars[SpecialCharacterIndices::VTIME as usize] = 0; // with blocking read + raw.control_chars[SCI::VMIN as usize] = 1; // One character-at-a-time input + raw.control_chars[SCI::VTIME as usize] = 0; // with blocking read + + let mut key_map: HashMap = HashMap::with_capacity(3); + map_key(&mut key_map, &raw, SCI::VEOF, "VEOF", Cmd::EndOfFile); + map_key(&mut key_map, &raw, SCI::VINTR, "VINTR", Cmd::Interrupt); + map_key(&mut key_map, &raw, SCI::VQUIT, "VQUIT", Cmd::Interrupt); + map_key(&mut key_map, &raw, SCI::VSUSP, "VSUSP", Cmd::Suspend); + termios::tcsetattr(STDIN_FILENO, SetArg::TCSADRAIN, &raw)?; // enable bracketed paste @@ -1138,15 +1167,18 @@ impl Term for PosixTerminal { } else { Some(self.stream_type) }; - Ok(PosixMode { - termios: original_mode, - out, - }) + Ok(( + PosixMode { + termios: original_mode, + out, + }, + key_map, + )) } /// Create a RAW reader - fn create_reader(&self, config: &Config) -> Result { - Ok(PosixRawReader::new(config)) + fn create_reader(&self, config: &Config, key_map: PosixKeyMap) -> Result { + Ok(PosixRawReader::new(config, key_map)) } fn create_writer(&self) -> PosixRenderer { diff --git a/src/tty/windows.rs b/src/tty/windows.rs index 16d78b5da..c2933d54b 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -17,12 +17,11 @@ use winapi::um::{consoleapi, processenv, winbase, winuser}; use super::{width, RawMode, RawReader, Renderer, Term}; use crate::config::{BellStyle, ColorMode, Config, OutputStreamType}; -use crate::error; use crate::highlight::Highlighter; use crate::keys::{KeyCode as K, KeyEvent, Modifiers as M}; use crate::layout::{Layout, Position}; use crate::line_buffer::LineBuffer; -use crate::Result; +use crate::{error, Cmd, Result}; const STDIN_FILENO: DWORD = winbase::STD_INPUT_HANDLE; const STDOUT_FILENO: DWORD = winbase::STD_OUTPUT_HANDLE; @@ -66,6 +65,10 @@ fn get_console_mode(handle: HANDLE) -> Result { Ok(original_mode) } +type ConsoleKeyMap = (); +#[cfg(not(test))] +pub type KeyMap = ConsoleKeyMap; + #[must_use = "You must restore default mode (disable_raw_mode)"] #[cfg(not(test))] pub type Mode = ConsoleMode; @@ -211,6 +214,10 @@ impl RawReader for ConsoleRawReader { fn read_pasted_text(&mut self) -> Result { Ok(clipboard_win::get_clipboard_string()?) } + + fn find_binding(&self, _: &KeyEvent) -> Option { + None + } } pub struct ConsoleRenderer { @@ -523,6 +530,7 @@ impl Console { } impl Term for Console { + type KeyMap = ConsoleKeyMap; type Mode = ConsoleMode; type Reader = ConsoleRawReader; type Writer = ConsoleRenderer; @@ -587,7 +595,7 @@ impl Term for Console { // } /// Enable RAW mode for the terminal. - fn enable_raw_mode(&mut self) -> Result { + fn enable_raw_mode(&mut self) -> Result<(ConsoleMode, ConsoleKeyMap)> { if !self.stdin_isatty { Err(io::Error::new( io::ErrorKind::Other, @@ -642,15 +650,18 @@ impl Term for Console { None }; - Ok(ConsoleMode { - original_stdin_mode, - stdin_handle: self.stdin_handle, - original_stdstream_mode, - stdstream_handle: self.stdstream_handle, - }) + Ok(( + ConsoleMode { + original_stdin_mode, + stdin_handle: self.stdin_handle, + original_stdstream_mode, + stdstream_handle: self.stdstream_handle, + }, + (), + )) } - fn create_reader(&self, _: &Config) -> Result { + fn create_reader(&self, _: &Config, _: ConsoleKeyMap) -> Result { ConsoleRawReader::create() }