blob: ffb81e9783b4bc95fce3e82facbf7c0b0e23b300 [file] [log] [blame]
//! Unix specific definitions
use std;
use std::io::{self, Read, Stdout, Write};
use std::sync;
use std::sync::atomic;
use libc;
use nix;
use nix::poll::{self, EventFlags};
use nix::sys::signal;
use nix::sys::termios;
use nix::sys::termios::SetArg;
use unicode_segmentation::UnicodeSegmentation;
use utf8parse::{Parser, Receiver};
use super::{truncate, width, Position, RawMode, RawReader, Renderer, Term};
use config::{ColorMode, Config};
use error;
use highlight::Highlighter;
use keys::{self, KeyPress};
use line_buffer::LineBuffer;
use Result;
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"];
#[allow(identity_conversion)]
fn get_win_size() -> (usize, usize) {
use std::mem::zeroed;
unsafe {
let mut size: libc::winsize = zeroed();
// https://github.com/rust-lang/libc/pull/704
// FIXME: ".into()" used as a temporary fix for a libc bug
match libc::ioctl(STDOUT_FILENO, libc::TIOCGWINSZ.into(), &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 {
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, SetArg::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
|| SIGWINCH.load(atomic::Ordering::Relaxed)
{
return Err(error);
}
} else {
return Ok(res as usize);
}
}
}
}
/// Console input reader
pub struct PosixRawReader {
stdin: StdinRaw,
timeout_ms: i32,
buf: [u8; 1],
parser: Parser,
receiver: Utf8,
}
struct Utf8 {
c: Option<char>,
valid: bool,
}
impl PosixRawReader {
fn new(config: &Config) -> Result<PosixRawReader> {
Ok(PosixRawReader {
stdin: StdinRaw {},
timeout_ms: config.keyseq_timeout(),
buf: [0; 1],
parser: Parser::new(),
receiver: Utf8 {
c: None,
valid: true,
},
})
}
/// Handle ESC <seq1> sequences
fn escape_sequence(&mut self) -> Result<KeyPress> {
// Read the next byte representing the escape sequence.
let seq1 = try!(self.next_char());
if seq1 == '[' {
// ESC [ sequences. (CSI)
self.escape_csi()
} else if seq1 == 'O' {
// xterm
// ESC O sequences. (SS3)
self.escape_o()
} else if seq1 == '\x1b' {
// ESC ESC
Ok(KeyPress::Esc)
} else {
// TODO ESC-R (r): Undo all changes made to this line.
Ok(KeyPress::Meta(seq1))
}
}
/// Handle ESC [ <seq2> escape sequences
fn escape_csi(&mut self) -> Result<KeyPress> {
let seq2 = try!(self.next_char());
if seq2.is_digit(10) {
match seq2 {
'0' | '9' => {
debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2);
Ok(KeyPress::UnknownEscSeq)
}
_ => {
// Extended escape, read additional byte.
self.extended_escape(seq2)
}
}
} else {
// ANSI
Ok(match seq2 {
'A' => KeyPress::Up, // kcuu1
'B' => KeyPress::Down, // kcud1
'C' => KeyPress::Right, // kcuf1
'D' => KeyPress::Left, // kcub1
'F' => KeyPress::End,
'H' => KeyPress::Home, // khome
_ => {
debug!(target: "rustyline", "unsupported esc sequence: ESC [ {:?}", seq2);
KeyPress::UnknownEscSeq
}
})
}
}
/// Handle ESC [ <seq2:digit> escape sequences
fn extended_escape(&mut self, seq2: char) -> Result<KeyPress> {
let seq3 = try!(self.next_char());
if seq3 == '~' {
Ok(match seq2 {
'1' | '7' => KeyPress::Home, // tmux, xrvt
'2' => KeyPress::Insert,
'3' => KeyPress::Delete, // kdch1
'4' | '8' => KeyPress::End, // tmux, xrvt
'5' => KeyPress::PageUp, // kpp
'6' => KeyPress::PageDown, // knp
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} ~", seq2);
KeyPress::UnknownEscSeq
}
})
} else if seq3.is_digit(10) {
let seq4 = try!(self.next_char());
if seq4 == '~' {
Ok(match (seq2, seq3) {
('1', '1') => KeyPress::F(1), // rxvt-unicode
('1', '2') => KeyPress::F(2), // rxvt-unicode
('1', '3') => KeyPress::F(3), // rxvt-unicode
('1', '4') => KeyPress::F(4), // rxvt-unicode
('1', '5') => KeyPress::F(5), // kf5
('1', '7') => KeyPress::F(6), // kf6
('1', '8') => KeyPress::F(7), // kf7
('1', '9') => KeyPress::F(8), // kf8
('2', '0') => KeyPress::F(9), // kf9
('2', '1') => KeyPress::F(10), // kf10
('2', '3') => KeyPress::F(11), // kf11
('2', '4') => KeyPress::F(12), // kf12
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} ~", seq2, seq3);
KeyPress::UnknownEscSeq
}
})
} else if seq4 == ';' {
let seq5 = try!(self.next_char());
if seq5.is_digit(10) {
let seq6 = try!(self.next_char()); // '~' expected
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} ; {} {}", seq2, seq3, seq5, seq6);
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} ; {:?}", seq2, seq3, seq5);
}
Ok(KeyPress::UnknownEscSeq)
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {}{} {:?}", seq2, seq3, seq4);
Ok(KeyPress::UnknownEscSeq)
}
} else if seq3 == ';' {
let seq4 = try!(self.next_char());
if seq4.is_digit(10) {
let seq5 = try!(self.next_char());
if seq2 == '1' {
Ok(match (seq4, seq5) {
('5', 'A') => KeyPress::ControlUp,
('5', 'B') => KeyPress::ControlDown,
('5', 'C') => KeyPress::ControlRight,
('5', 'D') => KeyPress::ControlLeft,
('2', 'A') => KeyPress::ShiftUp,
('2', 'B') => KeyPress::ShiftDown,
('2', 'C') => KeyPress::ShiftRight,
('2', 'D') => KeyPress::ShiftLeft,
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5);
KeyPress::UnknownEscSeq
}
})
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} ; {} {}", seq2, seq4, seq5);
Ok(KeyPress::UnknownEscSeq)
}
} else {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} ; {:?}", seq2, seq4);
Ok(KeyPress::UnknownEscSeq)
}
} else {
Ok(match (seq2, seq3) {
('5', 'A') => KeyPress::ControlUp,
('5', 'B') => KeyPress::ControlDown,
('5', 'C') => KeyPress::ControlRight,
('5', 'D') => KeyPress::ControlLeft,
_ => {
debug!(target: "rustyline",
"unsupported esc sequence: ESC [ {} {:?}", seq2, seq3);
KeyPress::UnknownEscSeq
}
})
}
}
/// Handle ESC O <seq2> escape sequences
fn escape_o(&mut self) -> Result<KeyPress> {
let seq2 = try!(self.next_char());
Ok(match seq2 {
'A' => KeyPress::Up, // kcuu1
'B' => KeyPress::Down, // kcud1
'C' => KeyPress::Right, // kcuf1
'D' => KeyPress::Left, // kcub1
'F' => KeyPress::End, // kend
'H' => KeyPress::Home, // khome
'P' => KeyPress::F(1), // kf1
'Q' => KeyPress::F(2), // kf2
'R' => KeyPress::F(3), // kf3
'S' => KeyPress::F(4), // kf4
'a' => KeyPress::ControlUp,
'b' => KeyPress::ControlDown,
'c' => KeyPress::ControlRight,
'd' => KeyPress::ControlLeft,
_ => {
debug!(target: "rustyline", "unsupported esc sequence: ESC O {:?}", seq2);
KeyPress::UnknownEscSeq
}
})
}
}
impl RawReader for PosixRawReader {
fn next_key(&mut self, single_esc_abort: bool) -> Result<KeyPress> {
let c = try!(self.next_char());
let mut key = keys::char_to_key_press(c);
if key == KeyPress::Esc {
let timeout_ms = if single_esc_abort && self.timeout_ms == -1 {
0
} else {
self.timeout_ms
};
let mut fds = [poll::PollFd::new(STDIN_FILENO, EventFlags::POLLIN)];
match poll::poll(&mut fds, 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> {
loop {
let n = try!(self.stdin.read(&mut self.buf));
if n == 0 {
return Err(error::ReadlineError::Eof);
}
let b = self.buf[0];
self.parser.advance(&mut self.receiver, b);
if !self.receiver.valid {
return Err(error::ReadlineError::Utf8Error);
} else if self.receiver.c.is_some() {
return Ok(self.receiver.c.take().unwrap());
}
}
}
}
impl Receiver for Utf8 {
/// Called whenever a codepoint is parsed successfully
fn codepoint(&mut self, c: char) {
self.c = Some(c);
self.valid = true;
}
/// Called when an invalid_sequence is detected
fn invalid_sequence(&mut self) {
self.c = None;
self.valid = false;
}
}
/// Console output writer
pub struct PosixRenderer {
out: Stdout,
cols: usize, // Number of columns in terminal
buffer: String,
}
impl PosixRenderer {
fn new() -> PosixRenderer {
let (cols, _) = get_win_size();
PosixRenderer {
out: io::stdout(),
cols,
buffer: String::with_capacity(1024),
}
}
}
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,
hint: Option<String>,
current_row: usize,
old_rows: usize,
highlighter: Option<&Highlighter>,
) -> Result<(Position, Position)> {
use std::fmt::Write;
self.buffer.clear();
// 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);
// self.old_rows < self.cursor.row if the prompt spans multiple lines and if
// this is the default State.
let cursor_row_movement = old_rows.checked_sub(current_row).unwrap_or(0);
// move the cursor down as required
if cursor_row_movement > 0 {
write!(self.buffer, "\x1b[{}B", cursor_row_movement).unwrap();
}
// clear old rows
for _ in 0..old_rows {
self.buffer.push_str("\r\x1b[0K\x1b[A");
}
// clear the line
self.buffer.push_str("\r\x1b[0K");
if let Some(highlighter) = highlighter {
// display the prompt
self.buffer.push_str(&highlighter.highlight_prompt(prompt));
// display the input line
self.buffer
.push_str(&highlighter.highlight(line, line.pos()));
} else {
// display the prompt
self.buffer.push_str(prompt);
// display the input line
self.buffer.push_str(line);
}
// display hint
if let Some(hint) = hint {
let truncate = truncate(&hint, end_pos.col, self.cols);
if let Some(highlighter) = highlighter {
self.buffer.push_str(&highlighter.highlight_hint(truncate));
} else {
self.buffer.push_str(truncate);
}
}
// we have to generate our own newline on line wrap
if end_pos.col == 0 && end_pos.row > 0 {
self.buffer.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!(self.buffer, "\x1b[{}A", cursor_row_movement).unwrap();
}
// position the cursor within the line
if cursor.col > 0 {
write!(self.buffer, "\r\x1b[{}C", cursor.col).unwrap();
} else {
self.buffer.push('\r');
}
try!(self.out.write_all(self.buffer.as_bytes()));
try!(self.out.flush());
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).
fn calculate_position(&self, s: &str, orig: Position) -> Position {
let mut pos = orig;
let mut esc_seq = 0;
for c in s.graphemes(true) {
if c == "\n" {
pos.row += 1;
pos.col = 0;
continue;
}
let cw = width(c, &mut esc_seq);
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,
stdout_isatty: bool,
pub(crate) color_mode: ColorMode,
}
impl Term for PosixTerminal {
type Mode = Mode;
type Reader = PosixRawReader;
type Writer = PosixRenderer;
fn new(color_mode: ColorMode) -> PosixTerminal {
let term = PosixTerminal {
unsupported: is_unsupported_term(),
stdin_isatty: is_a_tty(STDIN_FILENO),
stdout_isatty: is_a_tty(STDOUT_FILENO),
color_mode,
};
if !term.unsupported && term.stdin_isatty && term.stdout_isatty {
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
}
/// Check if output supports colors.
fn colors_enabled(&self) -> bool {
match self.color_mode {
ColorMode::Enabled => self.stdout_isatty,
ColorMode::Forced => true,
ColorMode::Disabled => false,
}
}
// Interactive loop:
fn enable_raw_mode(&mut self) -> Result<Mode> {
use nix::errno::Errno::ENOTTY;
use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, SpecialCharacterIndices};
if !self.stdin_isatty {
try!(Err(nix::Error::from_errno(ENOTTY)));
}
let original_mode = try!(termios::tcgetattr(STDIN_FILENO));
let mut raw = original_mode.clone();
// disable BREAK interrupt, CR to NL conversion on input,
// input parity check, strip high bit (bit 8), output flow control
raw.input_flags &= !(InputFlags::BRKINT
| InputFlags::ICRNL
| InputFlags::INPCK
| InputFlags::ISTRIP
| InputFlags::IXON);
// we don't want raw output, it turns newlines into straight linefeeds
// disable all output processing
// raw.c_oflag = raw.c_oflag & !(OutputFlags::OPOST);
// character-size mark (8 bits)
raw.control_flags |= ControlFlags::CS8;
// disable echoing, canonical mode, extended input processing and signals
raw.local_flags &=
!(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG);
raw.control_chars[SpecialCharacterIndices::VMIN as usize] = 1; // One character-at-a-time input
raw.control_chars[SpecialCharacterIndices::VTIME as usize] = 0; // with blocking read
try!(termios::tcsetattr(STDIN_FILENO, SetArg::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<()> {
use nix::unistd::Pid;
// suspend the whole process group
try!(signal::kill(Pid::from_raw(0), signal::SIGTSTP));
Ok(())
}
#[cfg(all(unix, test))]
mod test {
use super::{Position, Renderer};
use std::io::{self, Stdout};
#[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());
}
}