diff --git a/Cargo.toml b/Cargo.toml index 69996f00a30d1c987196f812338238e9939fa5b7..bb21214d86795abc66905baa6076b044d0be9dd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,13 @@ license = "MIT" libc = "0.2.7" unicode-width = "0.1.3" encode_unicode = "0.1.3" -clippy = {version = "~0.0.58", optional = true} [target.'cfg(unix)'.dependencies] nix = "0.5.0" [target.'cfg(windows)'.dependencies] winapi = "0.2" -kernel32-sys = "0.2.2" +kernel32-sys = "0.2" [dev-dependencies] tempdir = "0.3.4" diff --git a/README.md b/README.md index 3737f41a042c0fb9568e5021a08693d97558c278..22872813fd78e4de462e7e9bdae7feb29df02d9b 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,8 @@ Ctrl-D | (if line *is* empty) End of File Ctrl-E, End | Move cursor to end of line Ctrl-F, Right| Move cursor one character right Ctrl-H, BackSpace | Delete character before cursor -Ctrl-J, Return | Finish the line entry +Ctrl-I, Tab | Next completion +Ctrl-J, Ctrl-M, Enter | Finish the line entry Ctrl-K | Delete from cursor to end of line Ctrl-L | Clear screen Ctrl-N, Down | Next match from history @@ -98,19 +99,41 @@ Ctrl-T | Transpose previous character with current character Ctrl-U | Delete from start of line to cursor Ctrl-V | Insert any special character without perfoming its associated action Ctrl-W | Delete word leading up to cursor (using white space as a word boundary) -Ctrl-Y | Paste from Yank buffer (Alt-Y to paste next yank instead) -Tab | Next completion -Alt-B, Alt-Left | Move cursor to previous word -Alt-C | Capitalize the current word -Alt-D | Delete forwards one word -Alt-F, Alt-Right | Move cursor to next word -Alt-L | Lower-case the next word -Alt-T | Transpose words -Alt-U | Upper-case the next word -Alt-Y | See Ctrl-Y -Alt-BackSpace | Kill from the start of the current word, or, if between words, to the start of the previous word +Ctrl-Y | Paste from Yank buffer (Meta-Y to paste next yank instead) +Meta-< | Move to first entry in history +Meta-> | Move to last entry in history +Meta-B, Alt-Left | Move cursor to previous word +Meta-C | Capitalize the current word +Meta-D | Delete forwards one word +Meta-F, Alt-Right | Move cursor to next word +Meta-L | Lower-case the next word +Meta-T | Transpose words +Meta-U | Upper-case the next word +Meta-Y | See Ctrl-Y +Meta-BackSpace | Kill from the start of the current word, or, if between words, to the start of the previous word ## ToDo - Show completion list + - Undos + - Read input with timeout to properly handle single ESC key - expose an API callable from C + +## Wine + +```sh +$ cargo run --example example --target 'x86_64-pc-windows-gnu' +... +Error: Io(Error { repr: Os { code: 6, message: "Invalid handle." } }) +$ wineconsole --backend=curses target/x86_64-pc-windows-gnu/debug/examples/example.exe +... +``` + +## Similar projects + + - [copperline](https://github.com/srijs/rust-copperline) (Rust) + - [liner](https://github.com/MovingtoMars/liner) (Rust) + - [linenoise-ng](https://github.com/arangodb/linenoise-ng) (C++) + - [liner](https://github.com/peterh/liner) (Go) + - [readline](https://github.com/chzyer/readline) (Go) + - [haskeline](https://github.com/judah/haskeline) (Haskell) diff --git a/appveyor.yml b/appveyor.yml index 03a90c521251b46bb68211871f7dc97fb6deaf88..e0a1efeeb52d234caff1ce7415647c59b873c1b2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,4 +19,3 @@ test_script: cache: - C:\Users\appveyor\.cargo - diff --git a/examples/example.rs b/examples/example.rs index 134e1e25c88dc78478502a9d0f5fd86238810409..62fb4fe609146ce1fcb0ed1ba734f2726929fca5 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -8,7 +8,7 @@ use rustyline::Editor; #[cfg(unix)] static PROMPT: &'static str = "\x1b[1;32m>>\x1b[0m "; -// Windows consoles typically don't support ANSI escape sequences out +// Windows consoles typically don't support ANSI escape sequences out // of the box #[cfg(windows)] static PROMPT: &'static str = ">> "; diff --git a/src/char_iter.rs b/src/char_iter.rs index 129786d34dd55d0a60a38dec7d93d3b3ed4a60c7..0cfdb071ccf5adcc5fab766aad77ab673451fe46 100644 --- a/src/char_iter.rs +++ b/src/char_iter.rs @@ -9,7 +9,9 @@ use std::io; use std::io::Read; use std::str; -pub fn chars<R: Read>(read: R) -> Chars<R> where R: Sized { +pub fn chars<R: Read>(read: R) -> Chars<R> + where R: Sized +{ Chars { inner: read } } @@ -60,8 +62,12 @@ impl<R: Read> Iterator for Chars<R> { Err(e) => return Some(Err(CharsError::Other(e))), }; let width = utf8_char_width(first_byte); - if width == 1 { return Some(Ok(first_byte as char)) } - if width == 0 { return Some(Err(CharsError::NotUtf8)) } + if width == 1 { + return Some(Ok(first_byte as char)); + } + if width == 0 { + return Some(Err(CharsError::NotUtf8)); + } let mut buf = [first_byte, 0, 0, 0]; { let mut start = 1; @@ -98,9 +104,7 @@ impl error::Error for CharsError { impl fmt::Display for CharsError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - CharsError::NotUtf8 => { - "byte stream did not contain valid utf8".fmt(f) - } + CharsError::NotUtf8 => "byte stream did not contain valid utf8".fmt(f), CharsError::Other(ref e) => e.fmt(f), } } diff --git a/src/consts.rs b/src/consts.rs index e0a6d3ce7ad325cceb6bd71b58234a91302f6f7f..5fc32ea3b41016e3a801c7d54c1d094fe9d3d292 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,75 +1,56 @@ - #[derive(Debug, Clone, PartialEq, Copy)] pub enum KeyPress { - NULL, - CTRL_A, - CTRL_B, - CTRL_C, - CTRL_D, - CTRL_E, - CTRL_F, - CTRL_G, - CTRL_H, - TAB, - CTRL_J, - CTRL_K, - CTRL_L, - ENTER, - CTRL_N, - CTRL_P, - CTRL_R, - CTRL_S, - CTRL_T, - CTRL_U, - CTRL_V, - CTRL_W, - CTRL_Y, - CTRL_Z, - ESC, - BACKSPACE, UNKNOWN_ESC_SEQ, - ESC_SEQ_DELETE, - ESC_BACKSPACE, - ESC_B, - ESC_C, - ESC_D, - ESC_F, - ESC_L, - ESC_T, - ESC_U, - ESC_Y, + Backspace, + Char(char), + Ctrl(char), + Delete, + Down, + End, + Enter, // Ctrl('M') + Esc, + Home, + Left, + Meta(char), + Null, + Right, + Tab, // Ctrl('I') + Up, } #[cfg_attr(feature="clippy", allow(match_same_arms))] pub fn char_to_key_press(c: char) -> KeyPress { + if !c.is_control() { + return KeyPress::Char(c); + } match c { - '\x00' => KeyPress::NULL, - '\x01' => KeyPress::CTRL_A, - '\x02' => KeyPress::CTRL_B, - '\x03' => KeyPress::CTRL_C, - '\x04' => KeyPress::CTRL_D, - '\x05' => KeyPress::CTRL_E, - '\x06' => KeyPress::CTRL_F, - '\x07' => KeyPress::CTRL_G, - '\x08' => KeyPress::CTRL_H, - '\x09' => KeyPress::TAB, - '\x0a' => KeyPress::CTRL_J, - '\x0b' => KeyPress::CTRL_K, - '\x0c' => KeyPress::CTRL_L, - '\x0d' => KeyPress::ENTER, - '\x0e' => KeyPress::CTRL_N, - '\x10' => KeyPress::CTRL_P, - '\x12' => KeyPress::CTRL_R, - '\x13' => KeyPress::CTRL_S, - '\x14' => KeyPress::CTRL_T, - '\x15' => KeyPress::CTRL_U, - '\x16' => KeyPress::CTRL_V, - '\x17' => KeyPress::CTRL_W, - '\x19' => KeyPress::CTRL_Y, - '\x1a' => KeyPress::CTRL_Z, - '\x1b' => KeyPress::ESC, - '\x7f' => KeyPress::BACKSPACE, - _ => KeyPress::NULL, + '\x00' => KeyPress::Null, + '\x01' => KeyPress::Ctrl('A'), + '\x02' => KeyPress::Ctrl('B'), + '\x03' => KeyPress::Ctrl('C'), + '\x04' => KeyPress::Ctrl('D'), + '\x05' => KeyPress::Ctrl('E'), + '\x06' => KeyPress::Ctrl('F'), + '\x07' => KeyPress::Ctrl('G'), + '\x08' => KeyPress::Backspace, + '\x09' => KeyPress::Tab, + '\x0a' => KeyPress::Ctrl('J'), + '\x0b' => KeyPress::Ctrl('K'), + '\x0c' => KeyPress::Ctrl('L'), + '\x0d' => KeyPress::Enter, + '\x0e' => KeyPress::Ctrl('N'), + '\x10' => KeyPress::Ctrl('P'), + '\x12' => KeyPress::Ctrl('R'), + '\x13' => KeyPress::Ctrl('S'), + '\x14' => KeyPress::Ctrl('T'), + '\x15' => KeyPress::Ctrl('U'), + '\x16' => KeyPress::Ctrl('V'), + '\x17' => KeyPress::Ctrl('W'), + '\x19' => KeyPress::Ctrl('Y'), + '\x1a' => KeyPress::Ctrl('Z'), + '\x1b' => KeyPress::Esc, + '\x7f' => KeyPress::Backspace, // TODO Validate + _ => KeyPress::Null, } } diff --git a/src/error.rs b/src/error.rs index 2f1b259a59b717a70ce8b268070dc67224df3a71..e516a1957a25bc709d34323808933d893eac9a8d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,6 @@ //! Contains error type for handling I/O and Errno errors +#[cfg(windows)] +use std::char; use std::io; use std::error; use std::fmt; @@ -22,6 +24,10 @@ pub enum ReadlineError { /// Unix Error from syscall #[cfg(unix)] Errno(nix::Error), + #[cfg(windows)] + WindowResize, + #[cfg(windows)] + Decode(char::DecodeUtf16Error), } impl fmt::Display for ReadlineError { @@ -33,6 +39,10 @@ impl fmt::Display for ReadlineError { ReadlineError::Interrupted => write!(f, "Interrupted"), #[cfg(unix)] ReadlineError::Errno(ref err) => write!(f, "Errno: {}", err.errno().desc()), + #[cfg(windows)] + ReadlineError::WindowResize => write!(f, "WindowResize"), + #[cfg(windows)] + ReadlineError::Decode(ref err) => err.fmt(f), } } } @@ -46,6 +56,10 @@ impl error::Error for ReadlineError { ReadlineError::Interrupted => "Interrupted", #[cfg(unix)] ReadlineError::Errno(ref err) => err.errno().desc(), + #[cfg(windows)] + ReadlineError::WindowResize => "WindowResize", + #[cfg(windows)] + ReadlineError::Decode(ref err) => err.description(), } } } @@ -63,15 +77,16 @@ impl From<nix::Error> for ReadlineError { } } +#[cfg(unix)] impl From<char_iter::CharsError> for ReadlineError { fn from(err: char_iter::CharsError) -> ReadlineError { ReadlineError::Char(err) } } -impl ReadlineError { - #[cfg(unix)] - pub fn from_errno(errno: nix::errno::Errno) -> ReadlineError { - ReadlineError::from(nix::Error::from_errno(errno)) +#[cfg(windows)] +impl From<char::DecodeUtf16Error> for ReadlineError { + fn from(err: char::DecodeUtf16Error) -> ReadlineError { + ReadlineError::Decode(err) } } diff --git a/src/history.rs b/src/history.rs index 39667917f40233c1ee29dc04164ca1afe00133a6..8b28f24987f7772a76b29290c60d70c07ab4a743 100644 --- a/src/history.rs +++ b/src/history.rs @@ -20,7 +20,7 @@ impl History { History { entries: VecDeque::new(), max_len: DEFAULT_HISTORY_MAX_LEN, - ignore_space: true, + ignore_space: false, ignore_dups: true, } } @@ -169,6 +169,7 @@ mod tests { #[test] fn add() { let mut history = super::History::new(); + history.ignore_space(true); assert!(history.add("line1")); assert!(history.add("line2")); assert!(!history.add("line2")); diff --git a/src/lib.rs b/src/lib.rs index 03a0bd9b8c41f807bea7b78aeccabf0fc3fdb299..4912cbccf6c1a39862864ad162061e67ae981698 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,10 @@ extern crate libc; extern crate nix; extern crate unicode_width; extern crate encode_unicode; +#[cfg(windows)] +extern crate winapi; +#[cfg(windows)] +extern crate kernel32; pub mod completion; #[allow(non_camel_case_types)] @@ -33,20 +37,19 @@ mod char_iter; mod tty; use std::fmt; -use std::io::{self, Write}; +use std::io::{self, Read, Write}; use std::mem; use std::path::Path; use std::result; #[cfg(unix)] use std::sync; use std::sync::atomic; - #[cfg(unix)] use nix::sys::signal; + use encode_unicode::CharExt; -use tty::Terminal; use completion::Completer; -use consts::{KeyPress, char_to_key_press}; +use consts::KeyPress; use history::History; use line_buffer::{LineBuffer, MAX_LINE, WordAction}; use kill_ring::KillRing; @@ -62,8 +65,10 @@ struct State<'out, 'prompt> { line: LineBuffer, // Edited line buffer cursor: Position, // Cursor position (relative to the start of the prompt for `row`) cols: usize, // Number of columns in terminal - history_index: usize, // The history index we are currently editing. + old_rows: usize, // Number of rows used so far (from start of prompt to end of input) + history_index: usize, // The history index we are currently editing snapshot: LineBuffer, // Current edited line before history browsing/completion + output_handle: tty::Handle, // output handle (for windows) } #[derive(Copy, Clone, Debug, Default)] @@ -74,11 +79,12 @@ struct Position { impl<'out, 'prompt> State<'out, 'prompt> { fn new(out: &'out mut Write, + output_handle: tty::Handle, prompt: &'prompt str, - capacity: usize, - cols: usize, history_index: usize) -> State<'out, 'prompt> { + let capacity = MAX_LINE; + let cols = tty::get_columns(output_handle); let prompt_size = calculate_position(prompt, Default::default(), cols); State { out: out, @@ -87,8 +93,10 @@ impl<'out, 'prompt> State<'out, 'prompt> { line: LineBuffer::with_capacity(capacity), cursor: prompt_size, cols: cols, + old_rows: prompt_size.row, history_index: history_index, snapshot: LineBuffer::with_capacity(capacity), + output_handle: output_handle, } } @@ -112,20 +120,29 @@ impl<'out, 'prompt> State<'out, 'prompt> { self.refresh(prompt, prompt_size) } + #[cfg(unix)] fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> { use std::fmt::Write; + // calculate the position of the end of the input line let end_pos = calculate_position(&self.line, prompt_size, self.cols); + // calculate the desired position of the cursor let cursor = calculate_position(&self.line[..self.line.pos()], prompt_size, self.cols); let mut ab = String::new(); - let cursor_row_movement = self.cursor.row - self.prompt_size.row; - // move the cursor up as required + + let cursor_row_movement = self.old_rows - self.cursor.row; + // move the cursor down as required if cursor_row_movement > 0 { - write!(ab, "\x1b[{}A", cursor_row_movement).unwrap(); + write!(ab, "\x1b[{}B", cursor_row_movement).unwrap(); } - // position at the start of the prompt, clear to end of screen - ab.push_str("\r\x1b[J"); + // clear old rows + for _ in 0..self.old_rows { + ab.push_str("\r\x1b[0K\x1b[1A"); + } + // clear the line + ab.push_str("\r\x1b[0K"); + // display the prompt ab.push_str(prompt); // display the input line @@ -148,9 +165,56 @@ impl<'out, 'prompt> State<'out, 'prompt> { } self.cursor = cursor; + self.old_rows = end_pos.row; write_and_flush(self.out, ab.as_bytes()) } + + #[cfg(windows)] + fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> { + let handle = self.output_handle; + if cfg!(test) && handle.is_null() { + return Ok(()); + } + // calculate the position of the end of the input line + let end_pos = calculate_position(&self.line, prompt_size, self.cols); + // calculate the desired position of the cursor + let cursor = calculate_position(&self.line[..self.line.pos()], prompt_size, self.cols); + + // position at the start of the prompt, clear to end of previous input + let mut info = unsafe { mem::zeroed() }; + check!(kernel32::GetConsoleScreenBufferInfo(handle, &mut info)); + info.dwCursorPosition.X = 0; + info.dwCursorPosition.Y -= self.cursor.row as i16; + check!(kernel32::SetConsoleCursorPosition(handle, info.dwCursorPosition)); + let mut _count = 0; + check!(kernel32::FillConsoleOutputCharacterA(handle, + ' ' as winapi::CHAR, + (info.dwSize.X * (self.old_rows as i16 +1)) as winapi::DWORD, + info.dwCursorPosition, + &mut _count)); + let mut ab = String::new(); + // display the prompt + ab.push_str(prompt); // TODO handle ansi escape code (SetConsoleTextAttribute) + // display the input line + ab.push_str(&self.line); + try!(write_and_flush(self.out, ab.as_bytes())); + + // position the cursor + check!(kernel32::GetConsoleScreenBufferInfo(handle, &mut info)); + info.dwCursorPosition.X = cursor.col as i16; + info.dwCursorPosition.Y -= (end_pos.row - cursor.row) as i16; + check!(kernel32::SetConsoleCursorPosition(handle, info.dwCursorPosition)); + + self.cursor = cursor; + self.old_rows = end_pos.row; + + Ok(()) + } + + fn update_columns(&mut self) { + self.cols = tty::get_columns(self.output_handle); + } } impl<'out, 'prompt> fmt::Debug for State<'out, 'prompt> { @@ -161,13 +225,13 @@ impl<'out, 'prompt> fmt::Debug for State<'out, 'prompt> { .field("buf", &self.line) .field("cursor", &self.cursor) .field("cols", &self.cols) + .field("old_rows", &self.old_rows) .field("history_index", &self.history_index) .field("snapshot", &self.snapshot) .finish() } } - fn write_and_flush(w: &mut Write, buf: &[u8]) -> Result<()> { try!(w.write_all(buf)); try!(w.flush()); @@ -175,8 +239,24 @@ fn write_and_flush(w: &mut Write, buf: &[u8]) -> Result<()> { } /// Clear the screen. Used to handle ctrl+l -fn clear_screen(out: &mut Write) -> Result<()> { - write_and_flush(out, b"\x1b[H\x1b[2J") +#[cfg(unix)] +fn clear_screen(s: &mut State) -> Result<()> { + write_and_flush(s.out, b"\x1b[H\x1b[2J") +} +#[cfg(windows)] +fn clear_screen(s: &mut State) -> Result<()> { + let handle = s.output_handle; + let mut info = unsafe { mem::zeroed() }; + check!(kernel32::GetConsoleScreenBufferInfo(handle, &mut info)); + let coord = winapi::COORD { X: 0, Y: 0 }; + check!(kernel32::SetConsoleCursorPosition(handle, coord)); + let mut _count = 0; + check!(kernel32::FillConsoleOutputCharacterA(handle, + ' ' as winapi::CHAR, + (info.dwSize.X * info.dwSize.Y) as winapi::DWORD, + coord, + &mut _count)); + Ok(()) } /// Beep, used for completion when there is nothing to complete or when all @@ -244,6 +324,8 @@ fn edit_insert(s: &mut State, ch: char) -> Result<()> { if push { if s.cursor.col + unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0) < s.cols { // Avoid a full update of the line in the trivial case. + let cursor = calculate_position(&s.line[..s.line.pos()], s.prompt_size, s.cols); + s.cursor = cursor; write_and_flush(s.out, ch.to_utf8().as_bytes()) } else { s.refresh_line() @@ -437,15 +519,42 @@ fn edit_history_next(s: &mut State, history: &History, prev: bool) -> Result<()> } else { // Restore current edited line s.snapshot(); - }; + } + s.refresh_line() +} + +/// Substitute the currently edited line with the first/last history entry. +fn edit_history(s: &mut State, history: &History, first: bool) -> Result<()> { + if history.is_empty() { + return Ok(()); + } + if s.history_index == history.len() { + if first { + // Save the current edited line before to overwrite it + s.snapshot(); + } else { + return Ok(()); + } + } else if s.history_index == 0 && first { + return Ok(()); + } + if first { + s.history_index = 0; + let buf = history.get(s.history_index).unwrap(); + s.line.update(buf, buf.len()); + } else { + s.history_index = history.len(); + // Restore current edited line + s.snapshot(); + } s.refresh_line() } /// Completes the line/word -fn complete_line<R: io::Read>(chars: &mut char_iter::Chars<R>, - s: &mut State, - completer: &Completer) - -> Result<Option<char>> { +fn complete_line<R: Read>(rdr: &mut tty::RawReader<R>, + s: &mut State, + completer: &Completer) + -> Result<Option<KeyPress>> { let (start, candidates) = try!(completer.complete(&s.line, s.line.pos())); if candidates.is_empty() { try!(beep()); @@ -453,7 +562,7 @@ fn complete_line<R: io::Read>(chars: &mut char_iter::Chars<R>, } else { // Save the current edited line before to overwrite it s.backup(); - let mut ch; + let mut key; let mut i = 0; loop { // Show completion or original buffer @@ -467,16 +576,15 @@ fn complete_line<R: io::Read>(chars: &mut char_iter::Chars<R>, s.snapshot(); } - ch = try!(chars.next().unwrap()); - let key = char_to_key_press(ch); + key = try!(rdr.next_key(false)); match key { - KeyPress::TAB => { + KeyPress::Tab => { i = (i + 1) % (candidates.len() + 1); // Circular if i == candidates.len() { try!(beep()); } } - KeyPress::ESC => { + KeyPress::Esc => { // Re-show original buffer s.snapshot(); if i < candidates.len() { @@ -489,16 +597,19 @@ fn complete_line<R: io::Read>(chars: &mut char_iter::Chars<R>, } } } - Ok(Some(ch)) + Ok(Some(key)) } } /// Incremental search #[cfg_attr(feature="clippy", allow(if_not_else))] -fn reverse_incremental_search<R: io::Read>(chars: &mut char_iter::Chars<R>, - s: &mut State, - history: &History) - -> Result<Option<KeyPress>> { +fn reverse_incremental_search<R: Read>(rdr: &mut tty::RawReader<R>, + s: &mut State, + history: &History) + -> Result<Option<KeyPress>> { + if history.is_empty() { + return Ok(None); + } // Save the current edited line (and cursor position) before to overwrite it s.snapshot(); @@ -507,7 +618,6 @@ fn reverse_incremental_search<R: io::Read>(chars: &mut char_iter::Chars<R>, let mut reverse = true; let mut success = true; - let mut ch; let mut key; // Display the reverse-i-search prompt and process chars loop { @@ -518,20 +628,17 @@ fn reverse_incremental_search<R: io::Read>(chars: &mut char_iter::Chars<R>, }; try!(s.refresh_prompt_and_line(&prompt)); - ch = try!(chars.next().unwrap()); - if !ch.is_control() { - search_buf.push(ch); + key = try!(rdr.next_key(true)); + if let KeyPress::Char(c) = key { + search_buf.push(c); } else { - key = char_to_key_press(ch); - if key == KeyPress::ESC { - key = try!(escape_sequence(chars)); - } match key { - KeyPress::CTRL_H | KeyPress::BACKSPACE => { + KeyPress::Ctrl('H') | + KeyPress::Backspace => { search_buf.pop(); continue; } - KeyPress::CTRL_R => { + KeyPress::Ctrl('R') => { reverse = true; if history_idx > 0 { history_idx -= 1; @@ -540,7 +647,7 @@ fn reverse_incremental_search<R: io::Read>(chars: &mut char_iter::Chars<R>, continue; } } - KeyPress::CTRL_S => { + KeyPress::Ctrl('S') => { reverse = false; if history_idx < history.len() - 1 { history_idx += 1; @@ -549,7 +656,7 @@ fn reverse_incremental_search<R: io::Read>(chars: &mut char_iter::Chars<R>, continue; } } - KeyPress::CTRL_G => { + KeyPress::Ctrl('G') => { // Restore current edited line (before search) s.snapshot(); try!(s.refresh_line()); @@ -572,146 +679,82 @@ fn reverse_incremental_search<R: io::Read>(chars: &mut char_iter::Chars<R>, Ok(Some(key)) } -fn escape_sequence<R: io::Read>(chars: &mut char_iter::Chars<R>) -> Result<KeyPress> { - // Read the next two bytes representing the escape sequence. - let seq1 = try!(chars.next().unwrap()); - if seq1 == '[' { - // ESC [ sequences. - let seq2 = try!(chars.next().unwrap()); - if seq2.is_digit(10) { - // Extended escape, read additional byte. - let seq3 = try!(chars.next().unwrap()); - if seq3 == '~' { - match seq2 { - '3' => Ok(KeyPress::ESC_SEQ_DELETE), - // TODO '1' // Home - // TODO '4' // End - _ => Ok(KeyPress::UNKNOWN_ESC_SEQ), - } - } else { - Ok(KeyPress::UNKNOWN_ESC_SEQ) - } - } else { - match seq2 { - 'A' => Ok(KeyPress::CTRL_P), // Up - 'B' => Ok(KeyPress::CTRL_N), // Down - 'C' => Ok(KeyPress::CTRL_F), // Right - 'D' => Ok(KeyPress::CTRL_B), // Left - 'F' => Ok(KeyPress::CTRL_E), // End - 'H' => Ok(KeyPress::CTRL_A), // Home - _ => Ok(KeyPress::UNKNOWN_ESC_SEQ), - } - } - } else if seq1 == 'O' { - // ESC O sequences. - let seq2 = try!(chars.next().unwrap()); - match seq2 { - 'F' => Ok(KeyPress::CTRL_E), - 'H' => Ok(KeyPress::CTRL_A), - _ => Ok(KeyPress::UNKNOWN_ESC_SEQ), - } - } else { - // TODO ESC-N (n): search history forward not interactively - // TODO ESC-P (p): search history backward not interactively - // TODO ESC-R (r): Undo all changes made to this line. - // TODO ESC-<: move to first entry in history - // TODO ESC->: move to last entry in history - match seq1 { - 'b' | 'B' => Ok(KeyPress::ESC_B), - 'c' | 'C' => Ok(KeyPress::ESC_C), - 'd' | 'D' => Ok(KeyPress::ESC_D), - 'f' | 'F' => Ok(KeyPress::ESC_F), - 'l' | 'L' => Ok(KeyPress::ESC_L), - 't' | 'T' => Ok(KeyPress::ESC_T), - 'u' | 'U' => Ok(KeyPress::ESC_U), - 'y' | 'Y' => Ok(KeyPress::ESC_Y), - '\x08' | '\x7f' => Ok(KeyPress::ESC_BACKSPACE), - _ => { - writeln!(io::stderr(), "key: {:?}, seq1, {:?}", KeyPress::ESC, seq1).unwrap(); - Ok(KeyPress::UNKNOWN_ESC_SEQ) - } - } - } -} - /// Handles reading and editting the readline buffer. /// It will also handle special inputs in an appropriate fashion /// (e.g., C-c will exit readline) #[cfg_attr(feature="clippy", allow(cyclomatic_complexity))] -fn readline_edit<T: tty::Terminal>(prompt: &str, +fn readline_edit(prompt: &str, history: &mut History, completer: Option<&Completer>, kill_ring: &mut KillRing, - mut term: T) + original_mode: tty::Mode) -> Result<String> { let mut stdout = io::stdout(); - try!(write_and_flush(&mut stdout, prompt.as_bytes())); + let stdout_handle = try!(tty::stdout_handle()); kill_ring.reset(); - let mut s = State::new(&mut stdout, prompt, MAX_LINE, tty::get_columns(), history.len()); - let stdin = io::stdin(); - let mut chars = char_iter::chars(stdin.lock()); + let mut s = State::new(&mut stdout, stdout_handle, prompt, history.len()); + try!(s.refresh_line()); + + let mut rdr = try!(tty::RawReader::new(io::stdin())); + loop { - let c = chars.next().unwrap(); - if c.is_err() && SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) { - s.cols = tty::get_columns(); + let rk = rdr.next_key(true); + if rk.is_err() && SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) { + s.update_columns(); try!(s.refresh_line()); continue; } - let mut ch = try!(c); - if !ch.is_control() { + let mut key = try!(rk); + if let KeyPress::Char(c) = key { kill_ring.reset(); - try!(edit_insert(&mut s, ch)); + try!(edit_insert(&mut s, c)); continue; } - let mut key = char_to_key_press(ch); // autocomplete - if key == KeyPress::TAB && completer.is_some() { - let next = try!(complete_line(&mut chars, &mut s, completer.unwrap())); + if key == KeyPress::Tab && completer.is_some() { + let next = try!(complete_line(&mut rdr, &mut s, completer.unwrap())); if next.is_some() { kill_ring.reset(); - ch = next.unwrap(); - if !ch.is_control() { - try!(edit_insert(&mut s, ch)); + key = next.unwrap(); + if let KeyPress::Char(c) = key { + try!(edit_insert(&mut s, c)); continue; } - key = char_to_key_press(ch); } else { continue; } - } else if key == KeyPress::CTRL_R { + } else if key == KeyPress::Ctrl('R') { // Search history backward - let next = try!(reverse_incremental_search(&mut chars, &mut s, history)); + let next = try!(reverse_incremental_search(&mut rdr, &mut s, history)); if next.is_some() { key = next.unwrap(); } else { continue; } - } else if key == KeyPress::ESC { - // escape sequence - key = try!(escape_sequence(&mut chars)); - if key == KeyPress::UNKNOWN_ESC_SEQ { - continue; - } + } else if key == KeyPress::UNKNOWN_ESC_SEQ { + continue; } match key { - KeyPress::CTRL_A => { + KeyPress::Ctrl('A') | + KeyPress::Home => { kill_ring.reset(); // Move to the beginning of line. try!(edit_move_home(&mut s)) } - KeyPress::CTRL_B => { + KeyPress::Ctrl('B') | + KeyPress::Left => { kill_ring.reset(); // Move back a character. try!(edit_move_left(&mut s)) } - KeyPress::CTRL_C => { + KeyPress::Ctrl('C') => { kill_ring.reset(); return Err(error::ReadlineError::Interrupted); } - KeyPress::CTRL_D => { + KeyPress::Ctrl('D') => { kill_ring.reset(); if s.line.is_empty() { return Err(error::ReadlineError::Eof); @@ -720,87 +763,94 @@ fn readline_edit<T: tty::Terminal>(prompt: &str, try!(edit_delete(&mut s)) } } - KeyPress::CTRL_E => { + KeyPress::Ctrl('E') | + KeyPress::End => { kill_ring.reset(); // Move to the end of line. try!(edit_move_end(&mut s)) } - KeyPress::CTRL_F => { + KeyPress::Ctrl('F') | + KeyPress::Right => { kill_ring.reset(); // Move forward a character. try!(edit_move_right(&mut s)) } - KeyPress::CTRL_H | KeyPress::BACKSPACE => { + KeyPress::Ctrl('H') | + KeyPress::Backspace => { kill_ring.reset(); // Delete one character backward. try!(edit_backspace(&mut s)) } - KeyPress::CTRL_K => { + KeyPress::Ctrl('K') => { // Kill the text from point to the end of the line. if let Some(text) = try!(edit_kill_line(&mut s)) { kill_ring.kill(&text, true) } } - KeyPress::CTRL_L => { + KeyPress::Ctrl('L') => { // Clear the screen leaving the current line at the top of the screen. - try!(clear_screen(s.out)); + try!(clear_screen(&mut s)); try!(s.refresh_line()) } - KeyPress::CTRL_N => { + KeyPress::Ctrl('N') | + KeyPress::Down => { kill_ring.reset(); // Fetch the next command from the history list. try!(edit_history_next(&mut s, history, false)) } - KeyPress::CTRL_P => { + KeyPress::Ctrl('P') | + KeyPress::Up => { kill_ring.reset(); // Fetch the previous command from the history list. try!(edit_history_next(&mut s, history, true)) } - KeyPress::CTRL_T => { + KeyPress::Ctrl('T') => { kill_ring.reset(); // Exchange the char before cursor with the character at cursor. try!(edit_transpose_chars(&mut s)) } - KeyPress::CTRL_U => { + KeyPress::Ctrl('U') => { // Kill backward from point to the beginning of the line. if let Some(text) = try!(edit_discard_line(&mut s)) { kill_ring.kill(&text, false) } } - KeyPress::CTRL_V => { + #[cfg(unix)] + KeyPress::Ctrl('V') => { // Quoted insert kill_ring.reset(); - let c = chars.next().unwrap(); - let ch = try!(c); - try!(edit_insert(&mut s, ch)) + let c = try!(rdr.next_char()); + try!(edit_insert(&mut s, c)) // FIXME } - KeyPress::CTRL_W => { + KeyPress::Ctrl('W') => { // Kill the word behind point, using white space as a word boundary if let Some(text) = try!(edit_delete_prev_word(&mut s, char::is_whitespace)) { kill_ring.kill(&text, false) } } - KeyPress::CTRL_Y => { + KeyPress::Ctrl('Y') => { // retrieve (yank) last item killed if let Some(text) = kill_ring.yank() { try!(edit_yank(&mut s, text)) } } #[cfg(unix)] - KeyPress::CTRL_Z => { - try!(term.disable_raw_mode()); + KeyPress::Ctrl('Z') => { + try!(tty::disable_raw_mode(original_mode)); try!(signal::raise(signal::SIGSTOP)); - try!(term.enable_raw_mode()); // TODO term may have changed + try!(tty::enable_raw_mode()); // TODO original_mode may have changed try!(s.refresh_line()) } // TODO CTRL-_ // undo - KeyPress::ENTER | KeyPress::CTRL_J => { + KeyPress::Enter | + KeyPress::Ctrl('J') => { // Accept the line regardless of where the cursor is. kill_ring.reset(); try!(edit_move_end(&mut s)); break; } - KeyPress::ESC_BACKSPACE => { + KeyPress::Meta('\x08') | + KeyPress::Meta('\x7f') => { // kill one word backward // Kill from the cursor to the start of the current word, or, if between words, to the start of the previous word. if let Some(text) = try!(edit_delete_prev_word(&mut s, @@ -808,62 +858,81 @@ fn readline_edit<T: tty::Terminal>(prompt: &str, kill_ring.kill(&text, false) } } - KeyPress::ESC_B => { + KeyPress::Meta('<') => { + // move to first entry in history + kill_ring.reset(); + try!(edit_history(&mut s, history, true)) + } + KeyPress::Meta('>') => { + // move to last entry in history + kill_ring.reset(); + try!(edit_history(&mut s, history, false)) + } + KeyPress::Meta('B') => { // move backwards one word kill_ring.reset(); try!(edit_move_to_prev_word(&mut s)) } - KeyPress::ESC_C => { + KeyPress::Meta('C') => { // capitalize word after point kill_ring.reset(); try!(edit_word(&mut s, WordAction::CAPITALIZE)) } - KeyPress::ESC_D => { + KeyPress::Meta('D') => { // kill one word forward if let Some(text) = try!(edit_delete_word(&mut s)) { kill_ring.kill(&text, true) } } - KeyPress::ESC_F => { + KeyPress::Meta('F') => { // move forwards one word kill_ring.reset(); try!(edit_move_to_next_word(&mut s)) } - KeyPress::ESC_L => { + KeyPress::Meta('L') => { // lowercase word after point kill_ring.reset(); try!(edit_word(&mut s, WordAction::LOWERCASE)) } - KeyPress::ESC_T => { + KeyPress::Meta('T') => { // transpose words kill_ring.reset(); try!(edit_transpose_words(&mut s)) } - KeyPress::ESC_U => { + KeyPress::Meta('U') => { // uppercase word after point kill_ring.reset(); try!(edit_word(&mut s, WordAction::UPPERCASE)) } - KeyPress::ESC_Y => { + KeyPress::Meta('Y') => { // yank-pop if let Some((yank_size, text)) = kill_ring.yank_pop() { try!(edit_yank_pop(&mut s, yank_size, text)) } } - KeyPress::ESC_SEQ_DELETE => { + KeyPress::Delete => { kill_ring.reset(); try!(edit_delete(&mut s)) } _ => { kill_ring.reset(); - // Insert the character typed. - try!(edit_insert(&mut s, ch)) + // Ignore the character typed. } } } Ok(s.line.into_string()) } +struct Guard(tty::Mode); + +#[allow(unused_must_use)] +impl Drop for Guard { + fn drop(&mut self) { + let Guard(mode) = *self; + tty::disable_raw_mode(mode); + } +} + /// Readline method that will enable RAW mode, call the `readline_edit()` /// method and disable raw mode fn readline_raw(prompt: &str, @@ -871,9 +940,10 @@ fn readline_raw(prompt: &str, completer: Option<&Completer>, kill_ring: &mut KillRing) -> Result<String> { - let mut term = tty::get_terminal(); - try!(term.enable_raw_mode()); - let user_input = readline_edit(prompt, history, completer, kill_ring, term); + let original_mode = try!(tty::enable_raw_mode()); + let guard = Guard(original_mode); + let user_input = readline_edit(prompt, history, completer, kill_ring, original_mode); + drop(guard); // try!(disable_raw_mode(original_mode)); println!(""); user_input } @@ -904,8 +974,8 @@ impl<C> Editor<C> { // if the number of columns is stored here, we need a SIGWINCH handler... let editor = Editor { unsupported_term: tty::is_unsupported_term(), - stdin_isatty: tty::is_a_tty(tty::StandardStream::StdIn), - stdout_isatty: tty::is_a_tty(tty::StandardStream::StdOut), + stdin_isatty: tty::is_a_tty(tty::STDIN_FILENO), + stdout_isatty: tty::is_a_tty(tty::STDOUT_FILENO), history: History::new(), completer: None, kill_ring: KillRing::new(60), @@ -916,13 +986,13 @@ impl<C> Editor<C> { editor } - pub fn history_ignore_space(mut self, yes: bool) -> Editor<C> { - self.history.ignore_space(yes); + pub fn history_ignore_dups(mut self, yes: bool) -> Editor<C> { + self.history.ignore_dups(yes); self } - pub fn history_ignore_dups(mut self, yes: bool) -> Editor<C> { - self.history.ignore_dups(yes); + pub fn history_ignore_space(mut self, yes: bool) -> Editor<C> { + self.history.ignore_space(yes); self } @@ -1006,25 +1076,37 @@ fn install_sigwinch_handler() { let _ = signal::sigaction(signal::SIGWINCH, &sigwinch); }); } - -// no-op on windows -#[cfg(windows)] -fn install_sigwinch_handler() { -} - #[cfg(unix)] extern "C" fn sigwinch_handler(_: signal::SigNum) { SIGWINCH.store(true, atomic::Ordering::SeqCst); } +#[cfg(windows)] +fn install_sigwinch_handler() { + // See ReadConsoleInputW && WINDOW_BUFFER_SIZE_EVENT +} #[cfg(test)] mod test { use std::io::Write; use line_buffer::LineBuffer; use history::History; + #[cfg(unix)] use completion::Completer; + #[cfg(unix)] + use consts::KeyPress; use State; use super::Result; + use tty::{Handle, RawReader}; + + #[cfg(unix)] + fn default_handle() -> Handle { + () + } + #[cfg(windows)] + fn default_handle() -> Handle { + ::std::ptr::null_mut() + // super::get_std_handle(super::STDOUT_FILENO).expect("Valid stdout") + } fn init_state<'out>(out: &'out mut Write, line: &str, @@ -1038,8 +1120,10 @@ mod test { line: LineBuffer::init(line, pos), cursor: Default::default(), cols: cols, + old_rows: 0, history_index: 0, snapshot: LineBuffer::with_capacity(100), + output_handle: default_handle(), } } @@ -1081,7 +1165,9 @@ mod test { assert_eq!(line, s.line.as_str()); } + #[cfg(unix)] struct SimpleCompleter; + #[cfg(unix)] impl Completer for SimpleCompleter { fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>)> { Ok((0, vec![line.to_string() + "t"])) @@ -1089,14 +1175,15 @@ mod test { } #[test] + #[cfg(unix)] fn complete_line() { let mut out = ::std::io::sink(); let mut s = init_state(&mut out, "rus", 3, 80); let input = b"\n"; - let mut chars = ::char_iter::chars(&input[..]); + let mut rdr = RawReader::new(&input[..]).unwrap(); let completer = SimpleCompleter; - let ch = super::complete_line(&mut chars, &mut s, &completer).unwrap(); - assert_eq!(Some('\n'), ch); + let key = super::complete_line(&mut rdr, &mut s, &completer).unwrap(); + assert_eq!(Some(KeyPress::Ctrl('J')), key); assert_eq!("rust", s.line.as_str()); assert_eq!(4, s.line.pos()); } diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 72cc3f368b50b8701dce469782a26ea0db95cadd..3a15003c03894e4571346db022b1727d45997893 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -1,33 +1,14 @@ //! This module implements and describes common TTY methods & traits extern crate libc; -use super::Result; -// If on Windows platform import Windows TTY module +// If on Windows platform import Windows TTY module // and re-export into mod.rs scope -#[cfg(windows)] mod windows; -#[cfg(windows)] pub use self::windows::*; +#[cfg(windows)]mod windows; +#[cfg(windows)] +pub use self::windows::*; -// If on Unix platform import Unix TTY module +// If on Unix platform import Unix TTY module // and re-export into mod.rs scope -#[cfg(unix)] mod unix; -#[cfg(unix)] pub use self::unix::*; - -/// Trait that should be for each TTY/Terminal on various platforms -/// (e.g. unix & windows) -pub trait Terminal { - /// Enable RAW mode for the terminal - fn enable_raw_mode(&mut self) -> Result<()>; - - /// Disable RAW mode for the terminal - fn disable_raw_mode(&self) -> Result<()>; -} - -/// Enum for Standard Streams -/// -/// libc::STDIN_FILENO/STDOUT_FILENO/STDERR_FILENO is not defined for the -/// Windows platform. We will use this enum instead when calling isatty -/// function -pub enum StandardStream { - StdIn, - StdOut, -} +#[cfg(unix)]mod unix; +#[cfg(unix)] +pub use self::unix::*; diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 2f3d99409887dfe9715ce1eddc814cd5632ec2d8..fceb3e85ca80cffcfe74d832b0008880aac4b195 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -2,13 +2,18 @@ extern crate nix; extern crate libc; use std; +use std::io::Read; use nix::sys::termios; -use nix::errno::Errno; -use super::Terminal; -use super::StandardStream; +use char_iter; +use consts::{self, KeyPress}; use ::Result; use ::error; +pub type Handle = (); +pub type Mode = termios::Termios; +pub const STDIN_FILENO: libc::c_int = libc::STDIN_FILENO; +pub const STDOUT_FILENO: libc::c_int = libc::STDOUT_FILENO; + /// Unsupported Terminals that don't support RAW mode static UNSUPPORTED_TERM: [&'static str; 3] = ["dumb", "cons25", "emacs"]; @@ -23,7 +28,7 @@ const TIOCGWINSZ: libc::c_int = 0x5413; /// Try to get the number of columns in the current terminal, /// or assume 80 if it fails. -pub fn get_columns() -> usize { +pub fn get_columns(_: Handle) -> usize { use std::mem::zeroed; use libc::c_ushort; use libc; @@ -38,18 +43,13 @@ pub fn get_columns() -> usize { } let mut size: winsize = zeroed(); - match libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut size) { + match libc::ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut size) { 0 => size.ws_col as usize, // TODO getCursorPosition _ => 80, } } } -/// Get UnixTerminal struct -pub fn get_terminal() -> UnixTerminal { - UnixTerminal{ original_termios: None } -} - /// Check TERM environment variable to see if current term is in our /// unsupported list pub fn is_unsupported_term() -> bool { @@ -68,62 +68,136 @@ pub fn is_unsupported_term() -> bool { /// Return whether or not STDIN, STDOUT or STDERR is a TTY -pub fn is_a_tty(stream: StandardStream) -> bool { - extern crate libc; +pub fn is_a_tty(fd: libc::c_int) -> bool { + unsafe { libc::isatty(fd) != 0 } +} - let fd = match stream { - StandardStream::StdIn => libc::STDIN_FILENO, - StandardStream::StdOut => libc::STDOUT_FILENO, - }; +/// Enable raw mode for the TERM +pub fn enable_raw_mode() -> Result<Mode> { + use nix::errno::Errno::ENOTTY; + use nix::sys::termios::{BRKINT, CS8, ECHO, ICANON, ICRNL, IEXTEN, INPCK, ISIG, ISTRIP, IXON, + /* OPOST, */ VMIN, VTIME}; + if !is_a_tty(STDIN_FILENO) { + try!(Err(nix::Error::from_errno(ENOTTY))); + } + let original_mode = try!(termios::tcgetattr(STDIN_FILENO)); + let mut raw = original_mode; + // disable BREAK interrupt, CR to NL conversion on input, + // input parity check, strip high bit (bit 8), output flow control + raw.c_iflag = raw.c_iflag & !(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + // we don't want raw output, it turns newlines into straight linefeeds + // raw.c_oflag = raw.c_oflag & !(OPOST); // disable all output processing + raw.c_cflag = raw.c_cflag | (CS8); // character-size mark (8 bits) + // disable echoing, canonical mode, extended input processing and signals + raw.c_lflag = raw.c_lflag & !(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 1; // One character-at-a-time input + raw.c_cc[VTIME] = 0; // with blocking read + try!(termios::tcsetattr(STDIN_FILENO, termios::TCSAFLUSH, &raw)); + Ok(original_mode) +} - unsafe { libc::isatty(fd) != 0 } +/// Disable Raw mode for the term +pub fn disable_raw_mode(original_mode: Mode) -> Result<()> { + try!(termios::tcsetattr(STDIN_FILENO, termios::TCSAFLUSH, &original_mode)); + Ok(()) +} + +pub fn stdout_handle() -> Result<Handle> { + Ok(()) } -/// Structure that will contain the original termios before enabling RAW mode -pub struct UnixTerminal { - original_termios: Option<termios::Termios> +/// Console input reader +pub struct RawReader<R> { + chars: char_iter::Chars<R>, } -impl Terminal for UnixTerminal { - /// Enable raw mode for the TERM - fn enable_raw_mode(&mut self) -> Result<()> { - use nix::sys::termios::{BRKINT, CS8, ECHO, ICANON, ICRNL, IEXTEN, INPCK, ISIG, ISTRIP, IXON, - /*OPOST, */VMIN, VTIME}; - if !is_a_tty(StandardStream::StdIn) { - return Err(error::ReadlineError::from_errno(Errno::ENOTTY)); +impl<R: Read> RawReader<R> { + pub fn new(stdin: R) -> Result<RawReader<R>> { + Ok(RawReader { chars: char_iter::chars(stdin) }) + } + + // As there is no read timeout to properly handle single ESC key, + // we make possible to deactivate escape sequence processing. + pub fn next_key(&mut self, esc_seq: bool) -> Result<KeyPress> { + let c = try!(self.next_char()); + + let mut key = consts::char_to_key_press(c); + if esc_seq && key == KeyPress::Esc { + // escape sequence + key = try!(self.escape_sequence()); } - let original_termios = try!(termios::tcgetattr(libc::STDIN_FILENO)); - let mut raw = original_termios; - // disable BREAK interrupt, CR to NL conversion on input, - // input parity check, strip high bit (bit 8), output flow control - raw.c_iflag = raw.c_iflag & !(BRKINT | ICRNL | INPCK | ISTRIP | IXON); - // we don't want raw output, it turns newlines into straight linefeeds - //raw.c_oflag = raw.c_oflag & !(OPOST); // disable all output processing - raw.c_cflag = raw.c_cflag | (CS8); // character-size mark (8 bits) - // disable echoing, canonical mode, extended input processing and signals - raw.c_lflag = raw.c_lflag & !(ECHO | ICANON | IEXTEN | ISIG); - raw.c_cc[VMIN] = 1; // One character-at-a-time input - raw.c_cc[VTIME] = 0; // with blocking read - try!(termios::tcsetattr(libc::STDIN_FILENO, termios::TCSAFLUSH, &raw)); - - // Set the original terminal to the struct field - self.original_termios = Some(original_termios); - Ok(()) + Ok(key) } - /// Disable Raw mode for the term - fn disable_raw_mode(&self) -> Result<()> { - try!(termios::tcsetattr(libc::STDIN_FILENO, - termios::TCSAFLUSH, - &self.original_termios.expect("RAW MODE was not enabled previously"))); - Ok(()) + pub fn next_char(&mut self) -> Result<char> { + match self.chars.next() { + Some(c) => { + Ok(try!(c)) // TODO SIGWINCH + } + None => Err(error::ReadlineError::Eof), + } } -} -/// Ensure that RAW mode is disabled even in the case of a panic! -#[allow(unused_must_use)] -impl Drop for UnixTerminal { - fn drop(&mut self) { - self.disable_raw_mode(); + fn escape_sequence(&mut self) -> Result<KeyPress> { + // Read the next two bytes representing the escape sequence. + let seq1 = try!(self.next_char()); + if seq1 == '[' { + // ESC [ sequences. + let seq2 = try!(self.next_char()); + if seq2.is_digit(10) { + // Extended escape, read additional byte. + let seq3 = try!(self.next_char()); + if seq3 == '~' { + match seq2 { + '3' => Ok(KeyPress::Delete), + // TODO '1' // Home + // TODO '4' // End + _ => Ok(KeyPress::UNKNOWN_ESC_SEQ), + } + } else { + Ok(KeyPress::UNKNOWN_ESC_SEQ) + } + } else { + match seq2 { + 'A' => Ok(KeyPress::Up), + 'B' => Ok(KeyPress::Down), + 'C' => Ok(KeyPress::Right), + 'D' => Ok(KeyPress::Left), + 'F' => Ok(KeyPress::End), + 'H' => Ok(KeyPress::Home), + _ => Ok(KeyPress::UNKNOWN_ESC_SEQ), + } + } + } else if seq1 == 'O' { + // ESC O sequences. + let seq2 = try!(self.next_char()); + match seq2 { + 'F' => Ok(KeyPress::End), + 'H' => Ok(KeyPress::Home), + _ => Ok(KeyPress::UNKNOWN_ESC_SEQ), + } + } else { + // TODO ESC-N (n): search history forward not interactively + // TODO ESC-P (p): search history backward not interactively + // TODO ESC-R (r): Undo all changes made to this line. + match seq1 { + '\x08' => Ok(KeyPress::Meta('\x08')), // Backspace + '<' => Ok(KeyPress::Meta('<')), + '>' => Ok(KeyPress::Meta('>')), + 'b' | 'B' => Ok(KeyPress::Meta('B')), + 'c' | 'C' => Ok(KeyPress::Meta('C')), + 'd' | 'D' => Ok(KeyPress::Meta('D')), + 'f' | 'F' => Ok(KeyPress::Meta('F')), + 'l' | 'L' => Ok(KeyPress::Meta('L')), + 't' | 'T' => Ok(KeyPress::Meta('T')), + 'u' | 'U' => Ok(KeyPress::Meta('U')), + 'y' | 'Y' => Ok(KeyPress::Meta('Y')), + '\x7f' => Ok(KeyPress::Meta('\x7f')), // Delete + _ => { + // writeln!(io::stderr(), "key: {:?}, seq1: {:?}", KeyPress::Esc, seq1).unwrap(); + Ok(KeyPress::UNKNOWN_ESC_SEQ) + } + } + } } } diff --git a/src/tty/windows.rs b/src/tty/windows.rs index c7fc35b911083bac836b614ed7589c38818d2823..2244d8ce12c285793ee57aaf95204c1d024a8ec2 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -2,109 +2,192 @@ extern crate kernel32; extern crate winapi; use std::io; -use super::StandardStream; -use super::Terminal; +use std::marker::PhantomData; use ::Result; +pub type Handle = winapi::HANDLE; +pub type Mode = winapi::DWORD; +pub const STDIN_FILENO: winapi::DWORD = winapi::STD_INPUT_HANDLE; +pub const STDOUT_FILENO: winapi::DWORD = winapi::STD_OUTPUT_HANDLE; + +fn get_std_handle(fd: winapi::DWORD) -> Result<winapi::HANDLE> { + let handle = unsafe { kernel32::GetStdHandle(fd) }; + if handle == winapi::INVALID_HANDLE_VALUE { + try!(Err(io::Error::last_os_error())); + } else if handle.is_null() { + try!(Err(io::Error::new(io::ErrorKind::Other, + "no stdio handle available for this process"))); + } + Ok(handle) +} + macro_rules! check { - ($funcall:expr) => ( - if $funcall == 0 { - return Err(From::from(io::Error::last_os_error())); + ($funcall:expr) => { + { + let rc = unsafe { $funcall }; + if rc == 0 { + try!(Err(io::Error::last_os_error())); } - ); + rc + } + }; } /// Try to get the number of columns in the current terminal, or assume 80 if it fails. -pub fn get_columns() -> usize { - // Get HANDLE to stdout - let handle = unsafe { kernel32::GetStdHandle(winapi::STD_OUTPUT_HANDLE) }; - - // Create CONSOLE_SCREEN_BUFFER_INFO with some default values - let mut csbi = winapi::wincon::CONSOLE_SCREEN_BUFFER_INFO { - dwSize: winapi::wincon::COORD { X: 0, Y: 0 }, - dwCursorPosition: winapi::wincon::COORD { X: 0, Y: 0 }, - wAttributes: 0, - srWindow: winapi::wincon::SMALL_RECT { - Left: 0, - Top: 0, - Right: 0, - Bottom: 0, - }, - dwMaximumWindowSize: winapi::wincon::COORD { X: 0, Y: 0 }, - }; - - let success: bool = unsafe { kernel32::GetConsoleScreenBufferInfo(handle, &mut csbi) != 0 }; - - // If we were not able to retrieve console info successfully, - // we will default to a column size of 80 - if success && csbi.dwSize.X > 0 { - csbi.dwSize.X as usize - } else { - 80 +pub fn get_columns(handle: Handle) -> usize { + let mut info = unsafe { mem::zeroed() }; + match unsafe { kernel32::GetConsoleScreenBufferInfo(handle, &mut info) } { + 0 => 80, + _ => info.dwSize.X as usize, } } -/// Get WindowsTerminal struct -pub fn get_terminal() -> WindowsTerminal { - WindowsTerminal{ original_mode: None } -} - /// Checking for an unsupported TERM in windows is a no-op pub fn is_unsupported_term() -> bool { false } -/// Return whether or not STDIN, STDOUT or STDERR is a TTY -pub fn is_a_tty(stream: StandardStream) -> bool { - let handle = match stream { - StandardStream::StdIn => winapi::STD_INPUT_HANDLE, - StandardStream::StdOut => winapi::STD_OUTPUT_HANDLE, - }; +pub fn get_console_mode(handle: winapi::HANDLE) -> Result<Mode> { + let mut original_mode = 0; + check!(kernel32::GetConsoleMode(handle, &mut original_mode)); + Ok(original_mode) +} - unsafe { - let handle = kernel32::GetStdHandle(handle); - let mut out = 0; - kernel32::GetConsoleMode(handle, &mut out) != 0 +/// Return whether or not STDIN, STDOUT or STDERR is a TTY +pub fn is_a_tty(fd: winapi::DWORD) -> bool { + let handle = get_std_handle(fd); + match handle { + Ok(handle) => { + // If this function doesn't fail then fd is a TTY + get_console_mode(handle).is_ok() + } + Err(_) => false, } } -pub struct WindowsTerminal { - original_mode: Option<winapi::minwindef::DWORD> +/// Enable raw mode for the TERM +pub fn enable_raw_mode() -> Result<Mode> { + let handle = try!(get_std_handle(STDIN_FILENO)); + let original_mode = try!(get_console_mode(handle)); + let raw = original_mode & + !(winapi::wincon::ENABLE_LINE_INPUT | winapi::wincon::ENABLE_ECHO_INPUT | + winapi::wincon::ENABLE_PROCESSED_INPUT); + check!(kernel32::SetConsoleMode(handle, raw)); + Ok(original_mode) +} + +/// Disable Raw mode for the term +pub fn disable_raw_mode(original_mode: Mode) -> Result<()> { + let handle = try!(get_std_handle(STDIN_FILENO)); + check!(kernel32::SetConsoleMode(handle, original_mode)); + Ok(()) } -impl Terminal for WindowsTerminal { - /// Enable raw mode for the TERM - fn enable_raw_mode(&mut self) -> Result<()> { - let mut original_mode: winapi::minwindef::DWORD = 0; - unsafe { - let handle = kernel32::GetStdHandle(winapi::STD_INPUT_HANDLE); - check!(kernel32::GetConsoleMode(handle, &mut original_mode)); - check!(kernel32::SetConsoleMode( - handle, - original_mode & !(winapi::wincon::ENABLE_LINE_INPUT | - winapi::wincon::ENABLE_ECHO_INPUT | - winapi::wincon::ENABLE_PROCESSED_INPUT) - )); - }; - self.original_mode = Some(original_mode); - Ok(()) +pub fn stdout_handle() -> Result<Handle> { + let handle = try!(get_std_handle(STDOUT_FILENO)); + Ok(handle) +} + +/// Console input reader +pub struct RawReader<R> { + handle: winapi::HANDLE, + buf: Option<u16>, + phantom: PhantomData<R>, +} + +impl<R: Read> RawReader<R> { + pub fn new(_: R) -> Result<RawReader<R>> { + let handle = try!(get_std_handle(STDIN_FILENO)); + Ok(RawReader { + handle: handle, + buf: None, + phantom: PhantomData, + }) } - /// Disable Raw mode for the term - fn disable_raw_mode(&self) -> Result<()> { - unsafe { - let handle = kernel32::GetStdHandle(winapi::STD_INPUT_HANDLE); - check!(kernel32::SetConsoleMode(handle, - self.original_mode.expect("RAW MODE was not enabled previously"))); + pub fn next_key(&mut self, _: bool) -> Result<KeyPress> { + use std::char::decode_utf16; + + let mut rec: winapi::INPUT_RECORD = unsafe { mem::zeroed() }; + let mut count = 0; + let mut esc_seen = false; + loop { + // TODO GetNumberOfConsoleInputEvents + check!(kernel32::ReadConsoleInputW(self.handle, + &mut rec, + 1 as winapi::DWORD, + &mut count)); + + // TODO ENABLE_WINDOW_INPUT ??? + if rec.EventType == winapi::WINDOW_BUFFER_SIZE_EVENT { + SIGWINCH.store(true, atomic::Ordering::SeqCst); + return Err(error::ReadlineError::WindowResize); + } else if rec.EventType != winapi::KEY_EVENT { + continue; + } + let key_event = unsafe { rec.KeyEvent() }; + // writeln!(io::stderr(), "key_event: {:?}", key_event).unwrap(); + if key_event.bKeyDown == 0 && + key_event.wVirtualKeyCode != winapi::VK_MENU as winapi::WORD { + continue; + } + + let ctrl = key_event.dwControlKeyState & + (winapi::LEFT_CTRL_PRESSED | winapi::RIGHT_CTRL_PRESSED) != + 0; + let meta = (key_event.dwControlKeyState & + (winapi::LEFT_ALT_PRESSED | winapi::RIGHT_ALT_PRESSED) != + 0) || esc_seen; + + let utf16 = key_event.UnicodeChar; + if utf16 == 0 { + match key_event.wVirtualKeyCode as i32 { + winapi::VK_LEFT => return Ok(KeyPress::Left), + winapi::VK_RIGHT => return Ok(KeyPress::Right), + winapi::VK_UP => return Ok(KeyPress::Up), + winapi::VK_DOWN => return Ok(KeyPress::Down), + winapi::VK_DELETE => return Ok(KeyPress::Delete), + winapi::VK_HOME => return Ok(KeyPress::Home), + winapi::VK_END => return Ok(KeyPress::End), + _ => continue, + }; + } else if utf16 == 27 { + esc_seen = true; + continue; + } else { + // TODO How to support surrogate pair ? + self.buf = Some(utf16); + let orc = decode_utf16(self).next(); + if orc.is_none() { + return Err(error::ReadlineError::Eof); + } + let c = try!(orc.unwrap()); + if meta { + match c { + 'b' | 'B' => return Ok(KeyPress::Meta('B')), + 'c' | 'C' => return Ok(KeyPress::Meta('C')), + 'd' | 'D' => return Ok(KeyPress::Meta('D')), + 'f' | 'F' => return Ok(KeyPress::Meta('F')), + 'l' | 'L' => return Ok(KeyPress::Meta('L')), + 't' | 'T' => return Ok(KeyPress::Meta('T')), + 'u' | 'U' => return Ok(KeyPress::Meta('U')), + 'y' | 'Y' => return Ok(KeyPress::Meta('Y')), + _ => return Ok(KeyPress::UNKNOWN_ESC_SEQ), + } + } else { + return Ok(consts::char_to_key_press(c)); + } + } } - Ok(()) } } -/// Ensure that RAW mode is disabled even in the case of a panic! -#[allow(unused_must_use)] -impl Drop for WindowsTerminal { - fn drop(&mut self) { - self.disable_raw_mode(); +impl<R: Read> Iterator for RawReader<R> { + type Item = u16; + + fn next(&mut self) -> Option<u16> { + let buf = self.buf; + self.buf = None; + buf } }