diff options
author | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
---|---|---|
committer | Valentin Popov <valentin@popov.link> | 2024-01-08 00:21:28 +0300 |
commit | 1b6a04ca5504955c571d1c97504fb45ea0befee4 (patch) | |
tree | 7579f518b23313e8a9748a88ab6173d5e030b227 /vendor/console/src/term.rs | |
parent | 5ecd8cf2cba827454317368b68571df0d13d7842 (diff) | |
download | fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.tar.xz fparkan-1b6a04ca5504955c571d1c97504fb45ea0befee4.zip |
Initial vendor packages
Signed-off-by: Valentin Popov <valentin@popov.link>
Diffstat (limited to 'vendor/console/src/term.rs')
-rw-r--r-- | vendor/console/src/term.rs | 632 |
1 files changed, 632 insertions, 0 deletions
diff --git a/vendor/console/src/term.rs b/vendor/console/src/term.rs new file mode 100644 index 0000000..0a40258 --- /dev/null +++ b/vendor/console/src/term.rs @@ -0,0 +1,632 @@ +use std::fmt::{Debug, Display}; +use std::io::{self, Read, Write}; +use std::sync::{Arc, Mutex}; + +#[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>>>, +} + +/// 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(unix)] + { + 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, + }) + } + + /// Return a new unbuffered terminal to stderr. + #[inline] + pub fn stderr() -> Term { + Term::with_inner(TermInner { + target: TermTarget::Stderr, + buffer: None, + }) + } + + /// Return a new buffered terminal. + pub fn buffered_stdout() -> Term { + Term::with_inner(TermInner { + target: TermTarget::Stdout, + buffer: Some(Mutex::new(vec![])), + }) + } + + /// Return a new buffered terminal to stderr. + pub fn buffered_stderr() -> Term { + Term::with_inner(TermInner { + target: TermTarget::Stderr, + buffer: Some(Mutex::new(vec![])), + }) + } + + /// 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, + }) + } + + /// 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<()> { + match self.inner.buffer { + Some(ref mutex) => { + let mut buffer = mutex.lock().unwrap(); + buffer.extend_from_slice(s.as_bytes()); + buffer.push(b'\n'); + Ok(()) + } + None => self.write_through(format!("{}\n", s).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() + } + } + + /// 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> { + if !self.is_tty { + return Ok("".into()); + } + let mut rv = String::new(); + io::stdin().read_line(&mut rv)?; + let len = rv.trim_end_matches(&['\r', '\n'][..]).len(); + rv.truncate(len); + Ok(rv) + } + + /// Read one line of input with initial text. + /// + /// 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.write_str(initial)?; + + let mut chars: Vec<char> = initial.chars().collect(); + + loop { + match self.read_key()? { + Key::Backspace => { + if chars.pop().is_some() { + self.clear_chars(1)?; + } + self.flush()?; + } + Key::Char(chr) => { + chars.push(chr); + let mut bytes_char = [0; 4]; + chr.encode_utf8(&mut bytes_char); + self.write_str(chr.encode_utf8(&mut bytes_char))?; + self.flush()?; + } + Key::Enter => { + self.write_line("")?; + break; + } + _ => (), + } + } + Ok(chars.iter().collect::<String>()) + } + + /// 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(unix)] +pub use crate::unix_term::*; +#[cfg(target_arch = "wasm32")] +pub use crate::wasm_term::*; +#[cfg(windows)] +pub use crate::windows_term::*; |