2022-06-25 16:14:34 +02:00
use crossterm ::{
cursor ,
event ::{ Event , KeyCode , KeyEvent , KeyModifiers } ,
execute ,
2022-06-25 16:57:37 +02:00
style ::{ Color , Print , PrintStyledContent , SetForegroundColor , Stylize } ,
2022-06-25 16:14:34 +02:00
terminal ::{ Clear , ClearType , EnterAlternateScreen , LeaveAlternateScreen } ,
QueueableCommand ,
} ;
2021-08-14 16:01:25 +02:00
use log ::* ;
use regex ::Regex ;
use std ::collections ::HashSet ;
2022-06-25 16:57:37 +02:00
use std ::io ::{ stdout , Write } ;
2021-08-14 16:01:25 +02:00
use steamguard ::Confirmation ;
lazy_static! {
static ref CAPTCHA_VALID_CHARS : Regex =
Regex ::new ( " ^([A-H]|[J-N]|[P-R]|[T-Z]|[2-4]|[7-9]|[@%&])+$ " ) . unwrap ( ) ;
}
pub fn validate_captcha_text ( text : & String ) -> bool {
return CAPTCHA_VALID_CHARS . is_match ( text ) ;
}
#[ test ]
fn test_validate_captcha_text ( ) {
assert! ( validate_captcha_text ( & String ::from ( " 2WWUA@ " ) ) ) ;
assert! ( validate_captcha_text ( & String ::from ( " 3G8HT2 " ) ) ) ;
assert! ( validate_captcha_text ( & String ::from ( " 3J%@X3 " ) ) ) ;
assert! ( validate_captcha_text ( & String ::from ( " 2GCZ4A " ) ) ) ;
assert! ( validate_captcha_text ( & String ::from ( " 3G8HT2 " ) ) ) ;
assert! ( ! validate_captcha_text ( & String ::from ( " asd823 " ) ) ) ;
assert! ( ! validate_captcha_text ( & String ::from ( " !PQ4RD " ) ) ) ;
assert! ( ! validate_captcha_text ( & String ::from ( " 1GQ4XZ " ) ) ) ;
assert! ( ! validate_captcha_text ( & String ::from ( " 8GO4XZ " ) ) ) ;
assert! ( ! validate_captcha_text ( & String ::from ( " IPQ4RD " ) ) ) ;
assert! ( ! validate_captcha_text ( & String ::from ( " 0PT4RD " ) ) ) ;
assert! ( ! validate_captcha_text ( & String ::from ( " APTSRD " ) ) ) ;
assert! ( ! validate_captcha_text ( & String ::from ( " AP5TRD " ) ) ) ;
assert! ( ! validate_captcha_text ( & String ::from ( " AP6TRD " ) ) ) ;
}
/// Prompt the user for text input.
2022-02-04 19:08:23 +01:00
pub ( crate ) fn prompt ( ) -> String {
2022-06-25 16:57:37 +02:00
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
2021-08-14 16:01:25 +02:00
}
2022-02-04 19:08:23 +01:00
pub ( crate ) fn prompt_captcha_text ( captcha_gid : & String ) -> String {
2021-08-14 16:01:25 +02:00
println! ( " Captcha required. Open this link in your web browser: https://steamcommunity.com/public/captcha.php?gid= {} " , captcha_gid ) ;
let mut captcha_text ;
loop {
print! ( " Enter captcha text: " ) ;
captcha_text = prompt ( ) ;
if captcha_text . len ( ) > 0 & & validate_captcha_text ( & captcha_text ) {
break ;
}
warn! ( " Invalid chars for captcha text found in user's input. Prompting again... " ) ;
}
return captcha_text ;
}
2021-08-14 17:10:21 +02:00
/// 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.
2022-02-04 19:08:23 +01:00
pub ( crate ) fn prompt_char ( text : & str , chars : & str ) -> char {
2022-06-25 16:57:37 +02:00
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 ;
}
}
2021-08-14 17:10:21 +02:00
}
2022-06-25 16:57:37 +02:00
fn prompt_char_impl < T > ( input : T , chars : & str ) -> anyhow ::Result < char >
where
T : Into < String > ,
{
2021-08-14 17:10:21 +02:00
let uppers = chars . replace ( char ::is_lowercase , " " ) ;
if uppers . len ( ) > 1 {
panic! ( " Invalid chars for prompt_char. Maximum 1 uppercase letter is allowed. " ) ;
}
let default_answer : Option < char > = if uppers . len ( ) = = 1 {
Some ( uppers . chars ( ) . collect ::< Vec < char > > ( ) [ 0 ] . to_ascii_lowercase ( ) )
} else {
None
} ;
2022-06-25 16:57:37 +02:00
let answer : String = input . into ( ) . to_ascii_lowercase ( ) ;
2021-08-14 17:10:21 +02:00
2022-06-25 16:57:37 +02:00
if answer . len ( ) = = 0 {
if let Some ( a ) = default_answer {
return Ok ( a ) ;
} else {
bail! ( " no valid answer " )
2021-08-14 17:10:21 +02:00
}
2022-06-25 16:57:37 +02:00
} 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 ) ;
2021-08-14 17:10:21 +02:00
}
2022-06-25 16:57:37 +02:00
bail! ( " no valid answer " )
2021-08-14 17:10:21 +02:00
}
2021-08-14 16:01:25 +02:00
/// Returns a tuple of (accepted, denied). Ignored confirmations are not included.
2022-02-04 19:08:23 +01:00
pub ( crate ) fn prompt_confirmation_menu (
2021-08-14 16:01:25 +02:00
confirmations : Vec < Confirmation > ,
2022-06-25 16:14:34 +02:00
) -> anyhow ::Result < ( Vec < Confirmation > , Vec < Confirmation > ) > {
2021-08-14 16:01:25 +02:00
let mut to_accept_idx : HashSet < usize > = HashSet ::new ( ) ;
let mut to_deny_idx : HashSet < usize > = HashSet ::new ( ) ;
2022-06-25 16:14:34 +02:00
execute! ( stdout ( ) , EnterAlternateScreen ) ? ;
crossterm ::terminal ::enable_raw_mode ( ) ? ;
2021-08-14 16:01:25 +02:00
let mut selected_idx = 0 ;
2022-06-25 16:14:34 +02:00
loop {
execute! (
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' ) ,
..
} ) = > {
2021-08-14 16:01:25 +02:00
to_accept_idx . insert ( selected_idx ) ;
to_deny_idx . remove ( & selected_idx ) ;
}
2022-06-25 16:14:34 +02:00
Event ::Key ( KeyEvent {
code : KeyCode ::Char ( 'd' ) ,
..
} ) = > {
2021-08-14 16:01:25 +02:00
to_accept_idx . remove ( & selected_idx ) ;
to_deny_idx . insert ( selected_idx ) ;
}
2022-06-25 16:14:34 +02:00
Event ::Key ( KeyEvent {
code : KeyCode ::Char ( 'i' ) ,
..
} ) = > {
2021-08-14 16:01:25 +02:00
to_accept_idx . remove ( & selected_idx ) ;
to_deny_idx . remove ( & selected_idx ) ;
}
2022-06-25 16:14:34 +02:00
Event ::Key ( KeyEvent {
code : KeyCode ::Char ( 'A' ) ,
..
} ) = > {
2021-08-14 16:01:25 +02:00
( 0 .. confirmations . len ( ) ) . for_each ( | i | {
to_accept_idx . insert ( i ) ;
to_deny_idx . remove ( & i ) ;
} ) ;
}
2022-06-25 16:14:34 +02:00
Event ::Key ( KeyEvent {
code : KeyCode ::Char ( 'D' ) ,
..
} ) = > {
2021-08-14 16:01:25 +02:00
( 0 .. confirmations . len ( ) ) . for_each ( | i | {
to_accept_idx . remove ( & i ) ;
to_deny_idx . insert ( i ) ;
} ) ;
}
2022-06-25 16:14:34 +02:00
Event ::Key ( KeyEvent {
code : KeyCode ::Char ( 'I' ) ,
..
} ) = > {
2021-08-14 16:01:25 +02:00
( 0 .. confirmations . len ( ) ) . for_each ( | i | {
to_accept_idx . remove ( & i ) ;
to_deny_idx . remove ( & i ) ;
} ) ;
}
2022-06-25 16:14:34 +02:00
Event ::Key ( KeyEvent {
code : KeyCode ::Up , ..
} ) if selected_idx > 0 = > {
2021-08-14 16:01:25 +02:00
selected_idx - = 1 ;
}
2022-06-25 16:14:34 +02:00
Event ::Key ( KeyEvent {
code : KeyCode ::Down ,
..
} ) if selected_idx < confirmations . len ( ) - 1 = > {
2021-08-14 16:01:25 +02:00
selected_idx + = 1 ;
}
2022-06-25 16:14:34 +02:00
Event ::Key ( KeyEvent {
code : KeyCode ::Enter ,
..
} ) = > {
2021-08-14 16:01:25 +02:00
break ;
}
2022-06-25 16:14:34 +02:00
Event ::Key ( KeyEvent {
code : KeyCode ::Esc , ..
} )
| Event ::Key ( KeyEvent {
code : KeyCode ::Char ( 'c' ) ,
modifiers : KeyModifiers ::CONTROL ,
} ) = > {
return Ok ( ( vec! [ ] , vec! [ ] ) ) ;
2021-08-14 16:01:25 +02:00
}
_ = > { }
}
}
2022-06-25 16:14:34 +02:00
execute! ( stdout ( ) , LeaveAlternateScreen ) ? ;
crossterm ::terminal ::disable_raw_mode ( ) ? ;
return Ok ( (
2021-08-14 18:16:40 +02:00
to_accept_idx
. iter ( )
. map ( | i | confirmations [ * i ] . clone ( ) )
. collect ( ) ,
to_deny_idx
. iter ( )
. map ( | i | confirmations [ * i ] . clone ( ) )
. collect ( ) ,
2022-06-25 16:14:34 +02:00
) ) ;
2021-08-14 16:01:25 +02:00
}
2022-02-04 19:08:23 +01:00
pub ( crate ) fn pause ( ) {
2021-08-14 16:01:25 +02:00
println! ( " Press any key to continue... " ) ;
2022-06-25 16:57:37 +02:00
let _ = stdout ( ) . flush ( ) ;
loop {
match crossterm ::event ::read ( ) . expect ( " could not read terminal events " ) {
Event ::Key ( _ ) = > break ,
_ = > continue ,
}
}
2021-08-14 16:01:25 +02:00
}
2021-08-14 17:10:21 +02:00
#[ cfg(test) ]
mod prompt_char_tests {
use super ::* ;
#[ test ]
fn test_gives_answer ( ) {
2022-06-25 17:07:57 +02:00
let answer = prompt_char_impl ( " y " , " yn " ) . unwrap ( ) ;
2021-08-14 17:10:21 +02:00
assert_eq! ( answer , 'y' ) ;
}
#[ test ]
fn test_gives_default ( ) {
2022-06-25 17:07:57 +02:00
let answer = prompt_char_impl ( " " , " Yn " ) . unwrap ( ) ;
2021-08-14 17:10:21 +02:00
assert_eq! ( answer , 'y' ) ;
}
#[ test ]
fn test_should_not_give_default ( ) {
2022-06-25 17:07:57 +02:00
let answer = prompt_char_impl ( " n " , " Yn " ) . unwrap ( ) ;
2021-08-14 17:10:21 +02:00
assert_eq! ( answer , 'n' ) ;
}
#[ test ]
fn test_should_not_give_invalid ( ) {
2022-06-25 17:07:57 +02:00
let answer = prompt_char_impl ( " g " , " yn " ) ;
assert! ( matches! ( answer , Err ( _ ) ) ) ;
let answer = prompt_char_impl ( " n " , " yn " ) . unwrap ( ) ;
2021-08-14 17:10:21 +02:00
assert_eq! ( answer , 'n' ) ;
}
#[ test ]
fn test_should_not_give_multichar ( ) {
2022-06-25 17:07:57 +02:00
let answer = prompt_char_impl ( " yy " , " yn " ) ;
assert! ( matches! ( answer , Err ( _ ) ) ) ;
2021-08-14 17:10:21 +02:00
}
}