@@ -28,48 +28,36 @@ class GetPassWarning(UserWarning): pass
2828
2929# Default POSIX control character mappings
3030_POSIX_CTRL_CHARS = {
31- 'ERASE' : b '\x7f ' , # DEL/Backspace
32- 'KILL' : b '\x15 ' , # Ctrl+U - kill line
33- 'WERASE' : b '\x17 ' , # Ctrl+W - erase word
34- 'LNEXT' : b '\x16 ' , # Ctrl+V - literal next
35- 'EOF' : b '\x04 ' , # Ctrl+D - EOF
36- 'INTR' : b '\x03 ' , # Ctrl+C - interrupt
37- 'SOH' : b '\x01 ' , # Ctrl+A - start of heading (beginning of line)
38- 'ENQ' : b '\x05 ' , # Ctrl+E - enquiry (end of line)
39- 'VT' : b '\x0b ' , # Ctrl+K - vertical tab (kill forward)
31+ 'ERASE' : '\x7f ' , # DEL/Backspace
32+ 'KILL' : '\x15 ' , # Ctrl+U - kill line
33+ 'WERASE' : '\x17 ' , # Ctrl+W - erase word
34+ 'LNEXT' : '\x16 ' , # Ctrl+V - literal next
35+ 'EOF' : '\x04 ' , # Ctrl+D - EOF
36+ 'INTR' : '\x03 ' , # Ctrl+C - interrupt
37+ 'SOH' : '\x01 ' , # Ctrl+A - start of heading (beginning of line)
38+ 'ENQ' : '\x05 ' , # Ctrl+E - enquiry (end of line)
39+ 'VT' : '\x0b ' , # Ctrl+K - vertical tab (kill forward)
4040}
4141
4242
4343def _get_terminal_ctrl_chars (fd ):
4444 """Extract control characters from terminal settings.
4545
46- Returns a dict mapping control char names to their byte values.
46+ Returns a dict mapping control char names to their str values.
4747 Falls back to POSIX defaults if termios isn't available.
4848 """
49+ res = _POSIX_CTRL_CHARS .copy ()
4950 try :
5051 old = termios .tcgetattr (fd )
5152 cc = old [6 ] # Index 6 is the control characters array
52- return {
53- 'ERASE' : cc [termios .VERASE ] if termios .VERASE < len (cc ) else _POSIX_CTRL_CHARS ['ERASE' ],
54- 'KILL' : cc [termios .VKILL ] if termios .VKILL < len (cc ) else _POSIX_CTRL_CHARS ['KILL' ],
55- 'WERASE' : cc [termios .VWERASE ] if termios .VWERASE < len (cc ) else _POSIX_CTRL_CHARS ['WERASE' ],
56- 'LNEXT' : cc [termios .VLNEXT ] if termios .VLNEXT < len (cc ) else _POSIX_CTRL_CHARS ['LNEXT' ],
57- 'EOF' : cc [termios .VEOF ] if termios .VEOF < len (cc ) else _POSIX_CTRL_CHARS ['EOF' ],
58- 'INTR' : cc [termios .VINTR ] if termios .VINTR < len (cc ) else _POSIX_CTRL_CHARS ['INTR' ],
59- # Ctrl+A/E/K are not in termios, use POSIX defaults
60- 'SOH' : _POSIX_CTRL_CHARS ['SOH' ],
61- 'ENQ' : _POSIX_CTRL_CHARS ['ENQ' ],
62- 'VT' : _POSIX_CTRL_CHARS ['VT' ],
63- }
6453 except (termios .error , OSError ):
65- return _POSIX_CTRL_CHARS .copy ()
66-
67-
68- def _decode_ctrl_char (char_value ):
69- """Convert a control character from bytes to str."""
70- if isinstance (char_value , bytes ):
71- return char_value .decode ('latin-1' )
72- return char_value
54+ return res
55+ # Ctrl+A/E/K are not in termios, use POSIX defaults
56+ for name in ('ERASE' , 'KILL' , 'WERASE' , 'LNEXT' , 'EOF' , 'INTR' ):
57+ cap = getattr (termios , f'V{ name } ' )
58+ if cap < len (cc ):
59+ res [name ] = cc [cap ].decode ('latin-1' )
60+ return res
7361
7462
7563def unix_getpass (prompt = 'Password: ' , stream = None , * , echo_char = None ):
@@ -248,78 +236,75 @@ def __init__(self, stream, echo_char, ctrl_chars):
248236 self .cursor_pos = 0
249237 self .eof_pressed = False
250238 self .literal_next = False
251- self .ctrl = {name : _decode_ctrl_char (value )
252- for name , value in ctrl_chars .items ()}
239+ self .ctrl = ctrl_chars
240+ self ._dispatch = {
241+ ctrl_chars ['SOH' ]: self ._handle_move_start , # Ctrl+A
242+ ctrl_chars ['ENQ' ]: self ._handle_move_end , # Ctrl+E
243+ ctrl_chars ['VT' ]: self ._handle_kill_forward , # Ctrl+K
244+ ctrl_chars ['KILL' ]: self ._handle_kill_line , # Ctrl+U
245+ ctrl_chars ['WERASE' ]: self ._handle_erase_word , # Ctrl+W
246+ ctrl_chars ['ERASE' ]: self ._handle_erase , # DEL
247+ '\b ' : self ._handle_erase , # Backspace
248+ }
253249
254- def refresh_display (self ):
255- """Redraw the entire password line with asterisks ."""
256- self .stream .write ('\r ' + ' ' * ( len (self .passwd ) + 20 ) + '\r ' )
250+ def _refresh_display (self ):
251+ """Redraw the entire password line with *echo_char* ."""
252+ self .stream .write ('\r ' + ' ' * len (self .passwd ) + '\r ' )
257253 self .stream .write (self .echo_char * len (self .passwd ))
258254 if self .cursor_pos < len (self .passwd ):
259255 self .stream .write ('\b ' * (len (self .passwd ) - self .cursor_pos ))
260256 self .stream .flush ()
261257
262- def erase_chars (self , count ):
263- """Erase count asterisks from display."""
258+ def _erase_chars (self , count ):
259+ """Erase count echo characters from display."""
264260 self .stream .write ("\b \b " * count )
265261
266- def insert_char (self , char ):
262+ def _insert_char (self , char ):
267263 """Insert character at cursor position."""
268264 self .passwd = self .passwd [:self .cursor_pos ] + char + self .passwd [self .cursor_pos :]
269265 self .cursor_pos += 1
270266 # Only refresh if inserting in middle
271267 if self .cursor_pos < len (self .passwd ):
272- self .refresh_display ()
268+ self ._refresh_display ()
273269 else :
274270 self .stream .write (self .echo_char )
275271 self .stream .flush ()
276272
277- def handle_literal_next (self , char ):
278- """Insert next character literally (Ctrl+V)."""
279- self .insert_char (char )
280- self .literal_next = False
281- self .eof_pressed = False
282-
283- def handle_move_start (self ):
273+ def _handle_move_start (self ):
284274 """Move cursor to beginning (Ctrl+A)."""
285275 self .cursor_pos = 0
286- self .eof_pressed = False
287276
288- def handle_move_end (self ):
277+ def _handle_move_end (self ):
289278 """Move cursor to end (Ctrl+E)."""
290279 self .cursor_pos = len (self .passwd )
291- self .eof_pressed = False
292280
293- def handle_erase (self ):
281+ def _handle_erase (self ):
294282 """Delete character before cursor (Backspace/DEL)."""
295283 if self .cursor_pos > 0 :
296284 self .passwd = self .passwd [:self .cursor_pos - 1 ] + self .passwd [self .cursor_pos :]
297285 self .cursor_pos -= 1
298286 # Only refresh if deleting from middle
299287 if self .cursor_pos < len (self .passwd ):
300- self .refresh_display ()
288+ self ._refresh_display ()
301289 else :
302290 self .stream .write ("\b \b " )
303291 self .stream .flush ()
304- self .eof_pressed = False
305292
306- def handle_kill_line (self ):
293+ def _handle_kill_line (self ):
307294 """Erase entire line (Ctrl+U)."""
308- self .erase_chars (len (self .passwd ))
295+ self ._erase_chars (len (self .passwd ))
309296 self .passwd = ""
310297 self .cursor_pos = 0
311298 self .stream .flush ()
312- self .eof_pressed = False
313299
314- def handle_kill_forward (self ):
300+ def _handle_kill_forward (self ):
315301 """Kill from cursor to end (Ctrl+K)."""
316302 chars_to_delete = len (self .passwd ) - self .cursor_pos
317303 self .passwd = self .passwd [:self .cursor_pos ]
318- self .erase_chars (chars_to_delete )
304+ self ._erase_chars (chars_to_delete )
319305 self .stream .flush ()
320- self .eof_pressed = False
321306
322- def handle_erase_word (self ):
307+ def _handle_erase_word (self ):
323308 """Erase previous word (Ctrl+W)."""
324309 old_cursor = self .cursor_pos
325310 # Skip trailing spaces
@@ -330,20 +315,16 @@ def handle_erase_word(self):
330315 self .cursor_pos -= 1
331316 # Remove the deleted portion
332317 self .passwd = self .passwd [:self .cursor_pos ] + self .passwd [old_cursor :]
333- self .refresh_display ()
334- self .eof_pressed = False
318+ self ._refresh_display ()
335319
336- def build_dispatch_table (self ):
337- """Build dispatch table mapping control chars to handlers."""
338- return {
339- self .ctrl ['SOH' ]: self .handle_move_start , # Ctrl+A
340- self .ctrl ['ENQ' ]: self .handle_move_end , # Ctrl+E
341- self .ctrl ['VT' ]: self .handle_kill_forward , # Ctrl+K
342- self .ctrl ['KILL' ]: self .handle_kill_line , # Ctrl+U
343- self .ctrl ['WERASE' ]: self .handle_erase_word , # Ctrl+W
344- self .ctrl ['ERASE' ]: self .handle_erase , # DEL
345- '\b ' : self .handle_erase , # Backspace
346- }
320+ def handle (self , char ):
321+ """Handle a single character input. Returns True if handled."""
322+ self .eof_pressed = False
323+ handler = self ._dispatch .get (char )
324+ if handler :
325+ handler ()
326+ return True
327+ return False
347328
348329
349330def _readline_with_echo_char (stream , input , echo_char , term_ctrl_chars = None ):
@@ -352,43 +333,34 @@ def _readline_with_echo_char(stream, input, echo_char, term_ctrl_chars=None):
352333 term_ctrl_chars = _POSIX_CTRL_CHARS .copy ()
353334
354335 editor = _PasswordLineEditor (stream , echo_char , term_ctrl_chars )
355- dispatch = editor .build_dispatch_table ()
356336
357337 while True :
358338 char = input .read (1 )
359339
360340 # Check for line terminators
361341 if char in ('\n ' , '\r ' ):
362342 break
363-
364343 # Handle literal next mode FIRST (Ctrl+V quotes next char)
365- if editor .literal_next :
366- editor .handle_literal_next (char )
367- continue
368-
344+ elif editor .literal_next :
345+ editor ._insert_char (char )
346+ editor . literal_next = False
347+ editor . eof_pressed = False
369348 # Check if it's the LNEXT character
370- if char == editor .ctrl ['LNEXT' ]:
349+ elif char == editor .ctrl ['LNEXT' ]:
371350 editor .literal_next = True
372351 editor .eof_pressed = False
373- continue
374-
375352 # Check for special control characters
376- if char == editor .ctrl ['INTR' ]:
353+ elif char == editor .ctrl ['INTR' ]:
377354 raise KeyboardInterrupt
378- if char == editor .ctrl ['EOF' ]:
355+ elif char == editor .ctrl ['EOF' ]:
379356 if editor .eof_pressed :
380357 break
381358 editor .eof_pressed = True
382- continue
383- if char == '\x00 ' :
384- continue
385-
359+ elif char == '\x00 ' :
360+ pass
386361 # Dispatch to handler or insert as normal character
387- handler = dispatch .get (char )
388- if handler :
389- handler ()
390- else :
391- editor .insert_char (char )
362+ elif not editor .handle (char ):
363+ editor ._insert_char (char )
392364 editor .eof_pressed = False
393365
394366 return editor .passwd
0 commit comments