mirror of https://github.com/tauri-apps/tauri
refactor(cli.rs): drop `dialoguer` and `console` soft fork (#2790)
This commit is contained in:
parent
da35d55b09
commit
b1f5c6d7ac
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"cli.rs": patch
|
||||
---
|
||||
|
||||
Drop the `dialoguer` soft fork and use the published version instead.
|
|
@ -277,6 +277,21 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"terminal_size",
|
||||
"unicode-width",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.1"
|
||||
|
@ -410,6 +425,18 @@ dependencies = [
|
|||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb"
|
||||
dependencies = [
|
||||
"console",
|
||||
"lazy_static",
|
||||
"tempfile",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
|
@ -1933,6 +1960,7 @@ dependencies = [
|
|||
"base64",
|
||||
"clap",
|
||||
"colored",
|
||||
"dialoguer",
|
||||
"encode_unicode",
|
||||
"glob",
|
||||
"handlebars",
|
||||
|
|
|
@ -49,6 +49,7 @@ tempfile = "3"
|
|||
zeroize = "1.4"
|
||||
glob = "0.3"
|
||||
heck = "0.3"
|
||||
dialoguer = "0.9"
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
winapi = { version = "0.3", features = [ "winbase", "winuser", "consoleapi", "processenv", "wincon" ] }
|
||||
|
|
|
@ -1,137 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use regex::{Matches, Regex};
|
||||
|
||||
lazy_static::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();
|
||||
}
|
||||
|
||||
/// Helper function to strip ansi codes.
|
||||
pub fn strip_ansi_codes(s: &str) -> Cow<'_, str> {
|
||||
STRIP_ANSI_RE.replace_all(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<'static, '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: STRIP_ANSI_RE.find_iter(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 super::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 super::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);
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::io;
|
||||
|
||||
use super::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")
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/// Key mapping
|
||||
///
|
||||
/// This is an incomplete mapping of keys that are supported for reading
|
||||
/// from the keyboard.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
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,
|
||||
Del,
|
||||
Shift,
|
||||
Insert,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Char(char),
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! 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` uses `clicolors-control` to control colors. 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).
|
||||
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pub use kb::Key;
|
||||
pub use term::{user_attended, user_attended_stderr, Term, TermFamily, TermFeatures, TermTarget};
|
||||
pub use 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,
|
||||
};
|
||||
|
||||
pub use 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;
|
||||
|
||||
mod ansi;
|
|
@ -1,624 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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 super::{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 {
|
||||
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> {
|
||||
/// Checks if this is a real user attended terminal (`isatty`)
|
||||
#[inline]
|
||||
pub fn is_attended(&self) -> bool {
|
||||
is_a_terminal(self.0)
|
||||
}
|
||||
|
||||
/// Checks 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)
|
||||
}
|
||||
|
||||
/// Checks 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
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if this terminal wants emojis.
|
||||
#[inline]
|
||||
pub fn wants_emoji(&self) -> bool {
|
||||
self.is_attended() && wants_emoji()
|
||||
}
|
||||
|
||||
/// Returns 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,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the style for the term
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the target
|
||||
#[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()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a string to the terminal and adds 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
|
||||
/// is entered.
|
||||
pub fn read_char(&self) -> io::Result<char> {
|
||||
loop {
|
||||
match self.read_key()? {
|
||||
Key::Char(c) => {
|
||||
return Ok(c);
|
||||
}
|
||||
Key::Enter => {
|
||||
return Ok('\n');
|
||||
}
|
||||
Key::Unknown => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
"Not a terminal",
|
||||
))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 => break,
|
||||
Key::Unknown => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
"Not a terminal",
|
||||
))
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(chars.iter().collect::<String>())
|
||||
}
|
||||
|
||||
/// Read securely a line of input.
|
||||
///
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Flushes 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(())
|
||||
}
|
||||
|
||||
/// Checks if the terminal is indeed a terminal.
|
||||
#[inline]
|
||||
pub fn is_term(&self) -> bool {
|
||||
self.is_tty
|
||||
}
|
||||
|
||||
/// Checks for common terminal features.
|
||||
#[inline]
|
||||
pub fn features(&self) -> TermFeatures<'_> {
|
||||
TermFeatures(self)
|
||||
}
|
||||
|
||||
/// Returns 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))
|
||||
}
|
||||
|
||||
/// Returns 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)
|
||||
}
|
||||
|
||||
/// Moves the cursor to `x` and `y`
|
||||
#[inline]
|
||||
pub fn move_cursor_to(&self, x: usize, y: usize) -> io::Result<()> {
|
||||
move_cursor_to(self, x, y)
|
||||
}
|
||||
|
||||
/// Moves the cursor up `n` lines
|
||||
#[inline]
|
||||
pub fn move_cursor_up(&self, n: usize) -> io::Result<()> {
|
||||
move_cursor_up(self, n)
|
||||
}
|
||||
|
||||
/// Moves the cursor down `n` lines
|
||||
#[inline]
|
||||
pub fn move_cursor_down(&self, n: usize) -> io::Result<()> {
|
||||
move_cursor_down(self, n)
|
||||
}
|
||||
|
||||
/// Moves the cursor left `n` lines
|
||||
#[inline]
|
||||
pub fn move_cursor_left(&self, n: usize) -> io::Result<()> {
|
||||
move_cursor_left(self, n)
|
||||
}
|
||||
|
||||
/// Moves the cursor down `n` lines
|
||||
#[inline]
|
||||
pub fn move_cursor_right(&self, n: usize) -> io::Result<()> {
|
||||
move_cursor_right(self, n)
|
||||
}
|
||||
|
||||
/// Clears the current line.
|
||||
///
|
||||
/// The positions the cursor at the beginning of the line again.
|
||||
#[inline]
|
||||
pub fn clear_line(&self) -> io::Result<()> {
|
||||
clear_line(self)
|
||||
}
|
||||
|
||||
/// Clear the last `n` lines.
|
||||
///
|
||||
/// This positions 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(())
|
||||
}
|
||||
|
||||
/// Clears the entire screen.
|
||||
#[inline]
|
||||
pub fn clear_screen(&self) -> io::Result<()> {
|
||||
clear_screen(self)
|
||||
}
|
||||
|
||||
/// Clears the entire screen.
|
||||
#[inline]
|
||||
pub fn clear_to_end_of_screen(&self) -> io::Result<()> {
|
||||
clear_to_end_of_screen(self)
|
||||
}
|
||||
|
||||
/// Clears the last char in the 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);
|
||||
}
|
||||
|
||||
/// Makes cursor visible again
|
||||
#[inline]
|
||||
pub fn show_cursor(&self) -> io::Result<()> {
|
||||
show_cursor(self)
|
||||
}
|
||||
|
||||
/// Hides 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 {
|
||||
use winapi_util::console::Console;
|
||||
|
||||
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 winapi::um::processenv::GetStdHandle;
|
||||
use winapi::um::winbase::{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 super::unix_term::*;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use super::wasm_term::*;
|
||||
#[cfg(windows)]
|
||||
pub use super::windows_term::*;
|
|
@ -1,299 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::str;
|
||||
|
||||
use super::kb::Key;
|
||||
use super::term::Term;
|
||||
|
||||
pub use super::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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn terminal_size(out: &Term) -> Option<(u16, u16)> {
|
||||
terminal_size::terminal_size_using_fd(out.as_raw_fd()).map(|x| ((x.1).0, (x.0).0))
|
||||
}
|
||||
|
||||
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::File::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 read_single_char(fd: i32) -> io::Result<Option<char>> {
|
||||
let mut pollfd = libc::pollfd {
|
||||
fd,
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
|
||||
// timeout of zero means that it will not block
|
||||
let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, 0) };
|
||||
if ret < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let is_ready = pollfd.revents & libc::POLLIN != 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)
|
||||
}
|
||||
}
|
||||
|
||||
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::File::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) };
|
||||
c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &termios) })?;
|
||||
|
||||
let rv = match read_single_char(fd)? {
|
||||
Some('\x1b') => {
|
||||
// Escape was read, keep reading in case we find a familiar key
|
||||
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];
|
||||
|
||||
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
|
||||
|
||||
let mut pollfd = libc::pollfd {
|
||||
fd,
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
|
||||
// negative timeout means that it will block indefinitely
|
||||
let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, -1) };
|
||||
if ret < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
read_single_key()
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
|
@ -1,900 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use super::term::{wants_emoji, Term};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use super::ansi::{strip_ansi_codes, AnsiCodeIterator};
|
||||
|
||||
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,
|
||||
Reverse,
|
||||
Hidden,
|
||||
}
|
||||
|
||||
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::Reverse => 7,
|
||||
Attribute::Hidden => 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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. 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(),
|
||||
"reverse" => rv.reverse(),
|
||||
"hidden" => rv.hidden(),
|
||||
_ => {
|
||||
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 detection from `clicolors-control`.
|
||||
#[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 reverse(self) -> Style {
|
||||
self.attr(Attribute::Reverse)
|
||||
}
|
||||
#[inline]
|
||||
pub fn hidden(self) -> Style {
|
||||
self.attr(Attribute::Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 detection from `clicolors-control`.
|
||||
#[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 reverse(self) -> StyledObject<D> {
|
||||
self.attr(Attribute::Reverse)
|
||||
}
|
||||
#[inline]
|
||||
pub fn hidden(self) -> StyledObject<D> {
|
||||
self.attr(Attribute::Hidden)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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), 3);
|
||||
}
|
||||
|
||||
#[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..."
|
||||
);
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::io;
|
||||
|
||||
use super::kb::Key;
|
||||
use super::term::Term;
|
||||
|
||||
pub use super::common_term::*;
|
||||
|
||||
pub const DEFAULT_WIDTH: u16 = 80;
|
||||
|
||||
#[inline]
|
||||
pub fn is_a_terminal(_out: &Term) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_a_color_terminal(_out: &Term) -> bool {
|
||||
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) {}
|
|
@ -1,600 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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::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;
|
||||
#[cfg(feature = "windows-console-colors")]
|
||||
use regex::Regex;
|
||||
use winapi::ctypes::c_void;
|
||||
use winapi::shared::minwindef::DWORD;
|
||||
use winapi::shared::minwindef::MAX_PATH;
|
||||
use winapi::um::consoleapi::{GetConsoleMode, SetConsoleMode};
|
||||
use winapi::um::consoleapi::{GetNumberOfConsoleInputEvents, ReadConsoleInputW};
|
||||
use winapi::um::fileapi::FILE_NAME_INFO;
|
||||
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
|
||||
use winapi::um::minwinbase::FileNameInfo;
|
||||
use winapi::um::processenv::GetStdHandle;
|
||||
use winapi::um::winbase::GetFileInformationByHandleEx;
|
||||
use winapi::um::winbase::{STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE};
|
||||
use winapi::um::wincon::{
|
||||
FillConsoleOutputAttribute, FillConsoleOutputCharacterA, GetConsoleCursorInfo,
|
||||
GetConsoleScreenBufferInfo, SetConsoleCursorInfo, SetConsoleCursorPosition, SetConsoleTitleW,
|
||||
CONSOLE_CURSOR_INFO, CONSOLE_SCREEN_BUFFER_INFO, COORD, INPUT_RECORD, KEY_EVENT,
|
||||
KEY_EVENT_RECORD,
|
||||
};
|
||||
use winapi::um::winnt::{CHAR, HANDLE, INT, WCHAR};
|
||||
#[cfg(feature = "windows-console-colors")]
|
||||
use winapi_util::console::{Color, Console, Intense};
|
||||
|
||||
use super::common_term;
|
||||
use super::kb::Key;
|
||||
use super::term::{Term, TermTarget};
|
||||
|
||||
#[cfg(feature = "windows-console-colors")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref INTENSE_COLOR_RE: Regex = Regex::new(r"\x1b\[(3|4)8;5;(8|9|1[0-5])m").unwrap();
|
||||
static ref NORMAL_COLOR_RE: Regex = Regex::new(r"\x1b\[(3|4)([0-7])m").unwrap();
|
||||
static ref ATTR_RE: Regex = Regex::new(r"\x1b\[([1-8])m").unwrap();
|
||||
}
|
||||
|
||||
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x4;
|
||||
pub const DEFAULT_WIDTH: u16 = 79;
|
||||
|
||||
pub fn as_handle(term: &Term) -> HANDLE {
|
||||
// convert between winapi::um::winnt::HANDLE and std::os::windows::raw::HANDLE
|
||||
// which are both c_void. would be nice to find a better way to do this
|
||||
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: &[DWORD]) -> bool {
|
||||
for &fd in fds {
|
||||
let mut out = 0;
|
||||
let handle = GetStdHandle(fd);
|
||||
if GetConsoleMode(handle, &mut out) != 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn terminal_size(out: &Term) -> Option<(u16, u16)> {
|
||||
terminal_size::terminal_size_using_handle(out.as_raw_handle()).map(|x| ((x.1).0, (x.0).0))
|
||||
}
|
||||
|
||||
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 DWORD, pos, &mut written);
|
||||
FillConsoleOutputAttribute(hand, csbi.wAttributes, width as DWORD, 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 DWORD, pos, &mut written);
|
||||
FillConsoleOutputAttribute(hand, csbi.wAttributes, width as DWORD, 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 DWORD * csbi.dwSize.Y as DWORD; // as DWORD, 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 DWORD 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 DWORD * csbi.srWindow.Bottom as DWORD;
|
||||
let cells = bottom - (csbi.dwCursorPosition.X as DWORD * csbi.dwCursorPosition.Y as DWORD); // as DWORD, 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 DWORD 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: INT) -> Key {
|
||||
match code {
|
||||
winapi::um::winuser::VK_LEFT => Key::ArrowLeft,
|
||||
winapi::um::winuser::VK_RIGHT => Key::ArrowRight,
|
||||
winapi::um::winuser::VK_UP => Key::ArrowUp,
|
||||
winapi::um::winuser::VK_DOWN => Key::ArrowDown,
|
||||
winapi::um::winuser::VK_RETURN => Key::Enter,
|
||||
winapi::um::winuser::VK_ESCAPE => Key::Escape,
|
||||
winapi::um::winuser::VK_BACK => Key::Backspace,
|
||||
winapi::um::winuser::VK_TAB => Key::Tab,
|
||||
winapi::um::winuser::VK_HOME => Key::Home,
|
||||
winapi::um::winuser::VK_END => Key::End,
|
||||
winapi::um::winuser::VK_DELETE => Key::Del,
|
||||
winapi::um::winuser::VK_SHIFT => Key::Shift,
|
||||
_ => 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 as INT))
|
||||
} 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<DWORD> {
|
||||
let handle = get_stdin_handle()?;
|
||||
let mut event_count: DWORD = 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: DWORD = 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 {
|
||||
// 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 *mut _, 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 * mem::size_of::<WCHAR>()];
|
||||
let res = GetFileInformationByHandleEx(
|
||||
handle as *mut _,
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "windows-console-colors")]
|
||||
pub fn console_colors(out: &Term, mut con: Console, bytes: &[u8]) -> io::Result<()> {
|
||||
use super::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(cap) = INTENSE_COLOR_RE.captures(part) {
|
||||
let color = get_color_from_ansi(cap.get(2).unwrap().as_str());
|
||||
|
||||
match cap.get(1).unwrap().as_str() {
|
||||
"3" => con.fg(Intense::Yes, color)?,
|
||||
"4" => con.bg(Intense::Yes, color)?,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
} else if let Some(cap) = NORMAL_COLOR_RE.captures(part) {
|
||||
let color = get_color_from_ansi(cap.get(2).unwrap().as_str());
|
||||
|
||||
match cap.get(1).unwrap().as_str() {
|
||||
"3" => con.fg(Intense::No, color)?,
|
||||
"4" => con.bg(Intense::No, color)?,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
} else if !ATTR_RE.is_match(part) {
|
||||
out.write_through_common(part.as_bytes())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "windows-console-colors")]
|
||||
fn get_color_from_ansi(ansi: &str) -> Color {
|
||||
match ansi {
|
||||
"0" | "8" => Color::Black,
|
||||
"1" | "9" => Color::Red,
|
||||
"2" | "10" => Color::Green,
|
||||
"3" | "11" => Color::Yellow,
|
||||
"4" | "12" => Color::Blue,
|
||||
"5" | "13" => Color::Magenta,
|
||||
"6" | "14" => Color::Cyan,
|
||||
"7" | "15" => Color::White,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
env,
|
||||
ffi::{OsStr, OsString},
|
||||
fs, io,
|
||||
io::{Read, Write},
|
||||
process,
|
||||
};
|
||||
|
||||
/// Launches the default editor to edit a string.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::Editor;
|
||||
///
|
||||
/// if let Some(rv) = Editor::new().edit("Enter a commit message").unwrap() {
|
||||
/// println!("Your message:");
|
||||
/// println!("{}", rv);
|
||||
/// } else {
|
||||
/// println!("Abort!");
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Editor {
|
||||
editor: OsString,
|
||||
extension: String,
|
||||
require_save: bool,
|
||||
trim_newlines: bool,
|
||||
}
|
||||
|
||||
fn get_default_editor() -> OsString {
|
||||
if let Some(prog) = env::var_os("VISUAL") {
|
||||
return prog;
|
||||
}
|
||||
if let Some(prog) = env::var_os("EDITOR") {
|
||||
return prog;
|
||||
}
|
||||
if cfg!(windows) {
|
||||
"notepad.exe".into()
|
||||
} else {
|
||||
"vi".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Editor {
|
||||
fn default() -> Editor {
|
||||
Editor::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
/// Creates a new editor.
|
||||
pub fn new() -> Editor {
|
||||
Editor {
|
||||
editor: get_default_editor(),
|
||||
extension: ".txt".into(),
|
||||
require_save: true,
|
||||
trim_newlines: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a specific editor executable.
|
||||
pub fn executable<S: AsRef<OsStr>>(&mut self, val: S) -> &mut Editor {
|
||||
self.editor = val.as_ref().into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a specific extension
|
||||
pub fn extension(&mut self, val: &str) -> &mut Editor {
|
||||
self.extension = val.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables or disables the save requirement.
|
||||
pub fn require_save(&mut self, val: bool) -> &mut Editor {
|
||||
self.require_save = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables or disables trailing newline stripping.
|
||||
///
|
||||
/// This is on by default.
|
||||
pub fn trim_newlines(&mut self, val: bool) -> &mut Editor {
|
||||
self.trim_newlines = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Launches the editor to edit a string.
|
||||
///
|
||||
/// Returns `None` if the file was not saved or otherwise the
|
||||
/// entered text.
|
||||
pub fn edit(&self, s: &str) -> io::Result<Option<String>> {
|
||||
let mut f = tempfile::Builder::new()
|
||||
.prefix("edit-")
|
||||
.suffix(&self.extension)
|
||||
.rand_bytes(12)
|
||||
.tempfile()?;
|
||||
f.write_all(s.as_bytes())?;
|
||||
f.flush()?;
|
||||
let ts = fs::metadata(f.path())?.modified()?;
|
||||
|
||||
let s: String = self.editor.clone().into_string().unwrap();
|
||||
let mut iterator = s.split(' ');
|
||||
let cmd = iterator.next().unwrap();
|
||||
let args: Vec<&str> = iterator.collect();
|
||||
|
||||
let rv = process::Command::new(cmd)
|
||||
.args(args)
|
||||
.arg(f.path())
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
|
||||
if rv.success() && self.require_save && ts >= fs::metadata(f.path())?.modified()? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut new_f = fs::File::open(f.path())?;
|
||||
let mut rv = String::new();
|
||||
new_f.read_to_string(&mut rv)?;
|
||||
|
||||
if self.trim_newlines {
|
||||
let len = rv.trim_end_matches(&['\n', '\r'][..]).len();
|
||||
rv.truncate(len);
|
||||
}
|
||||
|
||||
Ok(Some(rv))
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! dialoguer is a library for Rust that helps you build useful small
|
||||
//! interactive user inputs for the command line. It provides utilities
|
||||
//! to render various simple dialogs like confirmation prompts, text
|
||||
//! inputs and more.
|
||||
//!
|
||||
//! Best paired with other libraries in the family:
|
||||
//!
|
||||
//! * [indicatif](https://docs.rs/indicatif)
|
||||
//! * [console](https://docs.rs/console)
|
||||
//!
|
||||
//! # Crate Contents
|
||||
//!
|
||||
//! * Confirmation prompts
|
||||
//! * Input prompts (regular and password)
|
||||
//! * Input validation
|
||||
//! * Selections prompts (single and multi)
|
||||
//! * Other kind of prompts
|
||||
//! * Editor launching
|
||||
|
||||
pub use edit::Editor;
|
||||
pub use prompts::{
|
||||
confirm::Confirm, input::Input, multi_select::MultiSelect, password::Password, select::Select,
|
||||
sort::Sort,
|
||||
};
|
||||
pub use validate::Validator;
|
||||
|
||||
mod edit;
|
||||
mod prompts;
|
||||
pub mod theme;
|
||||
mod validate;
|
|
@ -1,259 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::io;
|
||||
|
||||
use super::super::theme::{SimpleTheme, TermThemeRenderer, Theme};
|
||||
|
||||
use crate::console::{Key, Term};
|
||||
|
||||
/// Renders a confirm prompt.
|
||||
///
|
||||
/// ## Example usage
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use dialoguer::Confirm;
|
||||
///
|
||||
/// if Confirm::new().with_prompt("Do you want to continue?").interact()? {
|
||||
/// println!("Looks like you want to continue");
|
||||
/// } else {
|
||||
/// println!("nevermind then :(");
|
||||
/// }
|
||||
/// # Ok(()) } fn main() { test().unwrap(); }
|
||||
/// ```
|
||||
pub struct Confirm<'a> {
|
||||
prompt: String,
|
||||
default: Option<bool>,
|
||||
show_default: bool,
|
||||
wait_for_newline: bool,
|
||||
theme: &'a dyn Theme,
|
||||
}
|
||||
|
||||
impl<'a> Default for Confirm<'a> {
|
||||
fn default() -> Confirm<'a> {
|
||||
Confirm::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Confirm<'a> {
|
||||
/// Creates a confirm prompt.
|
||||
pub fn new() -> Confirm<'static> {
|
||||
Confirm::with_theme(&SimpleTheme)
|
||||
}
|
||||
|
||||
/// Creates a confirm prompt with a specific theme.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::{
|
||||
/// Confirm,
|
||||
/// theme::ColorfulTheme
|
||||
/// };
|
||||
///
|
||||
/// # fn main() -> std::io::Result<()> {
|
||||
/// let proceed = Confirm::with_theme(&ColorfulTheme::default())
|
||||
/// .with_prompt("Do you wish to continue?")
|
||||
/// .interact()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn with_theme(theme: &'a dyn Theme) -> Confirm<'a> {
|
||||
Confirm {
|
||||
prompt: "".into(),
|
||||
default: None,
|
||||
show_default: true,
|
||||
wait_for_newline: false,
|
||||
theme,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the confirm prompt.
|
||||
pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Confirm<'a> {
|
||||
self.prompt = prompt.into();
|
||||
self
|
||||
}
|
||||
|
||||
#[deprecated(note = "Use with_prompt() instead", since = "0.6.0")]
|
||||
#[inline]
|
||||
pub fn with_text(&mut self, text: &str) -> &mut Confirm<'a> {
|
||||
self.with_prompt(text)
|
||||
}
|
||||
|
||||
/// Sets when to react to user input.
|
||||
///
|
||||
/// When `false` (default), we check on each user keystroke immediately as
|
||||
/// it is typed. Valid inputs can be one of 'y', 'n', or a newline to accept
|
||||
/// the default.
|
||||
///
|
||||
/// When `true`, the user must type their choice and hit the Enter key before
|
||||
/// proceeding. Valid inputs can be "yes", "no", "y", "n", or an empty string
|
||||
/// to accept the default.
|
||||
pub fn wait_for_newline(&mut self, wait: bool) -> &mut Confirm<'a> {
|
||||
self.wait_for_newline = wait;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a default.
|
||||
///
|
||||
/// Out of the box the prompt does not have a default and will continue
|
||||
/// to display until the user inputs something and hits enter. If a default is set the user
|
||||
/// can instead accept the default with enter.
|
||||
pub fn default(&mut self, val: bool) -> &mut Confirm<'a> {
|
||||
self.default = Some(val);
|
||||
self
|
||||
}
|
||||
|
||||
/// Disables or enables the default value display.
|
||||
///
|
||||
/// The default is to append the default value to the prompt to tell the user.
|
||||
pub fn show_default(&mut self, val: bool) -> &mut Confirm<'a> {
|
||||
self.show_default = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables user interaction and returns the result.
|
||||
///
|
||||
/// If the user confirms the result is `true`, `false` if declines or default (configured in [default](#method.default)) if pushes enter.
|
||||
/// Otherwise function discards input waiting for valid one.
|
||||
///
|
||||
/// The dialog is rendered on stderr.
|
||||
pub fn interact(&self) -> io::Result<bool> {
|
||||
self.interact_on(&Term::stderr())
|
||||
}
|
||||
|
||||
/// Enables user interaction and returns the result.
|
||||
///
|
||||
/// This method is similar to [interact_on_opt](#method.interact_on_opt) except for the fact that it does not allow selection of the terminal.
|
||||
/// The dialog is rendered on stderr.
|
||||
/// Result contains `Some(bool)` if user answered "yes" or "no" or `None` if user cancelled with 'Esc' or 'q'.
|
||||
pub fn interact_opt(&self) -> io::Result<Option<bool>> {
|
||||
self.interact_on_opt(&Term::stderr())
|
||||
}
|
||||
|
||||
/// Like [interact](#method.interact) but allows a specific terminal to be set.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::Confirm;
|
||||
/// use console::Term;
|
||||
///
|
||||
/// # fn main() -> std::io::Result<()> {
|
||||
/// let proceed = Confirm::new()
|
||||
/// .with_prompt("Do you wish to continue?")
|
||||
/// .interact_on(&Term::stderr())?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn interact_on(&self, term: &Term) -> io::Result<bool> {
|
||||
self
|
||||
._interact_on(term, false)?
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))
|
||||
}
|
||||
|
||||
/// Like [interact_opt](#method.interact_opt) but allows a specific terminal to be set.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::Confirm;
|
||||
/// use console::Term;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let confirmation = Confirm::new()
|
||||
/// .interact_on_opt(&Term::stdout())?;
|
||||
///
|
||||
/// match confirmation {
|
||||
/// Some(answer) => println!("User answered {}", if answer { "yes" } else { "no " }),
|
||||
/// None => println!("User did not answer")
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<bool>> {
|
||||
self._interact_on(term, true)
|
||||
}
|
||||
|
||||
fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<bool>> {
|
||||
let mut render = TermThemeRenderer::new(term, self.theme);
|
||||
|
||||
let default_if_show = if self.show_default {
|
||||
self.default
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
render.confirm_prompt(&self.prompt, default_if_show)?;
|
||||
|
||||
term.hide_cursor()?;
|
||||
term.flush()?;
|
||||
|
||||
let rv;
|
||||
|
||||
if self.wait_for_newline {
|
||||
// Waits for user input and for the user to hit the Enter key
|
||||
// before validation.
|
||||
let mut value = default_if_show;
|
||||
|
||||
loop {
|
||||
let input = term.read_key()?;
|
||||
|
||||
match input {
|
||||
Key::Char('y') | Key::Char('Y') => {
|
||||
value = Some(true);
|
||||
}
|
||||
Key::Char('n') | Key::Char('N') => {
|
||||
value = Some(false);
|
||||
}
|
||||
Key::Enter => {
|
||||
if !allow_quit {
|
||||
value = value.or(self.default);
|
||||
}
|
||||
|
||||
if value.is_some() || allow_quit {
|
||||
rv = value;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Key::Escape | Key::Char('q') if allow_quit => {
|
||||
value = None;
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
term.clear_line()?;
|
||||
render.confirm_prompt(&self.prompt, value)?;
|
||||
}
|
||||
} else {
|
||||
// Default behavior: matches continuously on every keystroke,
|
||||
// and does not wait for user to hit the Enter key.
|
||||
loop {
|
||||
let input = term.read_key()?;
|
||||
let value = match input {
|
||||
Key::Char('y') | Key::Char('Y') => Some(true),
|
||||
Key::Char('n') | Key::Char('N') => Some(false),
|
||||
Key::Enter if self.default.is_some() => Some(self.default.unwrap()),
|
||||
Key::Escape | Key::Char('q') if allow_quit => None,
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
rv = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
term.clear_line()?;
|
||||
render.confirm_prompt_selection(&self.prompt, rv)?;
|
||||
term.show_cursor()?;
|
||||
term.flush()?;
|
||||
|
||||
Ok(rv)
|
||||
}
|
||||
}
|
|
@ -1,361 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
io, iter,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use super::super::{
|
||||
theme::{SimpleTheme, TermThemeRenderer, Theme},
|
||||
validate::Validator,
|
||||
};
|
||||
|
||||
use crate::console::{Key, Term};
|
||||
|
||||
/// Renders an input prompt.
|
||||
///
|
||||
/// ## Example usage
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::Input;
|
||||
///
|
||||
/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let input : String = Input::new()
|
||||
/// .with_prompt("Tea or coffee?")
|
||||
/// .with_initial_text("Yes")
|
||||
/// .default("No".into())
|
||||
/// .interact_text()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
/// It can also be used with turbofish notation:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # use dialoguer::Input;
|
||||
/// let input = Input::<String>::new()
|
||||
/// .interact_text()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Input<'a, T> {
|
||||
prompt: String,
|
||||
default: Option<T>,
|
||||
show_default: bool,
|
||||
initial_text: Option<String>,
|
||||
theme: &'a dyn Theme,
|
||||
permit_empty: bool,
|
||||
validator: Option<Box<dyn FnMut(&T) -> Option<String> + 'a>>,
|
||||
}
|
||||
|
||||
impl<'a, T> Default for Input<'a, T>
|
||||
where
|
||||
T: Clone + FromStr + Display,
|
||||
T::Err: Display + Debug,
|
||||
{
|
||||
fn default() -> Input<'a, T> {
|
||||
Input::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Input<'a, T>
|
||||
where
|
||||
T: Clone + FromStr + Display,
|
||||
T::Err: Display + Debug,
|
||||
{
|
||||
/// Creates an input prompt.
|
||||
pub fn new() -> Input<'a, T> {
|
||||
Input::with_theme(&SimpleTheme)
|
||||
}
|
||||
|
||||
/// Creates an input prompt with a specific theme.
|
||||
pub fn with_theme(theme: &'a dyn Theme) -> Input<'a, T> {
|
||||
Input {
|
||||
prompt: "".into(),
|
||||
default: None,
|
||||
show_default: true,
|
||||
initial_text: None,
|
||||
theme,
|
||||
permit_empty: false,
|
||||
validator: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the input prompt.
|
||||
pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Input<'a, T> {
|
||||
self.prompt = prompt.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets initial text that user can accept or erase.
|
||||
pub fn with_initial_text<S: Into<String>>(&mut self, val: S) -> &mut Input<'a, T> {
|
||||
self.initial_text = Some(val.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a default.
|
||||
///
|
||||
/// Out of the box the prompt does not have a default and will continue
|
||||
/// to display until the user inputs something and hits enter. If a default is set the user
|
||||
/// can instead accept the default with enter.
|
||||
pub fn default(&mut self, value: T) -> &mut Input<'a, T> {
|
||||
self.default = Some(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables or disables an empty input
|
||||
///
|
||||
/// By default, if there is no default value set for the input, the user must input a non-empty string.
|
||||
pub fn allow_empty(&mut self, val: bool) -> &mut Input<'a, T> {
|
||||
self.permit_empty = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Disables or enables the default value display.
|
||||
///
|
||||
/// The default behaviour is to append [`default`] to the prompt to tell the
|
||||
/// user what is the default value.
|
||||
///
|
||||
/// This method does not affect existence of default value, only its display in the prompt!
|
||||
pub fn show_default(&mut self, val: bool) -> &mut Input<'a, T> {
|
||||
self.show_default = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Registers a validator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use dialoguer::Input;
|
||||
/// let mail: String = Input::new()
|
||||
/// .with_prompt("Enter email")
|
||||
/// .validate_with(|input: &String| -> Result<(), &str> {
|
||||
/// if input.contains('@') {
|
||||
/// Ok(())
|
||||
/// } else {
|
||||
/// Err("This is not a mail address")
|
||||
/// }
|
||||
/// })
|
||||
/// .interact()
|
||||
/// .unwrap();
|
||||
/// ```
|
||||
pub fn validate_with<V>(&mut self, mut validator: V) -> &mut Input<'a, T>
|
||||
where
|
||||
V: Validator<T> + 'a,
|
||||
T: 'a,
|
||||
{
|
||||
let mut old_validator_func = self.validator.take();
|
||||
|
||||
self.validator = Some(Box::new(move |value: &T| -> Option<String> {
|
||||
if let Some(old) = old_validator_func.as_mut() {
|
||||
if let Some(err) = old(value) {
|
||||
return Some(err);
|
||||
}
|
||||
}
|
||||
|
||||
match validator.validate(value) {
|
||||
Ok(()) => None,
|
||||
Err(err) => Some(err.to_string()),
|
||||
}
|
||||
}));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables the user to enter a printable ascii sequence and returns the result.
|
||||
///
|
||||
/// Its difference from [`interact`](#method.interact) is that it only allows ascii characters for string,
|
||||
/// while [`interact`](#method.interact) allows virtually any character to be used e.g arrow keys.
|
||||
///
|
||||
/// The dialog is rendered on stderr.
|
||||
pub fn interact_text(&mut self) -> io::Result<T> {
|
||||
self.interact_text_on(&Term::stderr())
|
||||
}
|
||||
|
||||
/// Like [`interact_text`](#method.interact_text) but allows a specific terminal to be set.
|
||||
pub fn interact_text_on(&mut self, term: &Term) -> io::Result<T> {
|
||||
let mut render = TermThemeRenderer::new(term, self.theme);
|
||||
|
||||
loop {
|
||||
let default_string = self.default.as_ref().map(|x| x.to_string());
|
||||
|
||||
render.input_prompt(
|
||||
&self.prompt,
|
||||
if self.show_default {
|
||||
default_string.as_deref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)?;
|
||||
term.flush()?;
|
||||
|
||||
// Read input by keystroke so that we can suppress ascii control characters
|
||||
if !term.features().is_attended() {
|
||||
return Ok("".to_owned().parse::<T>().unwrap());
|
||||
}
|
||||
|
||||
let mut chars: Vec<char> = Vec::new();
|
||||
let mut position = 0;
|
||||
|
||||
if let Some(initial) = self.initial_text.as_ref() {
|
||||
term.write_str(initial)?;
|
||||
chars = initial.chars().collect();
|
||||
position = chars.len();
|
||||
}
|
||||
|
||||
loop {
|
||||
match term.read_key()? {
|
||||
Key::Backspace if position > 0 => {
|
||||
position -= 1;
|
||||
chars.remove(position);
|
||||
term.clear_chars(1)?;
|
||||
|
||||
let tail: String = chars[position..].iter().collect();
|
||||
|
||||
if !tail.is_empty() {
|
||||
term.write_str(&tail)?;
|
||||
term.move_cursor_left(tail.len())?;
|
||||
}
|
||||
|
||||
term.flush()?;
|
||||
}
|
||||
Key::Char(chr) if !chr.is_ascii_control() => {
|
||||
chars.insert(position, chr);
|
||||
position += 1;
|
||||
let tail: String = iter::once(&chr).chain(chars[position..].iter()).collect();
|
||||
term.write_str(&tail)?;
|
||||
term.move_cursor_left(tail.len() - 1)?;
|
||||
term.flush()?;
|
||||
}
|
||||
Key::ArrowLeft if position > 0 => {
|
||||
term.move_cursor_left(1)?;
|
||||
position -= 1;
|
||||
term.flush()?;
|
||||
}
|
||||
Key::ArrowRight if position < chars.len() => {
|
||||
term.move_cursor_right(1)?;
|
||||
position += 1;
|
||||
term.flush()?;
|
||||
}
|
||||
Key::Enter => break,
|
||||
Key::Unknown => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
"Not a terminal",
|
||||
))
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
let input = chars.iter().collect::<String>();
|
||||
|
||||
term.clear_line()?;
|
||||
render.clear()?;
|
||||
|
||||
if chars.is_empty() {
|
||||
if let Some(ref default) = self.default {
|
||||
render.input_prompt_selection(&self.prompt, &default.to_string())?;
|
||||
term.flush()?;
|
||||
return Ok(default.clone());
|
||||
} else if !self.permit_empty {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match input.parse::<T>() {
|
||||
Ok(value) => {
|
||||
if let Some(ref mut validator) = self.validator {
|
||||
if let Some(err) = validator(&value) {
|
||||
render.error(&err)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
render.input_prompt_selection(&self.prompt, &input)?;
|
||||
term.flush()?;
|
||||
|
||||
return Ok(value);
|
||||
}
|
||||
Err(err) => {
|
||||
render.error(&err.to_string())?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables user interaction and returns the result.
|
||||
///
|
||||
/// Allows any characters as input, including e.g arrow keys.
|
||||
/// Some of the keys might have undesired behavior.
|
||||
/// For more limited version, see [`interact_text`](#method.interact_text).
|
||||
///
|
||||
/// If the user confirms the result is `true`, `false` otherwise.
|
||||
/// The dialog is rendered on stderr.
|
||||
pub fn interact(&mut self) -> io::Result<T> {
|
||||
self.interact_on(&Term::stderr())
|
||||
}
|
||||
|
||||
/// Like [`interact`](#method.interact) but allows a specific terminal to be set.
|
||||
pub fn interact_on(&mut self, term: &Term) -> io::Result<T> {
|
||||
let mut render = TermThemeRenderer::new(term, self.theme);
|
||||
|
||||
loop {
|
||||
let default_string = self.default.as_ref().map(|x| x.to_string());
|
||||
|
||||
render.input_prompt(
|
||||
&self.prompt,
|
||||
if self.show_default {
|
||||
default_string.as_deref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)?;
|
||||
term.flush()?;
|
||||
|
||||
let input = if let Some(initial_text) = self.initial_text.as_ref() {
|
||||
term.read_line_initial_text(initial_text)?
|
||||
} else {
|
||||
term.read_line()?
|
||||
};
|
||||
|
||||
render.add_line();
|
||||
term.clear_line()?;
|
||||
render.clear()?;
|
||||
|
||||
if input.is_empty() {
|
||||
if let Some(ref default) = self.default {
|
||||
render.input_prompt_selection(&self.prompt, &default.to_string())?;
|
||||
term.flush()?;
|
||||
return Ok(default.clone());
|
||||
} else if !self.permit_empty {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match input.parse::<T>() {
|
||||
Ok(value) => {
|
||||
if let Some(ref mut validator) = self.validator {
|
||||
if let Some(err) = validator(&value) {
|
||||
render.error(&err)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
render.input_prompt_selection(&self.prompt, &input)?;
|
||||
term.flush()?;
|
||||
|
||||
return Ok(value);
|
||||
}
|
||||
Err(err) => {
|
||||
render.error(&err.to_string())?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
|
||||
pub mod confirm;
|
||||
pub mod input;
|
||||
pub mod multi_select;
|
||||
pub mod password;
|
||||
pub mod select;
|
||||
pub mod sort;
|
|
@ -1,290 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{io, iter::repeat, ops::Rem};
|
||||
|
||||
use super::super::theme::{SimpleTheme, TermThemeRenderer, Theme};
|
||||
|
||||
use crate::console::{Key, Term};
|
||||
|
||||
/// Renders a multi select prompt.
|
||||
///
|
||||
/// ## Example usage
|
||||
/// ```rust,no_run
|
||||
/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// use dialoguer::MultiSelect;
|
||||
///
|
||||
/// let items = vec!["Option 1", "Option 2"];
|
||||
/// let chosen : Vec<usize> = MultiSelect::new()
|
||||
/// .items(&items)
|
||||
/// .interact()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct MultiSelect<'a> {
|
||||
defaults: Vec<bool>,
|
||||
items: Vec<String>,
|
||||
prompt: Option<String>,
|
||||
clear: bool,
|
||||
theme: &'a dyn Theme,
|
||||
paged: bool,
|
||||
}
|
||||
|
||||
impl<'a> Default for MultiSelect<'a> {
|
||||
fn default() -> MultiSelect<'a> {
|
||||
MultiSelect::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MultiSelect<'a> {
|
||||
/// Creates a multi select prompt.
|
||||
pub fn new() -> MultiSelect<'static> {
|
||||
MultiSelect::with_theme(&SimpleTheme)
|
||||
}
|
||||
|
||||
/// Creates a multi select prompt with a specific theme.
|
||||
pub fn with_theme(theme: &'a dyn Theme) -> MultiSelect<'a> {
|
||||
MultiSelect {
|
||||
items: vec![],
|
||||
defaults: vec![],
|
||||
clear: true,
|
||||
prompt: None,
|
||||
theme,
|
||||
paged: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables or disables paging
|
||||
pub fn paged(&mut self, val: bool) -> &mut MultiSelect<'a> {
|
||||
self.paged = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the clear behavior of the menu.
|
||||
///
|
||||
/// The default is to clear the menu.
|
||||
pub fn clear(&mut self, val: bool) -> &mut MultiSelect<'a> {
|
||||
self.clear = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a defaults for the menu.
|
||||
pub fn defaults(&mut self, val: &[bool]) -> &mut MultiSelect<'a> {
|
||||
self.defaults = val
|
||||
.to_vec()
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(repeat(false))
|
||||
.take(self.items.len())
|
||||
.collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a single item to the selector.
|
||||
#[inline]
|
||||
pub fn item<T: ToString>(&mut self, item: T) -> &mut MultiSelect<'a> {
|
||||
self.item_checked(item, false)
|
||||
}
|
||||
|
||||
/// Add a single item to the selector with a default checked state.
|
||||
pub fn item_checked<T: ToString>(&mut self, item: T, checked: bool) -> &mut MultiSelect<'a> {
|
||||
self.items.push(item.to_string());
|
||||
self.defaults.push(checked);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds multiple items to the selector.
|
||||
pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut MultiSelect<'a> {
|
||||
for item in items {
|
||||
self.items.push(item.to_string());
|
||||
self.defaults.push(false);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds multiple items to the selector with checked state
|
||||
pub fn items_checked<T: ToString>(&mut self, items: &[(T, bool)]) -> &mut MultiSelect<'a> {
|
||||
for &(ref item, checked) in items {
|
||||
self.items.push(item.to_string());
|
||||
self.defaults.push(checked);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Prefaces the menu with a prompt.
|
||||
///
|
||||
/// When a prompt is set the system also prints out a confirmation after
|
||||
/// the selection.
|
||||
pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut MultiSelect<'a> {
|
||||
self.prompt = Some(prompt.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables user interaction and returns the result.
|
||||
///
|
||||
/// The user can select the items with the space bar and on enter
|
||||
/// the selected items will be returned.
|
||||
pub fn interact(&self) -> io::Result<Vec<usize>> {
|
||||
self.interact_on(&Term::stderr())
|
||||
}
|
||||
|
||||
/// Like [interact](#method.interact) but allows a specific terminal to be set.
|
||||
pub fn interact_on(&self, term: &Term) -> io::Result<Vec<usize>> {
|
||||
let mut page = 0;
|
||||
|
||||
if self.items.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Empty list of items given to `MultiSelect`",
|
||||
));
|
||||
}
|
||||
|
||||
let capacity = if self.paged {
|
||||
term.size().0 as usize - 1
|
||||
} else {
|
||||
self.items.len()
|
||||
};
|
||||
|
||||
let pages = (self.items.len() as f64 / capacity as f64).ceil() as usize;
|
||||
|
||||
let mut render = TermThemeRenderer::new(term, self.theme);
|
||||
let mut sel = 0;
|
||||
|
||||
if let Some(ref prompt) = self.prompt {
|
||||
render.multi_select_prompt(prompt)?;
|
||||
}
|
||||
|
||||
let mut size_vec = Vec::new();
|
||||
|
||||
for items in self
|
||||
.items
|
||||
.iter()
|
||||
.flat_map(|i| i.split('\n'))
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
let size = &items.len();
|
||||
size_vec.push(*size);
|
||||
}
|
||||
|
||||
let mut checked: Vec<bool> = self.defaults.clone();
|
||||
|
||||
loop {
|
||||
for (idx, item) in self
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(page * capacity)
|
||||
.take(capacity)
|
||||
{
|
||||
render.multi_select_prompt_item(item, checked[idx], sel == idx)?;
|
||||
}
|
||||
|
||||
term.hide_cursor()?;
|
||||
term.flush()?;
|
||||
|
||||
match term.read_key()? {
|
||||
Key::ArrowDown | Key::Char('j') => {
|
||||
if sel == !0 {
|
||||
sel = 0;
|
||||
} else {
|
||||
sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize;
|
||||
}
|
||||
}
|
||||
Key::ArrowUp | Key::Char('k') => {
|
||||
if sel == !0 {
|
||||
sel = self.items.len() - 1;
|
||||
} else {
|
||||
sel = ((sel as i64 - 1 + self.items.len() as i64) % (self.items.len() as i64)) as usize;
|
||||
}
|
||||
}
|
||||
Key::ArrowLeft | Key::Char('h') => {
|
||||
if self.paged {
|
||||
if page == 0 {
|
||||
page = pages - 1;
|
||||
} else {
|
||||
page -= 1;
|
||||
}
|
||||
|
||||
sel = page * capacity;
|
||||
}
|
||||
}
|
||||
Key::ArrowRight | Key::Char('l') => {
|
||||
if self.paged {
|
||||
if page == pages - 1 {
|
||||
page = 0;
|
||||
} else {
|
||||
page += 1;
|
||||
}
|
||||
|
||||
sel = page * capacity;
|
||||
}
|
||||
}
|
||||
Key::Char(' ') => {
|
||||
checked[sel] = !checked[sel];
|
||||
}
|
||||
Key::Escape => {
|
||||
if self.clear {
|
||||
render.clear()?;
|
||||
}
|
||||
|
||||
if let Some(ref prompt) = self.prompt {
|
||||
render.multi_select_prompt_selection(prompt, &[][..])?;
|
||||
}
|
||||
|
||||
term.show_cursor()?;
|
||||
term.flush()?;
|
||||
|
||||
return Ok(
|
||||
self
|
||||
.defaults
|
||||
.clone()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, checked)| if checked { Some(idx) } else { None })
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
Key::Enter => {
|
||||
if self.clear {
|
||||
render.clear()?;
|
||||
}
|
||||
|
||||
if let Some(ref prompt) = self.prompt {
|
||||
let selections: Vec<_> = checked
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, &checked)| {
|
||||
if checked {
|
||||
Some(self.items[idx].as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
render.multi_select_prompt_selection(prompt, &selections[..])?;
|
||||
}
|
||||
|
||||
term.show_cursor()?;
|
||||
term.flush()?;
|
||||
|
||||
return Ok(
|
||||
checked
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, checked)| if checked { Some(idx) } else { None })
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if sel < page * capacity || sel >= (page + 1) * capacity {
|
||||
page = sel / capacity;
|
||||
}
|
||||
|
||||
render.clear_preserve_prompt(&size_vec)?;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::io;
|
||||
|
||||
use super::super::theme::{SimpleTheme, TermThemeRenderer, Theme};
|
||||
|
||||
use crate::console::Term;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
/// Renders a password input prompt.
|
||||
///
|
||||
/// ## Example usage
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # fn test() -> Result<(), Box<std::error::Error>> {
|
||||
/// use dialoguer::Password;
|
||||
///
|
||||
/// let password = Password::new().with_prompt("New Password")
|
||||
/// .with_confirmation("Confirm password", "Passwords mismatching")
|
||||
/// .interact()?;
|
||||
/// println!("Length of the password is: {}", password.len());
|
||||
/// # Ok(()) } fn main() { test().unwrap(); }
|
||||
/// ```
|
||||
pub struct Password<'a> {
|
||||
prompt: String,
|
||||
theme: &'a dyn Theme,
|
||||
allow_empty_password: bool,
|
||||
confirmation_prompt: Option<(String, String)>,
|
||||
}
|
||||
|
||||
impl<'a> Default for Password<'a> {
|
||||
fn default() -> Password<'a> {
|
||||
Password::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Password<'a> {
|
||||
/// Creates a password input prompt.
|
||||
pub fn new() -> Password<'static> {
|
||||
Password::with_theme(&SimpleTheme)
|
||||
}
|
||||
|
||||
/// Creates a password input prompt with a specific theme.
|
||||
pub fn with_theme(theme: &'a dyn Theme) -> Password<'a> {
|
||||
Password {
|
||||
prompt: "".into(),
|
||||
theme,
|
||||
allow_empty_password: false,
|
||||
confirmation_prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the password input prompt.
|
||||
pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Password<'a> {
|
||||
self.prompt = prompt.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables confirmation prompting.
|
||||
pub fn with_confirmation<A, B>(&mut self, prompt: A, mismatch_err: B) -> &mut Password<'a>
|
||||
where
|
||||
A: Into<String>,
|
||||
B: Into<String>,
|
||||
{
|
||||
self.confirmation_prompt = Some((prompt.into(), mismatch_err.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Allows/Disables empty password.
|
||||
///
|
||||
/// By default this setting is set to false (i.e. password is not empty).
|
||||
pub fn allow_empty_password(&mut self, allow_empty_password: bool) -> &mut Password<'a> {
|
||||
self.allow_empty_password = allow_empty_password;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables user interaction and returns the result.
|
||||
///
|
||||
/// If the user confirms the result is `true`, `false` otherwise.
|
||||
/// The dialog is rendered on stderr.
|
||||
pub fn interact(&self) -> io::Result<String> {
|
||||
self.interact_on(&Term::stderr())
|
||||
}
|
||||
|
||||
/// Like `interact` but allows a specific terminal to be set.
|
||||
pub fn interact_on(&self, term: &Term) -> io::Result<String> {
|
||||
let mut render = TermThemeRenderer::new(term, self.theme);
|
||||
render.set_prompts_reset_height(false);
|
||||
|
||||
loop {
|
||||
let password = Zeroizing::new(self.prompt_password(&mut render, &self.prompt)?);
|
||||
|
||||
if let Some((ref prompt, ref err)) = self.confirmation_prompt {
|
||||
let pw2 = Zeroizing::new(self.prompt_password(&mut render, prompt)?);
|
||||
|
||||
if *password == *pw2 {
|
||||
render.clear()?;
|
||||
render.password_prompt_selection(&self.prompt)?;
|
||||
term.flush()?;
|
||||
return Ok((*password).clone());
|
||||
}
|
||||
|
||||
render.error(err)?;
|
||||
} else {
|
||||
render.clear()?;
|
||||
render.password_prompt_selection(&self.prompt)?;
|
||||
term.flush()?;
|
||||
|
||||
return Ok((*password).clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_password(
|
||||
&self,
|
||||
render: &mut TermThemeRenderer<'_>,
|
||||
prompt: &str,
|
||||
) -> io::Result<String> {
|
||||
loop {
|
||||
render.password_prompt(prompt)?;
|
||||
render.term().flush()?;
|
||||
|
||||
let input = render.term().read_secure_line()?;
|
||||
|
||||
render.add_line();
|
||||
|
||||
if !input.is_empty() || self.allow_empty_password {
|
||||
return Ok(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,418 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{io, ops::Rem};
|
||||
|
||||
use super::super::theme::{SimpleTheme, TermThemeRenderer, Theme};
|
||||
|
||||
use crate::console::{Key, Term};
|
||||
|
||||
/// Renders a select prompt.
|
||||
///
|
||||
/// User can select from one or more options.
|
||||
/// Interaction returns index of an item selected in the order they appear in `item` invocation or `items` slice.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::{
|
||||
/// Select,
|
||||
/// theme::ColorfulTheme
|
||||
/// };
|
||||
/// use console::Term;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let items = vec!["Item 1", "item 2"];
|
||||
/// let selection = Select::with_theme(&ColorfulTheme::default())
|
||||
/// .items(&items)
|
||||
/// .default(0)
|
||||
/// .interact_on_opt(&Term::stderr())?;
|
||||
///
|
||||
/// match selection {
|
||||
/// Some(index) => println!("User selected item : {}", items[index]),
|
||||
/// None => println!("User did not select anything")
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Select<'a> {
|
||||
default: usize,
|
||||
items: Vec<String>,
|
||||
prompt: Option<String>,
|
||||
clear: bool,
|
||||
theme: &'a dyn Theme,
|
||||
paged: bool,
|
||||
}
|
||||
|
||||
impl<'a> Default for Select<'a> {
|
||||
fn default() -> Select<'a> {
|
||||
Select::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Select<'a> {
|
||||
/// Creates a select prompt builder with default theme.
|
||||
pub fn new() -> Select<'static> {
|
||||
Select::with_theme(&SimpleTheme)
|
||||
}
|
||||
|
||||
/// Creates a select prompt builder with a specific theme.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::{
|
||||
/// Select,
|
||||
/// theme::ColorfulTheme
|
||||
/// };
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let selection = Select::with_theme(&ColorfulTheme::default())
|
||||
/// .item("Option A")
|
||||
/// .item("Option B")
|
||||
/// .interact()?;
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn with_theme(theme: &'a dyn Theme) -> Select<'a> {
|
||||
Select {
|
||||
default: !0,
|
||||
items: vec![],
|
||||
prompt: None,
|
||||
clear: true,
|
||||
theme,
|
||||
paged: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables or disables paging
|
||||
///
|
||||
/// Paging is disabled by default
|
||||
pub fn paged(&mut self, val: bool) -> &mut Select<'a> {
|
||||
self.paged = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Indicates whether select menu should be erased from the screen after interaction.
|
||||
///
|
||||
/// The default is to clear the menu.
|
||||
pub fn clear(&mut self, val: bool) -> &mut Select<'a> {
|
||||
self.clear = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets initial selected element when select menu is rendered
|
||||
///
|
||||
/// Element is indicated by the index at which it appears in `item` method invocation or `items` slice.
|
||||
pub fn default(&mut self, val: usize) -> &mut Select<'a> {
|
||||
self.default = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a single item to the selector.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::Select;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let selection: usize = Select::new()
|
||||
/// .item("Item 1")
|
||||
/// .item("Item 2")
|
||||
/// .interact()?;
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn item<T: ToString>(&mut self, item: T) -> &mut Select<'a> {
|
||||
self.items.push(item.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds multiple items to the selector.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::Select;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let items = vec!["Item 1", "Item 2"];
|
||||
/// let selection: usize = Select::new()
|
||||
/// .items(&items)
|
||||
/// .interact()?;
|
||||
///
|
||||
/// println!("{}", items[selection]);
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut Select<'a> {
|
||||
for item in items {
|
||||
self.items.push(item.to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the select prompt.
|
||||
///
|
||||
/// When a prompt is set the system also prints out a confirmation after
|
||||
/// the selection.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::Select;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let selection = Select::new()
|
||||
/// .with_prompt("Which option do you prefer?")
|
||||
/// .item("Option A")
|
||||
/// .item("Option B")
|
||||
/// .interact()?;
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Select<'a> {
|
||||
self.prompt = Some(prompt.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables user interaction and returns the result.
|
||||
///
|
||||
/// Similar to [interact_on](#method.interact_on) except for the fact that it does not allow selection of the terminal.
|
||||
/// The dialog is rendered on stderr.
|
||||
/// Result contains index of a selected item.
|
||||
pub fn interact(&self) -> io::Result<usize> {
|
||||
self.interact_on(&Term::stderr())
|
||||
}
|
||||
|
||||
/// Enables user interaction and returns the result.
|
||||
///
|
||||
/// This method is similar to [interact_on_opt](#method.interact_on_opt) except for the fact that it does not allow selection of the terminal.
|
||||
/// The dialog is rendered on stderr.
|
||||
/// Result contains `Some(index)` if user selected one of items or `None` if user cancelled with 'Esc' or 'q'.
|
||||
pub fn interact_opt(&self) -> io::Result<Option<usize>> {
|
||||
self.interact_on_opt(&Term::stderr())
|
||||
}
|
||||
|
||||
/// Like [interact](#method.interact) but allows a specific terminal to be set.
|
||||
///
|
||||
/// ## Examples
|
||||
///```rust,no_run
|
||||
/// use dialoguer::Select;
|
||||
/// use console::Term;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let selection = Select::new()
|
||||
/// .item("Option A")
|
||||
/// .item("Option B")
|
||||
/// .interact_on(&Term::stderr())?;
|
||||
///
|
||||
/// println!("User selected option at index {}", selection);
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
///```
|
||||
pub fn interact_on(&self, term: &Term) -> io::Result<usize> {
|
||||
self
|
||||
._interact_on(term, false)?
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))
|
||||
}
|
||||
|
||||
/// Like [interact_opt](#method.interact_opt) but allows a specific terminal to be set.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::Select;
|
||||
/// use console::Term;
|
||||
///
|
||||
/// fn main() -> std::io::Result<()> {
|
||||
/// let selection = Select::new()
|
||||
/// .item("Option A")
|
||||
/// .item("Option B")
|
||||
/// .interact_on_opt(&Term::stdout())?;
|
||||
///
|
||||
/// match selection {
|
||||
/// Some(position) => println!("User selected option at index {}", position),
|
||||
/// None => println!("User did not select anything")
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<usize>> {
|
||||
self._interact_on(term, true)
|
||||
}
|
||||
|
||||
/// Like `interact` but allows a specific terminal to be set.
|
||||
fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<usize>> {
|
||||
let mut page = 0;
|
||||
|
||||
if self.items.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Empty list of items given to `Select`",
|
||||
));
|
||||
}
|
||||
|
||||
let capacity = if self.paged {
|
||||
term.size().0 as usize - 1
|
||||
} else {
|
||||
self.items.len()
|
||||
};
|
||||
|
||||
let pages = (self.items.len() as f64 / capacity as f64).ceil() as usize;
|
||||
|
||||
let mut render = TermThemeRenderer::new(term, self.theme);
|
||||
let mut sel = self.default;
|
||||
|
||||
if let Some(ref prompt) = self.prompt {
|
||||
render.select_prompt(prompt)?;
|
||||
}
|
||||
|
||||
let mut size_vec = Vec::new();
|
||||
|
||||
for items in self
|
||||
.items
|
||||
.iter()
|
||||
.flat_map(|i| i.split('\n'))
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
let size = &items.len();
|
||||
size_vec.push(*size);
|
||||
}
|
||||
|
||||
loop {
|
||||
for (idx, item) in self
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(page * capacity)
|
||||
.take(capacity)
|
||||
{
|
||||
render.select_prompt_item(item, sel == idx)?;
|
||||
}
|
||||
|
||||
term.hide_cursor()?;
|
||||
term.flush()?;
|
||||
|
||||
match term.read_key()? {
|
||||
Key::ArrowDown | Key::Char('j') => {
|
||||
if sel == !0 {
|
||||
sel = 0;
|
||||
} else {
|
||||
sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize;
|
||||
}
|
||||
}
|
||||
Key::Escape | Key::Char('q') => {
|
||||
if allow_quit {
|
||||
if self.clear {
|
||||
term.clear_last_lines(self.items.len())?;
|
||||
term.show_cursor()?;
|
||||
term.flush()?;
|
||||
}
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Key::ArrowUp | Key::Char('k') => {
|
||||
if sel == !0 {
|
||||
sel = self.items.len() - 1;
|
||||
} else {
|
||||
sel = ((sel as i64 - 1 + self.items.len() as i64) % (self.items.len() as i64)) as usize;
|
||||
}
|
||||
}
|
||||
Key::ArrowLeft | Key::Char('h') => {
|
||||
if self.paged {
|
||||
if page == 0 {
|
||||
page = pages - 1;
|
||||
} else {
|
||||
page -= 1;
|
||||
}
|
||||
|
||||
sel = page * capacity;
|
||||
}
|
||||
}
|
||||
Key::ArrowRight | Key::Char('l') => {
|
||||
if self.paged {
|
||||
if page == pages - 1 {
|
||||
page = 0;
|
||||
} else {
|
||||
page += 1;
|
||||
}
|
||||
|
||||
sel = page * capacity;
|
||||
}
|
||||
}
|
||||
|
||||
Key::Enter | Key::Char(' ') if sel != !0 => {
|
||||
if self.clear {
|
||||
render.clear()?;
|
||||
}
|
||||
|
||||
if let Some(ref prompt) = self.prompt {
|
||||
render.select_prompt_selection(prompt, &self.items[sel])?;
|
||||
}
|
||||
|
||||
term.show_cursor()?;
|
||||
term.flush()?;
|
||||
|
||||
return Ok(Some(sel));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if sel != !0 && (sel < page * capacity || sel >= (page + 1) * capacity) {
|
||||
page = sel / capacity;
|
||||
}
|
||||
|
||||
render.clear_preserve_prompt(&size_vec)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_str() {
|
||||
let selections = &[
|
||||
"Ice Cream",
|
||||
"Vanilla Cupcake",
|
||||
"Chocolate Muffin",
|
||||
"A Pile of sweet, sweet mustard",
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
Select::new().default(0).items(&selections[..]).items,
|
||||
selections
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string() {
|
||||
let selections = vec!["a".to_string(), "b".to_string()];
|
||||
|
||||
assert_eq!(
|
||||
Select::new().default(0).items(&selections[..]).items,
|
||||
selections
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ref_str() {
|
||||
let a = "a";
|
||||
let b = "b";
|
||||
|
||||
let selections = &[a, b];
|
||||
|
||||
assert_eq!(
|
||||
Select::new().default(0).items(&selections[..]).items,
|
||||
selections
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,269 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{io, ops::Rem};
|
||||
|
||||
use super::super::theme::{SimpleTheme, TermThemeRenderer, Theme};
|
||||
|
||||
use crate::console::{Key, Term};
|
||||
|
||||
/// Renders a sort prompt.
|
||||
///
|
||||
/// Returns list of indices in original items list sorted according to user input.
|
||||
///
|
||||
/// ## Example usage
|
||||
/// ```rust,no_run
|
||||
/// use dialoguer::Sort;
|
||||
///
|
||||
/// # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let items_to_order = vec!["Item 1", "Item 2", "Item 3"];
|
||||
/// let ordered = Sort::new()
|
||||
/// .with_prompt("Order the items")
|
||||
/// .items(&items_to_order)
|
||||
/// .interact()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Sort<'a> {
|
||||
items: Vec<String>,
|
||||
prompt: Option<String>,
|
||||
clear: bool,
|
||||
theme: &'a dyn Theme,
|
||||
paged: bool,
|
||||
}
|
||||
|
||||
impl<'a> Default for Sort<'a> {
|
||||
fn default() -> Sort<'a> {
|
||||
Sort::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Sort<'a> {
|
||||
/// Creates a sort prompt.
|
||||
pub fn new() -> Sort<'static> {
|
||||
Sort::with_theme(&SimpleTheme)
|
||||
}
|
||||
|
||||
/// Creates a sort prompt with a specific theme.
|
||||
pub fn with_theme(theme: &'a dyn Theme) -> Sort<'a> {
|
||||
Sort {
|
||||
items: vec![],
|
||||
clear: true,
|
||||
prompt: None,
|
||||
theme,
|
||||
paged: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables or disables paging
|
||||
pub fn paged(&mut self, val: bool) -> &mut Sort<'a> {
|
||||
self.paged = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the clear behavior of the menu.
|
||||
///
|
||||
/// The default is to clear the menu after user interaction.
|
||||
pub fn clear(&mut self, val: bool) -> &mut Sort<'a> {
|
||||
self.clear = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a single item to the selector.
|
||||
pub fn item<T: ToString>(&mut self, item: T) -> &mut Sort<'a> {
|
||||
self.items.push(item.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds multiple items to the selector.
|
||||
pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut Sort<'a> {
|
||||
for item in items {
|
||||
self.items.push(item.to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Prefaces the menu with a prompt.
|
||||
///
|
||||
/// When a prompt is set the system also prints out a confirmation after
|
||||
/// the selection.
|
||||
pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Sort<'a> {
|
||||
self.prompt = Some(prompt.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables user interaction and returns the result.
|
||||
///
|
||||
/// The user can order the items with the space bar and the arrows.
|
||||
/// On enter the ordered list will be returned.
|
||||
pub fn interact(&self) -> io::Result<Vec<usize>> {
|
||||
self.interact_on(&Term::stderr())
|
||||
}
|
||||
|
||||
/// Like [interact](#method.interact) but allows a specific terminal to be set.
|
||||
pub fn interact_on(&self, term: &Term) -> io::Result<Vec<usize>> {
|
||||
let mut page = 0;
|
||||
|
||||
if self.items.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Empty list of items given to `Sort`",
|
||||
));
|
||||
}
|
||||
|
||||
let capacity = if self.paged {
|
||||
term.size().0 as usize - 1
|
||||
} else {
|
||||
self.items.len()
|
||||
};
|
||||
|
||||
let pages = (self.items.len() as f64 / capacity as f64).ceil() as usize;
|
||||
|
||||
let mut render = TermThemeRenderer::new(term, self.theme);
|
||||
let mut sel = 0;
|
||||
|
||||
if let Some(ref prompt) = self.prompt {
|
||||
render.sort_prompt(prompt)?;
|
||||
}
|
||||
|
||||
let mut size_vec = Vec::new();
|
||||
|
||||
for items in self.items.iter().as_slice() {
|
||||
let size = &items.len();
|
||||
size_vec.push(*size);
|
||||
}
|
||||
|
||||
let mut order: Vec<_> = (0..self.items.len()).collect();
|
||||
let mut checked: bool = false;
|
||||
|
||||
loop {
|
||||
for (idx, item) in order
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(page * capacity)
|
||||
.take(capacity)
|
||||
{
|
||||
render.sort_prompt_item(&self.items[*item], checked, sel == idx)?;
|
||||
}
|
||||
|
||||
term.hide_cursor()?;
|
||||
term.flush()?;
|
||||
|
||||
match term.read_key()? {
|
||||
Key::ArrowDown | Key::Char('j') => {
|
||||
let old_sel = sel;
|
||||
|
||||
if sel == !0 {
|
||||
sel = 0;
|
||||
} else {
|
||||
sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize;
|
||||
}
|
||||
|
||||
if checked && old_sel != sel {
|
||||
order.swap(old_sel, sel);
|
||||
}
|
||||
}
|
||||
Key::ArrowUp | Key::Char('k') => {
|
||||
let old_sel = sel;
|
||||
|
||||
if sel == !0 {
|
||||
sel = self.items.len() - 1;
|
||||
} else {
|
||||
sel = ((sel as i64 - 1 + self.items.len() as i64) % (self.items.len() as i64)) as usize;
|
||||
}
|
||||
|
||||
if checked && old_sel != sel {
|
||||
order.swap(old_sel, sel);
|
||||
}
|
||||
}
|
||||
Key::ArrowLeft | Key::Char('h') => {
|
||||
if self.paged {
|
||||
let old_sel = sel;
|
||||
let old_page = page;
|
||||
|
||||
if page == 0 {
|
||||
page = pages - 1;
|
||||
} else {
|
||||
page -= 1;
|
||||
}
|
||||
|
||||
sel = page * capacity;
|
||||
|
||||
if checked {
|
||||
let indexes: Vec<_> = if old_page == 0 {
|
||||
let indexes1: Vec<_> = (0..=old_sel).rev().collect();
|
||||
let indexes2: Vec<_> = (sel..self.items.len()).rev().collect();
|
||||
[indexes1, indexes2].concat()
|
||||
} else {
|
||||
(sel..=old_sel).rev().collect()
|
||||
};
|
||||
|
||||
for index in 0..(indexes.len() - 1) {
|
||||
order.swap(indexes[index], indexes[index + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::ArrowRight | Key::Char('l') => {
|
||||
if self.paged {
|
||||
let old_sel = sel;
|
||||
let old_page = page;
|
||||
|
||||
if page == pages - 1 {
|
||||
page = 0;
|
||||
} else {
|
||||
page += 1;
|
||||
}
|
||||
|
||||
sel = page * capacity;
|
||||
|
||||
if checked {
|
||||
let indexes: Vec<_> = if old_page == pages - 1 {
|
||||
let indexes1: Vec<_> = (old_sel..self.items.len()).collect();
|
||||
let indexes2: Vec<_> = vec![0];
|
||||
[indexes1, indexes2].concat()
|
||||
} else {
|
||||
(old_sel..=sel).collect()
|
||||
};
|
||||
|
||||
for index in 0..(indexes.len() - 1) {
|
||||
order.swap(indexes[index], indexes[index + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::Char(' ') => {
|
||||
checked = !checked;
|
||||
}
|
||||
// TODO: Key::Escape
|
||||
Key::Enter => {
|
||||
if self.clear {
|
||||
render.clear()?;
|
||||
}
|
||||
|
||||
if let Some(ref prompt) = self.prompt {
|
||||
let list: Vec<_> = order
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(_, item)| self.items[*item].as_str())
|
||||
.collect();
|
||||
render.sort_prompt_selection(prompt, &list[..])?;
|
||||
}
|
||||
|
||||
term.show_cursor()?;
|
||||
term.flush()?;
|
||||
|
||||
return Ok(order);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if sel < page * capacity || sel >= (page + 1) * capacity {
|
||||
page = sel / capacity;
|
||||
}
|
||||
|
||||
render.clear_preserve_prompt(&size_vec)?;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,726 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Customizes the rendering of the elements.
|
||||
use std::{fmt, io};
|
||||
|
||||
use crate::console::{style, Style, StyledObject, Term};
|
||||
|
||||
/// Implements a theme for dialoguer.
|
||||
pub trait Theme {
|
||||
/// Formats a prompt.
|
||||
#[inline]
|
||||
fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
|
||||
write!(f, "{}:", prompt)
|
||||
}
|
||||
|
||||
/// Formats out an error.
|
||||
#[inline]
|
||||
fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result {
|
||||
write!(f, "error: {}", err)
|
||||
}
|
||||
|
||||
/// Formats a confirm prompt.
|
||||
fn format_confirm_prompt(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
default: Option<bool>,
|
||||
) -> fmt::Result {
|
||||
if !prompt.is_empty() {
|
||||
write!(f, "{} ", &prompt)?;
|
||||
}
|
||||
match default {
|
||||
None => write!(f, "[y/n] ")?,
|
||||
Some(true) => write!(f, "[Y/n] ")?,
|
||||
Some(false) => write!(f, "[y/N] ")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Formats a confirm prompt after selection.
|
||||
fn format_confirm_prompt_selection(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
selection: Option<bool>,
|
||||
) -> fmt::Result {
|
||||
let selection = selection.map(|b| if b { "yes" } else { "no" });
|
||||
|
||||
match selection {
|
||||
Some(selection) if prompt.is_empty() => {
|
||||
write!(f, "{}", selection)
|
||||
}
|
||||
Some(selection) => {
|
||||
write!(f, "{} {}", &prompt, selection)
|
||||
}
|
||||
None if prompt.is_empty() => Ok(()),
|
||||
None => {
|
||||
write!(f, "{}", &prompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an input prompt.
|
||||
fn format_input_prompt(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
default: Option<&str>,
|
||||
) -> fmt::Result {
|
||||
match default {
|
||||
Some(default) if prompt.is_empty() => write!(f, "[{}]: ", default),
|
||||
Some(default) => write!(f, "{} [{}]: ", prompt, default),
|
||||
None => write!(f, "{}: ", prompt),
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an input prompt after selection.
|
||||
#[inline]
|
||||
fn format_input_prompt_selection(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
sel: &str,
|
||||
) -> fmt::Result {
|
||||
write!(f, "{}: {}", prompt, sel)
|
||||
}
|
||||
|
||||
/// Formats a password prompt.
|
||||
#[inline]
|
||||
fn format_password_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
|
||||
self.format_input_prompt(f, prompt, None)
|
||||
}
|
||||
|
||||
/// Formats a password prompt after selection.
|
||||
#[inline]
|
||||
fn format_password_prompt_selection(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
|
||||
self.format_input_prompt_selection(f, prompt, "[hidden]")
|
||||
}
|
||||
|
||||
/// Formats a select prompt.
|
||||
#[inline]
|
||||
fn format_select_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
|
||||
self.format_prompt(f, prompt)
|
||||
}
|
||||
|
||||
/// Formats a select prompt after selection.
|
||||
#[inline]
|
||||
fn format_select_prompt_selection(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
sel: &str,
|
||||
) -> fmt::Result {
|
||||
self.format_input_prompt_selection(f, prompt, sel)
|
||||
}
|
||||
|
||||
/// Formats a multi select prompt.
|
||||
#[inline]
|
||||
fn format_multi_select_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
|
||||
self.format_prompt(f, prompt)
|
||||
}
|
||||
|
||||
/// Formats a sort prompt.
|
||||
#[inline]
|
||||
fn format_sort_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
|
||||
self.format_prompt(f, prompt)
|
||||
}
|
||||
|
||||
/// Formats a multi_select prompt after selection.
|
||||
fn format_multi_select_prompt_selection(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
selections: &[&str],
|
||||
) -> fmt::Result {
|
||||
write!(f, "{}: ", prompt)?;
|
||||
for (idx, sel) in selections.iter().enumerate() {
|
||||
write!(f, "{}{}", if idx == 0 { "" } else { ", " }, sel)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Formats a sort prompt after selection.
|
||||
#[inline]
|
||||
fn format_sort_prompt_selection(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
selections: &[&str],
|
||||
) -> fmt::Result {
|
||||
self.format_multi_select_prompt_selection(f, prompt, selections)
|
||||
}
|
||||
|
||||
/// Formats a select prompt item.
|
||||
fn format_select_prompt_item(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
text: &str,
|
||||
active: bool,
|
||||
) -> fmt::Result {
|
||||
write!(f, "{} {}", if active { ">" } else { " " }, text)
|
||||
}
|
||||
|
||||
/// Formats a multi select prompt item.
|
||||
fn format_multi_select_prompt_item(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
text: &str,
|
||||
checked: bool,
|
||||
active: bool,
|
||||
) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {}",
|
||||
match (checked, active) {
|
||||
(true, true) => "> [x]",
|
||||
(true, false) => " [x]",
|
||||
(false, true) => "> [ ]",
|
||||
(false, false) => " [ ]",
|
||||
},
|
||||
text
|
||||
)
|
||||
}
|
||||
|
||||
/// Formats a sort prompt item.
|
||||
fn format_sort_prompt_item(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
text: &str,
|
||||
picked: bool,
|
||||
active: bool,
|
||||
) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {}",
|
||||
match (picked, active) {
|
||||
(true, true) => "> [x]",
|
||||
(false, true) => "> [ ]",
|
||||
(_, false) => " [ ]",
|
||||
},
|
||||
text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default theme.
|
||||
pub struct SimpleTheme;
|
||||
|
||||
impl Theme for SimpleTheme {}
|
||||
|
||||
/// A colorful theme
|
||||
pub struct ColorfulTheme {
|
||||
/// The style for default values
|
||||
pub defaults_style: Style,
|
||||
/// The style for prompt
|
||||
pub prompt_style: Style,
|
||||
/// Prompt prefix value and style
|
||||
pub prompt_prefix: StyledObject<String>,
|
||||
/// Prompt suffix value and style
|
||||
pub prompt_suffix: StyledObject<String>,
|
||||
/// Prompt on success prefix value and style
|
||||
pub success_prefix: StyledObject<String>,
|
||||
/// Prompt on success suffix value and style
|
||||
pub success_suffix: StyledObject<String>,
|
||||
/// Error prefix value and style
|
||||
pub error_prefix: StyledObject<String>,
|
||||
/// The style for error message
|
||||
pub error_style: Style,
|
||||
/// The style for hints
|
||||
pub hint_style: Style,
|
||||
/// The style for values on prompt success
|
||||
pub values_style: Style,
|
||||
/// The style for active items
|
||||
pub active_item_style: Style,
|
||||
/// The style for inactive items
|
||||
pub inactive_item_style: Style,
|
||||
/// Active item in select prefix value and style
|
||||
pub active_item_prefix: StyledObject<String>,
|
||||
/// Inctive item in select prefix value and style
|
||||
pub inactive_item_prefix: StyledObject<String>,
|
||||
/// Checked item in multi select prefix value and style
|
||||
pub checked_item_prefix: StyledObject<String>,
|
||||
/// Unchecked item in multi select prefix value and style
|
||||
pub unchecked_item_prefix: StyledObject<String>,
|
||||
/// Picked item in sort prefix value and style
|
||||
pub picked_item_prefix: StyledObject<String>,
|
||||
/// Unpicked item in sort prefix value and style
|
||||
pub unpicked_item_prefix: StyledObject<String>,
|
||||
/// Show the selections from certain prompts inline
|
||||
pub inline_selections: bool,
|
||||
}
|
||||
|
||||
impl Default for ColorfulTheme {
|
||||
fn default() -> ColorfulTheme {
|
||||
ColorfulTheme {
|
||||
defaults_style: Style::new().for_stderr().cyan(),
|
||||
prompt_style: Style::new().for_stderr().bold(),
|
||||
prompt_prefix: style("?".to_string()).for_stderr().yellow(),
|
||||
prompt_suffix: style("›".to_string()).for_stderr().black().bright(),
|
||||
success_prefix: style("✔".to_string()).for_stderr().green(),
|
||||
success_suffix: style("·".to_string()).for_stderr().black().bright(),
|
||||
error_prefix: style("✘".to_string()).for_stderr().red(),
|
||||
error_style: Style::new().for_stderr().red(),
|
||||
hint_style: Style::new().for_stderr().black().bright(),
|
||||
values_style: Style::new().for_stderr().green(),
|
||||
active_item_style: Style::new().for_stderr().cyan(),
|
||||
inactive_item_style: Style::new().for_stderr(),
|
||||
active_item_prefix: style("❯".to_string()).for_stderr().green(),
|
||||
inactive_item_prefix: style(" ".to_string()).for_stderr(),
|
||||
checked_item_prefix: style("✔".to_string()).for_stderr().green(),
|
||||
unchecked_item_prefix: style("✔".to_string()).for_stderr().black(),
|
||||
picked_item_prefix: style("❯".to_string()).for_stderr().green(),
|
||||
unpicked_item_prefix: style(" ".to_string()).for_stderr(),
|
||||
inline_selections: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme for ColorfulTheme {
|
||||
/// Formats a prompt.
|
||||
fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
|
||||
if !prompt.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
"{} {} ",
|
||||
&self.prompt_prefix,
|
||||
self.prompt_style.apply_to(prompt)
|
||||
)?;
|
||||
}
|
||||
|
||||
write!(f, "{}", &self.prompt_suffix)
|
||||
}
|
||||
|
||||
/// Formats an error
|
||||
fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {}",
|
||||
&self.error_prefix,
|
||||
self.error_style.apply_to(err)
|
||||
)
|
||||
}
|
||||
|
||||
/// Formats an input prompt.
|
||||
fn format_input_prompt(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
default: Option<&str>,
|
||||
) -> fmt::Result {
|
||||
if !prompt.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
"{} {} ",
|
||||
&self.prompt_prefix,
|
||||
self.prompt_style.apply_to(prompt)
|
||||
)?;
|
||||
}
|
||||
|
||||
match default {
|
||||
Some(default) => write!(
|
||||
f,
|
||||
"{} {} ",
|
||||
self.hint_style.apply_to(&format!("({})", default)),
|
||||
&self.prompt_suffix
|
||||
),
|
||||
None => write!(f, "{} ", &self.prompt_suffix),
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a confirm prompt.
|
||||
fn format_confirm_prompt(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
default: Option<bool>,
|
||||
) -> fmt::Result {
|
||||
if !prompt.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
"{} {} ",
|
||||
&self.prompt_prefix,
|
||||
self.prompt_style.apply_to(prompt)
|
||||
)?;
|
||||
}
|
||||
|
||||
match default {
|
||||
None => write!(
|
||||
f,
|
||||
"{} {}",
|
||||
self.hint_style.apply_to("(y/n)"),
|
||||
&self.prompt_suffix
|
||||
),
|
||||
Some(true) => write!(
|
||||
f,
|
||||
"{} {} {}",
|
||||
self.hint_style.apply_to("(y/n)"),
|
||||
&self.prompt_suffix,
|
||||
self.defaults_style.apply_to("yes")
|
||||
),
|
||||
Some(false) => write!(
|
||||
f,
|
||||
"{} {} {}",
|
||||
self.hint_style.apply_to("(y/n)"),
|
||||
&self.prompt_suffix,
|
||||
self.defaults_style.apply_to("no")
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a confirm prompt after selection.
|
||||
fn format_confirm_prompt_selection(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
selection: Option<bool>,
|
||||
) -> fmt::Result {
|
||||
if !prompt.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
"{} {} ",
|
||||
&self.success_prefix,
|
||||
self.prompt_style.apply_to(prompt)
|
||||
)?;
|
||||
}
|
||||
let selection = selection.map(|b| if b { "yes" } else { "no" });
|
||||
|
||||
match selection {
|
||||
Some(selection) => {
|
||||
write!(
|
||||
f,
|
||||
"{} {}",
|
||||
&self.success_suffix,
|
||||
self.values_style.apply_to(selection)
|
||||
)
|
||||
}
|
||||
None => {
|
||||
write!(f, "{}", &self.success_suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats an input prompt after selection.
|
||||
fn format_input_prompt_selection(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
sel: &str,
|
||||
) -> fmt::Result {
|
||||
if !prompt.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
"{} {} ",
|
||||
&self.success_prefix,
|
||||
self.prompt_style.apply_to(prompt)
|
||||
)?;
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{} {}",
|
||||
&self.success_suffix,
|
||||
self.values_style.apply_to(sel)
|
||||
)
|
||||
}
|
||||
|
||||
/// Formats a password prompt after selection.
|
||||
fn format_password_prompt_selection(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result {
|
||||
self.format_input_prompt_selection(f, prompt, "********")
|
||||
}
|
||||
|
||||
/// Formats a multi select prompt after selection.
|
||||
fn format_multi_select_prompt_selection(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
prompt: &str,
|
||||
selections: &[&str],
|
||||
) -> fmt::Result {
|
||||
if !prompt.is_empty() {
|
||||
write!(
|
||||
f,
|
||||
"{} {} ",
|
||||
&self.success_prefix,
|
||||
self.prompt_style.apply_to(prompt)
|
||||
)?;
|
||||
}
|
||||
|
||||
write!(f, "{} ", &self.success_suffix)?;
|
||||
|
||||
if self.inline_selections {
|
||||
for (idx, sel) in selections.iter().enumerate() {
|
||||
write!(
|
||||
f,
|
||||
"{}{}",
|
||||
if idx == 0 { "" } else { ", " },
|
||||
self.values_style.apply_to(sel)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Formats a select prompt item.
|
||||
fn format_select_prompt_item(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
text: &str,
|
||||
active: bool,
|
||||
) -> fmt::Result {
|
||||
let details = match active {
|
||||
true => (
|
||||
&self.active_item_prefix,
|
||||
self.active_item_style.apply_to(text),
|
||||
),
|
||||
false => (
|
||||
&self.inactive_item_prefix,
|
||||
self.inactive_item_style.apply_to(text),
|
||||
),
|
||||
};
|
||||
|
||||
write!(f, "{} {}", details.0, details.1)
|
||||
}
|
||||
|
||||
/// Formats a multi select prompt item.
|
||||
fn format_multi_select_prompt_item(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
text: &str,
|
||||
checked: bool,
|
||||
active: bool,
|
||||
) -> fmt::Result {
|
||||
let details = match (checked, active) {
|
||||
(true, true) => (
|
||||
&self.checked_item_prefix,
|
||||
self.active_item_style.apply_to(text),
|
||||
),
|
||||
(true, false) => (
|
||||
&self.checked_item_prefix,
|
||||
self.inactive_item_style.apply_to(text),
|
||||
),
|
||||
(false, true) => (
|
||||
&self.unchecked_item_prefix,
|
||||
self.active_item_style.apply_to(text),
|
||||
),
|
||||
(false, false) => (
|
||||
&self.unchecked_item_prefix,
|
||||
self.inactive_item_style.apply_to(text),
|
||||
),
|
||||
};
|
||||
|
||||
write!(f, "{} {}", details.0, details.1)
|
||||
}
|
||||
|
||||
/// Formats a sort prompt item.
|
||||
fn format_sort_prompt_item(
|
||||
&self,
|
||||
f: &mut dyn fmt::Write,
|
||||
text: &str,
|
||||
picked: bool,
|
||||
active: bool,
|
||||
) -> fmt::Result {
|
||||
let details = match (picked, active) {
|
||||
(true, true) => (
|
||||
&self.picked_item_prefix,
|
||||
self.active_item_style.apply_to(text),
|
||||
),
|
||||
(false, true) => (
|
||||
&self.unpicked_item_prefix,
|
||||
self.active_item_style.apply_to(text),
|
||||
),
|
||||
(_, false) => (
|
||||
&self.unpicked_item_prefix,
|
||||
self.inactive_item_style.apply_to(text),
|
||||
),
|
||||
};
|
||||
|
||||
write!(f, "{} {}", details.0, details.1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper struct to conveniently render a theme ot a term.
|
||||
pub(crate) struct TermThemeRenderer<'a> {
|
||||
term: &'a Term,
|
||||
theme: &'a dyn Theme,
|
||||
height: usize,
|
||||
prompt_height: usize,
|
||||
prompts_reset_height: bool,
|
||||
}
|
||||
|
||||
impl<'a> TermThemeRenderer<'a> {
|
||||
pub fn new(term: &'a Term, theme: &'a dyn Theme) -> TermThemeRenderer<'a> {
|
||||
TermThemeRenderer {
|
||||
term,
|
||||
theme,
|
||||
height: 0,
|
||||
prompt_height: 0,
|
||||
prompts_reset_height: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_prompts_reset_height(&mut self, val: bool) {
|
||||
self.prompts_reset_height = val;
|
||||
}
|
||||
|
||||
pub fn term(&self) -> &Term {
|
||||
self.term
|
||||
}
|
||||
|
||||
pub fn add_line(&mut self) {
|
||||
self.height += 1;
|
||||
}
|
||||
|
||||
fn write_formatted_str<
|
||||
F: FnOnce(&mut TermThemeRenderer<'_>, &mut dyn fmt::Write) -> fmt::Result,
|
||||
>(
|
||||
&mut self,
|
||||
f: F,
|
||||
) -> io::Result<()> {
|
||||
let mut buf = String::new();
|
||||
f(self, &mut buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||
self.height += buf.chars().filter(|&x| x == '\n').count();
|
||||
self.term.write_str(&buf)
|
||||
}
|
||||
|
||||
fn write_formatted_line<
|
||||
F: FnOnce(&mut TermThemeRenderer<'_>, &mut dyn fmt::Write) -> fmt::Result,
|
||||
>(
|
||||
&mut self,
|
||||
f: F,
|
||||
) -> io::Result<()> {
|
||||
let mut buf = String::new();
|
||||
f(self, &mut buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||
self.height += buf.chars().filter(|&x| x == '\n').count() + 1;
|
||||
self.term.write_line(&buf)
|
||||
}
|
||||
|
||||
fn write_formatted_prompt<
|
||||
F: FnOnce(&mut TermThemeRenderer<'_>, &mut dyn fmt::Write) -> fmt::Result,
|
||||
>(
|
||||
&mut self,
|
||||
f: F,
|
||||
) -> io::Result<()> {
|
||||
self.write_formatted_line(f)?;
|
||||
if self.prompts_reset_height {
|
||||
self.prompt_height = self.height;
|
||||
self.height = 0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn error(&mut self, err: &str) -> io::Result<()> {
|
||||
self.write_formatted_line(|this, buf| this.theme.format_error(buf, err))
|
||||
}
|
||||
|
||||
pub fn confirm_prompt(&mut self, prompt: &str, default: Option<bool>) -> io::Result<()> {
|
||||
self.write_formatted_str(|this, buf| this.theme.format_confirm_prompt(buf, prompt, default))
|
||||
}
|
||||
|
||||
pub fn confirm_prompt_selection(&mut self, prompt: &str, sel: Option<bool>) -> io::Result<()> {
|
||||
self.write_formatted_prompt(|this, buf| {
|
||||
this.theme.format_confirm_prompt_selection(buf, prompt, sel)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn input_prompt(&mut self, prompt: &str, default: Option<&str>) -> io::Result<()> {
|
||||
self.write_formatted_str(|this, buf| this.theme.format_input_prompt(buf, prompt, default))
|
||||
}
|
||||
|
||||
pub fn input_prompt_selection(&mut self, prompt: &str, sel: &str) -> io::Result<()> {
|
||||
self.write_formatted_prompt(|this, buf| {
|
||||
this.theme.format_input_prompt_selection(buf, prompt, sel)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn password_prompt(&mut self, prompt: &str) -> io::Result<()> {
|
||||
self.write_formatted_str(|this, buf| {
|
||||
write!(buf, "\r")?;
|
||||
this.theme.format_password_prompt(buf, prompt)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn password_prompt_selection(&mut self, prompt: &str) -> io::Result<()> {
|
||||
self
|
||||
.write_formatted_prompt(|this, buf| this.theme.format_password_prompt_selection(buf, prompt))
|
||||
}
|
||||
|
||||
pub fn select_prompt(&mut self, prompt: &str) -> io::Result<()> {
|
||||
self.write_formatted_prompt(|this, buf| this.theme.format_select_prompt(buf, prompt))
|
||||
}
|
||||
|
||||
pub fn select_prompt_selection(&mut self, prompt: &str, sel: &str) -> io::Result<()> {
|
||||
self.write_formatted_prompt(|this, buf| {
|
||||
this.theme.format_select_prompt_selection(buf, prompt, sel)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_prompt_item(&mut self, text: &str, active: bool) -> io::Result<()> {
|
||||
self.write_formatted_line(|this, buf| this.theme.format_select_prompt_item(buf, text, active))
|
||||
}
|
||||
|
||||
pub fn multi_select_prompt(&mut self, prompt: &str) -> io::Result<()> {
|
||||
self.write_formatted_prompt(|this, buf| this.theme.format_multi_select_prompt(buf, prompt))
|
||||
}
|
||||
|
||||
pub fn multi_select_prompt_selection(&mut self, prompt: &str, sel: &[&str]) -> io::Result<()> {
|
||||
self.write_formatted_prompt(|this, buf| {
|
||||
this
|
||||
.theme
|
||||
.format_multi_select_prompt_selection(buf, prompt, sel)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn multi_select_prompt_item(
|
||||
&mut self,
|
||||
text: &str,
|
||||
checked: bool,
|
||||
active: bool,
|
||||
) -> io::Result<()> {
|
||||
self.write_formatted_line(|this, buf| {
|
||||
this
|
||||
.theme
|
||||
.format_multi_select_prompt_item(buf, text, checked, active)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sort_prompt(&mut self, prompt: &str) -> io::Result<()> {
|
||||
self.write_formatted_prompt(|this, buf| this.theme.format_sort_prompt(buf, prompt))
|
||||
}
|
||||
|
||||
pub fn sort_prompt_selection(&mut self, prompt: &str, sel: &[&str]) -> io::Result<()> {
|
||||
self
|
||||
.write_formatted_prompt(|this, buf| this.theme.format_sort_prompt_selection(buf, prompt, sel))
|
||||
}
|
||||
|
||||
pub fn sort_prompt_item(&mut self, text: &str, picked: bool, active: bool) -> io::Result<()> {
|
||||
self.write_formatted_line(|this, buf| {
|
||||
this
|
||||
.theme
|
||||
.format_sort_prompt_item(buf, text, picked, active)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
self
|
||||
.term
|
||||
.clear_last_lines(self.height + self.prompt_height)?;
|
||||
self.height = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_preserve_prompt(&mut self, size_vec: &[usize]) -> io::Result<()> {
|
||||
let mut new_height = self.height;
|
||||
//Check each item size, increment on finding an overflow
|
||||
for size in size_vec {
|
||||
if *size > self.term.size().1 as usize {
|
||||
new_height += 1;
|
||||
}
|
||||
}
|
||||
self.term.clear_last_lines(new_height)?;
|
||||
self.height = 0;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Provides validation for text inputs
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
/// Trait for input validators.
|
||||
///
|
||||
/// A generic implementation for `Fn(&str) -> Result<(), E>` is provided
|
||||
/// to facilitate development.
|
||||
pub trait Validator<T> {
|
||||
type Err: Debug + Display;
|
||||
|
||||
/// Invoked with the value to validate.
|
||||
///
|
||||
/// If this produces `Ok(())` then the value is used and parsed, if
|
||||
/// an error is returned validation fails with that error.
|
||||
fn validate(&mut self, input: &T) -> Result<(), Self::Err>;
|
||||
}
|
||||
|
||||
impl<T, F: FnMut(&T) -> Result<(), E>, E: Debug + Display> Validator<T> for F {
|
||||
type Err = E;
|
||||
|
||||
fn validate(&mut self, input: &T) -> Result<(), Self::Err> {
|
||||
self(input)
|
||||
}
|
||||
}
|
|
@ -16,13 +16,6 @@ mod interface;
|
|||
mod plugin;
|
||||
mod sign;
|
||||
|
||||
// temporary fork from https://github.com/mitsuhiko/console until 0.14.1+ release
|
||||
#[allow(dead_code)]
|
||||
mod console;
|
||||
// temporary fork from https://github.com/mitsuhiko/dialoguer until 0.8.0+ release
|
||||
#[allow(dead_code)]
|
||||
mod dialoguer;
|
||||
|
||||
use helpers::framework::{infer_from_package_json as infer_framework, Framework};
|
||||
|
||||
use std::{env::current_dir, fs::read_to_string, path::PathBuf};
|
||||
|
|
Loading…
Reference in New Issue