@@ -26,13 +26,54 @@ class RlType(Enum):
2626
2727
2828# Check what implementation of readline we are using
29-
3029rl_type = RlType .NONE
3130
31+ # Tells if the terminal we are running in supports vt100 control characters
32+ vt100_support = False
33+
3234# The order of this check matters since importing pyreadline will also show readline in the modules list
3335if 'pyreadline' in sys .modules :
3436 rl_type = RlType .PYREADLINE
3537
38+ from ctypes import byref
39+ from ctypes .wintypes import DWORD , HANDLE
40+ import atexit
41+
42+ # Check if we are running in a terminal
43+ if sys .stdout .isatty (): # pragma: no cover
44+ # noinspection PyPep8Naming
45+ def enable_win_vt100 (handle : HANDLE ) -> bool :
46+ """
47+ Enables VT100 character sequences in a Windows console
48+ This only works on Windows 10 and up
49+ :param handle: the handle on which to enable vt100
50+ :return: True if vt100 characters are enabled for the handle
51+ """
52+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
53+
54+ # Get the current mode for this handle in the console
55+ cur_mode = DWORD (0 )
56+ readline .rl .console .GetConsoleMode (handle , byref (cur_mode ))
57+
58+ retVal = False
59+
60+ # Check if ENABLE_VIRTUAL_TERMINAL_PROCESSING is already enabled
61+ if (cur_mode .value & ENABLE_VIRTUAL_TERMINAL_PROCESSING ) != 0 :
62+ retVal = True
63+
64+ elif readline .rl .console .SetConsoleMode (handle , cur_mode .value | ENABLE_VIRTUAL_TERMINAL_PROCESSING ):
65+ # Restore the original mode when we exit
66+ atexit .register (readline .rl .console .SetConsoleMode , handle , cur_mode )
67+ retVal = True
68+
69+ return retVal
70+
71+ # Enable VT100 sequences for stdout and stderr
72+ STD_OUT_HANDLE = - 11
73+ STD_ERROR_HANDLE = - 12
74+ vt100_support = (enable_win_vt100 (readline .rl .console .GetStdHandle (STD_OUT_HANDLE )) and
75+ enable_win_vt100 (readline .rl .console .GetStdHandle (STD_ERROR_HANDLE )))
76+
3677 ############################################################################################################
3778 # pyreadline is incomplete in terms of the Python readline API. Add the missing functions we need.
3879 ############################################################################################################
@@ -74,9 +115,13 @@ def pyreadline_remove_history_item(pos: int) -> None:
74115 import ctypes
75116 readline_lib = ctypes .CDLL (readline .__file__ )
76117
118+ # Check if we are running in a terminal
119+ if sys .stdout .isatty ():
120+ vt100_support = True
121+
77122
78123# noinspection PyProtectedMember
79- def rl_force_redisplay () -> None :
124+ def rl_force_redisplay () -> None : # pragma: no cover
80125 """
81126 Causes readline to display the prompt and input text wherever the cursor is and start
82127 reading input from this location. This is the proper way to restore the input line after
@@ -85,14 +130,77 @@ def rl_force_redisplay() -> None:
85130 if not sys .stdout .isatty ():
86131 return
87132
88- if rl_type == RlType .GNU : # pragma: no cover
133+ if rl_type == RlType .GNU :
89134 readline_lib .rl_forced_update_display ()
90135
91136 # After manually updating the display, readline asks that rl_display_fixed be set to 1 for efficiency
92137 display_fixed = ctypes .c_int .in_dll (readline_lib , "rl_display_fixed" )
93138 display_fixed .value = 1
94139
95- elif rl_type == RlType .PYREADLINE : # pragma: no cover
140+ elif rl_type == RlType .PYREADLINE :
96141 # Call _print_prompt() first to set the new location of the prompt
97142 readline .rl .mode ._print_prompt ()
98143 readline .rl .mode ._update_line ()
144+
145+
146+ # noinspection PyProtectedMember
147+ def rl_get_point () -> int : # pragma: no cover
148+ """
149+ Returns the offset of the current cursor position in rl_line_buffer
150+ """
151+ if rl_type == RlType .GNU :
152+ return ctypes .c_int .in_dll (readline_lib , "rl_point" ).value
153+
154+ elif rl_type == RlType .PYREADLINE :
155+ return readline .rl .mode .l_buffer .point
156+
157+ else :
158+ return 0
159+
160+
161+ # noinspection PyProtectedMember
162+ def rl_set_prompt (prompt : str ) -> None : # pragma: no cover
163+ """
164+ Sets readline's prompt
165+ :param prompt: the new prompt value
166+ """
167+ safe_prompt = rl_make_safe_prompt (prompt )
168+
169+ if rl_type == RlType .GNU :
170+ encoded_prompt = bytes (safe_prompt , encoding = 'utf-8' )
171+ readline_lib .rl_set_prompt (encoded_prompt )
172+
173+ elif rl_type == RlType .PYREADLINE :
174+ readline .rl ._set_prompt (safe_prompt )
175+
176+
177+ def rl_make_safe_prompt (prompt : str ) -> str : # pragma: no cover
178+ """Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes.
179+
180+ :param prompt: original prompt
181+ :return: prompt safe to pass to GNU Readline
182+ """
183+ if rl_type == RlType .GNU :
184+ # start code to tell GNU Readline about beginning of invisible characters
185+ start = "\x01 "
186+
187+ # end code to tell GNU Readline about end of invisible characters
188+ end = "\x02 "
189+
190+ escaped = False
191+ result = ""
192+
193+ for c in prompt :
194+ if c == "\x1b " and not escaped :
195+ result += start + c
196+ escaped = True
197+ elif c .isalpha () and escaped :
198+ result += c + end
199+ escaped = False
200+ else :
201+ result += c
202+
203+ return result
204+
205+ else :
206+ return prompt
0 commit comments