Newer
Older
//!
//!This implementation is based on [Antirez's Linenoise](https://github.com/antirez/linenoise)
//!
//!# Example
//!
//!Usage
//!
//!```
//!let readline = rustyline::readline(">> ", &mut None);
//!match readline {
//! Ok(line) => println!("Line: {:?}",line),
//! Err(_) => println!("No input"),
//! }
//!```
#![feature(io)]
#![feature(str_char)]
#![feature(unicode)]
extern crate nix;
extern crate unicode_width;
Katsu Kawakami
committed
use std::io;
use std::io::{Write, Read};
use nix::errno::Errno;
use nix::sys::termios;
use consts::{KeyPress, char_to_key_press};
/// 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<'prompt> {
prompt: &'prompt str, // Prompt to display
prompt_width: usize, // Prompt Unicode width
buf: String, // Edited line buffer
pos: usize, // Current cursor position (byte position)
cols: usize, // Number of columns in terminal
history_index: usize, // The history index we are currently editing.
bytes: [u8; 4],
impl<'prompt> State<'prompt> {
fn new(prompt: &'prompt str, capacity: usize, cols: usize) -> State<'prompt> {
State {
prompt: prompt,
prompt_width: unicode_width::UnicodeWidthStr::width(prompt),
buf: String::with_capacity(capacity),
pos: 0,
cols: cols,
static MAX_LINE: usize = 4096;
/// Unsupported Terminals that don't support RAW mode
static UNSUPPORTED_TERM: [&'static str; 3] = ["dumb","cons25","emacs"];
let isatty = unsafe { libc::isatty(libc::STDIN_FILENO as i32) } != 0;
isatty
}
/// Check to see if the current `TERM` is unsupported
use std::ascii::AsciiExt;
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))
}
fn enable_raw_mode() -> Result<termios::Termios> {
use nix::sys::termios::{BRKINT, ICRNL, INPCK, ISTRIP, IXON, OPOST, CS8, ECHO, ICANON, IEXTEN, ISIG, VMIN, VTIME};
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);
raw.c_oflag = raw.c_oflag & !(OPOST);
raw.c_cflag = raw.c_cflag | (CS8);
raw.c_lflag = raw.c_lflag & !(ECHO | ICANON | IEXTEN | ISIG);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
try!(termios::tcsetattr(libc::STDIN_FILENO, termios::TCSAFLUSH, &raw));
Ok(original_term)
}
fn disable_raw_mode(original_termios: termios::Termios) -> Result<()> {
try!(termios::tcsetattr(libc::STDIN_FILENO,
termios::TCSAFLUSH,
&original_termios));
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#[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 nix::sys::ioctl;
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 ioctl::read_into(libc::STDOUT_FILENO, TIOCGWINSZ, &mut size) {
Ok(_) => size.ws_col as usize, // TODO getCursorPosition
Err(_) => 80,
}
}
}
fn write_and_flush(w: &mut Write, buf: &[u8]) -> Result<()> {
try!(w.write_all(buf));
try!(w.flush());
/// Clear the screen. Used to handle ctrl+l
fn clear_screen(stdout: &mut io::Stdout) -> Result<()> {
write_and_flush(stdout, 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")
}*/
// Control characters are treated as having zero width.
fn width(s: &str) -> usize {
unicode_width::UnicodeWidthStr::width(s)
}
/// Rewrite the currently edited line accordingly to the buffer content,
/// cursor position, and number of columns of the terminal.
fn refresh_line(s: &mut State, stdout: &mut Write) -> Result<()> {
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
use std::fmt::Write;
use unicode_width::UnicodeWidthChar;
let buf = &s.buf;
let mut start = 0;
let mut w1 = width(&buf[start..s.pos]);
while s.prompt_width + w1 >= s.cols {
let ch = buf.char_at(start);
start += ch.len_utf8();
w1 -= UnicodeWidthChar::width(ch).unwrap_or(0);
}
let mut end = buf.len();
let mut w2 = width(&buf[start..end]);
while s.prompt_width + w2 > s.cols {
let ch = buf.char_at_reverse(end);
end -= ch.len_utf8();
w2 -= UnicodeWidthChar::width(ch).unwrap_or(0);
}
let mut ab = String::new();
// Cursor to left edge
ab.push('\r');
// Write the prompt and the current buffer content
ab.push_str(s.prompt);
ab.push_str(&s.buf[start..end]);
// Erase to right
ab.push_str("\x1b[0K");
// Move cursor to original position.
ab.write_fmt(format_args!("\r\x1b[{}C", w1 + s.prompt_width)).unwrap();
write_and_flush(stdout, ab.as_bytes())
}
/// Insert the character `ch` at cursor current position.
fn edit_insert(s: &mut State, stdout: &mut Write, ch: char) -> Result<()> {
if s.buf.len() < s.buf.capacity() {
if s.buf.len() == s.pos {
s.buf.push(ch);
let size = ch.encode_utf8(&mut s.bytes).unwrap();
s.pos += size;
if s.prompt_width + width(&s.buf) < s.cols {
// Avoid a full update of the line in the trivial case.
write_and_flush(stdout, &mut s.bytes[0..size])
} else {
refresh_line(s, stdout)
}
} else {
s.buf.insert(s.pos, ch);
refresh_line(s, stdout)
}
} else {
Ok(())
}
}
/// Move cursor on the left.
fn edit_move_left(s: &mut State, stdout: &mut Write) -> Result<()> {
if s.pos > 0 {
let ch = s.buf.char_at_reverse(s.pos);
refresh_line(s, stdout)
} else {
Ok(())
}
}
/// Move cursor on the right.
fn edit_move_right(s: &mut State, stdout: &mut Write) -> Result<()> {
if s.pos != s.buf.len() {
let ch = s.buf.char_at(s.pos);
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
refresh_line(s, stdout)
} else {
Ok(())
}
}
/// Move cursor to the start of the line.
fn edit_move_home(s: &mut State, stdout: &mut Write) -> Result<()> {
if s.pos > 0 {
s.pos = 0;
refresh_line(s, stdout)
} else {
Ok(())
}
}
/// Move cursor to the end of the line.
fn edit_move_end(s: &mut State, stdout: &mut Write) -> Result<()> {
if s.pos != s.buf.len() {
s.pos = s.buf.len();
refresh_line(s, stdout)
} 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, stdout: &mut Write) -> Result<()> {
if s.buf.len() > 0 && s.pos < s.buf.len() {
s.buf.remove(s.pos);
refresh_line(s, stdout)
} else {
Ok(())
}
}
/// Backspace implementation.
fn edit_backspace(s: &mut State, stdout: &mut Write) -> Result<()> {
if s.pos > 0 && s.buf.len() > 0 {
let ch = s.buf.char_at_reverse(s.pos);
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
s.buf.remove(s.pos);
refresh_line(s, stdout)
} else {
Ok(())
}
}
/// Kill the text from point to the end of the line.
fn edit_kill_line(s: &mut State, stdout: &mut Write) -> Result<()> {
if s.buf.len() > 0 && s.pos < s.buf.len() {
s.buf.drain(s.pos..);
refresh_line(s, stdout)
} else {
Ok(())
}
}
/// Kill backward from point to the beginning of the line.
fn edit_discard_line(s: &mut State, stdout: &mut Write) -> Result<()> {
if s.pos > 0 && s.buf.len() > 0 {
s.buf.drain(..s.pos);
s.pos = 0;
refresh_line(s, stdout)
} else {
Ok(())
}
}
/// Exchange the char before cursor with the character at cursor.
fn edit_transpose_chars(s: &mut State, stdout: &mut Write) -> Result<()> {
if s.pos > 0 && s.pos < s.buf.len() {
let ch = s.buf.remove(s.pos);
let size = ch.len_utf8();
let och = s.buf.char_at_reverse(s.pos);
let osize = och.len_utf8();
s.buf.insert(s.pos - osize, ch);
if s.pos != s.buf.len()-size {
s.pos += size;
} else {
if size >= osize {
s.pos += size - osize;
} else {
s.pos -= osize - size;
}
}
refresh_line(s, stdout)
} else {
Ok(())
}
}
/// Delete the previous word, maintaining the cursor at the start of the
/// current word.
fn edit_delete_prev_word(s: &mut State, stdout: &mut Write) -> Result<()> {
if s.pos > 0 {
let old_pos = s.pos;
let mut ch = s.buf.char_at_reverse(s.pos);
while s.pos > 0 && ch.is_whitespace() {
s.pos -= ch.len_utf8();
ch = s.buf.char_at_reverse(s.pos);
}
while s.pos > 0 && !ch.is_whitespace() {
s.pos -= ch.len_utf8();
ch = s.buf.char_at_reverse(s.pos);
}
s.buf.drain(s.pos..old_pos);
refresh_line(s, stdout)
} else {
Ok(())
}
}
/// Substitute the currently edited line with the next or previous history
/// entry.
fn edit_history_next(s: &mut State, history: &mut History, stdout: &mut Write, prev: bool) -> Result<()> {
if history.len() > 1 {
unimplemented!();
//s.buf = ;
//s.pos = s.buf.len();
//refresh_line(s, stdout)
} else {
Ok(())
}
}
/// 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(prompt: &str, history: &mut Option<History>) -> Result<String> {
let mut stdout = io::stdout();
try!(write_and_flush(&mut stdout, prompt.as_bytes()));
let mut s = State::new(prompt, MAX_LINE, get_columns());
let stdin = io::stdin();
let mut chars = stdin.lock().chars();
Main
committed
loop {
let ch = try!(chars.next().unwrap());
match char_to_key_press(ch) {
KeyPress::CTRL_A => try!(edit_move_home(&mut s, &mut stdout)), // Move to the beginning of line.
KeyPress::CTRL_B => try!(edit_move_left(&mut s, &mut stdout)), // Move back a character.
KeyPress::CTRL_C => {
return Err(from_errno(Errno::EAGAIN))
},
KeyPress::CTRL_D => {
if s.buf.len() > 0 { // Delete one character at point.
try!(edit_delete(&mut s, &mut stdout))
} else {
break
}
},
KeyPress::CTRL_E => try!(edit_move_end(&mut s, &mut stdout)), // Move to the end of line.
KeyPress::CTRL_F => try!(edit_move_right(&mut s, &mut stdout)), // Move forward a character.
KeyPress::CTRL_H | KeyPress::BACKSPACE => try!(edit_backspace(&mut s, &mut stdout)), // Delete one character backward.
KeyPress::CTRL_K => try!(edit_kill_line(&mut s, &mut stdout)), // Kill the text from point to the end of the line.
KeyPress::CTRL_L => { // Clear the screen leaving the current line at the top of the screen.
try!(clear_screen(&mut stdout));
try!(refresh_line(&mut s, &mut stdout))
},
KeyPress::CTRL_N => { // Fetch the next command from the history list.
if history.is_some() {
try!(edit_history_next(&mut s, history.as_mut().unwrap(), &mut stdout, false))
}
},
KeyPress::CTRL_P => { // Fetch the previous command from the history list.
if history.is_some() {
try!(edit_history_next(&mut s, history.as_mut().unwrap(), &mut stdout, true))
}
},
KeyPress::CTRL_T => try!(edit_transpose_chars(&mut s, &mut stdout)), // Exchange the char before cursor with the character at cursor.
KeyPress::CTRL_U => try!(edit_discard_line(&mut s, &mut stdout)), // Kill backward from point to the beginning of the line.
KeyPress::CTRL_W => try!(edit_delete_prev_word(&mut s, &mut stdout)), // Kill the word behind point, using white space as a word boundary
KeyPress::ESC => print!("Pressed esc"),
KeyPress::ENTER => break, // Accept the line regardless of where the cursor is.
_ => try!(edit_insert(&mut s, &mut stdout, ch)), // Insert the character typed.
Main
committed
}
}
/// Readline method that will enable RAW mode, call the ```readline_edit()```
/// method and disable raw mode
fn readline_raw(prompt: &str, history: &mut Option<History>) -> Result<String> {
if is_a_tty() {
let original_termios = try!(enable_raw_mode());
let user_input = readline_edit(prompt, history);
try!(disable_raw_mode(original_termios));
user_input
} else {
readline_direct()
}
}
fn readline_direct() -> Result<String> {
let mut line = String::new();
try!(io::stdin().read_line(&mut line));
Ok(line)
}
/// This method will read a line from STDIN and will display a `prompt`
pub fn readline(prompt: &str, history: &mut Option<History>) -> Result<String> {
// Write prompt and flush it to stdout
let mut stdout = io::stdout();
try!(write_and_flush(&mut stdout, prompt.as_bytes()));
readline_direct()
#[cfg(test)]
mod test {
use State;
fn init_state(line: &str, pos: usize, cols: usize) -> State<'static> {
State {
prompt: "",
prompt_width: 0,
buf: String::from(line),
pos: pos,
cols: cols,
let mut stdout = ::std::io::sink();
super::edit_insert(&mut s, &mut stdout, 'α').unwrap();
assert_eq!("α", s.buf);
assert_eq!(2, s.pos);
super::edit_insert(&mut s, &mut stdout, 'ß').unwrap();
assert_eq!("αß", s.buf);
assert_eq!(4, s.pos);
s.pos = 0;
super::edit_insert(&mut s, &mut stdout, 'γ').unwrap();
assert_eq!("γαß", s.buf);
assert_eq!(2, s.pos);
}
#[test]
let mut stdout = ::std::io::sink();
super::edit_move_left(&mut s, &mut stdout).unwrap();
assert_eq!("αß", s.buf);
assert_eq!(2, s.pos);
super::edit_move_right(&mut s, &mut stdout).unwrap();
assert_eq!("αß", s.buf);
assert_eq!(4, s.pos);
super::edit_move_home(&mut s, &mut stdout).unwrap();
assert_eq!("αß", s.buf);
assert_eq!(0, s.pos);
super::edit_move_end(&mut s, &mut stdout).unwrap();
assert_eq!("αß", s.buf);
assert_eq!(4, s.pos);
}
#[test]
let mut stdout = ::std::io::sink();
super::edit_delete(&mut s, &mut stdout).unwrap();
assert_eq!("α", s.buf);
assert_eq!(2, s.pos);
super::edit_backspace(&mut s, &mut stdout).unwrap();
assert_eq!("", s.buf);
assert_eq!(0, s.pos);
}
#[test]
let mut stdout = ::std::io::sink();
super::edit_kill_line(&mut s, &mut stdout).unwrap();
assert_eq!("αßγ", s.buf);
assert_eq!(6, s.pos);
s.pos = 4;
super::edit_discard_line(&mut s, &mut stdout).unwrap();
assert_eq!("γ", s.buf);
assert_eq!(0, s.pos);
}
#[test]
let mut stdout = ::std::io::sink();
super::edit_transpose_chars(&mut s, &mut stdout).unwrap();
assert_eq!("ßac", s.buf);
assert_eq!(3, s.pos);
s.buf = String::from("aßc");
s.pos = 3;
super::edit_transpose_chars(&mut s, &mut stdout).unwrap();
assert_eq!("acß", s.buf);
assert_eq!(2, s.pos);
}
#[test]
fn delete_prev_word() {
let mut stdout = ::std::io::sink();
super::edit_delete_prev_word(&mut s, &mut stdout).unwrap();
assert_eq!("a c", s.buf);
assert_eq!(2, s.pos);
}