diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 18:31:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 18:31:44 +0000 |
commit | c23a457e72abe608715ac76f076f47dc42af07a5 (patch) | |
tree | 2772049aaf84b5c9d0ed12ec8d86812f7a7904b6 /vendor/console/src | |
parent | Releasing progress-linux version 1.73.0+dfsg1-1~progress7.99u1. (diff) | |
download | rustc-c23a457e72abe608715ac76f076f47dc42af07a5.tar.xz rustc-c23a457e72abe608715ac76f076f47dc42af07a5.zip |
Merging upstream version 1.74.1+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/console/src')
-rw-r--r-- | vendor/console/src/ansi.rs | 438 | ||||
-rw-r--r-- | vendor/console/src/common_term.rs | 72 | ||||
-rw-r--r-- | vendor/console/src/kb.rs | 29 | ||||
-rw-r--r-- | vendor/console/src/lib.rs | 104 | ||||
-rw-r--r-- | vendor/console/src/term.rs | 632 | ||||
-rw-r--r-- | vendor/console/src/unix_term.rs | 362 | ||||
-rw-r--r-- | vendor/console/src/utils.rs | 962 | ||||
-rw-r--r-- | vendor/console/src/wasm_term.rs | 54 | ||||
-rw-r--r-- | vendor/console/src/windows_term/colors.rs | 451 | ||||
-rw-r--r-- | vendor/console/src/windows_term/mod.rs | 563 |
10 files changed, 3667 insertions, 0 deletions
diff --git a/vendor/console/src/ansi.rs b/vendor/console/src/ansi.rs new file mode 100644 index 000000000..3a3c96c37 --- /dev/null +++ b/vendor/console/src/ansi.rs @@ -0,0 +1,438 @@ +use std::{ + borrow::Cow, + iter::{FusedIterator, Peekable}, + str::CharIndices, +}; + +#[derive(Debug, Clone, Copy)] +enum State { + Start, + S1, + S2, + S3, + S4, + S5, + S6, + S7, + S8, + S9, + S10, + S11, + Trap, +} + +impl Default for State { + fn default() -> Self { + Self::Start + } +} + +impl State { + fn is_final(&self) -> bool { + #[allow(clippy::match_like_matches_macro)] + match self { + Self::S3 | Self::S5 | Self::S6 | Self::S7 | Self::S8 | Self::S9 | Self::S11 => true, + _ => false, + } + } + + fn is_trapped(&self) -> bool { + #[allow(clippy::match_like_matches_macro)] + match self { + Self::Trap => true, + _ => false, + } + } + + fn transition(&mut self, c: char) { + *self = match c { + '\u{1b}' | '\u{9b}' => match self { + Self::Start => Self::S1, + _ => Self::Trap, + }, + '(' | ')' => match self { + Self::S1 => Self::S2, + Self::S2 | Self::S4 => Self::S4, + _ => Self::Trap, + }, + ';' => match self { + Self::S1 | Self::S2 | Self::S4 => Self::S4, + Self::S5 | Self::S6 | Self::S7 | Self::S8 | Self::S10 => Self::S10, + _ => Self::Trap, + }, + + '[' | '#' | '?' => match self { + Self::S1 | Self::S2 | Self::S4 => Self::S4, + _ => Self::Trap, + }, + '0'..='2' => match self { + Self::S1 | Self::S4 => Self::S5, + Self::S2 => Self::S3, + Self::S5 => Self::S6, + Self::S6 => Self::S7, + Self::S7 => Self::S8, + Self::S8 => Self::S9, + Self::S10 => Self::S5, + _ => Self::Trap, + }, + '3'..='9' => match self { + Self::S1 | Self::S4 => Self::S5, + Self::S2 => Self::S5, + Self::S5 => Self::S6, + Self::S6 => Self::S7, + Self::S7 => Self::S8, + Self::S8 => Self::S9, + Self::S10 => Self::S5, + _ => Self::Trap, + }, + 'A'..='P' | 'R' | 'Z' | 'c' | 'f'..='n' | 'q' | 'r' | 'y' | '=' | '>' | '<' => { + match self { + Self::S1 + | Self::S2 + | Self::S4 + | Self::S5 + | Self::S6 + | Self::S7 + | Self::S8 + | Self::S10 => Self::S11, + _ => Self::Trap, + } + } + _ => Self::Trap, + }; + } +} + +#[derive(Debug)] +struct Matches<'a> { + s: &'a str, + it: Peekable<CharIndices<'a>>, +} + +impl<'a> Matches<'a> { + fn new(s: &'a str) -> Self { + let it = s.char_indices().peekable(); + Self { s, it } + } +} + +#[derive(Debug)] +struct Match<'a> { + text: &'a str, + start: usize, + end: usize, +} + +impl<'a> Match<'a> { + #[inline] + pub fn as_str(&self) -> &'a str { + &self.text[self.start..self.end] + } +} + +impl<'a> Iterator for Matches<'a> { + type Item = Match<'a>; + + fn next(&mut self) -> Option<Self::Item> { + find_ansi_code_exclusive(&mut self.it).map(|(start, end)| Match { + text: self.s, + start, + end, + }) + } +} + +impl<'a> FusedIterator for Matches<'a> {} + +fn find_ansi_code_exclusive(it: &mut Peekable<CharIndices>) -> Option<(usize, usize)> { + 'outer: loop { + if let (start, '\u{1b}') | (start, '\u{9b}') = it.peek()? { + let start = *start; + let mut state = State::default(); + let mut maybe_end = None; + + loop { + let item = it.peek(); + + if let Some((idx, c)) = item { + state.transition(*c); + + if state.is_final() { + maybe_end = Some(*idx); + } + } + + // The match is greedy so run till we hit the trap state no matter what. A valid + // match is just one that was final at some point + if state.is_trapped() || item.is_none() { + match maybe_end { + Some(end) => { + // All possible final characters are a single byte so it's safe to make + // the end exclusive by just adding one + return Some((start, end + 1)); + } + // The character we are peeking right now might be the start of a match so + // we want to continue the loop without popping off that char + None => continue 'outer, + } + } + + it.next(); + } + } + + it.next(); + } +} + +/// Helper function to strip ansi codes. +pub fn strip_ansi_codes(s: &str) -> Cow<str> { + let mut char_it = s.char_indices().peekable(); + match find_ansi_code_exclusive(&mut char_it) { + Some(_) => { + let stripped: String = AnsiCodeIterator::new(s) + .filter_map(|(text, is_ansi)| if is_ansi { None } else { Some(text) }) + .collect(); + Cow::Owned(stripped) + } + None => Cow::Borrowed(s), + } +} + +/// An iterator over ansi codes in a string. +/// +/// This type can be used to scan over ansi codes in a string. +/// It yields tuples in the form `(s, is_ansi)` where `s` is a slice of +/// the original string and `is_ansi` indicates if the slice contains +/// ansi codes or string values. +pub struct AnsiCodeIterator<'a> { + s: &'a str, + pending_item: Option<(&'a str, bool)>, + last_idx: usize, + cur_idx: usize, + iter: Matches<'a>, +} + +impl<'a> AnsiCodeIterator<'a> { + /// Creates a new ansi code iterator. + pub fn new(s: &'a str) -> AnsiCodeIterator<'a> { + AnsiCodeIterator { + s, + pending_item: None, + last_idx: 0, + cur_idx: 0, + iter: Matches::new(s), + } + } + + /// Returns the string slice up to the current match. + pub fn current_slice(&self) -> &str { + &self.s[..self.cur_idx] + } + + /// Returns the string slice from the current match to the end. + pub fn rest_slice(&self) -> &str { + &self.s[self.cur_idx..] + } +} + +impl<'a> Iterator for AnsiCodeIterator<'a> { + type Item = (&'a str, bool); + + fn next(&mut self) -> Option<(&'a str, bool)> { + if let Some(pending_item) = self.pending_item.take() { + self.cur_idx += pending_item.0.len(); + Some(pending_item) + } else if let Some(m) = self.iter.next() { + let s = &self.s[self.last_idx..m.start]; + self.last_idx = m.end; + if s.is_empty() { + self.cur_idx = m.end; + Some((m.as_str(), true)) + } else { + self.cur_idx = m.start; + self.pending_item = Some((m.as_str(), true)); + Some((s, false)) + } + } else if self.last_idx < self.s.len() { + let rv = &self.s[self.last_idx..]; + self.cur_idx = self.s.len(); + self.last_idx = self.s.len(); + Some((rv, false)) + } else { + None + } + } +} + +impl<'a> FusedIterator for AnsiCodeIterator<'a> {} + +#[cfg(test)] +mod tests { + use super::*; + + use lazy_static::lazy_static; + use proptest::prelude::*; + use regex::Regex; + + // The manual dfa `State` is a handwritten translation from the previously used regex. That + // regex is kept here and used to ensure that the new matches are the same as the old + lazy_static! { + static ref STRIP_ANSI_RE: Regex = Regex::new( + r"[\x1b\x9b]([()][012AB]|[\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><])", + ) + .unwrap(); + } + + impl<'a, 'b> PartialEq<Match<'a>> for regex::Match<'b> { + fn eq(&self, other: &Match<'a>) -> bool { + self.start() == other.start && self.end() == other.end + } + } + + proptest! { + #[test] + fn dfa_matches_old_regex(s in r"([\x1b\x9b]?.*){0,5}") { + let old_matches: Vec<_> = STRIP_ANSI_RE.find_iter(&s).collect(); + let new_matches: Vec<_> = Matches::new(&s).collect(); + assert_eq!(old_matches, new_matches); + } + } + + #[test] + fn dfa_matches_regex_on_small_strings() { + // To make sure the test runs in a reasonable time this is a slimmed down list of + // characters to reduce the groups that are only used with each other along with one + // arbitrarily chosen character not used in the regex (' ') + const POSSIBLE_BYTES: &[u8] = &[b' ', 0x1b, 0x9b, b'(', b'0', b'[', b';', b'3', b'C']; + + fn check_all_strings_of_len(len: usize) { + _check_all_strings_of_len(len, &mut Vec::with_capacity(len)); + } + + fn _check_all_strings_of_len(len: usize, chunk: &mut Vec<u8>) { + if len == 0 { + if let Ok(s) = std::str::from_utf8(chunk) { + let old_matches: Vec<_> = STRIP_ANSI_RE.find_iter(s).collect(); + let new_matches: Vec<_> = Matches::new(s).collect(); + assert_eq!(old_matches, new_matches); + } + + return; + } + + for b in POSSIBLE_BYTES { + chunk.push(*b); + _check_all_strings_of_len(len - 1, chunk); + chunk.pop(); + } + } + + for str_len in 0..=6 { + check_all_strings_of_len(str_len); + } + } + + #[test] + fn complex_data() { + let s = std::fs::read_to_string( + std::path::Path::new("tests") + .join("data") + .join("sample_zellij_session.log"), + ) + .unwrap(); + + let old_matches: Vec<_> = STRIP_ANSI_RE.find_iter(&s).collect(); + let new_matches: Vec<_> = Matches::new(&s).collect(); + assert_eq!(old_matches, new_matches); + } + + #[test] + fn state_machine() { + let ansi_code = "\x1b)B"; + let mut state = State::default(); + assert!(!state.is_final()); + + for c in ansi_code.chars() { + state.transition(c); + } + assert!(state.is_final()); + + state.transition('A'); + assert!(state.is_trapped()); + } + + #[test] + fn back_to_back_entry_char() { + let s = "\x1b\x1bf"; + let matches: Vec<_> = Matches::new(s).map(|m| m.as_str()).collect(); + assert_eq!(&["\x1bf"], matches.as_slice()); + } + + #[test] + fn early_paren_can_use_many_chars() { + let s = "\x1b(C"; + let matches: Vec<_> = Matches::new(s).map(|m| m.as_str()).collect(); + assert_eq!(&[s], matches.as_slice()); + } + + #[test] + fn long_run_of_digits() { + let s = "\u{1b}00000"; + let matches: Vec<_> = Matches::new(s).map(|m| m.as_str()).collect(); + assert_eq!(&[s], matches.as_slice()); + } + + #[test] + fn test_ansi_iter_re_vt100() { + let s = "\x1b(0lpq\x1b)Benglish"; + let mut iter = AnsiCodeIterator::new(s); + assert_eq!(iter.next(), Some(("\x1b(0", true))); + assert_eq!(iter.next(), Some(("lpq", false))); + assert_eq!(iter.next(), Some(("\x1b)B", true))); + assert_eq!(iter.next(), Some(("english", false))); + } + + #[test] + fn test_ansi_iter_re() { + use crate::style; + let s = format!("Hello {}!", style("World").red().force_styling(true)); + let mut iter = AnsiCodeIterator::new(&s); + assert_eq!(iter.next(), Some(("Hello ", false))); + assert_eq!(iter.current_slice(), "Hello "); + assert_eq!(iter.rest_slice(), "\x1b[31mWorld\x1b[0m!"); + assert_eq!(iter.next(), Some(("\x1b[31m", true))); + assert_eq!(iter.current_slice(), "Hello \x1b[31m"); + assert_eq!(iter.rest_slice(), "World\x1b[0m!"); + assert_eq!(iter.next(), Some(("World", false))); + assert_eq!(iter.current_slice(), "Hello \x1b[31mWorld"); + assert_eq!(iter.rest_slice(), "\x1b[0m!"); + assert_eq!(iter.next(), Some(("\x1b[0m", true))); + assert_eq!(iter.current_slice(), "Hello \x1b[31mWorld\x1b[0m"); + assert_eq!(iter.rest_slice(), "!"); + assert_eq!(iter.next(), Some(("!", false))); + assert_eq!(iter.current_slice(), "Hello \x1b[31mWorld\x1b[0m!"); + assert_eq!(iter.rest_slice(), ""); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_ansi_iter_re_on_multi() { + use crate::style; + let s = format!("{}", style("a").red().bold().force_styling(true)); + let mut iter = AnsiCodeIterator::new(&s); + assert_eq!(iter.next(), Some(("\x1b[31m", true))); + assert_eq!(iter.current_slice(), "\x1b[31m"); + assert_eq!(iter.rest_slice(), "\x1b[1ma\x1b[0m"); + assert_eq!(iter.next(), Some(("\x1b[1m", true))); + assert_eq!(iter.current_slice(), "\x1b[31m\x1b[1m"); + assert_eq!(iter.rest_slice(), "a\x1b[0m"); + assert_eq!(iter.next(), Some(("a", false))); + assert_eq!(iter.current_slice(), "\x1b[31m\x1b[1ma"); + assert_eq!(iter.rest_slice(), "\x1b[0m"); + assert_eq!(iter.next(), Some(("\x1b[0m", true))); + assert_eq!(iter.current_slice(), "\x1b[31m\x1b[1ma\x1b[0m"); + assert_eq!(iter.rest_slice(), ""); + assert_eq!(iter.next(), None); + } +} diff --git a/vendor/console/src/common_term.rs b/vendor/console/src/common_term.rs new file mode 100644 index 000000000..020660a5b --- /dev/null +++ b/vendor/console/src/common_term.rs @@ -0,0 +1,72 @@ +use std::io; + +use crate::term::Term; + +pub fn move_cursor_down(out: &Term, n: usize) -> io::Result<()> { + if n > 0 { + out.write_str(&format!("\x1b[{}B", n)) + } else { + Ok(()) + } +} + +pub fn move_cursor_up(out: &Term, n: usize) -> io::Result<()> { + if n > 0 { + out.write_str(&format!("\x1b[{}A", n)) + } else { + Ok(()) + } +} +pub fn move_cursor_left(out: &Term, n: usize) -> io::Result<()> { + if n > 0 { + out.write_str(&format!("\x1b[{}D", n)) + } else { + Ok(()) + } +} + +pub fn move_cursor_right(out: &Term, n: usize) -> io::Result<()> { + if n > 0 { + out.write_str(&format!("\x1b[{}C", n)) + } else { + Ok(()) + } +} + +#[inline] +pub fn move_cursor_to(out: &Term, x: usize, y: usize) -> io::Result<()> { + out.write_str(&format!("\x1B[{};{}H", y + 1, x + 1)) +} + +pub fn clear_chars(out: &Term, n: usize) -> io::Result<()> { + if n > 0 { + out.write_str(&format!("\x1b[{}D\x1b[0K", n)) + } else { + Ok(()) + } +} + +#[inline] +pub fn clear_line(out: &Term) -> io::Result<()> { + out.write_str("\r\x1b[2K") +} + +#[inline] +pub fn clear_screen(out: &Term) -> io::Result<()> { + out.write_str("\r\x1b[2J\r\x1b[H") +} + +#[inline] +pub fn clear_to_end_of_screen(out: &Term) -> io::Result<()> { + out.write_str("\r\x1b[0J") +} + +#[inline] +pub fn show_cursor(out: &Term) -> io::Result<()> { + out.write_str("\x1b[?25h") +} + +#[inline] +pub fn hide_cursor(out: &Term) -> io::Result<()> { + out.write_str("\x1b[?25l") +} diff --git a/vendor/console/src/kb.rs b/vendor/console/src/kb.rs new file mode 100644 index 000000000..5258c1350 --- /dev/null +++ b/vendor/console/src/kb.rs @@ -0,0 +1,29 @@ +/// Key mapping +/// +/// This is an incomplete mapping of keys that are supported for reading +/// from the keyboard. +#[non_exhaustive] +#[derive(Clone, PartialEq, Eq, Debug, Hash)] +pub enum Key { + Unknown, + /// Unrecognized sequence containing Esc and a list of chars + UnknownEscSeq(Vec<char>), + ArrowLeft, + ArrowRight, + ArrowUp, + ArrowDown, + Enter, + Escape, + Backspace, + Home, + End, + Tab, + BackTab, + Alt, + Del, + Shift, + Insert, + PageUp, + PageDown, + Char(char), +} diff --git a/vendor/console/src/lib.rs b/vendor/console/src/lib.rs new file mode 100644 index 000000000..1b18afc03 --- /dev/null +++ b/vendor/console/src/lib.rs @@ -0,0 +1,104 @@ +//! console is a library for Rust that provides access to various terminal +//! features so you can build nicer looking command line interfaces. It +//! comes with various tools and utilities for working with Terminals and +//! formatting text. +//! +//! Best paired with other libraries in the family: +//! +//! * [dialoguer](https://docs.rs/dialoguer) +//! * [indicatif](https://docs.rs/indicatif) +//! +//! # Terminal Access +//! +//! The terminal is abstracted through the `console::Term` type. It can +//! either directly provide access to the connected terminal or by buffering +//! up commands. A buffered terminal will however not be completely buffered +//! on windows where cursor movements are currently directly passed through. +//! +//! Example usage: +//! +//! ``` +//! # fn test() -> Result<(), Box<dyn std::error::Error>> { +//! use std::thread; +//! use std::time::Duration; +//! +//! use console::Term; +//! +//! let term = Term::stdout(); +//! term.write_line("Hello World!")?; +//! thread::sleep(Duration::from_millis(2000)); +//! term.clear_line()?; +//! # Ok(()) } test().unwrap(); +//! ``` +//! +//! # Colors and Styles +//! +//! `console` automaticaly detects when to use colors based on the tty flag. It also +//! provides higher level wrappers for styling text and other things that can be +//! displayed with the `style` function and utility types. +//! +//! Example usage: +//! +//! ``` +//! use console::style; +//! +//! println!("This is {} neat", style("quite").cyan()); +//! ``` +//! +//! You can also store styles and apply them to text later: +//! +//! ``` +//! use console::Style; +//! +//! let cyan = Style::new().cyan(); +//! println!("This is {} neat", cyan.apply_to("quite")); +//! ``` +//! +//! # Working with ANSI Codes +//! +//! The crate provids the function `strip_ansi_codes` to remove ANSI codes +//! from a string as well as `measure_text_width` to calculate the width of a +//! string as it would be displayed by the terminal. Both of those together +//! are useful for more complex formatting. +//! +//! # Unicode Width Support +//! +//! By default this crate depends on the `unicode-width` crate to calculate +//! the width of terminal characters. If you do not need this you can disable +//! the `unicode-width` feature which will cut down on dependencies. +//! +//! # Features +//! +//! By default all features are enabled. The following features exist: +//! +//! * `unicode-width`: adds support for unicode width calculations +//! * `ansi-parsing`: adds support for parsing ansi codes (this adds support +//! for stripping and taking ansi escape codes into account for length +//! calculations). + +pub use crate::kb::Key; +pub use crate::term::{ + user_attended, user_attended_stderr, Term, TermFamily, TermFeatures, TermTarget, +}; +pub use crate::utils::{ + colors_enabled, colors_enabled_stderr, measure_text_width, pad_str, pad_str_with, + set_colors_enabled, set_colors_enabled_stderr, style, truncate_str, Alignment, Attribute, + Color, Emoji, Style, StyledObject, +}; + +#[cfg(feature = "ansi-parsing")] +pub use crate::ansi::{strip_ansi_codes, AnsiCodeIterator}; + +mod common_term; +mod kb; +mod term; +#[cfg(unix)] +mod unix_term; +mod utils; +#[cfg(target_arch = "wasm32")] +mod wasm_term; +#[cfg(windows)] +mod windows_term; + +#[cfg(feature = "ansi-parsing")] +mod ansi; diff --git a/vendor/console/src/term.rs b/vendor/console/src/term.rs new file mode 100644 index 000000000..0a4025850 --- /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::*; diff --git a/vendor/console/src/unix_term.rs b/vendor/console/src/unix_term.rs new file mode 100644 index 000000000..8e1e59259 --- /dev/null +++ b/vendor/console/src/unix_term.rs @@ -0,0 +1,362 @@ +use std::env; +use std::fmt::Display; +use std::fs; +use std::io; +use std::io::{BufRead, BufReader}; +use std::mem; +use std::os::unix::io::AsRawFd; +use std::ptr; +use std::str; + +use crate::kb::Key; +use crate::term::Term; + +pub use crate::common_term::*; + +pub const DEFAULT_WIDTH: u16 = 80; + +#[inline] +pub fn is_a_terminal(out: &Term) -> bool { + unsafe { libc::isatty(out.as_raw_fd()) != 0 } +} + +pub fn is_a_color_terminal(out: &Term) -> bool { + if !is_a_terminal(out) { + return false; + } + + if env::var("NO_COLOR").is_ok() { + return false; + } + + match env::var("TERM") { + Ok(term) => term != "dumb", + Err(_) => false, + } +} + +pub fn c_result<F: FnOnce() -> libc::c_int>(f: F) -> io::Result<()> { + let res = f(); + if res != 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +pub fn terminal_size(out: &Term) -> Option<(u16, u16)> { + unsafe { + if libc::isatty(libc::STDOUT_FILENO) != 1 { + return None; + } + + let mut winsize: libc::winsize = std::mem::zeroed(); + + // FIXME: ".into()" used as a temporary fix for a libc bug + // https://github.com/rust-lang/libc/pull/704 + #[allow(clippy::useless_conversion)] + libc::ioctl(out.as_raw_fd(), libc::TIOCGWINSZ.into(), &mut winsize); + if winsize.ws_row > 0 && winsize.ws_col > 0 { + Some((winsize.ws_row as u16, winsize.ws_col as u16)) + } else { + None + } + } +} + +pub fn read_secure() -> io::Result<String> { + let f_tty; + let fd = unsafe { + if libc::isatty(libc::STDIN_FILENO) == 1 { + f_tty = None; + libc::STDIN_FILENO + } else { + let f = fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty")?; + let fd = f.as_raw_fd(); + f_tty = Some(BufReader::new(f)); + fd + } + }; + + let mut termios = core::mem::MaybeUninit::uninit(); + c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?; + let mut termios = unsafe { termios.assume_init() }; + let original = termios; + termios.c_lflag &= !libc::ECHO; + c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &termios) })?; + let mut rv = String::new(); + + let read_rv = if let Some(mut f) = f_tty { + f.read_line(&mut rv) + } else { + io::stdin().read_line(&mut rv) + }; + + c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &original) })?; + + read_rv.map(|_| { + let len = rv.trim_end_matches(&['\r', '\n'][..]).len(); + rv.truncate(len); + rv + }) +} + +fn poll_fd(fd: i32, timeout: i32) -> io::Result<bool> { + let mut pollfd = libc::pollfd { + fd, + events: libc::POLLIN, + revents: 0, + }; + let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) }; + if ret < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(pollfd.revents & libc::POLLIN != 0) + } +} + +#[cfg(target_os = "macos")] +fn select_fd(fd: i32, timeout: i32) -> io::Result<bool> { + unsafe { + let mut read_fd_set: libc::fd_set = mem::zeroed(); + + let mut timeout_val; + let timeout = if timeout < 0 { + ptr::null_mut() + } else { + timeout_val = libc::timeval { + tv_sec: (timeout / 1000) as _, + tv_usec: (timeout * 1000) as _, + }; + &mut timeout_val + }; + + libc::FD_ZERO(&mut read_fd_set); + libc::FD_SET(fd, &mut read_fd_set); + let ret = libc::select( + fd + 1, + &mut read_fd_set, + ptr::null_mut(), + ptr::null_mut(), + timeout, + ); + if ret < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(libc::FD_ISSET(fd, &read_fd_set)) + } + } +} + +fn select_or_poll_term_fd(fd: i32, timeout: i32) -> io::Result<bool> { + // There is a bug on macos that ttys cannot be polled, only select() + // works. However given how problematic select is in general, we + // normally want to use poll there too. + #[cfg(target_os = "macos")] + { + if unsafe { libc::isatty(fd) == 1 } { + return select_fd(fd, timeout); + } + } + poll_fd(fd, timeout) +} + +fn read_single_char(fd: i32) -> io::Result<Option<char>> { + // timeout of zero means that it will not block + let is_ready = select_or_poll_term_fd(fd, 0)?; + + if is_ready { + // if there is something to be read, take 1 byte from it + let mut buf: [u8; 1] = [0]; + + read_bytes(fd, &mut buf, 1)?; + Ok(Some(buf[0] as char)) + } else { + //there is nothing to be read + Ok(None) + } +} + +// Similar to libc::read. Read count bytes into slice buf from descriptor fd. +// If successful, return the number of bytes read. +// Will return an error if nothing was read, i.e when called at end of file. +fn read_bytes(fd: i32, buf: &mut [u8], count: u8) -> io::Result<u8> { + let read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, count as usize) }; + if read < 0 { + Err(io::Error::last_os_error()) + } else if read == 0 { + Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Reached end of file", + )) + } else if buf[0] == b'\x03' { + Err(io::Error::new( + io::ErrorKind::Interrupted, + "read interrupted", + )) + } else { + Ok(read as u8) + } +} + +fn read_single_key_impl(fd: i32) -> Result<Key, io::Error> { + loop { + match read_single_char(fd)? { + Some('\x1b') => { + // Escape was read, keep reading in case we find a familiar key + break if let Some(c1) = read_single_char(fd)? { + if c1 == '[' { + if let Some(c2) = read_single_char(fd)? { + match c2 { + 'A' => Ok(Key::ArrowUp), + 'B' => Ok(Key::ArrowDown), + 'C' => Ok(Key::ArrowRight), + 'D' => Ok(Key::ArrowLeft), + 'H' => Ok(Key::Home), + 'F' => Ok(Key::End), + 'Z' => Ok(Key::BackTab), + _ => { + let c3 = read_single_char(fd)?; + if let Some(c3) = c3 { + if c3 == '~' { + match c2 { + '1' => Ok(Key::Home), // tmux + '2' => Ok(Key::Insert), + '3' => Ok(Key::Del), + '4' => Ok(Key::End), // tmux + '5' => Ok(Key::PageUp), + '6' => Ok(Key::PageDown), + '7' => Ok(Key::Home), // xrvt + '8' => Ok(Key::End), // xrvt + _ => Ok(Key::UnknownEscSeq(vec![c1, c2, c3])), + } + } else { + Ok(Key::UnknownEscSeq(vec![c1, c2, c3])) + } + } else { + // \x1b[ and 1 more char + Ok(Key::UnknownEscSeq(vec![c1, c2])) + } + } + } + } else { + // \x1b[ and no more input + Ok(Key::UnknownEscSeq(vec![c1])) + } + } else { + // char after escape is not [ + Ok(Key::UnknownEscSeq(vec![c1])) + } + } else { + //nothing after escape + Ok(Key::Escape) + }; + } + Some(c) => { + let byte = c as u8; + let mut buf: [u8; 4] = [byte, 0, 0, 0]; + + break if byte & 224u8 == 192u8 { + // a two byte unicode character + read_bytes(fd, &mut buf[1..], 1)?; + Ok(key_from_utf8(&buf[..2])) + } else if byte & 240u8 == 224u8 { + // a three byte unicode character + read_bytes(fd, &mut buf[1..], 2)?; + Ok(key_from_utf8(&buf[..3])) + } else if byte & 248u8 == 240u8 { + // a four byte unicode character + read_bytes(fd, &mut buf[1..], 3)?; + Ok(key_from_utf8(&buf[..4])) + } else { + Ok(match c { + '\n' | '\r' => Key::Enter, + '\x7f' => Key::Backspace, + '\t' => Key::Tab, + '\x01' => Key::Home, // Control-A (home) + '\x05' => Key::End, // Control-E (end) + '\x08' => Key::Backspace, // Control-H (8) (Identical to '\b') + _ => Key::Char(c), + }) + }; + } + None => { + // there is no subsequent byte ready to be read, block and wait for input + // negative timeout means that it will block indefinitely + match select_or_poll_term_fd(fd, -1) { + Ok(_) => continue, + Err(_) => break Err(io::Error::last_os_error()), + } + } + } + } +} + +pub fn read_single_key() -> io::Result<Key> { + let tty_f; + let fd = unsafe { + if libc::isatty(libc::STDIN_FILENO) == 1 { + libc::STDIN_FILENO + } else { + tty_f = fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty")?; + tty_f.as_raw_fd() + } + }; + let mut termios = core::mem::MaybeUninit::uninit(); + c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?; + let mut termios = unsafe { termios.assume_init() }; + let original = termios; + unsafe { libc::cfmakeraw(&mut termios) }; + termios.c_oflag = original.c_oflag; + c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &termios) })?; + let rv: io::Result<Key> = read_single_key_impl(fd); + c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &original) })?; + + // if the user hit ^C we want to signal SIGINT to outselves. + if let Err(ref err) = rv { + if err.kind() == io::ErrorKind::Interrupted { + unsafe { + libc::raise(libc::SIGINT); + } + } + } + + rv +} + +pub fn key_from_utf8(buf: &[u8]) -> Key { + if let Ok(s) = str::from_utf8(buf) { + if let Some(c) = s.chars().next() { + return Key::Char(c); + } + } + Key::Unknown +} + +#[cfg(not(target_os = "macos"))] +lazy_static::lazy_static! { + static ref IS_LANG_UTF8: bool = match std::env::var("LANG") { + Ok(lang) => lang.to_uppercase().ends_with("UTF-8"), + _ => false, + }; +} + +#[cfg(target_os = "macos")] +pub fn wants_emoji() -> bool { + true +} + +#[cfg(not(target_os = "macos"))] +pub fn wants_emoji() -> bool { + *IS_LANG_UTF8 +} + +pub fn set_title<T: Display>(title: T) { + print!("\x1b]0;{}\x07", title); +} diff --git a/vendor/console/src/utils.rs b/vendor/console/src/utils.rs new file mode 100644 index 000000000..9e6b942f7 --- /dev/null +++ b/vendor/console/src/utils.rs @@ -0,0 +1,962 @@ +use std::borrow::Cow; +use std::collections::BTreeSet; +use std::env; +use std::fmt; +use std::sync::atomic::{AtomicBool, Ordering}; + +use lazy_static::lazy_static; + +use crate::term::{wants_emoji, Term}; + +#[cfg(feature = "ansi-parsing")] +use crate::ansi::{strip_ansi_codes, AnsiCodeIterator}; + +#[cfg(not(feature = "ansi-parsing"))] +fn strip_ansi_codes(s: &str) -> &str { + s +} + +fn default_colors_enabled(out: &Term) -> bool { + (out.features().colors_supported() + && &env::var("CLICOLOR").unwrap_or_else(|_| "1".into()) != "0") + || &env::var("CLICOLOR_FORCE").unwrap_or_else(|_| "0".into()) != "0" +} + +lazy_static! { + static ref STDOUT_COLORS: AtomicBool = AtomicBool::new(default_colors_enabled(&Term::stdout())); + static ref STDERR_COLORS: AtomicBool = AtomicBool::new(default_colors_enabled(&Term::stderr())); +} + +/// Returns `true` if colors should be enabled for stdout. +/// +/// This honors the [clicolors spec](http://bixense.com/clicolors/). +/// +/// * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped. +/// * `CLICOLOR == 0`: Don't output ANSI color escape codes. +/// * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what. +#[inline] +pub fn colors_enabled() -> bool { + STDOUT_COLORS.load(Ordering::Relaxed) +} + +/// Forces colorization on or off for stdout. +/// +/// This overrides the default for the current process and changes the return value of the +/// `colors_enabled` function. +#[inline] +pub fn set_colors_enabled(val: bool) { + STDOUT_COLORS.store(val, Ordering::Relaxed) +} + +/// Returns `true` if colors should be enabled for stderr. +/// +/// This honors the [clicolors spec](http://bixense.com/clicolors/). +/// +/// * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped. +/// * `CLICOLOR == 0`: Don't output ANSI color escape codes. +/// * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what. +#[inline] +pub fn colors_enabled_stderr() -> bool { + STDERR_COLORS.load(Ordering::Relaxed) +} + +/// Forces colorization on or off for stderr. +/// +/// This overrides the default for the current process and changes the return value of the +/// `colors_enabled` function. +#[inline] +pub fn set_colors_enabled_stderr(val: bool) { + STDERR_COLORS.store(val, Ordering::Relaxed) +} + +/// Measure the width of a string in terminal characters. +pub fn measure_text_width(s: &str) -> usize { + str_width(&strip_ansi_codes(s)) +} + +/// A terminal color. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Color { + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + Color256(u8), +} + +impl Color { + #[inline] + fn ansi_num(self) -> usize { + match self { + Color::Black => 0, + Color::Red => 1, + Color::Green => 2, + Color::Yellow => 3, + Color::Blue => 4, + Color::Magenta => 5, + Color::Cyan => 6, + Color::White => 7, + Color::Color256(x) => x as usize, + } + } + + #[inline] + fn is_color256(self) -> bool { + #[allow(clippy::match_like_matches_macro)] + match self { + Color::Color256(_) => true, + _ => false, + } + } +} + +/// A terminal style attribute. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] +pub enum Attribute { + Bold, + Dim, + Italic, + Underlined, + Blink, + BlinkFast, + Reverse, + Hidden, + StrikeThrough, +} + +impl Attribute { + #[inline] + fn ansi_num(self) -> usize { + match self { + Attribute::Bold => 1, + Attribute::Dim => 2, + Attribute::Italic => 3, + Attribute::Underlined => 4, + Attribute::Blink => 5, + Attribute::BlinkFast => 6, + Attribute::Reverse => 7, + Attribute::Hidden => 8, + Attribute::StrikeThrough => 9, + } + } +} + +/// Defines the alignment for padding operations. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Alignment { + Left, + Center, + Right, +} + +/// A stored style that can be applied. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Style { + fg: Option<Color>, + bg: Option<Color>, + fg_bright: bool, + bg_bright: bool, + attrs: BTreeSet<Attribute>, + force: Option<bool>, + for_stderr: bool, +} + +impl Default for Style { + fn default() -> Style { + Style::new() + } +} + +impl Style { + /// Returns an empty default style. + pub fn new() -> Style { + Style { + fg: None, + bg: None, + fg_bright: false, + bg_bright: false, + attrs: BTreeSet::new(), + force: None, + for_stderr: false, + } + } + + /// Creates a style from a dotted string. + /// + /// Effectively the string is split at each dot and then the + /// terms in between are applied. For instance `red.on_blue` will + /// create a string that is red on blue background. `9.on_12` is + /// the same, but using 256 color numbers. Unknown terms are + /// ignored. + pub fn from_dotted_str(s: &str) -> Style { + let mut rv = Style::new(); + for part in s.split('.') { + rv = match part { + "black" => rv.black(), + "red" => rv.red(), + "green" => rv.green(), + "yellow" => rv.yellow(), + "blue" => rv.blue(), + "magenta" => rv.magenta(), + "cyan" => rv.cyan(), + "white" => rv.white(), + "bright" => rv.bright(), + "on_black" => rv.on_black(), + "on_red" => rv.on_red(), + "on_green" => rv.on_green(), + "on_yellow" => rv.on_yellow(), + "on_blue" => rv.on_blue(), + "on_magenta" => rv.on_magenta(), + "on_cyan" => rv.on_cyan(), + "on_white" => rv.on_white(), + "on_bright" => rv.on_bright(), + "bold" => rv.bold(), + "dim" => rv.dim(), + "underlined" => rv.underlined(), + "blink" => rv.blink(), + "blink_fast" => rv.blink_fast(), + "reverse" => rv.reverse(), + "hidden" => rv.hidden(), + "strikethrough" => rv.strikethrough(), + on_c if on_c.starts_with("on_") => { + if let Ok(n) = on_c[3..].parse::<u8>() { + rv.on_color256(n) + } else { + continue; + } + } + c => { + if let Ok(n) = c.parse::<u8>() { + rv.color256(n) + } else { + continue; + } + } + }; + } + rv + } + + /// Apply the style to something that can be displayed. + pub fn apply_to<D>(&self, val: D) -> StyledObject<D> { + StyledObject { + style: self.clone(), + val, + } + } + + /// Forces styling on or off. + /// + /// This overrides the automatic detection. + #[inline] + pub fn force_styling(mut self, value: bool) -> Style { + self.force = Some(value); + self + } + + /// Specifies that style is applying to something being written on stderr. + #[inline] + pub fn for_stderr(mut self) -> Style { + self.for_stderr = true; + self + } + + /// Specifies that style is applying to something being written on stdout. + /// + /// This is the default behaviour. + #[inline] + pub fn for_stdout(mut self) -> Style { + self.for_stderr = false; + self + } + + /// Sets a foreground color. + #[inline] + pub fn fg(mut self, color: Color) -> Style { + self.fg = Some(color); + self + } + + /// Sets a background color. + #[inline] + pub fn bg(mut self, color: Color) -> Style { + self.bg = Some(color); + self + } + + /// Adds a attr. + #[inline] + pub fn attr(mut self, attr: Attribute) -> Style { + self.attrs.insert(attr); + self + } + + #[inline] + pub fn black(self) -> Style { + self.fg(Color::Black) + } + #[inline] + pub fn red(self) -> Style { + self.fg(Color::Red) + } + #[inline] + pub fn green(self) -> Style { + self.fg(Color::Green) + } + #[inline] + pub fn yellow(self) -> Style { + self.fg(Color::Yellow) + } + #[inline] + pub fn blue(self) -> Style { + self.fg(Color::Blue) + } + #[inline] + pub fn magenta(self) -> Style { + self.fg(Color::Magenta) + } + #[inline] + pub fn cyan(self) -> Style { + self.fg(Color::Cyan) + } + #[inline] + pub fn white(self) -> Style { + self.fg(Color::White) + } + #[inline] + pub fn color256(self, color: u8) -> Style { + self.fg(Color::Color256(color)) + } + + #[inline] + pub fn bright(mut self) -> Style { + self.fg_bright = true; + self + } + + #[inline] + pub fn on_black(self) -> Style { + self.bg(Color::Black) + } + #[inline] + pub fn on_red(self) -> Style { + self.bg(Color::Red) + } + #[inline] + pub fn on_green(self) -> Style { + self.bg(Color::Green) + } + #[inline] + pub fn on_yellow(self) -> Style { + self.bg(Color::Yellow) + } + #[inline] + pub fn on_blue(self) -> Style { + self.bg(Color::Blue) + } + #[inline] + pub fn on_magenta(self) -> Style { + self.bg(Color::Magenta) + } + #[inline] + pub fn on_cyan(self) -> Style { + self.bg(Color::Cyan) + } + #[inline] + pub fn on_white(self) -> Style { + self.bg(Color::White) + } + #[inline] + pub fn on_color256(self, color: u8) -> Style { + self.bg(Color::Color256(color)) + } + + #[inline] + pub fn on_bright(mut self) -> Style { + self.bg_bright = true; + self + } + + #[inline] + pub fn bold(self) -> Style { + self.attr(Attribute::Bold) + } + #[inline] + pub fn dim(self) -> Style { + self.attr(Attribute::Dim) + } + #[inline] + pub fn italic(self) -> Style { + self.attr(Attribute::Italic) + } + #[inline] + pub fn underlined(self) -> Style { + self.attr(Attribute::Underlined) + } + #[inline] + pub fn blink(self) -> Style { + self.attr(Attribute::Blink) + } + #[inline] + pub fn blink_fast(self) -> Style { + self.attr(Attribute::BlinkFast) + } + #[inline] + pub fn reverse(self) -> Style { + self.attr(Attribute::Reverse) + } + #[inline] + pub fn hidden(self) -> Style { + self.attr(Attribute::Hidden) + } + #[inline] + pub fn strikethrough(self) -> Style { + self.attr(Attribute::StrikeThrough) + } +} + +/// Wraps an object for formatting for styling. +/// +/// Example: +/// +/// ```rust,no_run +/// # use console::style; +/// format!("Hello {}", style("World").cyan()); +/// ``` +/// +/// This is a shortcut for making a new style and applying it +/// to a value: +/// +/// ```rust,no_run +/// # use console::Style; +/// format!("Hello {}", Style::new().cyan().apply_to("World")); +/// ``` +pub fn style<D>(val: D) -> StyledObject<D> { + Style::new().apply_to(val) +} + +/// A formatting wrapper that can be styled for a terminal. +#[derive(Clone)] +pub struct StyledObject<D> { + style: Style, + val: D, +} + +impl<D> StyledObject<D> { + /// Forces styling on or off. + /// + /// This overrides the automatic detection. + #[inline] + pub fn force_styling(mut self, value: bool) -> StyledObject<D> { + self.style = self.style.force_styling(value); + self + } + + /// Specifies that style is applying to something being written on stderr + #[inline] + pub fn for_stderr(mut self) -> StyledObject<D> { + self.style = self.style.for_stderr(); + self + } + + /// Specifies that style is applying to something being written on stdout + /// + /// This is the default + #[inline] + pub fn for_stdout(mut self) -> StyledObject<D> { + self.style = self.style.for_stdout(); + self + } + + /// Sets a foreground color. + #[inline] + pub fn fg(mut self, color: Color) -> StyledObject<D> { + self.style = self.style.fg(color); + self + } + + /// Sets a background color. + #[inline] + pub fn bg(mut self, color: Color) -> StyledObject<D> { + self.style = self.style.bg(color); + self + } + + /// Adds a attr. + #[inline] + pub fn attr(mut self, attr: Attribute) -> StyledObject<D> { + self.style = self.style.attr(attr); + self + } + + #[inline] + pub fn black(self) -> StyledObject<D> { + self.fg(Color::Black) + } + #[inline] + pub fn red(self) -> StyledObject<D> { + self.fg(Color::Red) + } + #[inline] + pub fn green(self) -> StyledObject<D> { + self.fg(Color::Green) + } + #[inline] + pub fn yellow(self) -> StyledObject<D> { + self.fg(Color::Yellow) + } + #[inline] + pub fn blue(self) -> StyledObject<D> { + self.fg(Color::Blue) + } + #[inline] + pub fn magenta(self) -> StyledObject<D> { + self.fg(Color::Magenta) + } + #[inline] + pub fn cyan(self) -> StyledObject<D> { + self.fg(Color::Cyan) + } + #[inline] + pub fn white(self) -> StyledObject<D> { + self.fg(Color::White) + } + #[inline] + pub fn color256(self, color: u8) -> StyledObject<D> { + self.fg(Color::Color256(color)) + } + + #[inline] + pub fn bright(mut self) -> StyledObject<D> { + self.style = self.style.bright(); + self + } + + #[inline] + pub fn on_black(self) -> StyledObject<D> { + self.bg(Color::Black) + } + #[inline] + pub fn on_red(self) -> StyledObject<D> { + self.bg(Color::Red) + } + #[inline] + pub fn on_green(self) -> StyledObject<D> { + self.bg(Color::Green) + } + #[inline] + pub fn on_yellow(self) -> StyledObject<D> { + self.bg(Color::Yellow) + } + #[inline] + pub fn on_blue(self) -> StyledObject<D> { + self.bg(Color::Blue) + } + #[inline] + pub fn on_magenta(self) -> StyledObject<D> { + self.bg(Color::Magenta) + } + #[inline] + pub fn on_cyan(self) -> StyledObject<D> { + self.bg(Color::Cyan) + } + #[inline] + pub fn on_white(self) -> StyledObject<D> { + self.bg(Color::White) + } + #[inline] + pub fn on_color256(self, color: u8) -> StyledObject<D> { + self.bg(Color::Color256(color)) + } + + #[inline] + pub fn on_bright(mut self) -> StyledObject<D> { + self.style = self.style.on_bright(); + self + } + + #[inline] + pub fn bold(self) -> StyledObject<D> { + self.attr(Attribute::Bold) + } + #[inline] + pub fn dim(self) -> StyledObject<D> { + self.attr(Attribute::Dim) + } + #[inline] + pub fn italic(self) -> StyledObject<D> { + self.attr(Attribute::Italic) + } + #[inline] + pub fn underlined(self) -> StyledObject<D> { + self.attr(Attribute::Underlined) + } + #[inline] + pub fn blink(self) -> StyledObject<D> { + self.attr(Attribute::Blink) + } + #[inline] + pub fn blink_fast(self) -> StyledObject<D> { + self.attr(Attribute::BlinkFast) + } + #[inline] + pub fn reverse(self) -> StyledObject<D> { + self.attr(Attribute::Reverse) + } + #[inline] + pub fn hidden(self) -> StyledObject<D> { + self.attr(Attribute::Hidden) + } + #[inline] + pub fn strikethrough(self) -> StyledObject<D> { + self.attr(Attribute::StrikeThrough) + } +} + +macro_rules! impl_fmt { + ($name:ident) => { + impl<D: fmt::$name> fmt::$name for StyledObject<D> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut reset = false; + if self + .style + .force + .unwrap_or_else(|| match self.style.for_stderr { + true => colors_enabled_stderr(), + false => colors_enabled(), + }) + { + if let Some(fg) = self.style.fg { + if fg.is_color256() { + write!(f, "\x1b[38;5;{}m", fg.ansi_num())?; + } else if self.style.fg_bright { + write!(f, "\x1b[38;5;{}m", fg.ansi_num() + 8)?; + } else { + write!(f, "\x1b[{}m", fg.ansi_num() + 30)?; + } + reset = true; + } + if let Some(bg) = self.style.bg { + if bg.is_color256() { + write!(f, "\x1b[48;5;{}m", bg.ansi_num())?; + } else if self.style.bg_bright { + write!(f, "\x1b[48;5;{}m", bg.ansi_num() + 8)?; + } else { + write!(f, "\x1b[{}m", bg.ansi_num() + 40)?; + } + reset = true; + } + for attr in &self.style.attrs { + write!(f, "\x1b[{}m", attr.ansi_num())?; + reset = true; + } + } + fmt::$name::fmt(&self.val, f)?; + if reset { + write!(f, "\x1b[0m")?; + } + Ok(()) + } + } + }; +} + +impl_fmt!(Binary); +impl_fmt!(Debug); +impl_fmt!(Display); +impl_fmt!(LowerExp); +impl_fmt!(LowerHex); +impl_fmt!(Octal); +impl_fmt!(Pointer); +impl_fmt!(UpperExp); +impl_fmt!(UpperHex); + +/// "Intelligent" emoji formatter. +/// +/// This struct intelligently wraps an emoji so that it is rendered +/// only on systems that want emojis and renders a fallback on others. +/// +/// Example: +/// +/// ```rust +/// use console::Emoji; +/// println!("[3/4] {}Downloading ...", Emoji("🚚 ", "")); +/// println!("[4/4] {} Done!", Emoji("✨", ":-)")); +/// ``` +#[derive(Copy, Clone)] +pub struct Emoji<'a, 'b>(pub &'a str, pub &'b str); + +impl<'a, 'b> Emoji<'a, 'b> { + pub fn new(emoji: &'a str, fallback: &'b str) -> Emoji<'a, 'b> { + Emoji(emoji, fallback) + } +} + +impl<'a, 'b> fmt::Display for Emoji<'a, 'b> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if wants_emoji() { + write!(f, "{}", self.0) + } else { + write!(f, "{}", self.1) + } + } +} + +fn str_width(s: &str) -> usize { + #[cfg(feature = "unicode-width")] + { + use unicode_width::UnicodeWidthStr; + s.width() + } + #[cfg(not(feature = "unicode-width"))] + { + s.chars().count() + } +} + +#[cfg(feature = "ansi-parsing")] +fn char_width(c: char) -> usize { + #[cfg(feature = "unicode-width")] + { + use unicode_width::UnicodeWidthChar; + c.width().unwrap_or(0) + } + #[cfg(not(feature = "unicode-width"))] + { + let _c = c; + 1 + } +} + +/// Truncates a string to a certain number of characters. +/// +/// This ensures that escape codes are not screwed up in the process. +/// If the maximum length is hit the string will be truncated but +/// escapes code will still be honored. If truncation takes place +/// the tail string will be appended. +pub fn truncate_str<'a>(s: &'a str, width: usize, tail: &str) -> Cow<'a, str> { + #[cfg(feature = "ansi-parsing")] + { + use std::cmp::Ordering; + let mut iter = AnsiCodeIterator::new(s); + let mut length = 0; + let mut rv = None; + + while let Some(item) = iter.next() { + match item { + (s, false) => { + if rv.is_none() { + if str_width(s) + length > width - str_width(tail) { + let ts = iter.current_slice(); + + let mut s_byte = 0; + let mut s_width = 0; + let rest_width = width - str_width(tail) - length; + for c in s.chars() { + s_byte += c.len_utf8(); + s_width += char_width(c); + match s_width.cmp(&rest_width) { + Ordering::Equal => break, + Ordering::Greater => { + s_byte -= c.len_utf8(); + break; + } + Ordering::Less => continue, + } + } + + let idx = ts.len() - s.len() + s_byte; + let mut buf = ts[..idx].to_string(); + buf.push_str(tail); + rv = Some(buf); + } + length += str_width(s); + } + } + (s, true) => { + if rv.is_some() { + rv.as_mut().unwrap().push_str(s); + } + } + } + } + + if let Some(buf) = rv { + Cow::Owned(buf) + } else { + Cow::Borrowed(s) + } + } + + #[cfg(not(feature = "ansi-parsing"))] + { + if s.len() <= width - tail.len() { + Cow::Borrowed(s) + } else { + Cow::Owned(format!( + "{}{}", + s.get(..width - tail.len()).unwrap_or_default(), + tail + )) + } + } +} + +/// Pads a string to fill a certain number of characters. +/// +/// This will honor ansi codes correctly and allows you to align a string +/// on the left, right or centered. Additionally truncation can be enabled +/// by setting `truncate` to a string that should be used as a truncation +/// marker. +pub fn pad_str<'a>( + s: &'a str, + width: usize, + align: Alignment, + truncate: Option<&str>, +) -> Cow<'a, str> { + pad_str_with(s, width, align, truncate, ' ') +} +/// Pads a string with specific padding to fill a certain number of characters. +/// +/// This will honor ansi codes correctly and allows you to align a string +/// on the left, right or centered. Additionally truncation can be enabled +/// by setting `truncate` to a string that should be used as a truncation +/// marker. +pub fn pad_str_with<'a>( + s: &'a str, + width: usize, + align: Alignment, + truncate: Option<&str>, + pad: char, +) -> Cow<'a, str> { + let cols = measure_text_width(s); + + if cols >= width { + return match truncate { + None => Cow::Borrowed(s), + Some(tail) => truncate_str(s, width, tail), + }; + } + + let diff = width - cols; + + let (left_pad, right_pad) = match align { + Alignment::Left => (0, diff), + Alignment::Right => (diff, 0), + Alignment::Center => (diff / 2, diff - diff / 2), + }; + + let mut rv = String::new(); + for _ in 0..left_pad { + rv.push(pad); + } + rv.push_str(s); + for _ in 0..right_pad { + rv.push(pad); + } + Cow::Owned(rv) +} + +#[test] +fn test_text_width() { + let s = style("foo") + .red() + .on_black() + .bold() + .force_styling(true) + .to_string(); + assert_eq!( + measure_text_width(&s), + if cfg!(feature = "ansi-parsing") { + 3 + } else if cfg!(feature = "unicode-width") { + 17 + } else { + 21 + } + ); +} + +#[test] +#[cfg(all(feature = "unicode-width", feature = "ansi-parsing"))] +fn test_truncate_str() { + let s = format!("foo {}", style("bar").red().force_styling(true)); + assert_eq!( + &truncate_str(&s, 5, ""), + &format!("foo {}", style("b").red().force_styling(true)) + ); + let s = format!("foo {}", style("bar").red().force_styling(true)); + assert_eq!( + &truncate_str(&s, 5, "!"), + &format!("foo {}", style("!").red().force_styling(true)) + ); + let s = format!("foo {} baz", style("bar").red().force_styling(true)); + assert_eq!( + &truncate_str(&s, 10, "..."), + &format!("foo {}...", style("bar").red().force_styling(true)) + ); + let s = format!("foo {}", style("バー").red().force_styling(true)); + assert_eq!( + &truncate_str(&s, 5, ""), + &format!("foo {}", style("").red().force_styling(true)) + ); + let s = format!("foo {}", style("バー").red().force_styling(true)); + assert_eq!( + &truncate_str(&s, 6, ""), + &format!("foo {}", style("バ").red().force_styling(true)) + ); +} + +#[test] +fn test_truncate_str_no_ansi() { + assert_eq!(&truncate_str("foo bar", 5, ""), "foo b"); + assert_eq!(&truncate_str("foo bar", 5, "!"), "foo !"); + assert_eq!(&truncate_str("foo bar baz", 10, "..."), "foo bar..."); +} + +#[test] +fn test_pad_str() { + assert_eq!(pad_str("foo", 7, Alignment::Center, None), " foo "); + assert_eq!(pad_str("foo", 7, Alignment::Left, None), "foo "); + assert_eq!(pad_str("foo", 7, Alignment::Right, None), " foo"); + assert_eq!(pad_str("foo", 3, Alignment::Left, None), "foo"); + assert_eq!(pad_str("foobar", 3, Alignment::Left, None), "foobar"); + assert_eq!(pad_str("foobar", 3, Alignment::Left, Some("")), "foo"); + assert_eq!( + pad_str("foobarbaz", 6, Alignment::Left, Some("...")), + "foo..." + ); +} + +#[test] +fn test_pad_str_with() { + assert_eq!( + pad_str_with("foo", 7, Alignment::Center, None, '#'), + "##foo##" + ); + assert_eq!( + pad_str_with("foo", 7, Alignment::Left, None, '#'), + "foo####" + ); + assert_eq!( + pad_str_with("foo", 7, Alignment::Right, None, '#'), + "####foo" + ); + assert_eq!(pad_str_with("foo", 3, Alignment::Left, None, '#'), "foo"); + assert_eq!( + pad_str_with("foobar", 3, Alignment::Left, None, '#'), + "foobar" + ); + assert_eq!( + pad_str_with("foobar", 3, Alignment::Left, Some(""), '#'), + "foo" + ); + assert_eq!( + pad_str_with("foobarbaz", 6, Alignment::Left, Some("..."), '#'), + "foo..." + ); +} diff --git a/vendor/console/src/wasm_term.rs b/vendor/console/src/wasm_term.rs new file mode 100644 index 000000000..764bc3414 --- /dev/null +++ b/vendor/console/src/wasm_term.rs @@ -0,0 +1,54 @@ +use std::fmt::Display; +use std::io; + +use crate::kb::Key; +use crate::term::Term; + +pub use crate::common_term::*; + +pub const DEFAULT_WIDTH: u16 = 80; + +#[inline] +pub fn is_a_terminal(_out: &Term) -> bool { + #[cfg(target = "wasm32-wasi")] + { + unsafe { libc::isatty(out.as_raw_fd()) != 0 } + } + #[cfg(not(target = "wasm32-wasi"))] + { + false + } +} + +#[inline] +pub fn is_a_color_terminal(_out: &Term) -> bool { + // We currently never report color terminals. For discussion see + // the issue in the WASI repo: https://github.com/WebAssembly/WASI/issues/162 + false +} + +#[inline] +pub fn terminal_size(_out: &Term) -> Option<(u16, u16)> { + None +} + +pub fn read_secure() -> io::Result<String> { + Err(io::Error::new( + io::ErrorKind::Other, + "unsupported operation", + )) +} + +pub fn read_single_key() -> io::Result<Key> { + Err(io::Error::new( + io::ErrorKind::Other, + "unsupported operation", + )) +} + +#[inline] +pub fn wants_emoji() -> bool { + false +} + +pub fn set_title<T: Display>(_title: T) {} diff --git a/vendor/console/src/windows_term/colors.rs b/vendor/console/src/windows_term/colors.rs new file mode 100644 index 000000000..dc8209ded --- /dev/null +++ b/vendor/console/src/windows_term/colors.rs @@ -0,0 +1,451 @@ +use std::io; +use std::mem; +use std::os::windows::io::AsRawHandle; +use std::str::Bytes; + +use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::System::Console::{ + GetConsoleScreenBufferInfo, SetConsoleTextAttribute, CONSOLE_SCREEN_BUFFER_INFO, + FOREGROUND_BLUE as FG_BLUE, FOREGROUND_GREEN as FG_GREEN, FOREGROUND_INTENSITY as FG_INTENSITY, + FOREGROUND_RED as FG_RED, +}; + +use crate::Term; + +type WORD = u16; + +const FG_CYAN: WORD = FG_BLUE | FG_GREEN; +const FG_MAGENTA: WORD = FG_BLUE | FG_RED; +const FG_YELLOW: WORD = FG_GREEN | FG_RED; +const FG_WHITE: WORD = FG_BLUE | FG_GREEN | FG_RED; + +/// Query the given handle for information about the console's screen buffer. +/// +/// The given handle should represent a console. Otherwise, an error is +/// returned. +/// +/// This corresponds to calling [`GetConsoleScreenBufferInfo`]. +/// +/// [`GetConsoleScreenBufferInfo`]: https://docs.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo +pub fn screen_buffer_info(h: HANDLE) -> io::Result<ScreenBufferInfo> { + unsafe { + let mut info: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed(); + let rc = GetConsoleScreenBufferInfo(h, &mut info); + if rc == 0 { + return Err(io::Error::last_os_error()); + } + Ok(ScreenBufferInfo(info)) + } +} + +/// Set the text attributes of the console represented by the given handle. +/// +/// This corresponds to calling [`SetConsoleTextAttribute`]. +/// +/// [`SetConsoleTextAttribute`]: https://docs.microsoft.com/en-us/windows/console/setconsoletextattribute +pub fn set_text_attributes(h: HANDLE, attributes: u16) -> io::Result<()> { + if unsafe { SetConsoleTextAttribute(h, attributes) } == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +/// Represents console screen buffer information such as size, cursor position +/// and styling attributes. +/// +/// This wraps a [`CONSOLE_SCREEN_BUFFER_INFO`]. +/// +/// [`CONSOLE_SCREEN_BUFFER_INFO`]: https://docs.microsoft.com/en-us/windows/console/console-screen-buffer-info-str +#[derive(Clone)] +pub struct ScreenBufferInfo(CONSOLE_SCREEN_BUFFER_INFO); + +impl ScreenBufferInfo { + /// Returns the character attributes associated with this console. + /// + /// This corresponds to `wAttributes`. + /// + /// See [`char info`] for more details. + /// + /// [`char info`]: https://docs.microsoft.com/en-us/windows/console/char-info-str + pub fn attributes(&self) -> u16 { + self.0.wAttributes + } +} + +/// A Windows console. +/// +/// This represents a very limited set of functionality available to a Windows +/// console. In particular, it can only change text attributes such as color +/// and intensity. This may grow over time. If you need more routines, please +/// file an issue and/or PR. +/// +/// There is no way to "write" to this console. Simply write to +/// stdout or stderr instead, while interleaving instructions to the console +/// to change text attributes. +/// +/// A common pitfall when using a console is to forget to flush writes to +/// stdout before setting new text attributes. +#[derive(Debug)] +pub struct Console { + kind: HandleKind, + start_attr: TextAttributes, + cur_attr: TextAttributes, +} + +#[derive(Clone, Copy, Debug)] +enum HandleKind { + Stdout, + Stderr, +} + +impl HandleKind { + fn handle(&self) -> HANDLE { + match *self { + HandleKind::Stdout => io::stdout().as_raw_handle() as HANDLE, + HandleKind::Stderr => io::stderr().as_raw_handle() as HANDLE, + } + } +} + +impl Console { + /// Get a console for a standard I/O stream. + fn create_for_stream(kind: HandleKind) -> io::Result<Console> { + let h = kind.handle(); + let info = screen_buffer_info(h)?; + let attr = TextAttributes::from_word(info.attributes()); + Ok(Console { + kind: kind, + start_attr: attr, + cur_attr: attr, + }) + } + + /// Create a new Console to stdout. + /// + /// If there was a problem creating the console, then an error is returned. + pub fn stdout() -> io::Result<Console> { + Self::create_for_stream(HandleKind::Stdout) + } + + /// Create a new Console to stderr. + /// + /// If there was a problem creating the console, then an error is returned. + pub fn stderr() -> io::Result<Console> { + Self::create_for_stream(HandleKind::Stderr) + } + + /// Applies the current text attributes. + fn set(&mut self) -> io::Result<()> { + set_text_attributes(self.kind.handle(), self.cur_attr.to_word()) + } + + /// Apply the given intensity and color attributes to the console + /// foreground. + /// + /// If there was a problem setting attributes on the console, then an error + /// is returned. + pub fn fg(&mut self, intense: Intense, color: Color) -> io::Result<()> { + self.cur_attr.fg_color = color; + self.cur_attr.fg_intense = intense; + self.set() + } + + /// Apply the given intensity and color attributes to the console + /// background. + /// + /// If there was a problem setting attributes on the console, then an error + /// is returned. + pub fn bg(&mut self, intense: Intense, color: Color) -> io::Result<()> { + self.cur_attr.bg_color = color; + self.cur_attr.bg_intense = intense; + self.set() + } + + /// Reset the console text attributes to their original settings. + /// + /// The original settings correspond to the text attributes on the console + /// when this `Console` value was created. + /// + /// If there was a problem setting attributes on the console, then an error + /// is returned. + pub fn reset(&mut self) -> io::Result<()> { + self.cur_attr = self.start_attr; + self.set() + } +} + +/// A representation of text attributes for the Windows console. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct TextAttributes { + fg_color: Color, + fg_intense: Intense, + bg_color: Color, + bg_intense: Intense, +} + +impl TextAttributes { + fn to_word(&self) -> WORD { + let mut w = 0; + w |= self.fg_color.to_fg(); + w |= self.fg_intense.to_fg(); + w |= self.bg_color.to_bg(); + w |= self.bg_intense.to_bg(); + w + } + + fn from_word(word: WORD) -> TextAttributes { + TextAttributes { + fg_color: Color::from_fg(word), + fg_intense: Intense::from_fg(word), + bg_color: Color::from_bg(word), + bg_intense: Intense::from_bg(word), + } + } +} + +/// Whether to use intense colors or not. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Intense { + Yes, + No, +} + +impl Intense { + fn to_bg(&self) -> WORD { + self.to_fg() << 4 + } + + fn from_bg(word: WORD) -> Intense { + Intense::from_fg(word >> 4) + } + + fn to_fg(&self) -> WORD { + match *self { + Intense::No => 0, + Intense::Yes => FG_INTENSITY, + } + } + + fn from_fg(word: WORD) -> Intense { + if word & FG_INTENSITY > 0 { + Intense::Yes + } else { + Intense::No + } + } +} + +/// The set of available colors for use with a Windows console. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Color { + Black, + Blue, + Green, + Red, + Cyan, + Magenta, + Yellow, + White, +} + +impl Color { + fn to_bg(&self) -> WORD { + self.to_fg() << 4 + } + + fn from_bg(word: WORD) -> Color { + Color::from_fg(word >> 4) + } + + fn to_fg(&self) -> WORD { + match *self { + Color::Black => 0, + Color::Blue => FG_BLUE, + Color::Green => FG_GREEN, + Color::Red => FG_RED, + Color::Cyan => FG_CYAN, + Color::Magenta => FG_MAGENTA, + Color::Yellow => FG_YELLOW, + Color::White => FG_WHITE, + } + } + + fn from_fg(word: WORD) -> Color { + match word & 0b111 { + FG_BLUE => Color::Blue, + FG_GREEN => Color::Green, + FG_RED => Color::Red, + FG_CYAN => Color::Cyan, + FG_MAGENTA => Color::Magenta, + FG_YELLOW => Color::Yellow, + FG_WHITE => Color::White, + _ => Color::Black, + } + } +} + +pub fn console_colors(out: &Term, mut con: Console, bytes: &[u8]) -> io::Result<()> { + use crate::ansi::AnsiCodeIterator; + use std::str::from_utf8; + + let s = from_utf8(bytes).expect("data to be printed is not an ansi string"); + let mut iter = AnsiCodeIterator::new(s); + + while !iter.rest_slice().is_empty() { + if let Some((part, is_esc)) = iter.next() { + if !is_esc { + out.write_through_common(part.as_bytes())?; + } else if part == "\x1b[0m" { + con.reset()?; + } else if let Some((intense, color, fg_bg)) = driver(parse_color, part) { + match fg_bg { + FgBg::Foreground => con.fg(intense, color), + FgBg::Background => con.bg(intense, color), + }?; + } else if driver(parse_attr, part).is_none() { + out.write_through_common(part.as_bytes())?; + } + } + } + + Ok(()) +} + +#[derive(Debug, PartialEq, Eq)] +enum FgBg { + Foreground, + Background, +} + +impl FgBg { + fn new(byte: u8) -> Option<Self> { + match byte { + b'3' => Some(Self::Foreground), + b'4' => Some(Self::Background), + _ => None, + } + } +} + +fn driver<Out>(parse: fn(Bytes<'_>) -> Option<Out>, part: &str) -> Option<Out> { + let mut bytes = part.bytes(); + + loop { + while bytes.next()? != b'\x1b' {} + + if let ret @ Some(_) = (parse)(bytes.clone()) { + return ret; + } + } +} + +// `driver(parse_color, s)` parses the equivalent of the regex +// \x1b\[(3|4)8;5;(8|9|1[0-5])m +// for intense or +// \x1b\[(3|4)([0-7])m +// for normal +fn parse_color(mut bytes: Bytes<'_>) -> Option<(Intense, Color, FgBg)> { + parse_prefix(&mut bytes)?; + + let fg_bg = FgBg::new(bytes.next()?)?; + let (intense, color) = match bytes.next()? { + b @ b'0'..=b'7' => (Intense::No, normal_color_ansi_from_byte(b)?), + b'8' => { + if &[bytes.next()?, bytes.next()?, bytes.next()?] != b";5;" { + return None; + } + (Intense::Yes, parse_intense_color_ansi(&mut bytes)?) + } + _ => return None, + }; + + parse_suffix(&mut bytes)?; + Some((intense, color, fg_bg)) +} + +// `driver(parse_attr, s)` parses the equivalent of the regex +// \x1b\[([1-8])m +fn parse_attr(mut bytes: Bytes<'_>) -> Option<u8> { + parse_prefix(&mut bytes)?; + let attr = match bytes.next()? { + attr @ b'1'..=b'8' => attr, + _ => return None, + }; + parse_suffix(&mut bytes)?; + Some(attr) +} + +fn parse_prefix(bytes: &mut Bytes<'_>) -> Option<()> { + if bytes.next()? == b'[' { + Some(()) + } else { + None + } +} + +fn parse_intense_color_ansi(bytes: &mut Bytes<'_>) -> Option<Color> { + let color = match bytes.next()? { + b'8' => Color::Black, + b'9' => Color::Red, + b'1' => match bytes.next()? { + b'0' => Color::Green, + b'1' => Color::Yellow, + b'2' => Color::Blue, + b'3' => Color::Magenta, + b'4' => Color::Cyan, + b'5' => Color::White, + _ => return None, + }, + _ => return None, + }; + Some(color) +} + +fn normal_color_ansi_from_byte(b: u8) -> Option<Color> { + let color = match b { + b'0' => Color::Black, + b'1' => Color::Red, + b'2' => Color::Green, + b'3' => Color::Yellow, + b'4' => Color::Blue, + b'5' => Color::Magenta, + b'6' => Color::Cyan, + b'7' => Color::White, + _ => return None, + }; + Some(color) +} + +fn parse_suffix(bytes: &mut Bytes<'_>) -> Option<()> { + if bytes.next()? == b'm' { + Some(()) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn color_parsing() { + let intense_color = "leading bytes \x1b[38;5;10m trailing bytes"; + let parsed = driver(parse_color, intense_color).unwrap(); + assert_eq!(parsed, (Intense::Yes, Color::Green, FgBg::Foreground)); + + let normal_color = "leading bytes \x1b[40m trailing bytes"; + let parsed = driver(parse_color, normal_color).unwrap(); + assert_eq!(parsed, (Intense::No, Color::Black, FgBg::Background)); + } + + #[test] + fn attr_parsing() { + let attr = "leading bytes \x1b[1m trailing bytes"; + let parsed = driver(parse_attr, attr).unwrap(); + assert_eq!(parsed, b'1'); + } +} diff --git a/vendor/console/src/windows_term/mod.rs b/vendor/console/src/windows_term/mod.rs new file mode 100644 index 000000000..c4ec193cf --- /dev/null +++ b/vendor/console/src/windows_term/mod.rs @@ -0,0 +1,563 @@ +use std::cmp; +use std::env; +use std::ffi::OsStr; +use std::fmt::Display; +use std::io; +use std::iter::once; +use std::mem; +use std::os::raw::c_void; +use std::os::windows::ffi::OsStrExt; +use std::os::windows::io::AsRawHandle; +use std::slice; +use std::{char, mem::MaybeUninit}; + +use encode_unicode::error::InvalidUtf16Tuple; +use encode_unicode::CharExt; +use windows_sys::Win32::Foundation::{CHAR, HANDLE, INVALID_HANDLE_VALUE, MAX_PATH}; +use windows_sys::Win32::Storage::FileSystem::{ + FileNameInfo, GetFileInformationByHandleEx, FILE_NAME_INFO, +}; +use windows_sys::Win32::System::Console::{ + FillConsoleOutputAttribute, FillConsoleOutputCharacterA, GetConsoleCursorInfo, GetConsoleMode, + GetConsoleScreenBufferInfo, GetNumberOfConsoleInputEvents, GetStdHandle, ReadConsoleInputW, + SetConsoleCursorInfo, SetConsoleCursorPosition, SetConsoleMode, SetConsoleTitleW, + CONSOLE_CURSOR_INFO, CONSOLE_SCREEN_BUFFER_INFO, COORD, INPUT_RECORD, KEY_EVENT, + KEY_EVENT_RECORD, STD_ERROR_HANDLE, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, +}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY; + +use crate::common_term; +use crate::kb::Key; +use crate::term::{Term, TermTarget}; + +#[cfg(feature = "windows-console-colors")] +mod colors; + +#[cfg(feature = "windows-console-colors")] +pub use self::colors::*; + +const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x4; +pub const DEFAULT_WIDTH: u16 = 79; + +pub fn as_handle(term: &Term) -> HANDLE { + // convert between windows_sys::Win32::Foundation::HANDLE and std::os::windows::raw::HANDLE + term.as_raw_handle() as HANDLE +} + +pub fn is_a_terminal(out: &Term) -> bool { + let (fd, others) = match out.target() { + TermTarget::Stdout => (STD_OUTPUT_HANDLE, [STD_INPUT_HANDLE, STD_ERROR_HANDLE]), + TermTarget::Stderr => (STD_ERROR_HANDLE, [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE]), + }; + + if unsafe { console_on_any(&[fd]) } { + // False positives aren't possible. If we got a console then + // we definitely have a tty on stdin. + return true; + } + + // At this point, we *could* have a false negative. We can determine that + // this is true negative if we can detect the presence of a console on + // any of the other streams. If another stream has a console, then we know + // we're in a Windows console and can therefore trust the negative. + if unsafe { console_on_any(&others) } { + return false; + } + + msys_tty_on(out) +} + +pub fn is_a_color_terminal(out: &Term) -> bool { + if !is_a_terminal(out) { + return false; + } + if msys_tty_on(out) { + return match env::var("TERM") { + Ok(term) => term != "dumb", + Err(_) => true, + }; + } + enable_ansi_on(out) +} + +fn enable_ansi_on(out: &Term) -> bool { + unsafe { + let handle = as_handle(out); + + let mut dw_mode = 0; + if GetConsoleMode(handle, &mut dw_mode) == 0 { + return false; + } + + dw_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if SetConsoleMode(handle, dw_mode) == 0 { + return false; + } + + true + } +} + +unsafe fn console_on_any(fds: &[STD_HANDLE]) -> bool { + for &fd in fds { + let mut out = 0; + let handle = GetStdHandle(fd); + if GetConsoleMode(handle, &mut out) != 0 { + return true; + } + } + false +} + +pub fn terminal_size(out: &Term) -> Option<(u16, u16)> { + use windows_sys::Win32::System::Console::SMALL_RECT; + + // convert between windows_sys::Win32::Foundation::HANDLE and std::os::windows::raw::HANDLE + let handle = out.as_raw_handle(); + let hand = handle as windows_sys::Win32::Foundation::HANDLE; + + if hand == INVALID_HANDLE_VALUE { + return None; + } + + let zc = COORD { X: 0, Y: 0 }; + let mut csbi = CONSOLE_SCREEN_BUFFER_INFO { + dwSize: zc, + dwCursorPosition: zc, + wAttributes: 0, + srWindow: SMALL_RECT { + Left: 0, + Top: 0, + Right: 0, + Bottom: 0, + }, + dwMaximumWindowSize: zc, + }; + if unsafe { GetConsoleScreenBufferInfo(hand, &mut csbi) } == 0 { + return None; + } + + let rows = (csbi.srWindow.Bottom - csbi.srWindow.Top + 1) as u16; + let columns = (csbi.srWindow.Right - csbi.srWindow.Left + 1) as u16; + + Some((rows, columns)) +} + +pub fn move_cursor_to(out: &Term, x: usize, y: usize) -> io::Result<()> { + if out.is_msys_tty { + return common_term::move_cursor_to(out, x, y); + } + if let Some((hand, _)) = get_console_screen_buffer_info(as_handle(out)) { + unsafe { + SetConsoleCursorPosition( + hand, + COORD { + X: x as i16, + Y: y as i16, + }, + ); + } + } + Ok(()) +} + +pub fn move_cursor_up(out: &Term, n: usize) -> io::Result<()> { + if out.is_msys_tty { + return common_term::move_cursor_up(out, n); + } + + if let Some((_, csbi)) = get_console_screen_buffer_info(as_handle(out)) { + move_cursor_to(out, 0, csbi.dwCursorPosition.Y as usize - n)?; + } + Ok(()) +} + +pub fn move_cursor_down(out: &Term, n: usize) -> io::Result<()> { + if out.is_msys_tty { + return common_term::move_cursor_down(out, n); + } + + if let Some((_, csbi)) = get_console_screen_buffer_info(as_handle(out)) { + move_cursor_to(out, 0, csbi.dwCursorPosition.Y as usize + n)?; + } + Ok(()) +} + +pub fn move_cursor_left(out: &Term, n: usize) -> io::Result<()> { + if out.is_msys_tty { + return common_term::move_cursor_left(out, n); + } + + if let Some((_, csbi)) = get_console_screen_buffer_info(as_handle(out)) { + move_cursor_to( + out, + csbi.dwCursorPosition.X as usize - n, + csbi.dwCursorPosition.Y as usize, + )?; + } + Ok(()) +} + +pub fn move_cursor_right(out: &Term, n: usize) -> io::Result<()> { + if out.is_msys_tty { + return common_term::move_cursor_right(out, n); + } + + if let Some((_, csbi)) = get_console_screen_buffer_info(as_handle(out)) { + move_cursor_to( + out, + csbi.dwCursorPosition.X as usize + n, + csbi.dwCursorPosition.Y as usize, + )?; + } + Ok(()) +} + +pub fn clear_line(out: &Term) -> io::Result<()> { + if out.is_msys_tty { + return common_term::clear_line(out); + } + if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) { + unsafe { + let width = csbi.srWindow.Right - csbi.srWindow.Left; + let pos = COORD { + X: 0, + Y: csbi.dwCursorPosition.Y, + }; + let mut written = 0; + FillConsoleOutputCharacterA(hand, b' ' as CHAR, width as u32, pos, &mut written); + FillConsoleOutputAttribute(hand, csbi.wAttributes, width as u32, pos, &mut written); + SetConsoleCursorPosition(hand, pos); + } + } + Ok(()) +} + +pub fn clear_chars(out: &Term, n: usize) -> io::Result<()> { + if out.is_msys_tty { + return common_term::clear_chars(out, n); + } + if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) { + unsafe { + let width = cmp::min(csbi.dwCursorPosition.X, n as i16); + let pos = COORD { + X: csbi.dwCursorPosition.X - width, + Y: csbi.dwCursorPosition.Y, + }; + let mut written = 0; + FillConsoleOutputCharacterA(hand, b' ' as CHAR, width as u32, pos, &mut written); + FillConsoleOutputAttribute(hand, csbi.wAttributes, width as u32, pos, &mut written); + SetConsoleCursorPosition(hand, pos); + } + } + Ok(()) +} + +pub fn clear_screen(out: &Term) -> io::Result<()> { + if out.is_msys_tty { + return common_term::clear_screen(out); + } + if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) { + unsafe { + let cells = csbi.dwSize.X as u32 * csbi.dwSize.Y as u32; // as u32, or else this causes stack overflows. + let pos = COORD { X: 0, Y: 0 }; + let mut written = 0; + FillConsoleOutputCharacterA(hand, b' ' as CHAR, cells, pos, &mut written); // cells as u32 no longer needed. + FillConsoleOutputAttribute(hand, csbi.wAttributes, cells, pos, &mut written); + SetConsoleCursorPosition(hand, pos); + } + } + Ok(()) +} + +pub fn clear_to_end_of_screen(out: &Term) -> io::Result<()> { + if out.is_msys_tty { + return common_term::clear_to_end_of_screen(out); + } + if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) { + unsafe { + let bottom = csbi.srWindow.Right as u32 * csbi.srWindow.Bottom as u32; + let cells = bottom - (csbi.dwCursorPosition.X as u32 * csbi.dwCursorPosition.Y as u32); // as u32, or else this causes stack overflows. + let pos = COORD { + X: 0, + Y: csbi.dwCursorPosition.Y, + }; + let mut written = 0; + FillConsoleOutputCharacterA(hand, b' ' as CHAR, cells, pos, &mut written); // cells as u32 no longer needed. + FillConsoleOutputAttribute(hand, csbi.wAttributes, cells, pos, &mut written); + SetConsoleCursorPosition(hand, pos); + } + } + Ok(()) +} + +pub fn show_cursor(out: &Term) -> io::Result<()> { + if out.is_msys_tty { + return common_term::show_cursor(out); + } + if let Some((hand, mut cci)) = get_console_cursor_info(as_handle(out)) { + unsafe { + cci.bVisible = 1; + SetConsoleCursorInfo(hand, &cci); + } + } + Ok(()) +} + +pub fn hide_cursor(out: &Term) -> io::Result<()> { + if out.is_msys_tty { + return common_term::hide_cursor(out); + } + if let Some((hand, mut cci)) = get_console_cursor_info(as_handle(out)) { + unsafe { + cci.bVisible = 0; + SetConsoleCursorInfo(hand, &cci); + } + } + Ok(()) +} + +fn get_console_screen_buffer_info(hand: HANDLE) -> Option<(HANDLE, CONSOLE_SCREEN_BUFFER_INFO)> { + let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = unsafe { mem::zeroed() }; + match unsafe { GetConsoleScreenBufferInfo(hand, &mut csbi) } { + 0 => None, + _ => Some((hand, csbi)), + } +} + +fn get_console_cursor_info(hand: HANDLE) -> Option<(HANDLE, CONSOLE_CURSOR_INFO)> { + let mut cci: CONSOLE_CURSOR_INFO = unsafe { mem::zeroed() }; + match unsafe { GetConsoleCursorInfo(hand, &mut cci) } { + 0 => None, + _ => Some((hand, cci)), + } +} + +pub fn key_from_key_code(code: VIRTUAL_KEY) -> Key { + use windows_sys::Win32::UI::Input::KeyboardAndMouse; + + match code { + KeyboardAndMouse::VK_LEFT => Key::ArrowLeft, + KeyboardAndMouse::VK_RIGHT => Key::ArrowRight, + KeyboardAndMouse::VK_UP => Key::ArrowUp, + KeyboardAndMouse::VK_DOWN => Key::ArrowDown, + KeyboardAndMouse::VK_RETURN => Key::Enter, + KeyboardAndMouse::VK_ESCAPE => Key::Escape, + KeyboardAndMouse::VK_BACK => Key::Backspace, + KeyboardAndMouse::VK_TAB => Key::Tab, + KeyboardAndMouse::VK_HOME => Key::Home, + KeyboardAndMouse::VK_END => Key::End, + KeyboardAndMouse::VK_DELETE => Key::Del, + KeyboardAndMouse::VK_SHIFT => Key::Shift, + KeyboardAndMouse::VK_MENU => Key::Alt, + _ => Key::Unknown, + } +} + +pub fn read_secure() -> io::Result<String> { + let mut rv = String::new(); + loop { + match read_single_key()? { + Key::Enter => { + break; + } + Key::Char('\x08') => { + if !rv.is_empty() { + let new_len = rv.len() - 1; + rv.truncate(new_len); + } + } + Key::Char(c) => { + rv.push(c); + } + _ => {} + } + } + Ok(rv) +} + +pub fn read_single_key() -> io::Result<Key> { + let key_event = read_key_event()?; + + let unicode_char = unsafe { key_event.uChar.UnicodeChar }; + if unicode_char == 0 { + Ok(key_from_key_code(key_event.wVirtualKeyCode)) + } else { + // This is a unicode character, in utf-16. Try to decode it by itself. + match char::from_utf16_tuple((unicode_char, None)) { + Ok(c) => { + // Maintain backward compatibility. The previous implementation (_getwch()) would return + // a special keycode for `Enter`, while ReadConsoleInputW() prefers to use '\r'. + if c == '\r' { + Ok(Key::Enter) + } else if c == '\x08' { + Ok(Key::Backspace) + } else if c == '\x1B' { + Ok(Key::Escape) + } else { + Ok(Key::Char(c)) + } + } + // This is part of a surrogate pair. Try to read the second half. + Err(InvalidUtf16Tuple::MissingSecond) => { + // Confirm that there is a next character to read. + if get_key_event_count()? == 0 { + let message = format!( + "Read invlid utf16 {}: {}", + unicode_char, + InvalidUtf16Tuple::MissingSecond + ); + return Err(io::Error::new(io::ErrorKind::InvalidData, message)); + } + + // Read the next character. + let next_event = read_key_event()?; + let next_surrogate = unsafe { next_event.uChar.UnicodeChar }; + + // Attempt to decode it. + match char::from_utf16_tuple((unicode_char, Some(next_surrogate))) { + Ok(c) => Ok(Key::Char(c)), + + // Return an InvalidData error. This is the recommended value for UTF-related I/O errors. + // (This error is given when reading a non-UTF8 file into a String, for example.) + Err(e) => { + let message = format!( + "Read invalid surrogate pair ({}, {}): {}", + unicode_char, next_surrogate, e + ); + Err(io::Error::new(io::ErrorKind::InvalidData, message)) + } + } + } + + // Return an InvalidData error. This is the recommended value for UTF-related I/O errors. + // (This error is given when reading a non-UTF8 file into a String, for example.) + Err(e) => { + let message = format!("Read invalid utf16 {}: {}", unicode_char, e); + Err(io::Error::new(io::ErrorKind::InvalidData, message)) + } + } + } +} + +fn get_stdin_handle() -> io::Result<HANDLE> { + let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) }; + if handle == INVALID_HANDLE_VALUE { + Err(io::Error::last_os_error()) + } else { + Ok(handle) + } +} + +/// Get the number of pending events in the ReadConsoleInput queue. Note that while +/// these aren't necessarily key events, the only way that multiple events can be +/// put into the queue simultaneously is if a unicode character spanning multiple u16's +/// is read. +/// +/// Therefore, this is accurate as long as at least one KEY_EVENT has already been read. +fn get_key_event_count() -> io::Result<u32> { + let handle = get_stdin_handle()?; + let mut event_count: u32 = unsafe { mem::zeroed() }; + + let success = unsafe { GetNumberOfConsoleInputEvents(handle, &mut event_count) }; + if success == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(event_count) + } +} + +fn read_key_event() -> io::Result<KEY_EVENT_RECORD> { + let handle = get_stdin_handle()?; + let mut buffer: INPUT_RECORD = unsafe { mem::zeroed() }; + + let mut events_read: u32 = unsafe { mem::zeroed() }; + + let mut key_event: KEY_EVENT_RECORD; + loop { + let success = unsafe { ReadConsoleInputW(handle, &mut buffer, 1, &mut events_read) }; + if success == 0 { + return Err(io::Error::last_os_error()); + } + if events_read == 0 { + return Err(io::Error::new( + io::ErrorKind::Other, + "ReadConsoleInput returned no events, instead of waiting for an event", + )); + } + + if events_read == 1 && buffer.EventType != KEY_EVENT as u16 { + // This isn't a key event; ignore it. + continue; + } + + key_event = unsafe { mem::transmute(buffer.Event) }; + + if key_event.bKeyDown == 0 { + // This is a key being released; ignore it. + continue; + } + + return Ok(key_event); + } +} + +pub fn wants_emoji() -> bool { + // If WT_SESSION is set, we can assume we're running in the nne + // Windows Terminal. The correct way to detect this is not available + // yet. See https://github.com/microsoft/terminal/issues/1040 + env::var("WT_SESSION").is_ok() +} + +/// Returns true if there is an MSYS tty on the given handle. +pub fn msys_tty_on(term: &Term) -> bool { + let handle = term.as_raw_handle(); + unsafe { + // Check whether the Windows 10 native pty is enabled + { + let mut out = MaybeUninit::uninit(); + let res = GetConsoleMode(handle as HANDLE, out.as_mut_ptr()); + if res != 0 // If res is true then out was initialized. + && (out.assume_init() & ENABLE_VIRTUAL_TERMINAL_PROCESSING) + == ENABLE_VIRTUAL_TERMINAL_PROCESSING + { + return true; + } + } + + let size = mem::size_of::<FILE_NAME_INFO>(); + let mut name_info_bytes = vec![0u8; size + MAX_PATH as usize * mem::size_of::<u16>()]; + let res = GetFileInformationByHandleEx( + handle as HANDLE, + FileNameInfo, + &mut *name_info_bytes as *mut _ as *mut c_void, + name_info_bytes.len() as u32, + ); + if res == 0 { + return false; + } + let name_info: &FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const FILE_NAME_INFO); + let s = slice::from_raw_parts( + name_info.FileName.as_ptr(), + name_info.FileNameLength as usize / 2, + ); + let name = String::from_utf16_lossy(s); + // This checks whether 'pty' exists in the file name, which indicates that + // a pseudo-terminal is attached. To mitigate against false positives + // (e.g., an actual file name that contains 'pty'), we also require that + // either the strings 'msys-' or 'cygwin-' are in the file name as well.) + let is_msys = name.contains("msys-") || name.contains("cygwin-"); + let is_pty = name.contains("-pty"); + is_msys && is_pty + } +} + +pub fn set_title<T: Display>(title: T) { + let buffer: Vec<u16> = OsStr::new(&format!("{}", title)) + .encode_wide() + .chain(once(0)) + .collect(); + unsafe { + SetConsoleTitleW(buffer.as_ptr()); + } +} |