diff --git a/src/cli.rs b/src/cli.rs index 03d6b20..30c2710 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -99,6 +99,10 @@ impl FromStr for Verbosity { #[derive(Debug, Clone, Parser)] #[clap(about = "Debug stuff, not useful for most users.")] 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.")] pub demo_conf_menu: bool, } diff --git a/src/demos.rs b/src/demos.rs index 88a9752..83d5aff 100644 --- a/src/demos.rs +++ b/src/demos.rs @@ -2,6 +2,22 @@ use crate::tui; use log::*; 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() { info!("showing demo menu"); let (accept, deny) = tui::prompt_confirmation_menu(vec![ @@ -33,6 +49,7 @@ pub fn demo_confirmation_menu() { creator: 09870987, description: "example confirmation".into(), }, - ]).expect("confirmation menu demo failed"); + ]) + .expect("confirmation menu demo failed"); println!("accept: {}, deny: {}", accept.len(), deny.len()); } diff --git a/src/main.rs b/src/main.rs index e6bae13..83d56de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -329,6 +329,12 @@ fn load_accounts_with_prompts(manifest: &mut accountmanager::Manifest) -> anyhow } 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 { demos::demo_confirmation_menu(); } diff --git a/src/tui.rs b/src/tui.rs index ef6fdf5..7c3f3a2 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -2,16 +2,15 @@ use crossterm::{ cursor, event::{Event, KeyCode, KeyEvent, KeyModifiers}, execute, - style::{Print, PrintStyledContent, Stylize, Color, SetForegroundColor}, + style::{Color, Print, PrintStyledContent, SetForegroundColor, Stylize}, terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, QueueableCommand, }; use log::*; use regex::Regex; use std::collections::HashSet; -use std::io::{stdin, stdout, Read, Write}; +use std::io::{stdout, Write}; use steamguard::Confirmation; -use termion::{input::TermRead, raw::IntoRawMode}; lazy_static! { static ref CAPTCHA_VALID_CHARS: Regex = @@ -42,12 +41,22 @@ fn test_validate_captcha_text() { /// Prompt the user for text input. pub(crate) fn prompt() -> String { - let mut text = String::new(); - let _ = std::io::stdout().flush(); - std::io::stdin() - .read_line(&mut text) - .expect("Did not enter a correct string"); - return String::from(text.strip_suffix('\n').unwrap()); + stdout().flush().unwrap(); + + let mut line = String::new(); + while let Event::Key(KeyEvent { code, .. }) = crossterm::event::read().unwrap() { + match code { + KeyCode::Enter => { + break; + } + KeyCode::Char(c) => { + line.push(c); + } + _ => {} + } + } + + line } pub(crate) fn prompt_captcha_text(captcha_gid: &String) -> String { @@ -68,10 +77,20 @@ pub(crate) fn prompt_captcha_text(captcha_gid: &String) -> String { /// /// `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(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(input: T, chars: &str) -> anyhow::Result +where + T: Into, +{ let uppers = chars.replace(char::is_lowercase, ""); if uppers.len() > 1 { panic!("Invalid chars for prompt_char. Maximum 1 uppercase letter is allowed."); @@ -82,27 +101,24 @@ fn prompt_char_impl(input: &mut impl Read, text: &str, chars: &str) -> char { None }; - loop { - 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: String = input.into().to_ascii_lowercase(); - let answer_char = answer.chars().collect::>()[0]; - if chars.to_ascii_lowercase().contains(answer_char) { - return answer_char; + if answer.len() == 0 { + if let Some(a) = default_answer { + return Ok(a); + } else { + bail!("no valid answer") } + } else if answer.len() > 1 { + bail!("answer too long") } + + let answer_char = answer.chars().collect::>()[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. @@ -254,9 +270,13 @@ pub(crate) fn prompt_confirmation_menu( pub(crate) fn pause() { println!("Press any key to continue..."); - let mut stdout = std::io::stdout().into_raw_mode().unwrap(); - stdout.flush().unwrap(); - std::io::stdin().events().next(); + let _ = stdout().flush(); + loop { + match crossterm::event::read().expect("could not read terminal events") { + Event::Key(_) => break, + _ => continue, + } + } } #[cfg(test)] @@ -266,35 +286,35 @@ mod prompt_char_tests { #[test] fn test_gives_answer() { let inputs = ['y', '\n'].iter().collect::(); - let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "yn"); + let answer = prompt_char_impl(inputs, "yn").unwrap(); assert_eq!(answer, 'y'); } #[test] fn test_gives_default() { let inputs = ['\n'].iter().collect::(); - let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "Yn"); + let answer = prompt_char_impl(inputs, "Yn").unwrap(); assert_eq!(answer, 'y'); } #[test] fn test_should_not_give_default() { let inputs = ['n', '\n'].iter().collect::(); - let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "Yn"); + let answer = prompt_char_impl(inputs, "Yn").unwrap(); assert_eq!(answer, 'n'); } #[test] fn test_should_not_give_invalid() { let inputs = ['g', '\n', 'n', '\n'].iter().collect::(); - let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "yn"); + let answer = prompt_char_impl(inputs, "yn").unwrap(); assert_eq!(answer, 'n'); } #[test] fn test_should_not_give_multichar() { let inputs = ['y', 'y', '\n', 'n', '\n'].iter().collect::(); - let answer = prompt_char_impl(&mut inputs.as_bytes(), "ligma balls", "yn"); + let answer = prompt_char_impl(inputs, "yn").unwrap(); assert_eq!(answer, 'n'); } }