Skip to content
Snippets Groups Projects
unix.rs 10.5 KiB
Newer Older
gwenn's avatar
gwenn committed
//! Unix specific definitions
use std::io::{self, Chars, Read, Write};
use std::sync;
use std::sync::atomic;
gwenn's avatar
gwenn committed
use libc;
use nix;
use nix::poll;
use nix::sys::signal;
gwenn's avatar
gwenn committed

use consts::{self, KeyPress};
use ::Result;
use ::error;
use super::{RawMode, RawReader, 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"];

gwenn's avatar
gwenn committed
fn get_win_size() -> (usize, usize) {
gwenn's avatar
gwenn committed
        let mut size: libc::winsize = zeroed();
        match libc::ioctl(STDOUT_FILENO, libc::TIOCGWINSZ, &mut size) {
gwenn's avatar
gwenn committed
            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

/// 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
gwenn's avatar
gwenn committed
pub struct PosixRawReader {
    chars: Chars<StdinRaw>,
gwenn's avatar
gwenn committed
impl PosixRawReader {
    fn new() -> Result<PosixRawReader> {
        let stdin = StdinRaw {};
gwenn's avatar
gwenn committed
        Ok(PosixRawReader { chars: stdin.chars() })
    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 {
gwenn's avatar
gwenn committed
                        '1' | '7' => KeyPress::Home, // '1': xterm
                        '3' => KeyPress::Delete,
gwenn's avatar
gwenn committed
                        '4' | '8' => KeyPress::End, // '4': xterm
                        '5' => KeyPress::PageUp,
                        '6' => KeyPress::PageDown,
                        _ => KeyPress::UnknownEscSeq,
                    })
gwenn's avatar
gwenn committed
                    Ok(KeyPress::UnknownEscSeq)
                Ok(match seq2 {
                    'A' => KeyPress::Up, // ANSI
                    'B' => KeyPress::Down,
                    'C' => KeyPress::Right,
                    'D' => KeyPress::Left,
                    'F' => KeyPress::End,
                    'H' => KeyPress::Home,
                    _ => 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,
                _ => KeyPress::UnknownEscSeq,
            })
        } else {
            // TODO ESC-N (n): search history forward not interactively
            // TODO ESC-P (p): search history backward not interactively
            // 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'),
                't' | 'T' => KeyPress::Meta('T'),
                'u' | 'U' => KeyPress::Meta('U'),
                'y' | 'Y' => KeyPress::Meta('Y'),
                '\x7f' => KeyPress::Meta('\x7f'), // Delete
                _ => {
                    // writeln!(io::stderr(), "key: {:?}, seq1: {:?}", KeyPress::Esc, seq1).unwrap();
                    KeyPress::UnknownEscSeq
gwenn's avatar
gwenn committed
impl RawReader for PosixRawReader {
    fn next_key(&mut self, timeout_ms: i32) -> Result<KeyPress> {
gwenn's avatar
gwenn committed
        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, 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()),
            }
gwenn's avatar
gwenn committed
        }
        Ok(key)
    }

    fn next_char(&mut self) -> Result<char> {
        match self.chars.next() {
            Some(c) => Ok(try!(c)),
            None => Err(error::ReadlineError::Eof),
        }
    }
}


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),
gwenn's avatar
gwenn committed
                                              signal::SaFlags::empty(),
                                              signal::SigSet::empty());
        let _ = signal::sigaction(signal::SIGWINCH, &sigwinch);
    });
}

gwenn's avatar
gwenn committed
extern "C" fn sigwinch_handler(_: libc::c_int) {
    SIGWINCH.store(true, atomic::Ordering::SeqCst);
}

pub type Terminal = PosixTerminal;

gwenn's avatar
gwenn committed
#[derive(Clone,Debug)]
pub struct PosixTerminal {
    unsupported: bool,
    stdin_isatty: bool,
}

gwenn's avatar
gwenn committed
impl Term for PosixTerminal {
    type Reader = PosixRawReader;
    type Mode = Mode;
gwenn's avatar
gwenn committed
    fn new() -> PosixTerminal {
gwenn's avatar
gwenn committed
        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();
        }
gwenn's avatar
gwenn committed
        term
    }

    // Init checks:

    /// Check if current terminal can provide a rich line-editing user interface.
gwenn's avatar
gwenn committed
    fn is_unsupported(&self) -> bool {
        self.unsupported
    }

    /// check if stdin is connected to a terminal.
gwenn's avatar
gwenn committed
    fn is_stdin_tty(&self) -> bool {
gwenn's avatar
gwenn committed
    // Interactive loop:
    /// Try to get the number of columns in the current terminal,
    /// or assume 80 if it fails.
gwenn's avatar
gwenn committed
    fn get_columns(&self) -> usize {
        let (cols, _) = get_win_size();
        cols
    /// Try to get the number of rows in the current terminal,
    /// or assume 24 if it fails.
gwenn's avatar
gwenn committed
    fn get_rows(&self) -> usize {
        let (_, rows) = get_win_size();
        rows
    }

    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 = 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 = raw.c_cflag | (CS8); // character-size mark (8 bits)
        // disable echoing, canonical mode, extended input processing and signals
        raw.c_lflag = 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) -> Result<PosixRawReader> {
        PosixRawReader::new()
    }

    /// Check if a SIGWINCH signal has been received
gwenn's avatar
gwenn committed
    fn sigwinch(&self) -> bool {
        SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst)
    }

    /// Clear the screen. Used to handle ctrl+l
gwenn's avatar
gwenn committed
    fn clear_screen(&mut self, w: &mut Write) -> Result<()> {
        try!(w.write_all(b"\x1b[H\x1b[2J"));
        try!(w.flush());
        Ok(())
gwenn's avatar
gwenn committed
#[cfg(unix)]
pub fn suspend() -> Result<()> {
    // For macos:
    try!(signal::kill(nix::unistd::getppid(), signal::SIGTSTP));
    try!(signal::kill(nix::unistd::getpid(), signal::SIGTSTP));
    Ok(())
}

#[cfg(all(unix,test))]
mod test {
    #[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());
    }
}