@@ -27,7 +27,7 @@ class GetPassWarning(UserWarning): pass
2727
2828
2929# Default POSIX control character mappings
30- _POSIX_CTRL_CHARS = {
30+ _POSIX_CTRL_CHARS = frozendict ( {
3131 'ERASE' : '\x7f ' , # DEL/Backspace
3232 'KILL' : '\x15 ' , # Ctrl+U - kill line
3333 'WERASE' : '\x17 ' , # Ctrl+W - erase word
@@ -37,7 +37,7 @@ class GetPassWarning(UserWarning): pass
3737 'SOH' : '\x01 ' , # Ctrl+A - start of heading (beginning of line)
3838 'ENQ' : '\x05 ' , # Ctrl+E - enquiry (end of line)
3939 'VT' : '\x0b ' , # Ctrl+K - vertical tab (kill forward)
40- }
40+ })
4141
4242
4343def _get_terminal_ctrl_chars (fd ):
@@ -46,18 +46,19 @@ def _get_terminal_ctrl_chars(fd):
4646 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 ( )
49+ ctrl = dict ( _POSIX_CTRL_CHARS )
5050 try :
5151 old = termios .tcgetattr (fd )
5252 cc = old [6 ] # Index 6 is the control characters array
5353 except (termios .error , OSError ):
54- return res
55- # Ctrl+A/E/K are not in termios, use POSIX defaults
54+ return ctrl
55+
56+ # Ctrl+A/E/K (SOH/ENQ/VT) are not in termios, use POSIX defaults
5657 for name in ('ERASE' , 'KILL' , 'WERASE' , 'LNEXT' , 'EOF' , 'INTR' ):
5758 cap = getattr (termios , f'V{ name } ' )
5859 if cap < len (cc ):
59- res [name ] = cc [cap ].decode ('latin-1' )
60- return res
60+ ctrl [name ] = cc [cap ].decode ('latin-1' )
61+ return ctrl
6162
6263
6364def unix_getpass (prompt = 'Password: ' , stream = None , * , echo_char = None ):
@@ -110,7 +111,10 @@ def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None):
110111 # Extract control characters before changing terminal mode
111112 term_ctrl_chars = None
112113 if echo_char :
114+ # Disable canonical mode so we can read char by char
113115 new [3 ] &= ~ termios .ICANON
116+ # Disable IEXTEN so Ctrl+V (LNEXT) is not intercepted
117+ # by the terminal driver and can be handled by our code
114118 new [3 ] &= ~ termios .IEXTEN
115119 term_ctrl_chars = _get_terminal_ctrl_chars (fd )
116120 tcsetattr_flags = termios .TCSAFLUSH
@@ -234,7 +238,7 @@ def __init__(self, stream, echo_char, ctrl_chars, prompt=""):
234238 self .stream = stream
235239 self .echo_char = echo_char
236240 self .prompt = prompt
237- self .passwd = []
241+ self .password = []
238242 self .cursor_pos = 0
239243 self .eof_pressed = False
240244 self .literal_next = False
@@ -253,20 +257,20 @@ def _refresh_display(self, prev_len=None):
253257 """Redraw the entire password line with *echo_char*."""
254258 prompt_len = len (self .prompt )
255259 # Use prev_len if given, otherwise current password length
256- clear_len = prev_len if prev_len is not None else len (self .passwd )
260+ clear_len = prev_len if prev_len is not None else len (self .password )
257261 # Clear the entire line (prompt + password) and rewrite
258262 self .stream .write ('\r ' + ' ' * (prompt_len + clear_len ) + '\r ' )
259- self .stream .write (self .prompt + self .echo_char * len (self .passwd ))
260- if self .cursor_pos < len (self .passwd ):
261- self .stream .write ('\b ' * (len (self .passwd ) - self .cursor_pos ))
263+ self .stream .write (self .prompt + self .echo_char * len (self .password ))
264+ if self .cursor_pos < len (self .password ):
265+ self .stream .write ('\b ' * (len (self .password ) - self .cursor_pos ))
262266 self .stream .flush ()
263267
264268 def _insert_char (self , char ):
265269 """Insert *char* at cursor position."""
266- self .passwd .insert (self .cursor_pos , char )
270+ self .password .insert (self .cursor_pos , char )
267271 self .cursor_pos += 1
268272 # Only refresh if inserting in middle
269- if self .cursor_pos < len (self .passwd ):
273+ if self .cursor_pos < len (self .password ):
270274 self ._refresh_display ()
271275 else :
272276 self .stream .write (self .echo_char )
@@ -278,45 +282,45 @@ def _handle_move_start(self):
278282
279283 def _handle_move_end (self ):
280284 """Move cursor to end (Ctrl+E)."""
281- self .cursor_pos = len (self .passwd )
285+ self .cursor_pos = len (self .password )
282286
283287 def _handle_erase (self ):
284288 """Delete character before cursor (Backspace/DEL)."""
285289 if self .cursor_pos <= 0 :
286290 return
287- prev_len = len (self .passwd )
288- del self .passwd [self .cursor_pos - 1 ]
291+ prev_len = len (self .password )
292+ del self .password [self .cursor_pos - 1 ]
289293 self .cursor_pos -= 1
290294 self ._refresh_display (prev_len )
291295
292296 def _handle_kill_line (self ):
293297 """Erase entire line (Ctrl+U)."""
294- prev_len = len (self .passwd )
295- self .passwd .clear ()
298+ prev_len = len (self .password )
299+ self .password .clear ()
296300 self .cursor_pos = 0
297301 self ._refresh_display (prev_len )
298302
299303 def _handle_kill_forward (self ):
300304 """Kill from cursor to end (Ctrl+K)."""
301- prev_len = len (self .passwd )
302- del self .passwd [self .cursor_pos :]
305+ prev_len = len (self .password )
306+ del self .password [self .cursor_pos :]
303307 self ._refresh_display (prev_len )
304308
305309 def _handle_erase_word (self ):
306310 """Erase previous word (Ctrl+W)."""
307311 old_cursor = self .cursor_pos
308312 # Skip trailing spaces
309- while self .cursor_pos > 0 and self .passwd [self .cursor_pos - 1 ] == ' ' :
313+ while self .cursor_pos > 0 and self .password [self .cursor_pos - 1 ] == ' ' :
310314 self .cursor_pos -= 1
311315 # Skip the word
312- while self .cursor_pos > 0 and self .passwd [self .cursor_pos - 1 ] != ' ' :
316+ while self .cursor_pos > 0 and self .password [self .cursor_pos - 1 ] != ' ' :
313317 self .cursor_pos -= 1
314318 # Remove the deleted portion
315- prev_len = len (self .passwd )
316- del self .passwd [self .cursor_pos :old_cursor ]
319+ prev_len = len (self .password )
320+ del self .password [self .cursor_pos :old_cursor ]
317321 self ._refresh_display (prev_len )
318322
319- def handle (self , char ):
323+ def _handle (self , char ):
320324 """Handle a single character input. Returns True if handled."""
321325 self .eof_pressed = False
322326 handler = self ._dispatch .get (char )
@@ -325,45 +329,51 @@ def handle(self, char):
325329 return True
326330 return False
327331
332+ def readline (self , input ):
333+ """Read a line of password input with echo character support."""
334+ while True :
335+ char = input .read (1 )
336+
337+ # Check for line terminators
338+ if char in ('\n ' , '\r ' ):
339+ break
340+ # Handle literal next mode FIRST (Ctrl+V quotes next char)
341+ elif self .literal_next :
342+ self ._insert_char (char )
343+ self .literal_next = False
344+ self .eof_pressed = False
345+ # Check if it's the LNEXT character
346+ elif char == self .ctrl ['LNEXT' ]:
347+ self .literal_next = True
348+ self .eof_pressed = False
349+ # Check for special control characters
350+ elif char == self .ctrl ['INTR' ]:
351+ raise KeyboardInterrupt
352+ elif char == self .ctrl ['EOF' ]:
353+ if self .eof_pressed :
354+ break
355+ self .eof_pressed = True
356+ elif char == '\x00 ' :
357+ pass
358+ elif self ._handle (char ):
359+ # Dispatched to handler
360+ pass
361+ else :
362+ # Insert as normal character
363+ self ._insert_char (char )
364+ self .eof_pressed = False
365+
366+ return '' .join (self .password )
367+
328368
329369def _readline_with_echo_char (stream , input , echo_char , term_ctrl_chars = None ,
330370 prompt = "" ):
331371 """Read password with echo character and line editing support."""
332372 if term_ctrl_chars is None :
333- term_ctrl_chars = _POSIX_CTRL_CHARS . copy ()
373+ term_ctrl_chars = _POSIX_CTRL_CHARS
334374
335375 editor = _PasswordLineEditor (stream , echo_char , term_ctrl_chars , prompt )
336-
337- while True :
338- char = input .read (1 )
339-
340- # Check for line terminators
341- if char in ('\n ' , '\r ' ):
342- break
343- # Handle literal next mode FIRST (Ctrl+V quotes next char)
344- elif editor .literal_next :
345- editor ._insert_char (char )
346- editor .literal_next = False
347- editor .eof_pressed = False
348- # Check if it's the LNEXT character
349- elif char == editor .ctrl ['LNEXT' ]:
350- editor .literal_next = True
351- editor .eof_pressed = False
352- # Check for special control characters
353- elif char == editor .ctrl ['INTR' ]:
354- raise KeyboardInterrupt
355- elif char == editor .ctrl ['EOF' ]:
356- if editor .eof_pressed :
357- break
358- editor .eof_pressed = True
359- elif char == '\x00 ' :
360- pass
361- # Dispatch to handler or insert as normal character
362- elif not editor .handle (char ):
363- editor ._insert_char (char )
364- editor .eof_pressed = False
365-
366- return '' .join (editor .passwd )
376+ return editor .readline (input )
367377
368378
369379def getuser ():
0 commit comments