Skip to content
Snippets Groups Projects
Commit 74ae8585 authored by Gwenael Treguier's avatar Gwenael Treguier
Browse files

Implement refreshSingleLine & linenoiseEditInsert.

And attempt to support unicode input.
parent a51028de
No related branches found
No related tags found
No related merge requests found
......@@ -4,5 +4,6 @@ version = "0.0.1"
authors = ["Katsu Kawakami <kkawa1570@gmail.com>"]
[dependencies]
libc = "0.1.8"
nix = "0.3.9"
libc = "~0.1.8"
nix = "~0.3.9"
unicode-width = "~0.1.2"
......@@ -2,48 +2,48 @@
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum KeyPress {
NULL = 0,
CTRL_A = 1,
CTRL_B = 2,
CTRL_C = 3,
CTRL_D = 4,
CTRL_E = 5,
CTRL_F = 6,
CTRL_H = 8,
TAB = 9,
CTRL_K = 11,
CTRL_L = 12,
ENTER = 13,
CTRL_N = 14,
CTRL_P = 16,
CTRL_T = 20,
CTRL_U = 21,
CTRL_W = 23,
ESC = 27,
BACKSPACE = 127,
NULL = 0,
CTRL_A = 1,
CTRL_B = 2,
CTRL_C = 3,
CTRL_D = 4,
CTRL_E = 5,
CTRL_F = 6,
CTRL_H = 8,
TAB = 9,
CTRL_K = 11,
CTRL_L = 12,
ENTER = 13,
CTRL_N = 14,
CTRL_P = 16,
CTRL_T = 20,
CTRL_U = 21,
CTRL_W = 23,
ESC = 27,
BACKSPACE = 127,
}
pub fn u8_to_key_press(i: u8) -> KeyPress {
match i {
0 => KeyPress::NULL,
1 => KeyPress::CTRL_A,
2 => KeyPress::CTRL_B,
3 => KeyPress::CTRL_C,
4 => KeyPress::CTRL_D,
5 => KeyPress::CTRL_E,
6 => KeyPress::CTRL_F,
8 => KeyPress::CTRL_H,
9 => KeyPress::TAB,
11 => KeyPress::CTRL_K,
12 => KeyPress::CTRL_L,
13 => KeyPress::ENTER,
14 => KeyPress::CTRL_N,
16 => KeyPress::CTRL_P,
20 => KeyPress::CTRL_T,
21 => KeyPress::CTRL_U,
23 => KeyPress::CTRL_W,
27 => KeyPress::ESC,
127 => KeyPress::BACKSPACE,
pub fn char_to_key_press(c: char) -> KeyPress {
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,
'\x08' => KeyPress::CTRL_H,
'\x09' => KeyPress::TAB,
'\x0b' => KeyPress::CTRL_K,
'\x0c' => KeyPress::CTRL_L,
'\x0d' => KeyPress::ENTER,
'\x0e' => KeyPress::CTRL_N,
'\x10' => KeyPress::CTRL_P,
'\x14' => KeyPress::CTRL_T,
'\x15' => KeyPress::CTRL_U,
'\x17' => KeyPress::CTRL_W,
'\x1b' => KeyPress::ESC,
'\x7f' => KeyPress::BACKSPACE,
_ => KeyPress::NULL
}
}
}
......@@ -11,14 +11,17 @@ pub enum ReadlineError {
/// I/O Error
Io(io::Error),
/// Error from syscall
Errno(nix::Error)
Errno(nix::Error),
/// Chars Error
Char(io::CharsError)
}
impl fmt::Display for ReadlineError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ReadlineError::Io(ref err) => err.fmt(f),
ReadlineError::Errno(ref err) => write!(f, "Errno: {}", err.errno().desc())
ReadlineError::Errno(ref err) => write!(f, "Errno: {}", err.errno().desc()),
ReadlineError::Char(ref err) => err.fmt(f),
}
}
}
......@@ -27,14 +30,15 @@ impl error::Error for ReadlineError {
fn description(&self) -> &str {
match *self {
ReadlineError::Io(ref err) => err.description(),
ReadlineError::Errno(ref err) => err.errno().desc()
ReadlineError::Errno(ref err) => err.errno().desc(),
ReadlineError::Char(ref err) => err.description(),
}
}
}
impl From<io::Error> for ReadlineError {
fn from(err: io::Error) -> ReadlineError {
ReadlineError::Io(err)
ReadlineError::Io(err)
}
}
......@@ -43,3 +47,9 @@ impl From<nix::Error> for ReadlineError {
ReadlineError::Errno(err)
}
}
impl From<io::CharsError> for ReadlineError {
fn from(err: io::CharsError) -> ReadlineError {
ReadlineError::Char(err)
}
}
\ No newline at end of file
......@@ -13,8 +13,12 @@
//! Err(_) => println!("No input"),
//! }
//!```
#![feature(io)]
#![feature(str_char)]
#![feature(unicode)]
extern crate libc;
extern crate nix;
extern crate unicode_width;
#[allow(non_camel_case_types)]
pub mod consts;
......@@ -25,12 +29,23 @@ use std::io;
use std::io::{Write, Read};
use nix::errno::Errno;
use nix::sys::termios;
use nix::sys::termios::{BRKINT, ICRNL, INPCK, ISTRIP, IXON, OPOST, CS8, ECHO, ICANON, IEXTEN, ISIG, VMIN, VTIME};
use consts::{KeyPress, u8_to_key_press};
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
// oldpos: usize, // Previous refresh cursor position
cols: usize, // Number of columns in terminal
bytes: [u8; 4]
}
/// Maximum buffer size for the line read
static MAX_LINE: usize = 4096;
......@@ -45,11 +60,12 @@ fn is_a_tty() -> bool {
/// 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 = term == *iter
unsupported = (*iter).eq_ignore_ascii_case(&term)
}
unsupported
}
......@@ -57,12 +73,15 @@ fn is_unsupported_term() -> bool {
}
}
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, ICRNL, INPCK, ISTRIP, IXON, OPOST, CS8, ECHO, ICANON, IEXTEN, ISIG, VMIN, VTIME};
if !is_a_tty() {
Err(error::ReadlineError
::from(nix::Error
::from_errno(Errno::ENOTTY)))
Err(from_errno(Errno::ENOTTY))
} else {
let original_term = try!(termios::tcgetattr(libc::STDIN_FILENO));
let mut raw = original_term;
......@@ -85,18 +104,134 @@ fn disable_raw_mode(original_termios: termios::Termios) -> Result<()> {
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 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(stdout: &mut io::Stdout, buf: &[u8]) -> Result<()> {
try!(stdout.write_all(buf));
try!(stdout.flush());
Ok(())
}
// 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 io::Stdout) -> Result<()> {
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 'c' at cursor current position.
fn edit_insert(s: &mut State, stdout: &mut io::Stdout, 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(())
}
}
/// 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() -> Result<String> {
let mut buffer = Vec::new();
let mut input: [u8; 1] = [0];
fn readline_edit(prompt: &str) -> Result<String> {
let mut stdout = io::stdout();
try!(write_and_flush(&mut stdout, prompt.as_bytes()));
let mut s = State {
prompt: prompt,
prompt_width: unicode_width::UnicodeWidthStr::width(prompt),
buf: String::with_capacity(MAX_LINE),
pos: 0,
// oldpos: 0,
cols: get_columns(),
bytes: [0; 4],
};
let stdin = io::stdin();
let mut chars = stdin.lock().chars();
loop {
io::stdin().read(&mut input).unwrap();
match u8_to_key_press(input[0]) {
let ch = try!(chars.next().unwrap());
match char_to_key_press(ch) {
KeyPress::CTRL_A => print!("Pressed C-a"),
KeyPress::CTRL_B => print!("Pressed C-b"),
KeyPress::CTRL_C => print!("Pressed C-c"),
KeyPress::CTRL_C => {
return Err(from_errno(Errno::EAGAIN))
},
KeyPress::CTRL_D => print!("Pressed C-d"),
KeyPress::CTRL_E => print!("Pressed C-e"),
KeyPress::CTRL_F => print!("Pressed C-f"),
......@@ -110,40 +245,41 @@ fn readline_edit() -> Result<String> {
KeyPress::CTRL_W => print!("Pressed C-w"),
KeyPress::ESC => print!("Pressed esc") ,
KeyPress::ENTER => break,
_ => { print!("{}", input[0]); try!(io::stdout().flush()); }
_ => try!(edit_insert(&mut s, &mut stdout, ch)),
}
buffer.push(input[0]);
}
Ok(String::from_utf8(buffer).unwrap())
Ok(s.buf)
}
/// Readline method that will enable RAW mode, call the ```readline_edit()```
/// method and disable raw mode
fn readline_raw() -> Result<String> {
fn readline_raw(prompt: &str) -> Result<String> {
if is_a_tty() {
let original_termios = try!(enable_raw_mode());
let user_input = readline_edit();
let user_input = readline_edit(prompt);
try!(disable_raw_mode(original_termios));
println!("");
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: &'static str) -> Result<String> {
// Write prompt and flush it to stdout
let mut stdout = io::stdout();
try!(stdout.write(prompt.as_bytes()));
try!(stdout.flush());
pub fn readline(prompt: &str) -> Result<String> {
if is_unsupported_term() {
let mut line = String::new();
try!(io::stdin().read_line(&mut line));
Ok(line)
// Write prompt and flush it to stdout
let mut stdout = io::stdout();
try!(write_and_flush(&mut stdout, prompt.as_bytes()));
readline_direct()
} else {
readline_raw()
readline_raw(prompt)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment