| //! Unix specific definitions |
| use std; |
| use std::io::{self, Chars, Read, Stdout, Write}; |
| use std::sync; |
| use std::sync::atomic; |
| |
| use libc; |
| use nix; |
| use nix::poll; |
| use nix::sys::signal; |
| use nix::sys::termios; |
| use unicode_width::UnicodeWidthChar; |
| |
| use config::Config; |
| use consts::{self, KeyPress}; |
| use Result; |
| use error; |
| use line_buffer::LineBuffer; |
| use super::{Position, RawMode, RawReader, Renderer, Term}; |
| |
| const STDIN_FILENO: libc::c_int = libc::STDIN_FILENO; |
| 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"]; |
| |
| fn get_win_size() -> (usize, usize) { |
| use std::mem::zeroed; |
| |
| unsafe { |
| let mut size: libc::winsize = zeroed(); |
| match libc::ioctl(STDOUT_FILENO, libc::TIOCGWINSZ, &mut size) { |
| 0 => (size.ws_col as usize, size.ws_row as usize), // TODO getCursorPosition |
| _ => (80, 24), |
| } |
| } |
| } |
| |
| /// Check TERM environment variable to see if current term is in our |
| /// unsupported list |
| fn is_unsupported_term() -> bool { |
| use std::ascii::AsciiExt; |
| match std::env::var("TERM") { |
| Ok(term) => { |
| for iter in &UNSUPPORTED_TERM { |
| if (*iter).eq_ignore_ascii_case(&term) { |
| return true; |
| } |
| } |
| false |
| } |
| Err(_) => false, |
| } |
| } |
| |
| |
| /// Return whether or not STDIN, STDOUT or STDERR is a TTY |
| fn is_a_tty(fd: libc::c_int) -> bool { |
| unsafe { libc::isatty(fd) != 0 } |
| } |
| |
| pub type Mode = termios::Termios; |
| |
| impl RawMode for Mode { |
| /// Disable RAW mode for the terminal. |
| fn disable_raw_mode(&self) -> Result<()> { |
| try!(termios::tcsetattr(STDIN_FILENO, termios::TCSADRAIN, self)); |
| Ok(()) |
| } |
| } |
| |
| // Rust std::io::Stdin is buffered with no way to know if bytes are available. |
| // So we use low-level stuff instead... |
| struct StdinRaw {} |
| |
| impl Read for StdinRaw { |
| fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
| loop { |
| let res = unsafe { |
| libc::read(STDIN_FILENO, |
| buf.as_mut_ptr() as *mut libc::c_void, |
| buf.len() as libc::size_t) |
| }; |
| if res == -1 { |
| let error = io::Error::last_os_error(); |
| if error.kind() != io::ErrorKind::Interrupted { |
| return Err(error); |
| } |
| } else { |
| return Ok(res as usize); |
| } |
| } |
| } |
| } |
| |
| /// Console input reader |
| pub struct PosixRawReader { |
| chars: Chars<StdinRaw>, |
| timeout_ms: i32, |
| } |
| |
| impl PosixRawReader { |
| fn new(config: &Config) -> Result<PosixRawReader> { |
| let stdin = StdinRaw {}; |
| Ok(PosixRawReader { |
| chars: stdin.chars(), |
| timeout_ms: config.keyseq_timeout(), |
| }) |
| } |
| |
| 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 == '~' { |
| Ok(match seq2 { |
| '1' | '7' => KeyPress::Home, // '1': xterm |
| '3' => KeyPress::Delete, |
| '4' | '8' => KeyPress::End, // '4': xterm |
| '5' => KeyPress::PageUp, |
| '6' => KeyPress::PageDown, |
| _ => { |
| debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}{:?}", seq1, seq2, seq3); |
| KeyPress::UnknownEscSeq |
| } |
| }) |
| } else { |
| debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}{:?}", seq1, seq2, seq3); |
| Ok(KeyPress::UnknownEscSeq) |
| } |
| } else { |
| Ok(match seq2 { |
| 'A' => KeyPress::Up, // ANSI |
| 'B' => KeyPress::Down, |
| 'C' => KeyPress::Right, |
| 'D' => KeyPress::Left, |
| 'F' => KeyPress::End, |
| 'H' => KeyPress::Home, |
| _ => { |
| debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}", seq1, seq2); |
| KeyPress::UnknownEscSeq |
| } |
| }) |
| } |
| } else if seq1 == 'O' { |
| // ESC O sequences. |
| let seq2 = try!(self.next_char()); |
| Ok(match seq2 { |
| 'A' => KeyPress::Up, |
| 'B' => KeyPress::Down, |
| 'C' => KeyPress::Right, |
| 'D' => KeyPress::Left, |
| 'F' => KeyPress::End, |
| 'H' => KeyPress::Home, |
| _ => { |
| debug!(target: "rustyline", "unsupported esc sequence: ESC{:?}{:?}", seq1, seq2); |
| KeyPress::UnknownEscSeq |
| } |
| }) |
| } else { |
| // TODO ESC-R (r): Undo all changes made to this line. |
| Ok(match seq1 { |
| '\x08' => KeyPress::Meta('\x08'), // Backspace |
| '-' => KeyPress::Meta('-'), |
| '0'...'9' => KeyPress::Meta(seq1), |
| '<' => KeyPress::Meta('<'), |
| '>' => KeyPress::Meta('>'), |
| 'b' | 'B' => KeyPress::Meta('B'), |
| 'c' | 'C' => KeyPress::Meta('C'), |
| 'd' | 'D' => KeyPress::Meta('D'), |
| 'f' | 'F' => KeyPress::Meta('F'), |
| 'l' | 'L' => KeyPress::Meta('L'), |
| 'n' | 'N' => KeyPress::Meta('N'), |
| 'p' | 'P' => KeyPress::Meta('P'), |
| 't' | 'T' => KeyPress::Meta('T'), |
| 'u' | 'U' => KeyPress::Meta('U'), |
| 'y' | 'Y' => KeyPress::Meta('Y'), |
| '\x7f' => KeyPress::Meta('\x7f'), // Delete |
| _ => { |
| debug!(target: "rustyline", "unsupported esc sequence: M-{:?}", seq1); |
| KeyPress::UnknownEscSeq |
| } |
| }) |
| } |
| } |
| } |
| |
| impl RawReader for PosixRawReader { |
| fn next_key(&mut self) -> Result<KeyPress> { |
| let c = try!(self.next_char()); |
| |
| let mut key = consts::char_to_key_press(c); |
| if key == KeyPress::Esc { |
| let mut fds = |
| [poll::PollFd::new(STDIN_FILENO, poll::POLLIN, poll::EventFlags::empty())]; |
| match poll::poll(&mut fds, self.timeout_ms) { |
| Ok(n) if n == 0 => { |
| // single escape |
| } |
| Ok(_) => { |
| // escape sequence |
| key = try!(self.escape_sequence()) |
| } |
| // Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, |
| Err(e) => return Err(e.into()), |
| } |
| } |
| debug!(target: "rustyline", "key: {:?}", key); |
| Ok(key) |
| } |
| |
| fn next_char(&mut self) -> Result<char> { |
| match self.chars.next() { |
| Some(c) => Ok(try!(c)), |
| None => Err(error::ReadlineError::Eof), |
| } |
| } |
| } |
| |
| /// Console output writer |
| pub struct PosixRenderer { |
| out: Stdout, |
| cols: usize, // Number of columns in terminal |
| } |
| |
| impl PosixRenderer { |
| fn new() -> PosixRenderer { |
| let (cols, _) = get_win_size(); |
| PosixRenderer { |
| out: io::stdout(), |
| cols: cols, |
| } |
| } |
| } |
| |
| impl Renderer for PosixRenderer { |
| fn move_cursor(&mut self, old: Position, new: Position) -> Result<()> { |
| use std::fmt::Write; |
| let mut ab = String::new(); |
| if new.row > old.row { |
| // move down |
| let row_shift = new.row - old.row; |
| if row_shift == 1 { |
| ab.push_str("\x1b[B"); |
| } else { |
| write!(ab, "\x1b[{}B", row_shift).unwrap(); |
| } |
| } else if new.row < old.row { |
| // move up |
| let row_shift = old.row - new.row; |
| if row_shift == 1 { |
| ab.push_str("\x1b[A"); |
| } else { |
| write!(ab, "\x1b[{}A", row_shift).unwrap(); |
| } |
| } |
| if new.col > old.col { |
| // move right |
| let col_shift = new.col - old.col; |
| if col_shift == 1 { |
| ab.push_str("\x1b[C"); |
| } else { |
| write!(ab, "\x1b[{}C", col_shift).unwrap(); |
| } |
| } else if new.col < old.col { |
| // move left |
| let col_shift = old.col - new.col; |
| if col_shift == 1 { |
| ab.push_str("\x1b[D"); |
| } else { |
| write!(ab, "\x1b[{}D", col_shift).unwrap(); |
| } |
| } |
| self.write_and_flush(ab.as_bytes()) |
| } |
| |
| fn refresh_line(&mut self, |
| prompt: &str, |
| prompt_size: Position, |
| line: &LineBuffer, |
| current_row: usize, |
| old_rows: usize) |
| -> Result<(Position, Position)> { |
| use std::fmt::Write; |
| let mut ab = String::new(); |
| |
| // calculate the position of the end of the input line |
| let end_pos = self.calculate_position(line, prompt_size); |
| // calculate the desired position of the cursor |
| let cursor = self.calculate_position(&line[..line.pos()], prompt_size); |
| |
| let cursor_row_movement = old_rows - current_row; |
| // move the cursor down as required |
| if cursor_row_movement > 0 { |
| write!(ab, "\x1b[{}B", cursor_row_movement).unwrap(); |
| } |
| // clear old rows |
| for _ in 0..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 |
| ab.push_str(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'); |
| } |
| |
| try!(self.write_and_flush(ab.as_bytes())); |
| Ok((cursor, end_pos)) |
| } |
| |
| fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> { |
| try!(self.out.write_all(buf)); |
| try!(self.out.flush()); |
| Ok(()) |
| } |
| |
| /// Control characters are treated as having zero width. |
| /// Characters with 2 column width are correctly handled (not splitted). |
| #[allow(if_same_then_else)] |
| fn calculate_position(&self, s: &str, orig: Position) -> 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 { |
| c.width() |
| }; |
| if let Some(cw) = cw { |
| pos.col += cw; |
| if pos.col > self.cols { |
| pos.row += 1; |
| pos.col = cw; |
| } |
| } |
| } |
| if pos.col == self.cols { |
| pos.col = 0; |
| pos.row += 1; |
| } |
| pos |
| } |
| |
| /// Clear the screen. Used to handle ctrl+l |
| fn clear_screen(&mut self) -> Result<()> { |
| self.write_and_flush(b"\x1b[H\x1b[2J") |
| } |
| |
| /// Check if a SIGWINCH signal has been received |
| fn sigwinch(&self) -> bool { |
| SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) |
| } |
| |
| /// Try to update the number of columns in the current terminal, |
| fn update_size(&mut self) { |
| let (cols, _) = get_win_size(); |
| self.cols = cols; |
| } |
| |
| fn get_columns(&self) -> usize { |
| self.cols |
| } |
| /// Try to get the number of rows in the current terminal, |
| /// or assume 24 if it fails. |
| fn get_rows(&self) -> usize { |
| let (_, rows) = get_win_size(); |
| rows |
| } |
| } |
| |
| 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::SaFlags::empty(), |
| signal::SigSet::empty()); |
| let _ = signal::sigaction(signal::SIGWINCH, &sigwinch); |
| }); |
| } |
| |
| extern "C" fn sigwinch_handler(_: libc::c_int) { |
| SIGWINCH.store(true, atomic::Ordering::SeqCst); |
| debug!(target: "rustyline", "SIGWINCH"); |
| } |
| |
| pub type Terminal = PosixTerminal; |
| |
| #[derive(Clone,Debug)] |
| pub struct PosixTerminal { |
| unsupported: bool, |
| stdin_isatty: bool, |
| } |
| |
| impl Term for PosixTerminal { |
| type Reader = PosixRawReader; |
| type Writer = PosixRenderer; |
| type Mode = Mode; |
| |
| fn new() -> PosixTerminal { |
| let term = PosixTerminal { |
| unsupported: is_unsupported_term(), |
| stdin_isatty: is_a_tty(STDIN_FILENO), |
| }; |
| if !term.unsupported && term.stdin_isatty && is_a_tty(STDOUT_FILENO) { |
| install_sigwinch_handler(); |
| } |
| term |
| } |
| |
| // Init checks: |
| |
| /// Check if current terminal can provide a rich line-editing user interface. |
| fn is_unsupported(&self) -> bool { |
| self.unsupported |
| } |
| |
| /// check if stdin is connected to a terminal. |
| fn is_stdin_tty(&self) -> bool { |
| self.stdin_isatty |
| } |
| |
| // Interactive loop: |
| |
| fn enable_raw_mode(&self) -> 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 !self.stdin_isatty { |
| 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 &= !(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 |= CS8; // character-size mark (8 bits) |
| // disable echoing, canonical mode, extended input processing and signals |
| 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::TCSADRAIN, &raw)); |
| Ok(original_mode) |
| } |
| |
| /// Create a RAW reader |
| fn create_reader(&self, config: &Config) -> Result<PosixRawReader> { |
| PosixRawReader::new(config) |
| } |
| |
| fn create_writer(&self) -> PosixRenderer { |
| PosixRenderer::new() |
| } |
| } |
| |
| #[cfg(unix)] |
| pub fn suspend() -> Result<()> { |
| // suspend the whole process group |
| try!(signal::kill(0, signal::SIGTSTP)); |
| Ok(()) |
| } |
| |
| #[cfg(all(unix,test))] |
| mod test { |
| use std::io::{self, Stdout}; |
| use super::{Position, Renderer}; |
| |
| #[test] |
| fn prompt_with_ansi_escape_codes() { |
| let out = io::stdout(); |
| let pos = out.calculate_position("\x1b[1;32m>>\x1b[0m ", Position::default(), 80); |
| assert_eq!(3, pos.col); |
| assert_eq!(0, pos.row); |
| } |
| |
| #[test] |
| fn test_unsupported_term() { |
| ::std::env::set_var("TERM", "xterm"); |
| assert_eq!(false, super::is_unsupported_term()); |
| |
| ::std::env::set_var("TERM", "dumb"); |
| assert_eq!(true, super::is_unsupported_term()); |
| } |
| } |