| //! Readline for Rust |
| //! |
| //! This implementation is based on [Antirez's Linenoise](https://github.com/antirez/linenoise) |
| //! |
| //! # Example |
| //! |
| //! Usage |
| //! |
| //! ``` |
| //! let mut rl = rustyline::Editor::new(); |
| //! let readline = rl.readline(">> "); |
| //! match readline { |
| //! Ok(line) => println!("Line: {:?}",line), |
| //! Err(_) => println!("No input"), |
| //! } |
| //! ``` |
| #![cfg_attr(feature="clippy", feature(plugin))] |
| #![cfg_attr(feature="clippy", plugin(clippy))] |
| |
| extern crate libc; |
| extern crate nix; |
| extern crate unicode_width; |
| extern crate encode_unicode; |
| |
| pub mod completion; |
| #[allow(non_camel_case_types)] |
| mod consts; |
| pub mod error; |
| pub mod history; |
| mod kill_ring; |
| pub mod line_buffer; |
| mod char_iter; |
| |
| use std::fmt; |
| use std::io::{self, Write}; |
| use std::mem; |
| use std::path::Path; |
| use std::result; |
| use std::sync; |
| use std::sync::atomic; |
| use nix::errno::Errno; |
| use nix::sys::signal; |
| use nix::sys::termios; |
| use encode_unicode::CharExt; |
| |
| use completion::Completer; |
| use consts::{KeyPress, char_to_key_press}; |
| use history::History; |
| use line_buffer::{LineBuffer, MAX_LINE, WordAction}; |
| use kill_ring::KillRing; |
| |
| /// The error type for I/O and Linux Syscalls (Errno) |
| pub type Result<T> = result::Result<T, error::ReadlineError>; |
| |
| // Represent the state during line editing. |
| struct State<'out, 'prompt> { |
| out: &'out mut Write, |
| prompt: &'prompt str, // Prompt to display |
| prompt_size: Position, // Prompt Unicode width and height |
| 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. |
| snapshot: LineBuffer, // Current edited line before history browsing/completion |
| } |
| |
| #[derive(Copy, Clone, Debug, Default)] |
| struct Position { |
| col: usize, |
| row: usize, |
| } |
| |
| impl<'out, 'prompt> State<'out, 'prompt> { |
| fn new(out: &'out mut Write, |
| prompt: &'prompt str, |
| capacity: usize, |
| cols: usize, |
| history_index: usize) |
| -> State<'out, 'prompt> { |
| let prompt_size = calculate_position(prompt, Default::default(), cols); |
| State { |
| out: out, |
| prompt: prompt, |
| prompt_size: prompt_size, |
| line: LineBuffer::with_capacity(capacity), |
| cursor: prompt_size, |
| cols: cols, |
| history_index: history_index, |
| snapshot: LineBuffer::with_capacity(capacity), |
| } |
| } |
| |
| fn snapshot(&mut self) { |
| mem::swap(&mut self.line, &mut self.snapshot); |
| } |
| |
| fn backup(&mut self) { |
| self.snapshot.backup(&self.line); |
| } |
| |
| /// Rewrite the currently edited line accordingly to the buffer content, |
| /// cursor position, and number of columns of the terminal. |
| fn refresh_line(&mut self) -> Result<()> { |
| let prompt_size = self.prompt_size; |
| self.refresh(self.prompt, prompt_size) |
| } |
| |
| fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { |
| let prompt_size = calculate_position(prompt, Default::default(), self.cols); |
| self.refresh(prompt, prompt_size) |
| } |
| |
| fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> { |
| use std::fmt::Write; |
| |
| let end_pos = calculate_position(&self.line, prompt_size, self.cols); |
| 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 |
| if cursor_row_movement > 0 { |
| write!(ab, "\x1b[{}A", cursor_row_movement).unwrap(); |
| } |
| // position at the start of the prompt, clear to end of screen |
| ab.push_str("\r\x1b[J"); |
| // display the prompt |
| ab.push_str(prompt); |
| // display the input line |
| ab.push_str(&self.line); |
| // we have to generate our own newline on line wrap |
| if end_pos.col == 0 && end_pos.row > 0 { |
| ab.push_str("\n"); |
| } |
| // position the cursor |
| let cursor_row_movement = end_pos.row - cursor.row; |
| // move the cursor up as required |
| if cursor_row_movement > 0 { |
| write!(ab, "\x1b[{}A", cursor_row_movement).unwrap(); |
| } |
| // position the cursor within the line |
| if cursor.col > 0 { |
| write!(ab, "\r\x1b[{}C", cursor.col).unwrap(); |
| } else { |
| ab.push('\r'); |
| } |
| |
| self.cursor = cursor; |
| |
| write_and_flush(self.out, ab.as_bytes()) |
| } |
| } |
| |
| impl<'out, 'prompt> fmt::Debug for State<'out, 'prompt> { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| f.debug_struct("State") |
| .field("prompt", &self.prompt) |
| .field("prompt_size", &self.prompt_size) |
| .field("buf", &self.line) |
| .field("cursor", &self.cursor) |
| .field("cols", &self.cols) |
| .field("history_index", &self.history_index) |
| .field("snapshot", &self.snapshot) |
| .finish() |
| } |
| } |
| |
| /// Unsupported Terminals that don't support RAW mode |
| static UNSUPPORTED_TERM: [&'static str; 3] = ["dumb", "cons25", "emacs"]; |
| |
| /// Check to see if `fd` is a TTY |
| fn is_a_tty(fd: libc::c_int) -> bool { |
| unsafe { libc::isatty(fd) != 0 } |
| } |
| |
| /// Check to see if the current `TERM` is unsupported |
| fn is_unsupported_term() -> bool { |
| use std::ascii::AsciiExt; |
| match std::env::var("TERM") { |
| Ok(term) => { |
| let mut unsupported = false; |
| for iter in &UNSUPPORTED_TERM { |
| unsupported = (*iter).eq_ignore_ascii_case(&term) |
| } |
| unsupported |
| } |
| Err(_) => false, |
| } |
| } |
| |
| fn from_errno(errno: Errno) -> error::ReadlineError { |
| error::ReadlineError::from(nix::Error::from_errno(errno)) |
| } |
| |
| /// Enable raw mode for the TERM |
| fn enable_raw_mode() -> Result<termios::Termios> { |
| use nix::sys::termios::{BRKINT, CS8, ECHO, ICANON, ICRNL, IEXTEN, INPCK, ISIG, ISTRIP, IXON, |
| OPOST, VMIN, VTIME}; |
| if !is_a_tty(libc::STDIN_FILENO) { |
| return Err(from_errno(Errno::ENOTTY)); |
| } |
| let original_term = try!(termios::tcgetattr(libc::STDIN_FILENO)); |
| let mut raw = original_term; |
| raw.c_iflag = raw.c_iflag & !(BRKINT | ICRNL | INPCK | ISTRIP | IXON); // disable BREAK interrupt, CR to NL conversion on input, input parity check, strip high bit (bit 8), output flow control |
| raw.c_oflag = raw.c_oflag & !(OPOST); // disable all output processing |
| raw.c_cflag = raw.c_cflag | (CS8); // character-size mark (8 bits) |
| raw.c_lflag = raw.c_lflag & !(ECHO | ICANON | IEXTEN | ISIG); // disable echoing, canonical mode, extended input processing and signals |
| 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)); |
| Ok(original_term) |
| } |
| |
| /// Disable Raw mode for the term |
| fn disable_raw_mode(original_termios: termios::Termios) -> Result<()> { |
| try!(termios::tcsetattr(libc::STDIN_FILENO, termios::TCSAFLUSH, &original_termios)); |
| Ok(()) |
| } |
| |
| #[cfg(any(target_os = "macos", target_os = "freebsd"))] |
| const TIOCGWINSZ: libc::c_ulong = 0x40087468; |
| |
| #[cfg(any(target_os = "linux", target_os = "android"))] |
| const TIOCGWINSZ: libc::c_ulong = 0x5413; |
| |
| /// Try to get the number of columns in the current terminal, |
| /// or assume 80 if it fails. |
| #[cfg(any(target_os = "linux", |
| target_os = "android", |
| target_os = "macos", |
| target_os = "freebsd"))] |
| fn get_columns() -> usize { |
| use std::mem::zeroed; |
| use libc::c_ushort; |
| use libc; |
| |
| unsafe { |
| #[repr(C)] |
| struct winsize { |
| ws_row: c_ushort, |
| ws_col: c_ushort, |
| ws_xpixel: c_ushort, |
| ws_ypixel: c_ushort, |
| } |
| |
| let mut size: winsize = zeroed(); |
| match libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut size) { |
| 0 => size.ws_col as usize, // TODO getCursorPosition |
| _ => 80, |
| } |
| } |
| } |
| |
| fn write_and_flush(w: &mut Write, buf: &[u8]) -> Result<()> { |
| try!(w.write_all(buf)); |
| try!(w.flush()); |
| Ok(()) |
| } |
| |
| /// Clear the screen. Used to handle ctrl+l |
| fn clear_screen(out: &mut Write) -> Result<()> { |
| write_and_flush(out, b"\x1b[H\x1b[2J") |
| } |
| |
| /// Beep, used for completion when there is nothing to complete or when all |
| /// the choices were already shown. |
| fn beep() -> Result<()> { |
| write_and_flush(&mut io::stderr(), b"\x07") // TODO bell-style |
| } |
| |
| /// Calculate the number of columns and rows used to display `s` on a `cols` width terminal |
| /// starting at `orig`. |
| /// Control characters are treated as having zero width. |
| /// Characters with 2 column width are correctly handled (not splitted). |
| #[cfg_attr(feature="clippy", allow(if_same_then_else))] |
| fn calculate_position(s: &str, orig: Position, cols: usize) -> Position { |
| let mut pos = orig; |
| let mut esc_seq = 0; |
| for c in s.chars() { |
| let cw = if esc_seq == 1 { |
| if c == '[' { |
| // CSI |
| esc_seq = 2; |
| } else { |
| // two-character sequence |
| esc_seq = 0; |
| } |
| None |
| } else if esc_seq == 2 { |
| if c == ';' || (c >= '0' && c <= '9') { |
| } else if c == 'm' { |
| // last |
| esc_seq = 0; |
| } else { |
| // not supported |
| esc_seq = 0; |
| } |
| None |
| } else if c == '\x1b' { |
| esc_seq = 1; |
| None |
| } else if c == '\n' { |
| pos.col = 0; |
| pos.row += 1; |
| None |
| } else { |
| unicode_width::UnicodeWidthChar::width(c) |
| }; |
| if let Some(cw) = cw { |
| pos.col += cw; |
| if pos.col > cols { |
| pos.row += 1; |
| pos.col = cw; |
| } |
| } |
| } |
| if pos.col == cols { |
| pos.col = 0; |
| pos.row += 1; |
| } |
| pos |
| } |
| |
| /// Insert the character `ch` at cursor current position. |
| fn edit_insert(s: &mut State, ch: char) -> Result<()> { |
| if let Some(push) = s.line.insert(ch) { |
| 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. |
| write_and_flush(s.out, ch.to_utf8().as_bytes()) |
| } else { |
| s.refresh_line() |
| } |
| } else { |
| s.refresh_line() |
| } |
| } else { |
| Ok(()) |
| } |
| } |
| |
| // Yank/paste `text` at current position. |
| fn edit_yank(s: &mut State, text: &str) -> Result<()> { |
| if let Some(_) = s.line.yank(text) { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| // Delete previously yanked text and yank/paste `text` at current position. |
| fn edit_yank_pop(s: &mut State, yank_size: usize, text: &str) -> Result<()> { |
| s.line.yank_pop(yank_size, text); |
| edit_yank(s, text) |
| } |
| |
| /// Move cursor on the left. |
| fn edit_move_left(s: &mut State) -> Result<()> { |
| if s.line.move_left() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Move cursor on the right. |
| fn edit_move_right(s: &mut State) -> Result<()> { |
| if s.line.move_right() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Move cursor to the start of the line. |
| fn edit_move_home(s: &mut State) -> Result<()> { |
| if s.line.move_home() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Move cursor to the end of the line. |
| fn edit_move_end(s: &mut State) -> Result<()> { |
| if s.line.move_end() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Delete the character at the right of the cursor without altering the cursor |
| /// position. Basically this is what happens with the "Delete" keyboard key. |
| fn edit_delete(s: &mut State) -> Result<()> { |
| if s.line.delete() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Backspace implementation. |
| fn edit_backspace(s: &mut State) -> Result<()> { |
| if s.line.backspace() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Kill the text from point to the end of the line. |
| fn edit_kill_line(s: &mut State) -> Result<Option<String>> { |
| if let Some(text) = s.line.kill_line() { |
| try!(s.refresh_line()); |
| Ok(Some(text)) |
| } else { |
| Ok(None) |
| } |
| } |
| |
| /// Kill backward from point to the beginning of the line. |
| fn edit_discard_line(s: &mut State) -> Result<Option<String>> { |
| if let Some(text) = s.line.discard_line() { |
| try!(s.refresh_line()); |
| Ok(Some(text)) |
| } else { |
| Ok(None) |
| } |
| } |
| |
| /// Exchange the char before cursor with the character at cursor. |
| fn edit_transpose_chars(s: &mut State) -> Result<()> { |
| if s.line.transpose_chars() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| fn edit_move_to_prev_word(s: &mut State) -> Result<()> { |
| if s.line.move_to_prev_word() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Delete the previous word, maintaining the cursor at the start of the |
| /// current word. |
| fn edit_delete_prev_word<F>(s: &mut State, test: F) -> Result<Option<String>> |
| where F: Fn(char) -> bool |
| { |
| if let Some(text) = s.line.delete_prev_word(test) { |
| try!(s.refresh_line()); |
| Ok(Some(text)) |
| } else { |
| Ok(None) |
| } |
| } |
| |
| fn edit_move_to_next_word(s: &mut State) -> Result<()> { |
| if s.line.move_to_next_word() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Kill from the cursor to the end of the current word, or, if between words, to the end of the next word. |
| fn edit_delete_word(s: &mut State) -> Result<Option<String>> { |
| if let Some(text) = s.line.delete_word() { |
| try!(s.refresh_line()); |
| Ok(Some(text)) |
| } else { |
| Ok(None) |
| } |
| } |
| |
| fn edit_word(s: &mut State, a: WordAction) -> Result<()> { |
| if s.line.edit_word(a) { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| fn edit_transpose_words(s: &mut State) -> Result<()> { |
| if s.line.transpose_words() { |
| s.refresh_line() |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Substitute the currently edited line with the next or previous history |
| /// entry. |
| fn edit_history_next(s: &mut State, history: &History, prev: bool) -> Result<()> { |
| if history.is_empty() { |
| return Ok(()); |
| } |
| if s.history_index == history.len() { |
| if prev { |
| // Save the current edited line before to overwrite it |
| s.snapshot(); |
| } else { |
| return Ok(()); |
| } |
| } else if s.history_index == 0 && prev { |
| return Ok(()); |
| } |
| if prev { |
| s.history_index -= 1; |
| } else { |
| s.history_index += 1; |
| } |
| if s.history_index < history.len() { |
| let buf = history.get(s.history_index).unwrap(); |
| s.line.update(buf, buf.len()); |
| } else { |
| // 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>> { |
| let (start, candidates) = try!(completer.complete(&s.line, s.line.pos())); |
| if candidates.is_empty() { |
| try!(beep()); |
| Ok(None) |
| } else { |
| // Save the current edited line before to overwrite it |
| s.backup(); |
| let mut ch; |
| let mut i = 0; |
| loop { |
| // Show completion or original buffer |
| if i < candidates.len() { |
| completer.update(&mut s.line, start, &candidates[i]); |
| try!(s.refresh_line()); |
| } else { |
| // Restore current edited line |
| s.snapshot(); |
| try!(s.refresh_line()); |
| s.snapshot(); |
| } |
| |
| ch = try!(chars.next().unwrap()); |
| let key = char_to_key_press(ch); |
| match key { |
| KeyPress::TAB => { |
| i = (i + 1) % (candidates.len() + 1); // Circular |
| if i == candidates.len() { |
| try!(beep()); |
| } |
| } |
| KeyPress::ESC => { |
| // Re-show original buffer |
| s.snapshot(); |
| if i < candidates.len() { |
| try!(s.refresh_line()); |
| } |
| return Ok(None); |
| } |
| _ => { |
| break; |
| } |
| } |
| } |
| Ok(Some(ch)) |
| } |
| } |
| |
| /// 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>> { |
| // Save the current edited line (and cursor position) before to overwrite it |
| s.snapshot(); |
| |
| let mut search_buf = String::new(); |
| let mut history_idx = history.len() - 1; |
| let mut reverse = true; |
| let mut success = true; |
| |
| let mut ch; |
| let mut key; |
| // Display the reverse-i-search prompt and process chars |
| loop { |
| let prompt = if success { |
| format!("(reverse-i-search)`{}': ", search_buf) |
| } else { |
| format!("(failed reverse-i-search)`{}': ", search_buf) |
| }; |
| try!(s.refresh_prompt_and_line(&prompt)); |
| |
| ch = try!(chars.next().unwrap()); |
| if !ch.is_control() { |
| search_buf.push(ch); |
| } else { |
| key = char_to_key_press(ch); |
| if key == KeyPress::ESC { |
| key = try!(escape_sequence(chars)); |
| } |
| match key { |
| KeyPress::CTRL_H | KeyPress::BACKSPACE => { |
| search_buf.pop(); |
| continue; |
| } |
| KeyPress::CTRL_R => { |
| reverse = true; |
| if history_idx > 0 { |
| history_idx -= 1; |
| } else { |
| success = false; |
| continue; |
| } |
| } |
| KeyPress::CTRL_S => { |
| reverse = false; |
| if history_idx < history.len() - 1 { |
| history_idx += 1; |
| } else { |
| success = false; |
| continue; |
| } |
| } |
| KeyPress::CTRL_G => { |
| // Restore current edited line (before search) |
| s.snapshot(); |
| try!(s.refresh_line()); |
| return Ok(None); |
| } |
| _ => break, |
| } |
| } |
| success = match history.search(&search_buf, history_idx, reverse) { |
| Some(idx) => { |
| history_idx = idx; |
| let entry = history.get(idx).unwrap(); |
| let pos = entry.find(&search_buf).unwrap(); |
| s.line.update(entry, pos); |
| true |
| } |
| _ => false, |
| }; |
| } |
| 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(prompt: &str, |
| history: &mut History, |
| completer: Option<&Completer>, |
| kill_ring: &mut KillRing, |
| original_termios: termios::Termios) |
| -> Result<String> { |
| let mut stdout = io::stdout(); |
| try!(write_and_flush(&mut stdout, prompt.as_bytes())); |
| |
| kill_ring.reset(); |
| let mut s = State::new(&mut stdout, prompt, MAX_LINE, get_columns(), history.len()); |
| let stdin = io::stdin(); |
| let mut chars = char_iter::chars(stdin.lock()); |
| loop { |
| let c = chars.next().unwrap(); |
| if c.is_err() && SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) { |
| s.cols = get_columns(); |
| try!(s.refresh_line()); |
| continue; |
| } |
| let mut ch = try!(c); |
| if !ch.is_control() { |
| kill_ring.reset(); |
| try!(edit_insert(&mut s, ch)); |
| 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 next.is_some() { |
| kill_ring.reset(); |
| ch = next.unwrap(); |
| if !ch.is_control() { |
| try!(edit_insert(&mut s, ch)); |
| continue; |
| } |
| key = char_to_key_press(ch); |
| } else { |
| continue; |
| } |
| } else if key == KeyPress::CTRL_R { |
| // Search history backward |
| let next = try!(reverse_incremental_search(&mut chars, &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; |
| } |
| } |
| |
| match key { |
| KeyPress::CTRL_A => { |
| kill_ring.reset(); |
| // Move to the beginning of line. |
| try!(edit_move_home(&mut s)) |
| } |
| KeyPress::CTRL_B => { |
| kill_ring.reset(); |
| // Move back a character. |
| try!(edit_move_left(&mut s)) |
| } |
| KeyPress::CTRL_C => { |
| kill_ring.reset(); |
| return Err(error::ReadlineError::Interrupted); |
| } |
| KeyPress::CTRL_D => { |
| kill_ring.reset(); |
| if s.line.is_empty() { |
| return Err(error::ReadlineError::Eof); |
| } else { |
| // Delete (forward) one character at point. |
| try!(edit_delete(&mut s)) |
| } |
| } |
| KeyPress::CTRL_E => { |
| kill_ring.reset(); |
| // Move to the end of line. |
| try!(edit_move_end(&mut s)) |
| } |
| KeyPress::CTRL_F => { |
| kill_ring.reset(); |
| // Move forward a character. |
| try!(edit_move_right(&mut s)) |
| } |
| KeyPress::CTRL_H | KeyPress::BACKSPACE => { |
| kill_ring.reset(); |
| // Delete one character backward. |
| try!(edit_backspace(&mut s)) |
| } |
| 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 => { |
| // Clear the screen leaving the current line at the top of the screen. |
| try!(clear_screen(s.out)); |
| try!(s.refresh_line()) |
| } |
| KeyPress::CTRL_N => { |
| kill_ring.reset(); |
| // Fetch the next command from the history list. |
| try!(edit_history_next(&mut s, history, false)) |
| } |
| KeyPress::CTRL_P => { |
| kill_ring.reset(); |
| // Fetch the previous command from the history list. |
| try!(edit_history_next(&mut s, history, true)) |
| } |
| 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 => { |
| // 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 => { |
| // Quoted insert |
| kill_ring.reset(); |
| let c = chars.next().unwrap(); |
| let ch = try!(c); |
| try!(edit_insert(&mut s, ch)) |
| } |
| 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 => { |
| // retrieve (yank) last item killed |
| if let Some(text) = kill_ring.yank() { |
| try!(edit_yank(&mut s, text)) |
| } |
| } |
| KeyPress::CTRL_Z => { |
| try!(disable_raw_mode(original_termios)); |
| try!(signal::raise(signal::SIGSTOP)); |
| try!(enable_raw_mode()); // TODO original_termios may have changed |
| try!(s.refresh_line()) |
| } |
| // TODO CTRL-_ // undo |
| 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 => { |
| // 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, |
| |ch| !ch.is_alphanumeric())) { |
| kill_ring.kill(&text, false) |
| } |
| } |
| KeyPress::ESC_B => { |
| // move backwards one word |
| kill_ring.reset(); |
| try!(edit_move_to_prev_word(&mut s)) |
| } |
| KeyPress::ESC_C => { |
| // capitalize word after point |
| kill_ring.reset(); |
| try!(edit_word(&mut s, WordAction::CAPITALIZE)) |
| } |
| KeyPress::ESC_D => { |
| // kill one word forward |
| if let Some(text) = try!(edit_delete_word(&mut s)) { |
| kill_ring.kill(&text, true) |
| } |
| } |
| KeyPress::ESC_F => { |
| // move forwards one word |
| kill_ring.reset(); |
| try!(edit_move_to_next_word(&mut s)) |
| } |
| KeyPress::ESC_L => { |
| // lowercase word after point |
| kill_ring.reset(); |
| try!(edit_word(&mut s, WordAction::LOWERCASE)) |
| } |
| KeyPress::ESC_T => { |
| // transpose words |
| kill_ring.reset(); |
| try!(edit_transpose_words(&mut s)) |
| } |
| KeyPress::ESC_U => { |
| // uppercase word after point |
| kill_ring.reset(); |
| try!(edit_word(&mut s, WordAction::UPPERCASE)) |
| } |
| KeyPress::ESC_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 => { |
| kill_ring.reset(); |
| try!(edit_delete(&mut s)) |
| } |
| _ => { |
| kill_ring.reset(); |
| // Insert the character typed. |
| try!(edit_insert(&mut s, ch)) |
| } |
| } |
| } |
| Ok(s.line.into_string()) |
| } |
| |
| struct Guard(termios::Termios); |
| |
| #[allow(unused_must_use)] |
| impl Drop for Guard { |
| fn drop(&mut self) { |
| let Guard(termios) = *self; |
| disable_raw_mode(termios); |
| } |
| } |
| |
| /// Readline method that will enable RAW mode, call the `readline_edit()` |
| /// method and disable raw mode |
| fn readline_raw(prompt: &str, |
| history: &mut History, |
| completer: Option<&Completer>, |
| kill_ring: &mut KillRing) |
| -> Result<String> { |
| let original_termios = try!(enable_raw_mode()); |
| let guard = Guard(original_termios); |
| let user_input = readline_edit(prompt, history, completer, kill_ring, original_termios); |
| drop(guard); // try!(disable_raw_mode(original_termios)); |
| println!(""); |
| user_input |
| } |
| |
| fn readline_direct() -> Result<String> { |
| let mut line = String::new(); |
| if try!(io::stdin().read_line(&mut line)) > 0 { |
| Ok(line) |
| } else { |
| Err(error::ReadlineError::Eof) |
| } |
| } |
| |
| /// Line editor |
| pub struct Editor<'completer> { |
| unsupported_term: bool, |
| stdin_isatty: bool, |
| stdout_isatty: bool, |
| // cols: usize, // Number of columns in terminal |
| history: History, |
| completer: Option<&'completer Completer>, |
| kill_ring: KillRing, |
| } |
| |
| impl<'completer> Editor<'completer> { |
| pub fn new() -> Editor<'completer> { |
| // TODO check what is done in rl_initialize() |
| // if the number of columns is stored here, we need a SIGWINCH handler... |
| let editor = Editor { |
| unsupported_term: is_unsupported_term(), |
| stdin_isatty: is_a_tty(libc::STDIN_FILENO), |
| stdout_isatty: is_a_tty(libc::STDOUT_FILENO), |
| history: History::new(), |
| completer: None, |
| kill_ring: KillRing::new(60), |
| }; |
| if !editor.unsupported_term && editor.stdin_isatty && editor.stdout_isatty { |
| install_sigwinch_handler(); |
| } |
| editor |
| } |
| |
| /// This method will read a line from STDIN and will display a `prompt` |
| #[cfg_attr(feature="clippy", allow(if_not_else))] |
| pub fn readline(&mut self, prompt: &str) -> Result<String> { |
| if self.unsupported_term { |
| // Write prompt and flush it to stdout |
| let mut stdout = io::stdout(); |
| try!(write_and_flush(&mut stdout, prompt.as_bytes())); |
| |
| readline_direct() |
| } else if !self.stdin_isatty { |
| // Not a tty: read from file / pipe. |
| readline_direct() |
| } else { |
| readline_raw(prompt, |
| &mut self.history, |
| self.completer, |
| &mut self.kill_ring) |
| } |
| } |
| |
| /// Load the history from the specified file. |
| pub fn load_history<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> { |
| self.history.load(path) |
| } |
| /// Save the history in the specified file. |
| pub fn save_history<P: AsRef<Path> + ?Sized>(&self, path: &P) -> Result<()> { |
| self.history.save(path) |
| } |
| /// Add a new entry in the history. |
| pub fn add_history_entry(&mut self, line: &str) -> bool { |
| self.history.add(line) |
| } |
| /// Set the maximum length for the history. |
| pub fn set_history_max_len(&mut self, max_len: usize) { |
| self.history.set_max_len(max_len) |
| } |
| /// Clear history. |
| pub fn clear_history(&mut self) { |
| self.history.clear() |
| } |
| /// Return a reference to the history object. |
| pub fn get_history(&mut self) -> &mut History { |
| &mut self.history |
| } |
| |
| /// Register a callback function to be called for tab-completion. |
| pub fn set_completer(&mut self, completer: Option<&'completer Completer>) { |
| self.completer = completer; |
| } |
| } |
| |
| impl<'completer> Default for Editor<'completer> { |
| fn default() -> Editor<'completer> { |
| Editor::new() |
| } |
| } |
| |
| impl<'completer> fmt::Debug for Editor<'completer> { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| f.debug_struct("State") |
| .field("unsupported_term", &self.unsupported_term) |
| .field("stdin_isatty", &self.stdin_isatty) |
| .finish() |
| } |
| } |
| |
| static SIGWINCH_ONCE: sync::Once = sync::ONCE_INIT; |
| static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT; |
| fn install_sigwinch_handler() { |
| SIGWINCH_ONCE.call_once(|| unsafe { |
| let sigwinch = signal::SigAction::new(signal::SigHandler::Handler(sigwinch_handler), |
| signal::SaFlag::empty(), |
| signal::SigSet::empty()); |
| let _ = signal::sigaction(signal::SIGWINCH, &sigwinch); |
| }); |
| } |
| extern "C" fn sigwinch_handler(_: signal::SigNum) { |
| SIGWINCH.store(true, atomic::Ordering::SeqCst); |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use std::io::Write; |
| use line_buffer::LineBuffer; |
| use history::History; |
| use completion::Completer; |
| use State; |
| use super::Result; |
| |
| fn init_state<'out>(out: &'out mut Write, |
| line: &str, |
| pos: usize, |
| cols: usize) |
| -> State<'out, 'static> { |
| State { |
| out: out, |
| prompt: "", |
| prompt_size: Default::default(), |
| line: LineBuffer::init(line, pos), |
| cursor: Default::default(), |
| cols: cols, |
| history_index: 0, |
| snapshot: LineBuffer::with_capacity(100), |
| } |
| } |
| |
| #[test] |
| fn edit_history_next() { |
| let mut out = ::std::io::sink(); |
| let line = "current edited line"; |
| let mut s = init_state(&mut out, line, 6, 80); |
| let mut history = History::new(); |
| history.add("line0"); |
| history.add("line1"); |
| s.history_index = history.len(); |
| |
| for _ in 0..2 { |
| super::edit_history_next(&mut s, &history, false).unwrap(); |
| assert_eq!(line, s.line.as_str()); |
| } |
| |
| super::edit_history_next(&mut s, &history, true).unwrap(); |
| assert_eq!(line, s.snapshot.as_str()); |
| assert_eq!(1, s.history_index); |
| assert_eq!("line1", s.line.as_str()); |
| |
| for _ in 0..2 { |
| super::edit_history_next(&mut s, &history, true).unwrap(); |
| assert_eq!(line, s.snapshot.as_str()); |
| assert_eq!(0, s.history_index); |
| assert_eq!("line0", s.line.as_str()); |
| } |
| |
| super::edit_history_next(&mut s, &history, false).unwrap(); |
| assert_eq!(line, s.snapshot.as_str()); |
| assert_eq!(1, s.history_index); |
| assert_eq!("line1", s.line.as_str()); |
| |
| super::edit_history_next(&mut s, &history, false).unwrap(); |
| // assert_eq!(line, s.snapshot); |
| assert_eq!(2, s.history_index); |
| assert_eq!(line, s.line.as_str()); |
| } |
| |
| struct SimpleCompleter; |
| impl Completer for SimpleCompleter { |
| fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>)> { |
| Ok((0, vec![line.to_string() + "t"])) |
| } |
| } |
| |
| #[test] |
| 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 completer = SimpleCompleter; |
| let ch = super::complete_line(&mut chars, &mut s, &completer).unwrap(); |
| assert_eq!(Some('\n'), ch); |
| assert_eq!("rust", s.line.as_str()); |
| assert_eq!(4, s.line.pos()); |
| } |
| |
| #[test] |
| fn prompt_with_ansi_escape_codes() { |
| let pos = super::calculate_position("\x1b[1;32m>>\x1b[0m ", Default::default(), 80); |
| assert_eq!(3, pos.col); |
| assert_eq!(0, pos.row); |
| } |
| } |