@@ -579,6 +579,56 @@ public function getInput(string $prompt, ?string $default = null, ?InputValidato
579579 return null ;
580580 }
581581 /**
582+ * Reads user input with characters masked by a specified character.
583+ *
584+ * This method is similar to getInput() but masks the input characters as the user types,
585+ * making it suitable for sensitive information like passwords, tokens, or secrets.
586+ * The actual input value is captured but only mask characters are displayed in the terminal.
587+ *
588+ * @param string $prompt The prompt message to display to the user. Must be non-empty.
589+ *
590+ * @param string $mask The character to display instead of the actual input characters.
591+ * Default is '*'. Can be any single character or string.
592+ *
593+ * @param string|null $default An optional default value to use if the user provides
594+ * empty input. If provided, it will be shown in the prompt.
595+ *
596+ * @param InputValidator|null $validator An optional validator to validate the input.
597+ * If validation fails, the user will be prompted again.
598+ *
599+ * @return string|null Returns the actual input value (not masked) if valid input is provided,
600+ * or null if the prompt is empty.
601+ *
602+ * @since 1.1.0
603+ */
604+ public function getMaskedInput (string $ prompt , string $ mask = '* ' , ?string $ default = null , ?InputValidator $ validator = null ): ?string {
605+ $ trimmed = trim ($ prompt );
606+
607+ if (strlen ($ trimmed ) > 0 ) {
608+ do {
609+ $ this ->prints ($ trimmed , [
610+ 'color ' => 'gray ' ,
611+ 'bold ' => true
612+ ]);
613+
614+ if ($ default !== null ) {
615+ $ this ->prints (" Enter = ' " .$ default ."' " , [
616+ 'color ' => 'light-blue '
617+ ]);
618+ }
619+ $ this ->println ();
620+ $ input = trim ($ this ->readMaskedLine ($ mask ));
621+
622+ $ check = $ this ->getInputHelper ($ input , $ validator , $ default );
623+
624+ if ($ check ['valid ' ]) {
625+ return $ check ['value ' ];
626+ }
627+ } while (true );
628+ }
629+
630+ return null ;
631+ } /**
582632 * Returns the stream at which the command is sing to read inputs.
583633 *
584634 * @return null|InputStream If the stream is set, it will be returned as
@@ -964,7 +1014,63 @@ public function readInteger(string $prompt, ?int $default = null) : int {
9641014 public function readln () : string {
9651015 return $ this ->getInputStream ()->readLine ();
9661016 }
967-
1017+ /**
1018+ * Reads a line from input stream with character masking.
1019+ *
1020+ * This method reads input character by character and displays mask characters
1021+ * instead of the actual input. It handles backspace for character deletion
1022+ * and ignores special keys like ESC and arrow keys.
1023+ *
1024+ * @param string $mask The character to display instead of actual input characters.
1025+ *
1026+ * @return string The actual input string (unmasked).
1027+ *
1028+ * @since 1.1.0
1029+ */
1030+ private function readMaskedLine (string $ mask = '* ' ): string {
1031+ $ input = '' ;
1032+
1033+ // For testing with ArrayInputStream, read the whole line at once
1034+ if ($ this ->getInputStream () instanceof \WebFiori \Cli \Streams \ArrayInputStream) {
1035+ $ input = $ this ->getInputStream ()->readLine ();
1036+ // Simulate masking output for testing
1037+ $ this ->prints (str_repeat ($ mask , strlen ($ input )));
1038+ $ this ->println ();
1039+ return $ input ;
1040+ }
1041+
1042+ // Set terminal to raw mode with echo disabled for real-time character reading
1043+ $ sttyMode = null ;
1044+ if (function_exists ('shell_exec ' ) && PHP_OS_FAMILY !== 'Windows ' ) {
1045+ $ sttyMode = shell_exec ('stty -g 2>/dev/null ' );
1046+ shell_exec ('stty -echo -icanon 2>/dev/null ' );
1047+ }
1048+
1049+ try {
1050+ // For real terminal input, read character by character
1051+ while (true ) {
1052+ $ char = KeysMap::readAndTranslate ($ this ->getInputStream ());
1053+
1054+ if ($ char === 'LF ' || $ char === 'CR ' || $ char === '' ) {
1055+ break ;
1056+ } elseif ($ char === 'BACKSPACE ' && strlen ($ input ) > 0 ) {
1057+ $ input = substr ($ input , 0 , -1 );
1058+ $ this ->prints ("\x08 \x08" ); // Backspace, space, backspace
1059+ } elseif ($ char !== 'BACKSPACE ' && $ char !== 'ESC ' && $ char !== 'DOWN ' && $ char !== 'UP ' && $ char !== 'LEFT ' && $ char !== 'RIGHT ' ) {
1060+ $ input .= $ char === 'SPACE ' ? ' ' : $ char ;
1061+ $ this ->prints ($ mask );
1062+ }
1063+ }
1064+ } finally {
1065+ // Restore terminal settings
1066+ if ($ sttyMode !== null ) {
1067+ shell_exec ('stty ' . $ sttyMode . ' 2>/dev/null ' );
1068+ }
1069+ }
1070+
1071+ $ this ->println ();
1072+ return $ input ;
1073+ }
9681074 /**
9691075 * Reads a string that represents class namespace.
9701076 *
0 commit comments