| use std::fmt::{Debug, Display}; |
| use std::io::{self, Read, Write}; |
| use std::sync::{Arc, Mutex, RwLock}; |
| |
| #[cfg(unix)] |
| use std::os::unix::io::{AsRawFd, RawFd}; |
| #[cfg(windows)] |
| use std::os::windows::io::{AsRawHandle, RawHandle}; |
| |
| use crate::{kb::Key, utils::Style}; |
| |
| #[cfg(unix)] |
| trait TermWrite: Write + Debug + AsRawFd + Send {} |
| #[cfg(unix)] |
| impl<T: Write + Debug + AsRawFd + Send> TermWrite for T {} |
| |
| #[cfg(unix)] |
| trait TermRead: Read + Debug + AsRawFd + Send {} |
| #[cfg(unix)] |
| impl<T: Read + Debug + AsRawFd + Send> TermRead for T {} |
| |
| #[cfg(unix)] |
| #[derive(Debug, Clone)] |
| pub struct ReadWritePair { |
| #[allow(unused)] |
| read: Arc<Mutex<dyn TermRead>>, |
| write: Arc<Mutex<dyn TermWrite>>, |
| style: Style, |
| } |
| |
| /// Where the term is writing. |
| #[derive(Debug, Clone)] |
| pub enum TermTarget { |
| Stdout, |
| Stderr, |
| #[cfg(unix)] |
| ReadWritePair(ReadWritePair), |
| } |
| |
| #[derive(Debug)] |
| pub struct TermInner { |
| target: TermTarget, |
| buffer: Option<Mutex<Vec<u8>>>, |
| prompt: RwLock<String>, |
| prompt_guard: Mutex<()>, |
| } |
| |
| /// The family of the terminal. |
| #[derive(Debug, Copy, Clone, PartialEq, Eq)] |
| pub enum TermFamily { |
| /// Redirected to a file or file like thing. |
| File, |
| /// A standard unix terminal. |
| UnixTerm, |
| /// A cmd.exe like windows console. |
| WindowsConsole, |
| /// A dummy terminal (for instance on wasm) |
| Dummy, |
| } |
| |
| /// Gives access to the terminal features. |
| #[derive(Debug, Clone)] |
| pub struct TermFeatures<'a>(&'a Term); |
| |
| impl<'a> TermFeatures<'a> { |
| /// Check if this is a real user attended terminal (`isatty`) |
| #[inline] |
| pub fn is_attended(&self) -> bool { |
| is_a_terminal(self.0) |
| } |
| |
| /// Check if colors are supported by this terminal. |
| /// |
| /// This does not check if colors are enabled. Currently all terminals |
| /// are considered to support colors |
| #[inline] |
| pub fn colors_supported(&self) -> bool { |
| is_a_color_terminal(self.0) |
| } |
| |
| /// Check if this terminal is an msys terminal. |
| /// |
| /// This is sometimes useful to disable features that are known to not |
| /// work on msys terminals or require special handling. |
| #[inline] |
| pub fn is_msys_tty(&self) -> bool { |
| #[cfg(windows)] |
| { |
| msys_tty_on(self.0) |
| } |
| #[cfg(not(windows))] |
| { |
| false |
| } |
| } |
| |
| /// Check if this terminal wants emojis. |
| #[inline] |
| pub fn wants_emoji(&self) -> bool { |
| self.is_attended() && wants_emoji() |
| } |
| |
| /// Return the family of the terminal. |
| #[inline] |
| pub fn family(&self) -> TermFamily { |
| if !self.is_attended() { |
| return TermFamily::File; |
| } |
| #[cfg(windows)] |
| { |
| TermFamily::WindowsConsole |
| } |
| #[cfg(all(unix, not(target_arch = "wasm32")))] |
| { |
| TermFamily::UnixTerm |
| } |
| #[cfg(target_arch = "wasm32")] |
| { |
| TermFamily::Dummy |
| } |
| } |
| } |
| |
| /// Abstraction around a terminal. |
| /// |
| /// A terminal can be cloned. If a buffer is used it's shared across all |
| /// clones which means it largely acts as a handle. |
| #[derive(Clone, Debug)] |
| pub struct Term { |
| inner: Arc<TermInner>, |
| pub(crate) is_msys_tty: bool, |
| pub(crate) is_tty: bool, |
| } |
| |
| impl Term { |
| fn with_inner(inner: TermInner) -> Term { |
| let mut term = Term { |
| inner: Arc::new(inner), |
| is_msys_tty: false, |
| is_tty: false, |
| }; |
| |
| term.is_msys_tty = term.features().is_msys_tty(); |
| term.is_tty = term.features().is_attended(); |
| term |
| } |
| |
| /// Return a new unbuffered terminal. |
| #[inline] |
| pub fn stdout() -> Term { |
| Term::with_inner(TermInner { |
| target: TermTarget::Stdout, |
| buffer: None, |
| prompt: RwLock::new(String::new()), |
| prompt_guard: Mutex::new(()), |
| }) |
| } |
| |
| /// Return a new unbuffered terminal to stderr. |
| #[inline] |
| pub fn stderr() -> Term { |
| Term::with_inner(TermInner { |
| target: TermTarget::Stderr, |
| buffer: None, |
| prompt: RwLock::new(String::new()), |
| prompt_guard: Mutex::new(()), |
| }) |
| } |
| |
| /// Return a new buffered terminal. |
| pub fn buffered_stdout() -> Term { |
| Term::with_inner(TermInner { |
| target: TermTarget::Stdout, |
| buffer: Some(Mutex::new(vec![])), |
| prompt: RwLock::new(String::new()), |
| prompt_guard: Mutex::new(()), |
| }) |
| } |
| |
| /// Return a new buffered terminal to stderr. |
| pub fn buffered_stderr() -> Term { |
| Term::with_inner(TermInner { |
| target: TermTarget::Stderr, |
| buffer: Some(Mutex::new(vec![])), |
| prompt: RwLock::new(String::new()), |
| prompt_guard: Mutex::new(()), |
| }) |
| } |
| |
| /// Return a terminal for the given Read/Write pair styled like stderr. |
| #[cfg(unix)] |
| pub fn read_write_pair<R, W>(read: R, write: W) -> Term |
| where |
| R: Read + Debug + AsRawFd + Send + 'static, |
| W: Write + Debug + AsRawFd + Send + 'static, |
| { |
| Self::read_write_pair_with_style(read, write, Style::new().for_stderr()) |
| } |
| |
| /// Return a terminal for the given Read/Write pair. |
| #[cfg(unix)] |
| pub fn read_write_pair_with_style<R, W>(read: R, write: W, style: Style) -> Term |
| where |
| R: Read + Debug + AsRawFd + Send + 'static, |
| W: Write + Debug + AsRawFd + Send + 'static, |
| { |
| Term::with_inner(TermInner { |
| target: TermTarget::ReadWritePair(ReadWritePair { |
| read: Arc::new(Mutex::new(read)), |
| write: Arc::new(Mutex::new(write)), |
| style, |
| }), |
| buffer: None, |
| prompt: RwLock::new(String::new()), |
| prompt_guard: Mutex::new(()), |
| }) |
| } |
| |
| /// Return the style for this terminal. |
| #[inline] |
| pub fn style(&self) -> Style { |
| match self.inner.target { |
| TermTarget::Stderr => Style::new().for_stderr(), |
| TermTarget::Stdout => Style::new().for_stdout(), |
| #[cfg(unix)] |
| TermTarget::ReadWritePair(ReadWritePair { ref style, .. }) => style.clone(), |
| } |
| } |
| |
| /// Return the target of this terminal. |
| #[inline] |
| pub fn target(&self) -> TermTarget { |
| self.inner.target.clone() |
| } |
| |
| #[doc(hidden)] |
| pub fn write_str(&self, s: &str) -> io::Result<()> { |
| match self.inner.buffer { |
| Some(ref buffer) => buffer.lock().unwrap().write_all(s.as_bytes()), |
| None => self.write_through(s.as_bytes()), |
| } |
| } |
| |
| /// Write a string to the terminal and add a newline. |
| pub fn write_line(&self, s: &str) -> io::Result<()> { |
| let prompt = self.inner.prompt.read().unwrap(); |
| if !prompt.is_empty() { |
| self.clear_line()?; |
| } |
| match self.inner.buffer { |
| Some(ref mutex) => { |
| let mut buffer = mutex.lock().unwrap(); |
| buffer.extend_from_slice(s.as_bytes()); |
| buffer.push(b'\n'); |
| buffer.extend_from_slice(prompt.as_bytes()); |
| Ok(()) |
| } |
| None => self.write_through(format!("{}\n{}", s, prompt.as_str()).as_bytes()), |
| } |
| } |
| |
| /// Read a single character from the terminal. |
| /// |
| /// This does not echo the character and blocks until a single character |
| /// or complete key chord is entered. If the terminal is not user attended |
| /// the return value will be an error. |
| pub fn read_char(&self) -> io::Result<char> { |
| if !self.is_tty { |
| return Err(io::Error::new( |
| io::ErrorKind::NotConnected, |
| "Not a terminal", |
| )); |
| } |
| loop { |
| match self.read_key()? { |
| Key::Char(c) => { |
| return Ok(c); |
| } |
| Key::Enter => { |
| return Ok('\n'); |
| } |
| _ => {} |
| } |
| } |
| } |
| |
| /// Read a single key form the terminal. |
| /// |
| /// This does not echo anything. If the terminal is not user attended |
| /// the return value will always be the unknown key. |
| pub fn read_key(&self) -> io::Result<Key> { |
| if !self.is_tty { |
| Ok(Key::Unknown) |
| } else { |
| read_single_key(false) |
| } |
| } |
| |
| pub fn read_key_raw(&self) -> io::Result<Key> { |
| if !self.is_tty { |
| Ok(Key::Unknown) |
| } else { |
| read_single_key(true) |
| } |
| } |
| |
| /// Read one line of input. |
| /// |
| /// This does not include the trailing newline. If the terminal is not |
| /// user attended the return value will always be an empty string. |
| pub fn read_line(&self) -> io::Result<String> { |
| self.read_line_initial_text("") |
| } |
| |
| /// Read one line of input with initial text. |
| /// |
| /// This method blocks until no other thread is waiting for this read_line |
| /// before reading a line from the terminal. |
| /// This does not include the trailing newline. If the terminal is not |
| /// user attended the return value will always be an empty string. |
| pub fn read_line_initial_text(&self, initial: &str) -> io::Result<String> { |
| if !self.is_tty { |
| return Ok("".into()); |
| } |
| *self.inner.prompt.write().unwrap() = initial.to_string(); |
| // use a guard in order to prevent races with other calls to read_line_initial_text |
| let _guard = self.inner.prompt_guard.lock().unwrap(); |
| |
| self.write_str(initial)?; |
| |
| fn read_line_internal(slf: &Term, initial: &str) -> io::Result<String> { |
| let prefix_len = initial.len(); |
| |
| let mut chars: Vec<char> = initial.chars().collect(); |
| |
| loop { |
| match slf.read_key()? { |
| Key::Backspace => { |
| if prefix_len < chars.len() && chars.pop().is_some() { |
| slf.clear_chars(1)?; |
| } |
| slf.flush()?; |
| } |
| Key::Char(chr) => { |
| chars.push(chr); |
| let mut bytes_char = [0; 4]; |
| chr.encode_utf8(&mut bytes_char); |
| slf.write_str(chr.encode_utf8(&mut bytes_char))?; |
| slf.flush()?; |
| } |
| Key::Enter => { |
| slf.write_through(format!("\n{}", initial).as_bytes())?; |
| break; |
| } |
| _ => (), |
| } |
| } |
| Ok(chars.iter().skip(prefix_len).collect::<String>()) |
| } |
| let ret = read_line_internal(self, initial); |
| |
| *self.inner.prompt.write().unwrap() = String::new(); |
| ret |
| } |
| |
| /// Read a line of input securely. |
| /// |
| /// This is similar to `read_line` but will not echo the output. This |
| /// also switches the terminal into a different mode where not all |
| /// characters might be accepted. |
| pub fn read_secure_line(&self) -> io::Result<String> { |
| if !self.is_tty { |
| return Ok("".into()); |
| } |
| match read_secure() { |
| Ok(rv) => { |
| self.write_line("")?; |
| Ok(rv) |
| } |
| Err(err) => Err(err), |
| } |
| } |
| |
| /// Flush internal buffers. |
| /// |
| /// This forces the contents of the internal buffer to be written to |
| /// the terminal. This is unnecessary for unbuffered terminals which |
| /// will automatically flush. |
| pub fn flush(&self) -> io::Result<()> { |
| if let Some(ref buffer) = self.inner.buffer { |
| let mut buffer = buffer.lock().unwrap(); |
| if !buffer.is_empty() { |
| self.write_through(&buffer[..])?; |
| buffer.clear(); |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Check if the terminal is indeed a terminal. |
| #[inline] |
| pub fn is_term(&self) -> bool { |
| self.is_tty |
| } |
| |
| /// Check for common terminal features. |
| #[inline] |
| pub fn features(&self) -> TermFeatures<'_> { |
| TermFeatures(self) |
| } |
| |
| /// Return the terminal size in rows and columns or gets sensible defaults. |
| #[inline] |
| pub fn size(&self) -> (u16, u16) { |
| self.size_checked().unwrap_or((24, DEFAULT_WIDTH)) |
| } |
| |
| /// Return the terminal size in rows and columns. |
| /// |
| /// If the size cannot be reliably determined `None` is returned. |
| #[inline] |
| pub fn size_checked(&self) -> Option<(u16, u16)> { |
| terminal_size(self) |
| } |
| |
| /// Move the cursor to row `x` and column `y`. Values are 0-based. |
| #[inline] |
| pub fn move_cursor_to(&self, x: usize, y: usize) -> io::Result<()> { |
| move_cursor_to(self, x, y) |
| } |
| |
| /// Move the cursor up by `n` lines, if possible. |
| /// |
| /// If there are less than `n` lines above the current cursor position, |
| /// the cursor is moved to the top line of the terminal (i.e., as far up as possible). |
| #[inline] |
| pub fn move_cursor_up(&self, n: usize) -> io::Result<()> { |
| move_cursor_up(self, n) |
| } |
| |
| /// Move the cursor down by `n` lines, if possible. |
| /// |
| /// If there are less than `n` lines below the current cursor position, |
| /// the cursor is moved to the bottom line of the terminal (i.e., as far down as possible). |
| #[inline] |
| pub fn move_cursor_down(&self, n: usize) -> io::Result<()> { |
| move_cursor_down(self, n) |
| } |
| |
| /// Move the cursor `n` characters to the left, if possible. |
| /// |
| /// If there are fewer than `n` characters to the left of the current cursor position, |
| /// the cursor is moved to the beginning of the line (i.e., as far to the left as possible). |
| #[inline] |
| pub fn move_cursor_left(&self, n: usize) -> io::Result<()> { |
| move_cursor_left(self, n) |
| } |
| |
| /// Move the cursor `n` characters to the right. |
| /// |
| /// If there are fewer than `n` characters to the right of the current cursor position, |
| /// the cursor is moved to the end of the current line (i.e., as far to the right as possible). |
| #[inline] |
| pub fn move_cursor_right(&self, n: usize) -> io::Result<()> { |
| move_cursor_right(self, n) |
| } |
| |
| /// Clear the current line. |
| /// |
| /// Position the cursor at the beginning of the current line. |
| #[inline] |
| pub fn clear_line(&self) -> io::Result<()> { |
| clear_line(self) |
| } |
| |
| /// Clear the last `n` lines before the current line. |
| /// |
| /// Position the cursor at the beginning of the first line that was cleared. |
| pub fn clear_last_lines(&self, n: usize) -> io::Result<()> { |
| self.move_cursor_up(n)?; |
| for _ in 0..n { |
| self.clear_line()?; |
| self.move_cursor_down(1)?; |
| } |
| self.move_cursor_up(n)?; |
| Ok(()) |
| } |
| |
| /// Clear the entire screen. |
| /// |
| /// Move the cursor to the upper left corner of the screen. |
| #[inline] |
| pub fn clear_screen(&self) -> io::Result<()> { |
| clear_screen(self) |
| } |
| |
| /// Clear everything from the current cursor position to the end of the screen. |
| /// The cursor stays in its position. |
| #[inline] |
| pub fn clear_to_end_of_screen(&self) -> io::Result<()> { |
| clear_to_end_of_screen(self) |
| } |
| |
| /// Clear the last `n` characters of the current line. |
| #[inline] |
| pub fn clear_chars(&self, n: usize) -> io::Result<()> { |
| clear_chars(self, n) |
| } |
| |
| /// Set the terminal title. |
| pub fn set_title<T: Display>(&self, title: T) { |
| if !self.is_tty { |
| return; |
| } |
| set_title(title); |
| } |
| |
| /// Make the cursor visible again. |
| #[inline] |
| pub fn show_cursor(&self) -> io::Result<()> { |
| show_cursor(self) |
| } |
| |
| /// Hide the cursor. |
| #[inline] |
| pub fn hide_cursor(&self) -> io::Result<()> { |
| hide_cursor(self) |
| } |
| |
| // helpers |
| |
| #[cfg(all(windows, feature = "windows-console-colors"))] |
| fn write_through(&self, bytes: &[u8]) -> io::Result<()> { |
| if self.is_msys_tty || !self.is_tty { |
| self.write_through_common(bytes) |
| } else { |
| match self.inner.target { |
| TermTarget::Stdout => console_colors(self, Console::stdout()?, bytes), |
| TermTarget::Stderr => console_colors(self, Console::stderr()?, bytes), |
| } |
| } |
| } |
| |
| #[cfg(not(all(windows, feature = "windows-console-colors")))] |
| fn write_through(&self, bytes: &[u8]) -> io::Result<()> { |
| self.write_through_common(bytes) |
| } |
| |
| pub(crate) fn write_through_common(&self, bytes: &[u8]) -> io::Result<()> { |
| match self.inner.target { |
| TermTarget::Stdout => { |
| io::stdout().write_all(bytes)?; |
| io::stdout().flush()?; |
| } |
| TermTarget::Stderr => { |
| io::stderr().write_all(bytes)?; |
| io::stderr().flush()?; |
| } |
| #[cfg(unix)] |
| TermTarget::ReadWritePair(ReadWritePair { ref write, .. }) => { |
| let mut write = write.lock().unwrap(); |
| write.write_all(bytes)?; |
| write.flush()?; |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| /// A fast way to check if the application has a user attended for stdout. |
| /// |
| /// This means that stdout is connected to a terminal instead of a |
| /// file or redirected by other means. This is a shortcut for |
| /// checking the `is_attended` feature on the stdout terminal. |
| #[inline] |
| pub fn user_attended() -> bool { |
| Term::stdout().features().is_attended() |
| } |
| |
| /// A fast way to check if the application has a user attended for stderr. |
| /// |
| /// This means that stderr is connected to a terminal instead of a |
| /// file or redirected by other means. This is a shortcut for |
| /// checking the `is_attended` feature on the stderr terminal. |
| #[inline] |
| pub fn user_attended_stderr() -> bool { |
| Term::stderr().features().is_attended() |
| } |
| |
| #[cfg(unix)] |
| impl AsRawFd for Term { |
| fn as_raw_fd(&self) -> RawFd { |
| match self.inner.target { |
| TermTarget::Stdout => libc::STDOUT_FILENO, |
| TermTarget::Stderr => libc::STDERR_FILENO, |
| TermTarget::ReadWritePair(ReadWritePair { ref write, .. }) => { |
| write.lock().unwrap().as_raw_fd() |
| } |
| } |
| } |
| } |
| |
| #[cfg(windows)] |
| impl AsRawHandle for Term { |
| fn as_raw_handle(&self) -> RawHandle { |
| use windows_sys::Win32::System::Console::{ |
| GetStdHandle, STD_ERROR_HANDLE, STD_OUTPUT_HANDLE, |
| }; |
| |
| unsafe { |
| GetStdHandle(match self.inner.target { |
| TermTarget::Stdout => STD_OUTPUT_HANDLE, |
| TermTarget::Stderr => STD_ERROR_HANDLE, |
| }) as RawHandle |
| } |
| } |
| } |
| |
| impl Write for Term { |
| fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
| match self.inner.buffer { |
| Some(ref buffer) => buffer.lock().unwrap().write_all(buf), |
| None => self.write_through(buf), |
| }?; |
| Ok(buf.len()) |
| } |
| |
| fn flush(&mut self) -> io::Result<()> { |
| Term::flush(self) |
| } |
| } |
| |
| impl<'a> Write for &'a Term { |
| fn write(&mut self, buf: &[u8]) -> io::Result<usize> { |
| match self.inner.buffer { |
| Some(ref buffer) => buffer.lock().unwrap().write_all(buf), |
| None => self.write_through(buf), |
| }?; |
| Ok(buf.len()) |
| } |
| |
| fn flush(&mut self) -> io::Result<()> { |
| Term::flush(self) |
| } |
| } |
| |
| impl Read for Term { |
| fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
| io::stdin().read(buf) |
| } |
| } |
| |
| impl<'a> Read for &'a Term { |
| fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
| io::stdin().read(buf) |
| } |
| } |
| |
| #[cfg(all(unix, not(target_arch = "wasm32")))] |
| pub use crate::unix_term::*; |
| #[cfg(target_arch = "wasm32")] |
| pub use crate::wasm_term::*; |
| #[cfg(windows)] |
| pub use crate::windows_term::*; |