diff --git a/rustfmt.toml b/rustfmt.toml index 44148a2d3c3ed6bc75e28e1c55eec63f4d720698..6496add0b747be108c2a26d5848d6604179d3a12 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,3 @@ -reorder_imports = true +reorder_imports = false +normalise_comments = false +write_mode = "Overwrite" \ No newline at end of file diff --git a/src/consts.rs b/src/consts.rs index 375afe632cbaa1a28f9c20ae157ed9b4762f1498..31cbead410b6d29ad1c2780cc20fee97f6839679 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -14,6 +14,8 @@ pub enum KeyPress { Left, Meta(char), Null, + PageDown, + PageUp, Right, Tab, // Ctrl('I') Up, @@ -33,12 +35,12 @@ pub fn char_to_key_press(c: char) -> KeyPress { '\x05' => KeyPress::Ctrl('E'), '\x06' => KeyPress::Ctrl('F'), '\x07' => KeyPress::Ctrl('G'), - '\x08' => KeyPress::Backspace, + '\x08' => KeyPress::Backspace, // '\b' '\x09' => KeyPress::Tab, - '\x0a' => KeyPress::Ctrl('J'), + '\x0a' => KeyPress::Ctrl('J'), // '\n' (10) '\x0b' => KeyPress::Ctrl('K'), '\x0c' => KeyPress::Ctrl('L'), - '\x0d' => KeyPress::Enter, + '\x0d' => KeyPress::Enter, // '\r' (13) '\x0e' => KeyPress::Ctrl('N'), '\x10' => KeyPress::Ctrl('P'), '\x12' => KeyPress::Ctrl('R'), diff --git a/src/lib.rs b/src/lib.rs index 214aff1de66bba055491222b1f9af571da6a7b92..1b0202d83eca6cb79dec43ba051e7a6f57b145c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,6 @@ pub mod line_buffer; mod char_iter; pub mod config; -#[macro_use] mod tty; use std::fmt; @@ -45,9 +44,9 @@ use std::io::{self, Read, Write}; use std::mem; use std::path::Path; use std::result; -use std::sync::atomic; #[cfg(unix)] use nix::sys::signal; +use tty::Terminal; use encode_unicode::CharExt; use completion::{Completer, longest_common_prefix}; @@ -71,7 +70,7 @@ struct State<'out, 'prompt> { old_rows: usize, // Number of rows used so far (from start of prompt to end of input) history_index: usize, // The history index we are currently editing snapshot: LineBuffer, // Current edited line before history browsing/completion - output_handle: tty::Handle, // output handle (for windows) + term: Terminal, // terminal } #[derive(Copy, Clone, Debug, Default)] @@ -82,12 +81,12 @@ struct Position { impl<'out, 'prompt> State<'out, 'prompt> { fn new(out: &'out mut Write, - output_handle: tty::Handle, + term: Terminal, prompt: &'prompt str, history_index: usize) -> State<'out, 'prompt> { let capacity = MAX_LINE; - let cols = tty::get_columns(output_handle); + let cols = term.get_columns(); let prompt_size = calculate_position(prompt, Position::default(), cols); State { out: out, @@ -99,7 +98,7 @@ impl<'out, 'prompt> State<'out, 'prompt> { old_rows: prompt_size.row, history_index: history_index, snapshot: LineBuffer::with_capacity(capacity), - output_handle: output_handle, + term: term, } } @@ -175,24 +174,20 @@ impl<'out, 'prompt> State<'out, 'prompt> { #[cfg(windows)] fn refresh(&mut self, prompt: &str, prompt_size: Position) -> Result<()> { - let handle = self.output_handle; // calculate the position of the end of the input line let end_pos = calculate_position(&self.line, prompt_size, self.cols); // calculate the desired position of the cursor let cursor = calculate_position(&self.line[..self.line.pos()], prompt_size, self.cols); // position at the start of the prompt, clear to end of previous input - let mut info = unsafe { mem::zeroed() }; - check!(kernel32::GetConsoleScreenBufferInfo(handle, &mut info)); + let mut info = try!(self.term.get_console_screen_buffer_info()); info.dwCursorPosition.X = 0; info.dwCursorPosition.Y -= self.cursor.row as i16; - check!(kernel32::SetConsoleCursorPosition(handle, info.dwCursorPosition)); + try!(self.term.set_console_cursor_position(info.dwCursorPosition)); let mut _count = 0; - check!(kernel32::FillConsoleOutputCharacterA(handle, - ' ' as winapi::CHAR, - (info.dwSize.X * (self.old_rows as i16 +1)) as winapi::DWORD, - info.dwCursorPosition, - &mut _count)); + try!(self.term + .fill_console_output_character((info.dwSize.X * (self.old_rows as i16 + 1)) as u32, + info.dwCursorPosition)); let mut ab = String::new(); // display the prompt ab.push_str(prompt); // TODO handle ansi escape code (SetConsoleTextAttribute) @@ -201,10 +196,10 @@ impl<'out, 'prompt> State<'out, 'prompt> { try!(write_and_flush(self.out, ab.as_bytes())); // position the cursor - check!(kernel32::GetConsoleScreenBufferInfo(handle, &mut info)); + let mut info = try!(self.term.get_console_screen_buffer_info()); info.dwCursorPosition.X = cursor.col as i16; info.dwCursorPosition.Y -= (end_pos.row - cursor.row) as i16; - check!(kernel32::SetConsoleCursorPosition(handle, info.dwCursorPosition)); + try!(self.term.set_console_cursor_position(info.dwCursorPosition)); self.cursor = cursor; self.old_rows = end_pos.row; @@ -213,7 +208,7 @@ impl<'out, 'prompt> State<'out, 'prompt> { } fn update_columns(&mut self) { - self.cols = tty::get_columns(self.output_handle); + self.cols = self.term.get_columns(); } } @@ -646,7 +641,7 @@ fn page_completions<R: Read>(rdr: &mut tty::RawReader<R>, .unwrap() + min_col_pad); let num_cols = s.cols / max_width; - let mut pause_row = tty::get_rows(s.output_handle) - 1; + let mut pause_row = s.term.get_rows() - 1; let num_rows = (candidates.len() + num_cols - 1) / num_cols; let mut ab = String::new(); for row in 0..num_rows { @@ -665,7 +660,7 @@ fn page_completions<R: Read>(rdr: &mut tty::RawReader<R>, KeyPress::Char('y') | KeyPress::Char('Y') | KeyPress::Char(' ') => { - pause_row += tty::get_rows(s.output_handle) - 1; + pause_row += s.term.get_rows() - 1; } KeyPress::Enter => { pause_row += 1; @@ -785,17 +780,19 @@ fn readline_edit<C: Completer>(prompt: &str, let completer = editor.completer.as_ref().map(|c| c as &Completer); let mut stdout = io::stdout(); - let stdout_handle = try!(tty::stdout_handle()); editor.kill_ring.reset(); - let mut s = State::new(&mut stdout, stdout_handle, prompt, editor.history.len()); + let mut s = State::new(&mut stdout, + editor.term.clone(), + prompt, + editor.history.len()); try!(s.refresh_line()); - let mut rdr = try!(tty::RawReader::new(io::stdin())); + let mut rdr = try!(s.term.create_reader()); loop { let rk = rdr.next_key(true); - if rk.is_err() && tty::SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) { + if rk.is_err() && s.term.sigwinch() { s.update_columns(); try!(s.refresh_line()); continue; @@ -884,7 +881,7 @@ fn readline_edit<C: Completer>(prompt: &str, } KeyPress::Ctrl('L') => { // Clear the screen leaving the current line at the top of the screen. - try!(tty::clear_screen(&mut s.out, s.output_handle)); + try!(s.term.clear_screen(&mut s.out)); try!(s.refresh_line()) } KeyPress::Ctrl('N') | @@ -1050,9 +1047,7 @@ fn readline_direct() -> Result<String> { /// Line editor pub struct Editor<C: Completer> { - unsupported_term: bool, - stdin_isatty: bool, - stdout_isatty: bool, + term: Terminal, history: History, completer: Option<C>, kill_ring: KillRing, @@ -1061,30 +1056,25 @@ pub struct Editor<C: Completer> { impl<C: Completer> Editor<C> { pub fn new(config: Config) -> Editor<C> { - let editor = Editor { - unsupported_term: tty::is_unsupported_term(), - stdin_isatty: tty::is_a_tty(tty::STDIN_FILENO), - stdout_isatty: tty::is_a_tty(tty::STDOUT_FILENO), + let term = Terminal::new(); + Editor { + term: term, history: History::new(config), completer: None, kill_ring: KillRing::new(60), config: config, - }; - if !editor.unsupported_term && editor.stdin_isatty && editor.stdout_isatty { - tty::install_sigwinch_handler(); } - editor } /// This method will read a line from STDIN and will display a `prompt` pub fn readline(&mut self, prompt: &str) -> Result<String> { - if self.unsupported_term { + if self.term.is_unsupported() { // Write prompt and flush it to stdout let mut stdout = io::stdout(); try!(write_and_flush(&mut stdout, prompt.as_bytes())); readline_direct() - } else if !self.stdin_isatty { + } else if !self.term.is_stdin_tty() { // Not a tty: read from file / pipe. readline_direct() } else { @@ -1121,9 +1111,9 @@ impl<C: Completer> Editor<C> { impl<C: Completer> fmt::Debug for Editor<C> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("State") - .field("unsupported_term", &self.unsupported_term) - .field("stdin_isatty", &self.stdin_isatty) + f.debug_struct("Editor") + .field("term", &self.term) + .field("config", &self.config) .finish() } } @@ -1137,17 +1127,14 @@ mod test { use config::Config; use {Position, State}; use super::Result; - use tty::Handle; - - fn default_handle() -> Handle { - () - } + use tty::Terminal; fn init_state<'out>(out: &'out mut Write, line: &str, pos: usize, cols: usize) -> State<'out, 'static> { + let term = Terminal::new(); State { out: out, prompt: "", @@ -1158,7 +1145,7 @@ mod test { old_rows: 0, history_index: 0, snapshot: LineBuffer::with_capacity(100), - output_handle: default_handle(), + term: term, } } diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 22c6793842de9d5abf9d74371b5ebc33f126f8e9..82e2ed670c3437b3caae1dd443c016ab5b436074 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -2,7 +2,6 @@ // If on Windows platform import Windows TTY module // and re-export into mod.rs scope -#[macro_use] #[cfg(windows)] mod windows; #[cfg(windows)] diff --git a/src/tty/unix.rs b/src/tty/unix.rs index ff1d0b31d8cf354262e5fd2418c801bb7a986f1d..bbc57b54cb68122d27b319e44d7bfc270ab84817 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -12,10 +12,9 @@ use consts::{self, KeyPress}; use ::Result; use ::error; -pub type Handle = (); pub type Mode = termios::Termios; -pub const STDIN_FILENO: libc::c_int = libc::STDIN_FILENO; -pub const STDOUT_FILENO: libc::c_int = libc::STDOUT_FILENO; +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"]; @@ -31,14 +30,14 @@ const TIOCGWINSZ: libc::c_int = 0x5413; /// Try to get the number of columns in the current terminal, /// or assume 80 if it fails. -pub fn get_columns(_: Handle) -> usize { +fn get_columns() -> usize { let (cols, _) = get_win_size(); cols } /// Try to get the number of rows in the current terminal, /// or assume 24 if it fails. -pub fn get_rows(_: Handle) -> usize { +fn get_rows() -> usize { let (_, rows) = get_win_size(); rows } @@ -66,7 +65,7 @@ fn get_win_size() -> (usize, usize) { /// Check TERM environment variable to see if current term is in our /// unsupported list -pub fn is_unsupported_term() -> bool { +fn is_unsupported_term() -> bool { use std::ascii::AsciiExt; match std::env::var("TERM") { Ok(term) => { @@ -82,11 +81,11 @@ pub fn is_unsupported_term() -> bool { /// Return whether or not STDIN, STDOUT or STDERR is a TTY -pub fn is_a_tty(fd: libc::c_int) -> bool { +fn is_a_tty(fd: libc::c_int) -> bool { unsafe { libc::isatty(fd) != 0 } } -/// Enable raw mode for the TERM +/// Enable RAW mode for the terminal. pub fn enable_raw_mode() -> Result<Mode> { use nix::errno::Errno::ENOTTY; use nix::sys::termios::{BRKINT, CS8, ECHO, ICANON, ICRNL, IEXTEN, INPCK, ISIG, ISTRIP, IXON, @@ -110,18 +109,13 @@ pub fn enable_raw_mode() -> Result<Mode> { Ok(original_mode) } -/// Disable Raw mode for the term +/// Disable RAW mode for the terminal. pub fn disable_raw_mode(original_mode: Mode) -> Result<()> { try!(termios::tcsetattr(STDIN_FILENO, termios::TCSAFLUSH, &original_mode)); Ok(()) } -pub fn stdout_handle() -> Result<Handle> { - Ok(()) -} - -/// Clear the screen. Used to handle ctrl+l -pub fn clear_screen(w: &mut Write, _: Handle) -> Result<()> { +fn clear_screen(w: &mut Write) -> Result<()> { try!(w.write_all(b"\x1b[H\x1b[2J")); try!(w.flush()); Ok(()) @@ -168,9 +162,13 @@ impl<R: Read> RawReader<R> { let seq3 = try!(self.next_char()); if seq3 == '~' { match seq2 { + // '1' => Ok(KeyPress::Home), '3' => Ok(KeyPress::Delete), - // TODO '1' // Home - // TODO '4' // End + // '4' => Ok(KeyPress::End), + '5' => Ok(KeyPress::PageUp), + '6' => Ok(KeyPress::PageDown), + '7' => Ok(KeyPress::Home), + '8' => Ok(KeyPress::End), _ => Ok(KeyPress::UnknownEscSeq), } } else { @@ -178,7 +176,7 @@ impl<R: Read> RawReader<R> { } } else { match seq2 { - 'A' => Ok(KeyPress::Up), + 'A' => Ok(KeyPress::Up), // ANSI 'B' => Ok(KeyPress::Down), 'C' => Ok(KeyPress::Right), 'D' => Ok(KeyPress::Left), @@ -191,6 +189,10 @@ impl<R: Read> RawReader<R> { // ESC O sequences. let seq2 = try!(self.next_char()); match seq2 { + 'A' => Ok(KeyPress::Up), + 'B' => Ok(KeyPress::Down), + 'C' => Ok(KeyPress::Right), + 'D' => Ok(KeyPress::Left), 'F' => Ok(KeyPress::End), 'H' => Ok(KeyPress::Home), _ => Ok(KeyPress::UnknownEscSeq), @@ -222,9 +224,9 @@ impl<R: Read> RawReader<R> { } static SIGWINCH_ONCE: sync::Once = sync::ONCE_INIT; -pub static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT; +static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT; -pub fn install_sigwinch_handler() { +fn install_sigwinch_handler() { SIGWINCH_ONCE.call_once(|| unsafe { let sigwinch = signal::SigAction::new(signal::SigHandler::Handler(sigwinch_handler), signal::SaFlag::empty(), @@ -236,3 +238,63 @@ pub fn install_sigwinch_handler() { extern "C" fn sigwinch_handler(_: signal::SigNum) { SIGWINCH.store(true, atomic::Ordering::SeqCst); } + +pub type Terminal = PosixTerminal; + +#[derive(Clone, Debug)] +pub struct PosixTerminal { + unsupported: bool, + stdin_isatty: bool, +} + +impl PosixTerminal { + pub 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. + pub fn is_unsupported(&self) -> bool { + self.unsupported + } + + /// check if stdin is connected to a terminal. + pub fn is_stdin_tty(&self) -> bool { + self.stdin_isatty + } + + // Interactive loop: + + /// Get the number of columns in the current terminal. + pub fn get_columns(&self) -> usize { + get_columns() + } + + /// Get the number of rows in the current terminal. + pub fn get_rows(&self) -> usize { + get_rows() + } + + /// Create a RAW reader + pub fn create_reader(&self) -> Result<RawReader<std::io::Stdin>> { + RawReader::new(std::io::stdin()) + } + + /// Check if a SIGWINCH signal has been received + pub fn sigwinch(&self) -> bool { + SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) + } + + /// Clear the screen. Used to handle ctrl+l + pub fn clear_screen(&mut self, w: &mut Write) -> Result<()> { + clear_screen(w) + } +} diff --git a/src/tty/windows.rs b/src/tty/windows.rs index f1b09587532fd332dccb852d4d5e48dcc8910c5c..310ed12ca9771fae4d1253d62a4c7ef44ce4fc66 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -11,10 +11,9 @@ use consts::{self, KeyPress}; use ::error; use ::Result; -pub type Handle = winapi::HANDLE; pub type Mode = winapi::DWORD; -pub const STDIN_FILENO: winapi::DWORD = winapi::STD_INPUT_HANDLE; -pub const STDOUT_FILENO: winapi::DWORD = winapi::STD_OUTPUT_HANDLE; +const STDIN_FILENO: winapi::DWORD = winapi::STD_INPUT_HANDLE; +const STDOUT_FILENO: winapi::DWORD = winapi::STD_OUTPUT_HANDLE; fn get_std_handle(fd: winapi::DWORD) -> Result<winapi::HANDLE> { let handle = unsafe { kernel32::GetStdHandle(fd) }; @@ -40,21 +39,17 @@ macro_rules! check { }; } -/// Try to get the number of columns in the current terminal, -/// or assume 80 if it fails. -pub fn get_columns(handle: Handle) -> usize { +fn get_columns(handle: winapi::HANDLE) -> usize { let (cols, _) = get_win_size(handle); cols } -/// Try to get the number of rows in the current terminal, -/// or assume 24 if it fails. -pub fn get_rows(handle: Handle) -> usize { +fn get_rows(handle: winapi::HANDLE) -> usize { let (_, rows) = get_win_size(handle); rows } -fn get_win_size(handle: Handle) -> (usize, usize) { +fn get_win_size(handle: winapi::HANDLE) -> (usize, usize) { let mut info = unsafe { mem::zeroed() }; match unsafe { kernel32::GetConsoleScreenBufferInfo(handle, &mut info) } { 0 => (80, 24), @@ -62,11 +57,6 @@ fn get_win_size(handle: Handle) -> (usize, usize) { } } -/// Checking for an unsupported TERM in windows is a no-op -pub fn is_unsupported_term() -> bool { - false -} - fn get_console_mode(handle: winapi::HANDLE) -> Result<Mode> { let mut original_mode = 0; check!(kernel32::GetConsoleMode(handle, &mut original_mode)); @@ -74,7 +64,7 @@ fn get_console_mode(handle: winapi::HANDLE) -> Result<Mode> { } /// Return whether or not STDIN, STDOUT or STDERR is a TTY -pub fn is_a_tty(fd: winapi::DWORD) -> bool { +fn is_a_tty(fd: winapi::DWORD) -> bool { let handle = get_std_handle(fd); match handle { Ok(handle) => { @@ -109,13 +99,8 @@ pub fn disable_raw_mode(original_mode: Mode) -> Result<()> { Ok(()) } -pub fn stdout_handle() -> Result<Handle> { - let handle = try!(get_std_handle(STDOUT_FILENO)); - Ok(handle) -} - /// Clear the screen. Used to handle ctrl+l -pub fn clear_screen(_: &mut Write, handle: Handle) -> Result<()> { +fn clear_screen(_: &mut Write, handle: winapi::HANDLE) -> Result<()> { let mut info = unsafe { mem::zeroed() }; check!(kernel32::GetConsoleScreenBufferInfo(handle, &mut info)); let coord = winapi::COORD { X: 0, Y: 0 }; @@ -236,8 +221,84 @@ impl<R: Read> Iterator for RawReader<R> { } } -pub static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT; +static SIGWINCH: atomic::AtomicBool = atomic::ATOMIC_BOOL_INIT; + +pub type Terminal = Console; + +#[derive(Clone,Debug)] +pub struct Console { + stdin_isatty: bool, + stdout_handle: winapi::HANDLE, +} + +impl Console { + pub fn new() -> Console { + use std::ptr; + let stdout_handle = get_std_handle(STDOUT_FILENO).unwrap_or(ptr::null_mut()); + Console { + stdin_isatty: is_a_tty(STDIN_FILENO), + stdout_handle: stdout_handle, + } + } + + /// Checking for an unsupported TERM in windows is a no-op + pub fn is_unsupported(&self) -> bool { + false + } + + pub fn is_stdin_tty(&self) -> bool { + self.stdin_isatty + } -pub fn install_sigwinch_handler() { + // pub fn install_sigwinch_handler(&mut self) { // See ReadConsoleInputW && WINDOW_BUFFER_SIZE_EVENT + // } + + /// Try to get the number of columns in the current terminal, + /// or assume 80 if it fails. + pub fn get_columns(&self) -> usize { + get_columns(self.stdout_handle) + } + + /// Try to get the number of rows in the current terminal, + /// or assume 24 if it fails. + pub fn get_rows(&self) -> usize { + get_rows(self.stdout_handle) + } + + pub fn create_reader(&self) -> Result<RawReader<io::Stdin>> { + RawReader::new(io::stdin()) // FIXME + } + + pub fn sigwinch(&self) -> bool { + SIGWINCH.compare_and_swap(true, false, atomic::Ordering::SeqCst) + } + + pub fn clear_screen(&mut self, w: &mut Write) -> Result<()> { + clear_screen(w, self.stdout_handle) + } + + pub fn get_console_screen_buffer_info(&self) -> Result<winapi::CONSOLE_SCREEN_BUFFER_INFO> { + let mut info = unsafe { mem::zeroed() }; + check!(kernel32::GetConsoleScreenBufferInfo(self.stdout_handle, &mut info)); + Ok(info) + } + + pub fn set_console_cursor_position(&mut self, pos: winapi::COORD) -> Result<()> { + check!(kernel32::SetConsoleCursorPosition(self.stdout_handle, pos)); + Ok(()) + } + + pub fn fill_console_output_character(&mut self, + length: winapi::DWORD, + pos: winapi::COORD) + -> Result<()> { + let mut _count = 0; + check!(kernel32::FillConsoleOutputCharacterA(self.stdout_handle, + ' ' as winapi::CHAR, + length, + pos, + &mut _count)); + Ok(()) + } }