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",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
@ -410,6 +425,18 @@ dependencies = [
|
||||||
"byteorder",
|
"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]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
@ -1933,6 +1960,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
"colored",
|
"colored",
|
||||||
|
"dialoguer",
|
||||||
"encode_unicode",
|
"encode_unicode",
|
||||||
"glob",
|
"glob",
|
||||||
"handlebars",
|
"handlebars",
|
||||||
|
|
|
@ -49,6 +49,7 @@ tempfile = "3"
|
||||||
zeroize = "1.4"
|
zeroize = "1.4"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
heck = "0.3"
|
heck = "0.3"
|
||||||
|
dialoguer = "0.9"
|
||||||
|
|
||||||
[target."cfg(windows)".dependencies]
|
[target."cfg(windows)".dependencies]
|
||||||
winapi = { version = "0.3", features = [ "winbase", "winuser", "consoleapi", "processenv", "wincon" ] }
|
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 plugin;
|
||||||
mod sign;
|
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 helpers::framework::{infer_from_package_json as infer_framework, Framework};
|
||||||
|
|
||||||
use std::{env::current_dir, fs::read_to_string, path::PathBuf};
|
use std::{env::current_dir, fs::read_to_string, path::PathBuf};
|
||||||
|
|
Loading…
Reference in New Issue