Merge pull request #159 from dyc3/crossterm
Switch to crossterm library for terminal ui stuff
This commit is contained in:
commit
c78dd00731
6 changed files with 255 additions and 127 deletions
58
Cargo.lock
generated
58
Cargo.lock
generated
|
@ -302,6 +302,32 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.23.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"futures-core",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-bigint"
|
name = "crypto-bigint"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
@ -1715,6 +1741,36 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
|
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
|
@ -1871,6 +1927,7 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"cookie 0.14.4",
|
"cookie 0.14.4",
|
||||||
|
"crossterm",
|
||||||
"dirs",
|
"dirs",
|
||||||
"hmac-sha1",
|
"hmac-sha1",
|
||||||
"lazy_static 1.4.0",
|
"lazy_static 1.4.0",
|
||||||
|
@ -1888,7 +1945,6 @@ dependencies = [
|
||||||
"stderrlog",
|
"stderrlog",
|
||||||
"steamguard",
|
"steamguard",
|
||||||
"tempdir",
|
"tempdir",
|
||||||
"termion",
|
|
||||||
"text_io",
|
"text_io",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|
|
@ -41,13 +41,13 @@ cookie = "0.14"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
uuid = { version = "0.8", features = ["v4"] }
|
uuid = { version = "0.8", features = ["v4"] }
|
||||||
termion = "1.5.6"
|
|
||||||
steamguard = { version ="^0.4.2", path = "./steamguard" }
|
steamguard = { version ="^0.4.2", path = "./steamguard" }
|
||||||
dirs = "3.0.2"
|
dirs = "3.0.2"
|
||||||
ring = "0.16.20"
|
ring = "0.16.20"
|
||||||
aes = "0.7.4"
|
aes = "0.7.4"
|
||||||
block-modes = "0.8.1"
|
block-modes = "0.8.1"
|
||||||
thiserror = "1.0.26"
|
thiserror = "1.0.26"
|
||||||
|
crossterm = { version = "0.23.2", features = ["event-stream"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempdir = "0.3"
|
tempdir = "0.3"
|
||||||
|
|
|
@ -99,6 +99,10 @@ impl FromStr for Verbosity {
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
#[clap(about = "Debug stuff, not useful for most users.")]
|
#[clap(about = "Debug stuff, not useful for most users.")]
|
||||||
pub(crate) struct ArgsDebug {
|
pub(crate) struct ArgsDebug {
|
||||||
|
#[clap(long, help = "Show a text prompt.")]
|
||||||
|
pub demo_prompt: bool,
|
||||||
|
#[clap(long, help = "Show a character prompt.")]
|
||||||
|
pub demo_prompt_char: bool,
|
||||||
#[clap(long, help = "Show an example confirmation menu using dummy data.")]
|
#[clap(long, help = "Show an example confirmation menu using dummy data.")]
|
||||||
pub demo_conf_menu: bool,
|
pub demo_conf_menu: bool,
|
||||||
}
|
}
|
||||||
|
|
19
src/demos.rs
19
src/demos.rs
|
@ -2,6 +2,22 @@ use crate::tui;
|
||||||
use log::*;
|
use log::*;
|
||||||
use steamguard::{Confirmation, ConfirmationType};
|
use steamguard::{Confirmation, ConfirmationType};
|
||||||
|
|
||||||
|
pub fn demo_prompt() {
|
||||||
|
print!("Prompt: ");
|
||||||
|
let result = tui::prompt();
|
||||||
|
println!("Result: {}", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn demo_prompt_char() {
|
||||||
|
println!("Showing prompt");
|
||||||
|
let result = tui::prompt_char("Continue?", "yn");
|
||||||
|
println!("Result: {}", result);
|
||||||
|
let result = tui::prompt_char("Continue?", "Yn");
|
||||||
|
println!("Result: {}", result);
|
||||||
|
let result = tui::prompt_char("Continue?", "yN");
|
||||||
|
println!("Result: {}", result);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn demo_confirmation_menu() {
|
pub fn demo_confirmation_menu() {
|
||||||
info!("showing demo menu");
|
info!("showing demo menu");
|
||||||
let (accept, deny) = tui::prompt_confirmation_menu(vec![
|
let (accept, deny) = tui::prompt_confirmation_menu(vec![
|
||||||
|
@ -33,6 +49,7 @@ pub fn demo_confirmation_menu() {
|
||||||
creator: 09870987,
|
creator: 09870987,
|
||||||
description: "example confirmation".into(),
|
description: "example confirmation".into(),
|
||||||
},
|
},
|
||||||
]);
|
])
|
||||||
|
.expect("confirmation menu demo failed");
|
||||||
println!("accept: {}, deny: {}", accept.len(), deny.len());
|
println!("accept: {}, deny: {}", accept.len(), deny.len());
|
||||||
}
|
}
|
||||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -1,5 +1,6 @@
|
||||||
extern crate rpassword;
|
extern crate rpassword;
|
||||||
use clap::{IntoApp, Parser};
|
use clap::{IntoApp, Parser};
|
||||||
|
use crossterm::tty::IsTty;
|
||||||
use log::*;
|
use log::*;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -28,7 +29,7 @@ mod cli;
|
||||||
mod demos;
|
mod demos;
|
||||||
mod encryption;
|
mod encryption;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod tui;
|
pub(crate) mod tui;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
std::process::exit(match run() {
|
std::process::exit(match run() {
|
||||||
|
@ -329,6 +330,12 @@ fn load_accounts_with_prompts(manifest: &mut accountmanager::Manifest) -> anyhow
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_subcmd_debug(args: cli::ArgsDebug) -> anyhow::Result<()> {
|
fn do_subcmd_debug(args: cli::ArgsDebug) -> anyhow::Result<()> {
|
||||||
|
if args.demo_prompt {
|
||||||
|
demos::demo_prompt();
|
||||||
|
}
|
||||||
|
if args.demo_prompt_char {
|
||||||
|
demos::demo_prompt_char();
|
||||||
|
}
|
||||||
if args.demo_conf_menu {
|
if args.demo_conf_menu {
|
||||||
demos::demo_confirmation_menu();
|
demos::demo_confirmation_menu();
|
||||||
}
|
}
|
||||||
|
@ -516,8 +523,8 @@ fn do_subcmd_trade(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if termion::is_tty(&stdout()) {
|
if stdout().is_tty() {
|
||||||
let (accept, deny) = tui::prompt_confirmation_menu(confirmations);
|
let (accept, deny) = tui::prompt_confirmation_menu(confirmations)?;
|
||||||
for conf in &accept {
|
for conf in &accept {
|
||||||
let result = account.accept_confirmation(conf);
|
let result = account.accept_confirmation(conf);
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
|
|
286
src/tui.rs
286
src/tui.rs
|
@ -1,14 +1,16 @@
|
||||||
|
use crossterm::{
|
||||||
|
cursor,
|
||||||
|
event::{Event, KeyCode, KeyEvent, KeyModifiers},
|
||||||
|
execute,
|
||||||
|
style::{Color, Print, PrintStyledContent, SetForegroundColor, Stylize},
|
||||||
|
terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
QueueableCommand,
|
||||||
|
};
|
||||||
use log::*;
|
use log::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::io::{Read, Write};
|
use std::io::{stdout, Write};
|
||||||
use steamguard::Confirmation;
|
use steamguard::Confirmation;
|
||||||
use termion::{
|
|
||||||
event::{Event, Key},
|
|
||||||
input::TermRead,
|
|
||||||
raw::IntoRawMode,
|
|
||||||
screen::AlternateScreen,
|
|
||||||
};
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CAPTCHA_VALID_CHARS: Regex =
|
static ref CAPTCHA_VALID_CHARS: Regex =
|
||||||
|
@ -38,16 +40,26 @@ fn test_validate_captcha_text() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prompt the user for text input.
|
/// Prompt the user for text input.
|
||||||
pub fn prompt() -> String {
|
pub(crate) fn prompt() -> String {
|
||||||
let mut text = String::new();
|
stdout().flush().unwrap();
|
||||||
let _ = std::io::stdout().flush();
|
|
||||||
std::io::stdin()
|
let mut line = String::new();
|
||||||
.read_line(&mut text)
|
while let Event::Key(KeyEvent { code, .. }) = crossterm::event::read().unwrap() {
|
||||||
.expect("Did not enter a correct string");
|
match code {
|
||||||
return String::from(text.strip_suffix('\n').unwrap());
|
KeyCode::Enter => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
line.push(c);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prompt_captcha_text(captcha_gid: &String) -> String {
|
pub(crate) fn prompt_captcha_text(captcha_gid: &String) -> String {
|
||||||
println!("Captcha required. Open this link in your web browser: https://steamcommunity.com/public/captcha.php?gid={}", captcha_gid);
|
println!("Captcha required. Open this link in your web browser: https://steamcommunity.com/public/captcha.php?gid={}", captcha_gid);
|
||||||
let mut captcha_text;
|
let mut captcha_text;
|
||||||
loop {
|
loop {
|
||||||
|
@ -64,11 +76,21 @@ pub fn prompt_captcha_text(captcha_gid: &String) -> String {
|
||||||
/// Prompt the user for a single character response. Useful for asking yes or no questions.
|
/// Prompt the user for a single character response. Useful for asking yes or no questions.
|
||||||
///
|
///
|
||||||
/// `chars` should be all lowercase characters, with at most 1 uppercase character. The uppercase character is the default answer if no answer is provided.
|
/// `chars` should be all lowercase characters, with at most 1 uppercase character. The uppercase character is the default answer if no answer is provided.
|
||||||
pub fn prompt_char(text: &str, chars: &str) -> char {
|
pub(crate) fn prompt_char(text: &str, chars: &str) -> char {
|
||||||
return prompt_char_impl(&mut std::io::stdin(), text, chars);
|
loop {
|
||||||
|
let _ = stdout().queue(Print(format!("{} [{}] ", text, chars)));
|
||||||
|
let _ = stdout().flush();
|
||||||
|
let input = prompt();
|
||||||
|
if let Ok(c) = prompt_char_impl(input, chars) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_char_impl(input: &mut impl Read, text: &str, chars: &str) -> char {
|
fn prompt_char_impl<T>(input: T, chars: &str) -> anyhow::Result<char>
|
||||||
|
where
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
let uppers = chars.replace(char::is_lowercase, "");
|
let uppers = chars.replace(char::is_lowercase, "");
|
||||||
if uppers.len() > 1 {
|
if uppers.len() > 1 {
|
||||||
panic!("Invalid chars for prompt_char. Maximum 1 uppercase letter is allowed.");
|
panic!("Invalid chars for prompt_char. Maximum 1 uppercase letter is allowed.");
|
||||||
|
@ -79,141 +101,162 @@ fn prompt_char_impl(input: &mut impl Read, text: &str, chars: &str) -> char {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
let answer: String = input.into().to_ascii_lowercase();
|
||||||
print!("{} [{}] ", text, chars);
|
|
||||||
let _ = std::io::stdout().flush();
|
|
||||||
let answer = input
|
|
||||||
.read_line()
|
|
||||||
.expect("Unable to read input")
|
|
||||||
.unwrap()
|
|
||||||
.to_ascii_lowercase();
|
|
||||||
if answer.len() == 0 {
|
|
||||||
if let Some(a) = default_answer {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
} else if answer.len() > 1 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let answer_char = answer.chars().collect::<Vec<char>>()[0];
|
if answer.len() == 0 {
|
||||||
if chars.to_ascii_lowercase().contains(answer_char) {
|
if let Some(a) = default_answer {
|
||||||
return answer_char;
|
return Ok(a);
|
||||||
|
} else {
|
||||||
|
bail!("no valid answer")
|
||||||
}
|
}
|
||||||
|
} else if answer.len() > 1 {
|
||||||
|
bail!("answer too long")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let answer_char = answer.chars().collect::<Vec<char>>()[0];
|
||||||
|
if chars.to_ascii_lowercase().contains(answer_char) {
|
||||||
|
return Ok(answer_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("no valid answer")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a tuple of (accepted, denied). Ignored confirmations are not included.
|
/// Returns a tuple of (accepted, denied). Ignored confirmations are not included.
|
||||||
pub fn prompt_confirmation_menu(
|
pub(crate) fn prompt_confirmation_menu(
|
||||||
confirmations: Vec<Confirmation>,
|
confirmations: Vec<Confirmation>,
|
||||||
) -> (Vec<Confirmation>, Vec<Confirmation>) {
|
) -> anyhow::Result<(Vec<Confirmation>, Vec<Confirmation>)> {
|
||||||
println!("press a key other than enter to show the menu.");
|
|
||||||
let mut to_accept_idx: HashSet<usize> = HashSet::new();
|
let mut to_accept_idx: HashSet<usize> = HashSet::new();
|
||||||
let mut to_deny_idx: HashSet<usize> = HashSet::new();
|
let mut to_deny_idx: HashSet<usize> = HashSet::new();
|
||||||
|
|
||||||
let mut screen = AlternateScreen::from(std::io::stdout().into_raw_mode().unwrap());
|
execute!(stdout(), EnterAlternateScreen)?;
|
||||||
let stdin = std::io::stdin();
|
crossterm::terminal::enable_raw_mode()?;
|
||||||
|
|
||||||
let mut selected_idx = 0;
|
let mut selected_idx = 0;
|
||||||
|
|
||||||
for c in stdin.events() {
|
loop {
|
||||||
match c.expect("could not get events") {
|
execute!(
|
||||||
Event::Key(Key::Char('a')) => {
|
stdout(),
|
||||||
|
Clear(ClearType::All),
|
||||||
|
cursor::MoveTo(1, 1),
|
||||||
|
PrintStyledContent(
|
||||||
|
"arrow keys to select, [a]ccept, [d]eny, [i]gnore, [enter] confirm choices\n\n"
|
||||||
|
.white()
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for i in 0..confirmations.len() {
|
||||||
|
stdout().queue(Print("\r"))?;
|
||||||
|
if selected_idx == i {
|
||||||
|
stdout().queue(SetForegroundColor(Color::Yellow))?;
|
||||||
|
stdout().queue(Print(" >"))?;
|
||||||
|
} else {
|
||||||
|
stdout().queue(SetForegroundColor(Color::White))?;
|
||||||
|
stdout().queue(Print(" "))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if to_accept_idx.contains(&i) {
|
||||||
|
stdout().queue(SetForegroundColor(Color::Green))?;
|
||||||
|
stdout().queue(Print("[a]"))?;
|
||||||
|
} else if to_deny_idx.contains(&i) {
|
||||||
|
stdout().queue(SetForegroundColor(Color::Red))?;
|
||||||
|
stdout().queue(Print("[d]"))?;
|
||||||
|
} else {
|
||||||
|
stdout().queue(Print("[ ]"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected_idx == i {
|
||||||
|
stdout().queue(SetForegroundColor(Color::Yellow))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout().queue(Print(format!(" {}\n", confirmations[i].description())))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout().flush()?;
|
||||||
|
|
||||||
|
match crossterm::event::read()? {
|
||||||
|
Event::Resize(_, _) => continue,
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('a'),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
to_accept_idx.insert(selected_idx);
|
to_accept_idx.insert(selected_idx);
|
||||||
to_deny_idx.remove(&selected_idx);
|
to_deny_idx.remove(&selected_idx);
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('d')) => {
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('d'),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
to_accept_idx.remove(&selected_idx);
|
to_accept_idx.remove(&selected_idx);
|
||||||
to_deny_idx.insert(selected_idx);
|
to_deny_idx.insert(selected_idx);
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('i')) => {
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('i'),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
to_accept_idx.remove(&selected_idx);
|
to_accept_idx.remove(&selected_idx);
|
||||||
to_deny_idx.remove(&selected_idx);
|
to_deny_idx.remove(&selected_idx);
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('A')) => {
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('A'),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
(0..confirmations.len()).for_each(|i| {
|
(0..confirmations.len()).for_each(|i| {
|
||||||
to_accept_idx.insert(i);
|
to_accept_idx.insert(i);
|
||||||
to_deny_idx.remove(&i);
|
to_deny_idx.remove(&i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('D')) => {
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('D'),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
(0..confirmations.len()).for_each(|i| {
|
(0..confirmations.len()).for_each(|i| {
|
||||||
to_accept_idx.remove(&i);
|
to_accept_idx.remove(&i);
|
||||||
to_deny_idx.insert(i);
|
to_deny_idx.insert(i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('I')) => {
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('I'),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
(0..confirmations.len()).for_each(|i| {
|
(0..confirmations.len()).for_each(|i| {
|
||||||
to_accept_idx.remove(&i);
|
to_accept_idx.remove(&i);
|
||||||
to_deny_idx.remove(&i);
|
to_deny_idx.remove(&i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Event::Key(Key::Up) if selected_idx > 0 => {
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Up, ..
|
||||||
|
}) if selected_idx > 0 => {
|
||||||
selected_idx -= 1;
|
selected_idx -= 1;
|
||||||
}
|
}
|
||||||
Event::Key(Key::Down) if selected_idx < confirmations.len() - 1 => {
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Down,
|
||||||
|
..
|
||||||
|
}) if selected_idx < confirmations.len() - 1 => {
|
||||||
selected_idx += 1;
|
selected_idx += 1;
|
||||||
}
|
}
|
||||||
Event::Key(Key::Char('\n')) => {
|
Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Enter,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Event::Key(Key::Esc) | Event::Key(Key::Ctrl('c')) => {
|
Event::Key(KeyEvent {
|
||||||
return (vec![], vec![]);
|
code: KeyCode::Esc, ..
|
||||||
|
})
|
||||||
|
| Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('c'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
}) => {
|
||||||
|
return Ok((vec![], vec![]));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(
|
|
||||||
screen,
|
|
||||||
"{}{}{}arrow keys to select, [a]ccept, [d]eny, [i]gnore, [enter] confirm choices\n\n",
|
|
||||||
termion::clear::All,
|
|
||||||
termion::cursor::Goto(1, 1),
|
|
||||||
termion::color::Fg(termion::color::White)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
for i in 0..confirmations.len() {
|
|
||||||
if selected_idx == i {
|
|
||||||
write!(
|
|
||||||
screen,
|
|
||||||
"\r{} >",
|
|
||||||
termion::color::Fg(termion::color::LightYellow)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
write!(screen, "\r{} ", termion::color::Fg(termion::color::White)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if to_accept_idx.contains(&i) {
|
|
||||||
write!(
|
|
||||||
screen,
|
|
||||||
"{}[a]",
|
|
||||||
termion::color::Fg(termion::color::LightGreen)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else if to_deny_idx.contains(&i) {
|
|
||||||
write!(
|
|
||||||
screen,
|
|
||||||
"{}[d]",
|
|
||||||
termion::color::Fg(termion::color::LightRed)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
write!(screen, "[ ]").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected_idx == i {
|
|
||||||
write!(
|
|
||||||
screen,
|
|
||||||
"{}",
|
|
||||||
termion::color::Fg(termion::color::LightYellow)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(screen, " {}\n", confirmations[i].description()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
execute!(stdout(), LeaveAlternateScreen)?;
|
||||||
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
|
|
||||||
|
return Ok((
|
||||||
to_accept_idx
|
to_accept_idx
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| confirmations[*i].clone())
|
.map(|i| confirmations[*i].clone())
|
||||||
|
@ -222,14 +265,18 @@ pub fn prompt_confirmation_menu(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| confirmations[*i].clone())
|
.map(|i| confirmations[*i].clone())
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pause() {
|
pub(crate) fn pause() {
|
||||||
println!("Press any key to continue...");
|
println!("Press any key to continue...");
|
||||||
let mut stdout = std::io::stdout().into_raw_mode().unwrap();
|
let _ = stdout().flush();
|
||||||
stdout.flush().unwrap();
|
loop {
|
||||||
std::io::stdin().events().next();
|
match crossterm::event::read().expect("could not read terminal events") {
|
||||||
|
Event::Key(_) => break,
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -238,36 +285,33 @@ mod prompt_char_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gives_answer() {
|
fn test_gives_answer() {
|
||||||
let inputs = ['y', '\n'].iter().collect::<String>();
|
let answer = prompt_char_impl("y", "yn").unwrap();
|
||||||
let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "yn");
|
|
||||||
assert_eq!(answer, 'y');
|
assert_eq!(answer, 'y');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gives_default() {
|
fn test_gives_default() {
|
||||||
let inputs = ['\n'].iter().collect::<String>();
|
let answer = prompt_char_impl("", "Yn").unwrap();
|
||||||
let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "Yn");
|
|
||||||
assert_eq!(answer, 'y');
|
assert_eq!(answer, 'y');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_not_give_default() {
|
fn test_should_not_give_default() {
|
||||||
let inputs = ['n', '\n'].iter().collect::<String>();
|
let answer = prompt_char_impl("n", "Yn").unwrap();
|
||||||
let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "Yn");
|
|
||||||
assert_eq!(answer, 'n');
|
assert_eq!(answer, 'n');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_not_give_invalid() {
|
fn test_should_not_give_invalid() {
|
||||||
let inputs = ['g', '\n', 'n', '\n'].iter().collect::<String>();
|
let answer = prompt_char_impl("g", "yn");
|
||||||
let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "yn");
|
assert!(matches!(answer, Err(_)));
|
||||||
|
let answer = prompt_char_impl("n", "yn").unwrap();
|
||||||
assert_eq!(answer, 'n');
|
assert_eq!(answer, 'n');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_not_give_multichar() {
|
fn test_should_not_give_multichar() {
|
||||||
let inputs = ['y', 'y', '\n', 'n', '\n'].iter().collect::<String>();
|
let answer = prompt_char_impl("yy", "yn");
|
||||||
let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "yn");
|
assert!(matches!(answer, Err(_)));
|
||||||
assert_eq!(answer, 'n');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue