@@ -73,15 +73,27 @@ def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None):
7373 old = termios .tcgetattr (fd ) # a copy to save
7474 new = old [:]
7575 new [3 ] &= ~ termios .ECHO # 3 == 'lflags'
76+ # Extract control characters before changing terminal mode
77+ term_ctrl_chars = None
7678 if echo_char :
7779 new [3 ] &= ~ termios .ICANON
80+ # Get control characters from terminal settings
81+ # Index 6 is cc (control characters array)
82+ cc = old [6 ]
83+ term_ctrl_chars = {
84+ 'ERASE' : cc [termios .VERASE ] if termios .VERASE < len (cc ) else b'\x7f ' ,
85+ 'KILL' : cc [termios .VKILL ] if termios .VKILL < len (cc ) else b'\x15 ' ,
86+ 'WERASE' : cc [termios .VWERASE ] if termios .VWERASE < len (cc ) else b'\x17 ' ,
87+ 'LNEXT' : cc [termios .VLNEXT ] if termios .VLNEXT < len (cc ) else b'\x16 ' ,
88+ }
7889 tcsetattr_flags = termios .TCSAFLUSH
7990 if hasattr (termios , 'TCSASOFT' ):
8091 tcsetattr_flags |= termios .TCSASOFT
8192 try :
8293 termios .tcsetattr (fd , tcsetattr_flags , new )
8394 passwd = _raw_input (prompt , stream , input = input ,
84- echo_char = echo_char )
95+ echo_char = echo_char ,
96+ term_ctrl_chars = term_ctrl_chars )
8597
8698 finally :
8799 termios .tcsetattr (fd , tcsetattr_flags , old )
@@ -159,7 +171,8 @@ def _check_echo_char(echo_char):
159171 f"character, got: { echo_char !r} " )
160172
161173
162- def _raw_input (prompt = "" , stream = None , input = None , echo_char = None ):
174+ def _raw_input (prompt = "" , stream = None , input = None , echo_char = None ,
175+ term_ctrl_chars = None ):
163176 # This doesn't save the string in the GNU readline history.
164177 if not stream :
165178 stream = sys .stderr
@@ -177,7 +190,8 @@ def _raw_input(prompt="", stream=None, input=None, echo_char=None):
177190 stream .flush ()
178191 # NOTE: The Python C API calls flockfile() (and unlock) during readline.
179192 if echo_char :
180- return _readline_with_echo_char (stream , input , echo_char )
193+ return _readline_with_echo_char (stream , input , echo_char ,
194+ term_ctrl_chars )
181195 line = input .readline ()
182196 if not line :
183197 raise EOFError
@@ -186,27 +200,78 @@ def _raw_input(prompt="", stream=None, input=None, echo_char=None):
186200 return line
187201
188202
189- def _readline_with_echo_char (stream , input , echo_char ):
203+ def _readline_with_echo_char (stream , input , echo_char , term_ctrl_chars = None ):
190204 passwd = ""
191205 eof_pressed = False
206+ literal_next = False # For LNEXT (Ctrl+V)
207+
208+ # Convert terminal control characters to strings for comparison
209+ # Default to standard POSIX values if not provided
210+ if term_ctrl_chars :
211+ # Control chars from termios are bytes, convert to str
212+ erase_char = term_ctrl_chars ['ERASE' ].decode ('latin-1' ) if isinstance (term_ctrl_chars ['ERASE' ], bytes ) else term_ctrl_chars ['ERASE' ]
213+ kill_char = term_ctrl_chars ['KILL' ].decode ('latin-1' ) if isinstance (term_ctrl_chars ['KILL' ], bytes ) else term_ctrl_chars ['KILL' ]
214+ werase_char = term_ctrl_chars ['WERASE' ].decode ('latin-1' ) if isinstance (term_ctrl_chars ['WERASE' ], bytes ) else term_ctrl_chars ['WERASE' ]
215+ lnext_char = term_ctrl_chars ['LNEXT' ].decode ('latin-1' ) if isinstance (term_ctrl_chars ['LNEXT' ], bytes ) else term_ctrl_chars ['LNEXT' ]
216+ else :
217+ # Standard POSIX defaults
218+ erase_char = '\x7f ' # DEL
219+ kill_char = '\x15 ' # Ctrl+U
220+ werase_char = '\x17 ' # Ctrl+W
221+ lnext_char = '\x16 ' # Ctrl+V
222+
192223 while True :
193224 char = input .read (1 )
225+
194226 if char == '\n ' or char == '\r ' :
195227 break
196228 elif char == '\x03 ' :
197229 raise KeyboardInterrupt
198- elif char == '\x7f ' or char == '\b ' :
199- if passwd :
200- stream .write ("\b \b " )
201- stream .flush ()
202- passwd = passwd [:- 1 ]
203230 elif char == '\x04 ' :
204231 if eof_pressed :
205232 break
206233 else :
207234 eof_pressed = True
208235 elif char == '\x00 ' :
209236 continue
237+ # Handle LNEXT (Ctrl+V) - insert next character literally
238+ elif literal_next :
239+ passwd += char
240+ stream .write (echo_char )
241+ stream .flush ()
242+ literal_next = False
243+ eof_pressed = False
244+ elif char == lnext_char :
245+ literal_next = True
246+ eof_pressed = False
247+ # Handle ERASE (Backspace/DEL) - delete one character
248+ elif char == erase_char or char == '\b ' :
249+ if passwd :
250+ stream .write ("\b \b " )
251+ stream .flush ()
252+ passwd = passwd [:- 1 ]
253+ eof_pressed = False
254+ # Handle KILL (Ctrl+U) - erase entire line
255+ elif char == kill_char :
256+ # Clear all echoed characters
257+ while passwd :
258+ stream .write ("\b \b " )
259+ passwd = passwd [:- 1 ]
260+ stream .flush ()
261+ eof_pressed = False
262+ # Handle WERASE (Ctrl+W) - erase previous word
263+ elif char == werase_char :
264+ # Delete backwards until we find a space or reach the beginning
265+ # First, skip any trailing spaces
266+ while passwd and passwd [- 1 ] == ' ' :
267+ stream .write ("\b \b " )
268+ passwd = passwd [:- 1 ]
269+ # Then delete the word
270+ while passwd and passwd [- 1 ] != ' ' :
271+ stream .write ("\b \b " )
272+ passwd = passwd [:- 1 ]
273+ stream .flush ()
274+ eof_pressed = False
210275 else :
211276 passwd += char
212277 stream .write (echo_char )
0 commit comments