readSecretInput reads secret input from the terminal rune-by-rune, masking each character with an asterisk.
(f *os.File, w io.Writer)
| 210 | // readSecretInput reads secret input from the terminal rune-by-rune, |
| 211 | // masking each character with an asterisk. |
| 212 | func readSecretInput(f *os.File, w io.Writer) (string, error) { |
| 213 | // Put terminal into raw mode (no echo, no line buffering). |
| 214 | oldState, err := pty.MakeInputRaw(f.Fd()) |
| 215 | if err != nil { |
| 216 | return "", err |
| 217 | } |
| 218 | defer func() { |
| 219 | _ = pty.RestoreTerminal(f.Fd(), oldState) |
| 220 | }() |
| 221 | |
| 222 | reader := bufio.NewReader(f) |
| 223 | var runes []rune |
| 224 | |
| 225 | for { |
| 226 | r, _, err := reader.ReadRune() |
| 227 | if err != nil { |
| 228 | return "", err |
| 229 | } |
| 230 | |
| 231 | switch { |
| 232 | case r == '\r' || r == '\n': |
| 233 | // Finish on Enter |
| 234 | if _, err := fmt.Fprint(w, "\r\n"); err != nil { |
| 235 | return "", err |
| 236 | } |
| 237 | return string(runes), nil |
| 238 | |
| 239 | case r == 3: |
| 240 | // Ctrl+C |
| 241 | return "", ErrCanceled |
| 242 | |
| 243 | case r == 127 || r == '\b': |
| 244 | // Backspace/Delete: remove last rune |
| 245 | if len(runes) > 0 { |
| 246 | // Erase the last '*' on the screen |
| 247 | if _, err := fmt.Fprint(w, "\b \b"); err != nil { |
| 248 | return "", err |
| 249 | } |
| 250 | runes = runes[:len(runes)-1] |
| 251 | } |
| 252 | |
| 253 | default: |
| 254 | // Only mask printable, non-control runes |
| 255 | if !unicode.IsControl(r) { |
| 256 | runes = append(runes, r) |
| 257 | if _, err := fmt.Fprint(w, "*"); err != nil { |
| 258 | return "", err |
| 259 | } |
| 260 | } |
| 261 | } |
| 262 | } |
| 263 | } |
no test coverage detected