Newer
Older
//! This implementation is based on [Antirez's Linenoise](https://github.com/antirez/linenoise)
//! let config = rustyline::Config::default();
//! let mut rl = rustyline::Editor::<()>::new(config);
//! Ok(line) => println!("Line: {:?}",line),
//! Err(_) => println!("No input"),
//! }
#![feature(io)]
#![feature(unicode)]
extern crate nix;
extern crate unicode_width;
#[cfg(windows)]
extern crate winapi;
#[cfg(windows)]
extern crate kernel32;
pub mod completion;
use std::path::Path;
use completion::{Completer, longest_common_prefix};
pub use config::{CompletionType, Config, HistoryDuplicates};
/// 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
cursor: Position, // Cursor position (relative to the start of the prompt for `row`)
cols: usize, // Number of columns in terminal
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)]
struct Position {
col: usize,
row: usize,
output_handle: tty::Handle,
prompt: &'prompt str,
history_index: usize)
let cols = tty::get_columns(output_handle);
let prompt_size = calculate_position(prompt, Position::default(), cols);
out: out,
prompt: prompt,
prompt_size: prompt_size,
line: LineBuffer::with_capacity(capacity),
cursor: prompt_size,
cols: cols,
old_rows: prompt_size.row,
history_index: history_index,
snapshot: LineBuffer::with_capacity(capacity),
/// 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, Position::default(), self.cols);
fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> {
// 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.old_rows - self.cursor.row;
// move the cursor down as required
write!(ab, "\x1b[{}B", cursor_row_movement).unwrap();
}
// 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");
// 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 {
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<()> {
// 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));
}
fn update_columns(&mut self) {
self.cols = tty::get_columns(self.output_handle);
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("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());
/// Beep, used for completion when there is nothing to complete or when all
/// the choices were already shown.
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).
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;
// 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;
// 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.
let cursor = calculate_position(&s.line[..s.line.pos()], s.prompt_size, s.cols);
s.cursor = cursor;
let bits = ch.encode_utf8();
let bits = bits.as_slice();
write_and_flush(s.out, bits)
}
} else {
Ok(())
}
}
if let Some(_) = s.line.yank(text) {
s.refresh_line()
// Delete previously yanked text and yank/paste `text` at current position.
fn edit_yank_pop(s: &mut State, yank_size: usize, text: &str) -> Result<()> {
fn edit_move_left(s: &mut State) -> Result<()> {
} else {
Ok(())
}
}
/// Move cursor on the right.
fn edit_move_right(s: &mut State) -> Result<()> {
s.refresh_line()
} else {
Ok(())
}
}
/// Move cursor to the start of the line.
fn edit_move_home(s: &mut State) -> Result<()> {
} 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.
} else {
Ok(())
}
}
/// Backspace implementation.
fn edit_backspace(s: &mut State) -> Result<()> {
} else {
Ok(())
}
}
/// Kill the text from point to the end of the line.
}
}
/// Kill backward from point to the beginning of the line.
}
}
/// Exchange the char before cursor with the character at cursor.
fn edit_transpose_chars(s: &mut State) -> Result<()> {
} else {
Ok(())
}
}
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) {
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>> {
try!(s.refresh_line());
Ok(Some(text))
} else {
Ok(None)
}
}
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() {
} else if s.history_index == 0 && prev {
return Ok(());
}
if prev {
s.history_index -= 1;
if s.history_index < history.len() {
let buf = history.get(s.history_index).unwrap();
} else {
// Restore current edited line
s.snapshot();
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
}
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();
}
fn complete_line<R: Read>(rdr: &mut tty::RawReader<R>,
-> Result<Option<KeyPress>> {
let (start, candidates) = try!(completer.complete(&s.line, s.line.pos()));
if candidates.is_empty() {
try!(beep());
Ok(None)
} else if CompletionType::Circular == config.completion_type() {
// Save the current edited line before to overwrite it
s.backup();
let mut i = 0;
loop {
// Show completion or original buffer
if i < candidates.len() {
completer.update(&mut s.line, start, &candidates[i]);
// Restore current edited line
s.snapshot();
if i == candidates.len() {
try!(beep());
}
} else if CompletionType::List == config.completion_type() {
// beep if ambiguous
if candidates.len() > 1 {
try!(beep());
}
if let Some(lcp) = longest_common_prefix(&candidates) {
// if we can extend the item, extend it and return to main loop
if lcp.len() > s.line.pos() - start {
try!(s.refresh_line());
return Ok(None);
}
}
// if any character other than tab, pass it to the main loop
if key != KeyPress::Tab {
// move cursor to EOL to avoid overwriting the command line
let save_pos = s.line.pos();
try!(edit_move_end(s));
s.line.set_pos(save_pos);
// we got a second tab, maybe show list of possible completions
let mut show_completions = true;
if candidates.len() > config.completion_prompt_limit() {
let msg = format!("\nDisplay all {} possibilities? (y or n)", candidates.len());
try!(write_and_flush(s.out, msg.as_bytes()));
s.old_rows += 1;
while key != KeyPress::Char('y') && key != KeyPress::Char('Y') &&
key != KeyPress::Char('n') && key != KeyPress::Char('N') &&
key != KeyPress::Backspace {
key = try!(rdr.next_key(true));
}
show_completions = match key {
KeyPress::Char('y') |
KeyPress::Char('Y') => true,
_ => false,
};
}
if show_completions {
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
fn page_completions<R: Read>(rdr: &mut tty::RawReader<R>,
s: &mut State,
candidates: &[String])
-> Result<Option<KeyPress>> {
use std::cmp;
use unicode_width::UnicodeWidthStr;
let min_col_pad = 2;
let max_width = cmp::min(s.cols,
candidates.into_iter()
.map(|s| UnicodeWidthStr::width(s.as_str()))
.max()
.unwrap() + min_col_pad);
let num_cols = s.cols / max_width;
let mut pause_row = tty::get_rows(s.output_handle) - 1;
let num_rows = (candidates.len() + num_cols - 1) / num_cols;
let mut ab = String::new();
for row in 0..num_rows {
if row == pause_row {
try!(write_and_flush(s.out, b"\n--More--"));
let mut key = KeyPress::Null;
while key != KeyPress::Char('y') && key != KeyPress::Char('Y') &&
key != KeyPress::Char('n') && key != KeyPress::Char('N') &&
key != KeyPress::Char('q') &&
key != KeyPress::Char('Q') &&
key != KeyPress::Char(' ') &&
key != KeyPress::Backspace && key != KeyPress::Enter {
key = try!(rdr.next_key(true));
}
match key {
KeyPress::Char('y') |
KeyPress::Char('Y') |
KeyPress::Char(' ') => {
pause_row += tty::get_rows(s.output_handle) - 1;
}
KeyPress::Enter => {
pause_row += 1;
}
_ => break,
}
} else {
try!(write_and_flush(s.out, b"\n"));
}
ab.clear();
for col in 0..num_cols {
let i = (col * num_rows) + row;
if i < candidates.len() {
let candidate = &candidates[i];
ab.push_str(candidate);
let width = UnicodeWidthStr::width(candidate.as_str());
if ((col + 1) * num_rows) + row < candidates.len() {
for _ in width..max_width {
ab.push(' ');
}
}
}
}
try!(write_and_flush(s.out, ab.as_bytes()));
}
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
let mut search_buf = String::new();
let mut history_idx = history.len() - 1;
let mut success = true;
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));
if let KeyPress::Char(c) = key {
search_buf.push(c);
if history_idx > 0 {
history_idx -= 1;
} else {
success = false;
continue;
}
reverse = false;
if history_idx < history.len() - 1 {
history_idx += 1;
} else {
success = false;
continue;
}
}
// Restore current edited line (before search)
s.snapshot();
success = match history.search(&search_buf, history_idx, reverse) {
Some(idx) => {
history_idx = idx;
let entry = history.get(idx).unwrap();
};
}
Ok(Some(key))
}
/// Handles reading and editting the readline buffer.
/// It will also handle special inputs in an appropriate fashion
/// (e.g., C-c will exit readline)
fn readline_edit<C: Completer>(prompt: &str,
editor: &mut Editor<C>,
original_mode: tty::Mode)
-> Result<String> {
let completer = editor.completer.as_ref().map(|c| c as &Completer);
let mut stdout = io::stdout();
let stdout_handle = try!(tty::stdout_handle());
editor.kill_ring.reset();
let mut s = State::new(&mut stdout, stdout_handle, prompt, editor.history.len());
let mut rdr = try!(tty::RawReader::new(io::stdin()));
Main
committed
loop {
if rk.is_err() && tty::SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) {
let mut key = try!(rk);
if let KeyPress::Char(c) = key {
try!(edit_insert(&mut s, c));
let next = try!(complete_line(&mut rdr, &mut s, completer.unwrap(), &editor.config));
key = next.unwrap();
if let KeyPress::Char(c) = key {
try!(edit_insert(&mut s, c));
let next = try!(reverse_incremental_search(&mut rdr, &mut s, &editor.history));
// Move to the beginning of line.
try!(edit_move_home(&mut s))
}
// Delete one character backward.
try!(edit_backspace(&mut s))
}
try!(tty::clear_screen(&mut s.out, s.output_handle));
try!(edit_history_next(&mut s, &editor.history, false))
try!(edit_history_next(&mut s, &editor.history, true))
// Exchange the char before cursor with the character at cursor.
try!(edit_transpose_chars(&mut s))
}
let c = try!(rdr.next_char());
try!(edit_insert(&mut s, c)) // FIXME
if let Some(text) = try!(edit_delete_prev_word(&mut s, char::is_whitespace)) {
if let Some(text) = editor.kill_ring.yank() {
try!(tty::disable_raw_mode(original_mode));
try!(tty::enable_raw_mode()); // TODO original_mode may have changed
KeyPress::Meta('\x08') |
KeyPress::Meta('\x7f') => {
// 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())) {
KeyPress::Meta('<') => {
// move to first entry in history
editor.kill_ring.reset();
try!(edit_history(&mut s, &editor.history, true))
}
KeyPress::Meta('>') => {
// move to last entry in history
editor.kill_ring.reset();
try!(edit_history(&mut s, &editor.history, false))
// kill one word forward
if let Some(text) = try!(edit_delete_word(&mut s)) {