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 ,
} ;
2023-07-05 14:35:32 +02:00
use secrecy ::SecretString ;
2021-08-14 16:01:25 +02:00
use std ::collections ::HashSet ;
2022-09-08 22:16:20 +02:00
use std ::io ::{ stderr , stdout , Write } ;
2021-08-14 16:01:25 +02:00
use steamguard ::Confirmation ;
/// Prompt the user for text input.
2022-02-04 19:08:23 +01:00
pub ( crate ) fn prompt ( ) -> String {
2023-03-18 15:20:37 +01:00
stdout ( ) . flush ( ) . expect ( " failed to flush stdout " ) ;
stderr ( ) . flush ( ) . expect ( " failed to flush stderr " ) ;
2022-06-25 16:57:37 +02:00
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
}
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 {
2022-09-08 22:16:20 +02:00
let _ = stderr ( ) . queue ( Print ( format! ( " {} [ {} ] " , text , chars ) ) ) ;
let _ = stderr ( ) . flush ( ) ;
2022-06-25 16:57:37 +02:00
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
2023-06-22 22:20:15 +02:00
if answer . is_empty ( ) {
2022-06-25 16:57:37 +02:00
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 > ) > {
2023-06-22 22:20:15 +02:00
if confirmations . is_empty ( ) {
2023-06-24 19:02:54 +02:00
return Ok ( ( vec! [ ] , vec! [ ] ) ) ;
2022-08-05 05:07:43 +02:00
}
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 ( )
) ,
) ? ;
2023-06-23 19:36:23 +02:00
for ( i , conf ) in confirmations . iter ( ) . enumerate ( ) {
2022-06-25 16:14:34 +02:00
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 ) ) ? ;
}
2023-06-23 19:36:23 +02:00
stdout ( ) . queue ( Print ( format! ( " {} \n " , conf . description ( ) ) ) ) ? ;
2022-06-25 16:14:34 +02:00
}
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 ( ) {
2023-03-18 15:20:37 +01:00
let _ = write! ( stderr ( ) , " Press enter to continue... " ) ;
2022-09-08 22:16:20 +02:00
let _ = stderr ( ) . flush ( ) ;
2022-06-25 16:57:37 +02:00
loop {
match crossterm ::event ::read ( ) . expect ( " could not read terminal events " ) {
2023-03-18 15:20:37 +01:00
Event ::Key ( KeyEvent {
code : KeyCode ::Enter ,
..
} ) = > break ,
2022-06-25 16:57:37 +02:00
_ = > continue ,
}
}
2021-08-14 16:01:25 +02:00
}
2021-08-14 17:10:21 +02:00
2023-07-05 14:35:32 +02:00
pub ( crate ) fn prompt_passkey ( ) -> std ::io ::Result < SecretString > {
loop {
let raw = rpassword ::prompt_password_stdout ( " Enter encryption passkey: " ) ? ;
if ! raw . is_empty ( ) {
return Ok ( SecretString ::new ( raw ) ) ;
}
}
}
pub ( crate ) fn prompt_password ( ) -> std ::io ::Result < SecretString > {
loop {
let raw = rpassword ::prompt_password_stdout ( " Password: " ) ? ;
if ! raw . is_empty ( ) {
return Ok ( SecretString ::new ( raw ) ) ;
}
}
}
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
}
}