Skip to content

Commit 718110f

Browse files
committed
Enable virtual terminal mode in Windows REPL
Windows REPL input was using virtual key mode, which does not support terminal escape sequences. This patch calls `SetConsoleMode` properly when initializing and send sequences to enable bracketed-paste modes to support verbatim copy-and-paste. Signed-off-by: y5c4l3 <y5c4l3@proton.me>
1 parent 8b2921b commit 718110f

File tree

1 file changed

+65
-7
lines changed

1 file changed

+65
-7
lines changed

Lib/_pyrepl/windows_console.py

+65-7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from .console import Event, Console
4343
from .trace import trace
4444
from .utils import wlen
45+
from .windows_eventqueue import EventQueue
4546

4647
try:
4748
from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined]
@@ -94,7 +95,9 @@ def __init__(self, err: int | None, descr: str | None = None) -> None:
9495
0x83: "f20", # VK_F20
9596
}
9697

97-
# Console escape codes: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
98+
# Virtual terminal output sequences
99+
# Reference: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#output-sequences
100+
# Check `windows_eventqueue.py` for input sequences
98101
ERASE_IN_LINE = "\x1b[K"
99102
MOVE_LEFT = "\x1b[{}D"
100103
MOVE_RIGHT = "\x1b[{}C"
@@ -106,6 +109,12 @@ def __init__(self, err: int | None, descr: str | None = None) -> None:
106109
class _error(Exception):
107110
pass
108111

112+
def _supports_vt():
113+
try:
114+
import nt
115+
return nt._supports_virtual_terminal()
116+
except (ImportError, AttributeError):
117+
return False
109118

110119
class WindowsConsole(Console):
111120
def __init__(
@@ -117,17 +126,36 @@ def __init__(
117126
):
118127
super().__init__(f_in, f_out, term, encoding)
119128

129+
self.__vt_support = _supports_vt()
130+
self.__vt_bracketed_paste = False
131+
132+
if self.__vt_support:
133+
trace('console supports virtual terminal')
134+
135+
# Should make educated guess to determine the terminal type.
136+
# Currently enable bracketed-paste only if it's Windows Terminal.
137+
if 'WT_SESSION' in os.environ:
138+
trace('console supports bracketed-paste sequence')
139+
self.__vt_bracketed_paste = True
140+
141+
# Save original console modes so we can recover on cleanup.
142+
original_input_mode = DWORD()
143+
GetConsoleMode(InHandle, original_input_mode)
144+
trace(f'saved original input mode 0x{original_input_mode.value:x}')
145+
self.__original_input_mode = original_input_mode.value
146+
120147
SetConsoleMode(
121148
OutHandle,
122149
ENABLE_WRAP_AT_EOL_OUTPUT
123150
| ENABLE_PROCESSED_OUTPUT
124151
| ENABLE_VIRTUAL_TERMINAL_PROCESSING,
125152
)
153+
126154
self.screen: list[str] = []
127155
self.width = 80
128156
self.height = 25
129157
self.__offset = 0
130-
self.event_queue: deque[Event] = deque()
158+
self.event_queue = EventQueue(encoding)
131159
try:
132160
self.out = io._WindowsConsoleIO(self.output_fd, "w") # type: ignore[attr-defined]
133161
except ValueError:
@@ -291,6 +319,12 @@ def _enable_blinking(self):
291319
def _disable_blinking(self):
292320
self.__write("\x1b[?12l")
293321

322+
def _enable_bracketed_paste(self) -> None:
323+
self.__write("\x1b[?2004h")
324+
325+
def _disable_bracketed_paste(self) -> None:
326+
self.__write("\x1b[?2004l")
327+
294328
def __write(self, text: str) -> None:
295329
if "\x1a" in text:
296330
text = ''.join(["^Z" if x == '\x1a' else x for x in text])
@@ -320,8 +354,17 @@ def prepare(self) -> None:
320354
self.__gone_tall = 0
321355
self.__offset = 0
322356

357+
if self.__vt_support:
358+
SetConsoleMode(InHandle, self.__original_input_mode | ENABLE_VIRTUAL_TERMINAL_INPUT)
359+
if self.__vt_bracketed_paste:
360+
self._enable_bracketed_paste()
361+
323362
def restore(self) -> None:
324-
pass
363+
if self.__vt_support:
364+
# Recover to original mode before running REPL
365+
SetConsoleMode(InHandle, self.__original_input_mode)
366+
if self.__vt_bracketed_paste:
367+
self._disable_bracketed_paste()
325368

326369
def _move_relative(self, x: int, y: int) -> None:
327370
"""Moves relative to the current __posxy"""
@@ -342,7 +385,7 @@ def move_cursor(self, x: int, y: int) -> None:
342385
raise ValueError(f"Bad cursor position {x}, {y}")
343386

344387
if y < self.__offset or y >= self.__offset + self.height:
345-
self.event_queue.insert(0, Event("scroll", ""))
388+
self.event_queue.insert(Event("scroll", ""))
346389
else:
347390
self._move_relative(x, y)
348391
self.__posxy = x, y
@@ -386,10 +429,8 @@ def get_event(self, block: bool = True) -> Event | None:
386429
"""Return an Event instance. Returns None if |block| is false
387430
and there is no event pending, otherwise waits for the
388431
completion of an event."""
389-
if self.event_queue:
390-
return self.event_queue.pop()
391432

392-
while True:
433+
while self.event_queue.empty():
393434
rec = self._read_input()
394435
if rec is None:
395436
if block:
@@ -428,8 +469,13 @@ def get_event(self, block: bool = True) -> Event | None:
428469
continue
429470

430471
return None
472+
elif self.__vt_support:
473+
# If virtual terminal is enabled, scanning VT sequences
474+
self.event_queue.push(rec.Event.KeyEvent.uChar.UnicodeChar)
475+
continue
431476

432477
return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar)
478+
return self.event_queue.get()
433479

434480
def push_char(self, char: int | bytes) -> None:
435481
"""
@@ -551,6 +597,13 @@ class INPUT_RECORD(Structure):
551597
MOUSE_EVENT = 0x02
552598
WINDOW_BUFFER_SIZE_EVENT = 0x04
553599

600+
ENABLE_PROCESSED_INPUT = 0x0001
601+
ENABLE_LINE_INPUT = 0x0002
602+
ENABLE_ECHO_INPUT = 0x0004
603+
ENABLE_MOUSE_INPUT = 0x0010
604+
ENABLE_INSERT_MODE = 0x0020
605+
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
606+
554607
ENABLE_PROCESSED_OUTPUT = 0x01
555608
ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
556609
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
@@ -582,6 +635,10 @@ class INPUT_RECORD(Structure):
582635
]
583636
ScrollConsoleScreenBuffer.restype = BOOL
584637

638+
GetConsoleMode = _KERNEL32.GetConsoleMode
639+
GetConsoleMode.argtypes = [HANDLE, POINTER(DWORD)]
640+
GetConsoleMode.restype = BOOL
641+
585642
SetConsoleMode = _KERNEL32.SetConsoleMode
586643
SetConsoleMode.argtypes = [HANDLE, DWORD]
587644
SetConsoleMode.restype = BOOL
@@ -600,6 +657,7 @@ def _win_only(*args, **kwargs):
600657
GetStdHandle = _win_only
601658
GetConsoleScreenBufferInfo = _win_only
602659
ScrollConsoleScreenBuffer = _win_only
660+
GetConsoleMode = _win_only
603661
SetConsoleMode = _win_only
604662
ReadConsoleInput = _win_only
605663
OutHandle = 0

0 commit comments

Comments
 (0)