From 7efa96a57b1ffd2990b621a0439e355b1c6ebf2d Mon Sep 17 00:00:00 2001 From: gwenn <gtreguier@gmail.com> Date: Fri, 9 Sep 2016 20:47:03 +0200 Subject: [PATCH] Refactor before loading terminfo capabilities. --- src/lib.rs | 87 ++++++++++++++------------------ src/tty/mod.rs | 1 - src/tty/unix.rs | 102 ++++++++++++++++++++++++++++++------- src/tty/windows.rs | 122 +++++++++++++++++++++++++++++++++++---------- 4 files changed, 219 insertions(+), 93 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 214aff1d..851beaba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,17 +37,18 @@ pub mod line_buffer; mod char_iter; pub mod config; -#[macro_use] mod tty; +use std::cell::RefCell; use std::fmt; use std::io::{self, Read, Write}; use std::mem; use std::path::Path; +use std::rc::Rc; 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 +72,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: Rc<RefCell<Terminal>>, // terminal } #[derive(Copy, Clone, Debug, Default)] @@ -82,12 +83,12 @@ struct Position { impl<'out, 'prompt> State<'out, 'prompt> { fn new(out: &'out mut Write, - output_handle: tty::Handle, + term: Rc<RefCell<Terminal>>, prompt: &'prompt str, history_index: usize) -> State<'out, 'prompt> { let capacity = MAX_LINE; - let cols = tty::get_columns(output_handle); + let cols = term.borrow().get_columns(); let prompt_size = calculate_position(prompt, Position::default(), cols); State { out: out, @@ -99,7 +100,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 +176,21 @@ 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); + let mut term = self.term.borrow_mut(); // 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!(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!(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!(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 +199,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!(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!(term.set_console_cursor_position(info.dwCursorPosition)); self.cursor = cursor; self.old_rows = end_pos.row; @@ -213,7 +211,7 @@ impl<'out, 'prompt> State<'out, 'prompt> { } fn update_columns(&mut self) { - self.cols = tty::get_columns(self.output_handle); + self.cols = self.term.borrow().get_columns(); } } @@ -646,7 +644,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.borrow().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 +663,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.borrow().get_rows() - 1; } KeyPress::Enter => { pause_row += 1; @@ -785,17 +783,16 @@ 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.borrow().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.borrow().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.borrow_mut().clear_screen(&mut s.out)); try!(s.refresh_line()) } KeyPress::Ctrl('N') | @@ -931,9 +928,9 @@ fn readline_edit<C: Completer>(prompt: &str, } #[cfg(unix)] KeyPress::Ctrl('Z') => { - try!(tty::disable_raw_mode(original_mode)); + try!(s.term.borrow_mut().disable_raw_mode(original_mode)); try!(signal::raise(signal::SIGSTOP)); - try!(tty::enable_raw_mode()); // TODO original_mode may have changed + try!(s.term.borrow_mut().enable_raw_mode()); // TODO original_mode may have changed try!(s.refresh_line()) } // TODO CTRL-_ // undo @@ -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: Rc<RefCell<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 = Rc::new(RefCell::new(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.borrow().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.borrow().is_stdin_tty() { // Not a tty: read from file / pipe. readline_direct() } else { @@ -1121,33 +1111,32 @@ 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() } } #[cfg(all(unix,test))] mod test { + use std::cell::RefCell; use std::io::Write; + use std::rc::Rc; use line_buffer::LineBuffer; use history::History; use completion::Completer; 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 = Rc::new(RefCell::new(Terminal::new())); State { out: out, prompt: "", @@ -1158,7 +1147,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 22c67938..82e2ed67 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 ff1d0b31..a08029a5 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,10 @@ 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 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 +108,12 @@ pub fn enable_raw_mode() -> Result<Mode> { Ok(original_mode) } -/// Disable Raw mode for the term 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(()) @@ -222,9 +214,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 +228,79 @@ pub fn install_sigwinch_handler() { extern "C" fn sigwinch_handler(_: signal::SigNum) { SIGWINCH.store(true, atomic::Ordering::SeqCst); } + +pub type Terminal = PosixTerminal; + +#[derive(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 + } + + // Init if terminal-style mode: + + // pub fn load_capabilities(&mut self) -> Result<()> { + // Ok(()) + // } + + // Interactive loop: + + /// Enable RAW mode for the terminal. + pub fn enable_raw_mode(&mut self) -> Result<Mode> { + enable_raw_mode() + } + + /// Disable RAW mode for the terminal. + pub fn disable_raw_mode(&mut self, mode: Mode) -> Result<()> { + disable_raw_mode(mode) + } + + /// 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 f1b09587..e0e62d04 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) => { @@ -85,7 +75,6 @@ pub fn is_a_tty(fd: winapi::DWORD) -> bool { } } -/// Enable raw mode for the TERM pub fn enable_raw_mode() -> Result<Mode> { let handle = try!(get_std_handle(STDIN_FILENO)); let original_mode = try!(get_console_mode(handle)); @@ -102,20 +91,14 @@ pub fn enable_raw_mode() -> Result<Mode> { Ok(original_mode) } -/// Disable Raw mode for the term pub fn disable_raw_mode(original_mode: Mode) -> Result<()> { let handle = try!(get_std_handle(STDIN_FILENO)); check!(kernel32::SetConsoleMode(handle, original_mode)); 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 +219,95 @@ 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(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 + // } + + pub fn load_capabilities(&mut self) -> Result<()> { + Ok(()) + } + + /// Enable raw mode for the TERM + pub fn enable_raw_mode(&mut self) -> Result<Mode> { + enable_raw_mode() + } + + /// Disable Raw mode for the term + pub fn disable_raw_mode(&mut self, mode: Mode) -> Result<()> { + disable_raw_mode(mode) + } + + /// 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(()) + } } -- GitLab