Skip to content

Commit 1ad693a

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 <[email protected]>
1 parent 69f4930 commit 1ad693a

File tree

1 file changed

+60
-7
lines changed

1 file changed

+60
-7
lines changed

Lib/_pyrepl/windows_console.py

Lines changed: 60 additions & 7 deletions
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"
@@ -117,17 +120,37 @@ def __init__(
117120
):
118121
super().__init__(f_in, f_out, term, encoding)
119122

123+
import nt
124+
self.__vt_support = nt._supports_virtual_terminal()
125+
self.__vt_bracketed_paste = False
126+
127+
if self.__vt_support:
128+
trace('console supports virtual terminal')
129+
130+
# Should make educated guess to determine the terminal type.
131+
# Currently enable bracketed-paste only if it's Windows Terminal.
132+
if 'WT_SESSION' in os.environ:
133+
trace('console supports bracketed-paste sequence')
134+
self.__vt_bracketed_paste = True
135+
136+
# Save original console modes so we can recover on cleanup.
137+
original_input_mode = DWORD()
138+
GetConsoleMode(InHandle, original_input_mode)
139+
trace(f'saved original input mode 0x{original_input_mode.value:x}')
140+
self.__original_input_mode = original_input_mode.value
141+
120142
SetConsoleMode(
121143
OutHandle,
122144
ENABLE_WRAP_AT_EOL_OUTPUT
123145
| ENABLE_PROCESSED_OUTPUT
124146
| ENABLE_VIRTUAL_TERMINAL_PROCESSING,
125147
)
148+
126149
self.screen: list[str] = []
127150
self.width = 80
128151
self.height = 25
129152
self.__offset = 0
130-
self.event_queue: deque[Event] = deque()
153+
self.event_queue = EventQueue(encoding)
131154
try:
132155
self.out = io._WindowsConsoleIO(self.output_fd, "w") # type: ignore[attr-defined]
133156
except ValueError:
@@ -291,6 +314,12 @@ def _enable_blinking(self):
291314
def _disable_blinking(self):
292315
self.__write("\x1b[?12l")
293316

317+
def _enable_bracketed_paste(self) -> None:
318+
self.__write("\x1b[?2004h")
319+
320+
def _disable_bracketed_paste(self) -> None:
321+
self.__write("\x1b[?2004l")
322+
294323
def __write(self, text: str) -> None:
295324
if "\x1a" in text:
296325
text = ''.join(["^Z" if x == '\x1a' else x for x in text])
@@ -320,8 +349,17 @@ def prepare(self) -> None:
320349
self.__gone_tall = 0
321350
self.__offset = 0
322351

352+
if self.__vt_support:
353+
SetConsoleMode(InHandle, self.__original_input_mode | ENABLE_VIRTUAL_TERMINAL_INPUT)
354+
if self.__vt_bracketed_paste:
355+
self._enable_bracketed_paste()
356+
323357
def restore(self) -> None:
324-
pass
358+
if self.__vt_support:
359+
# Recover to original mode before running REPL
360+
SetConsoleMode(InHandle, self.__original_input_mode)
361+
if self.__vt_bracketed_paste:
362+
self._disable_bracketed_paste()
325363

326364
def _move_relative(self, x: int, y: int) -> None:
327365
"""Moves relative to the current __posxy"""
@@ -342,7 +380,7 @@ def move_cursor(self, x: int, y: int) -> None:
342380
raise ValueError(f"Bad cursor position {x}, {y}")
343381

344382
if y < self.__offset or y >= self.__offset + self.height:
345-
self.event_queue.insert(0, Event("scroll", ""))
383+
self.event_queue.insert(Event("scroll", ""))
346384
else:
347385
self._move_relative(x, y)
348386
self.__posxy = x, y
@@ -386,10 +424,8 @@ def get_event(self, block: bool = True) -> Event | None:
386424
"""Return an Event instance. Returns None if |block| is false
387425
and there is no event pending, otherwise waits for the
388426
completion of an event."""
389-
if self.event_queue:
390-
return self.event_queue.pop()
391427

392-
while True:
428+
while self.event_queue.empty():
393429
rec = self._read_input()
394430
if rec is None:
395431
if block:
@@ -428,8 +464,13 @@ def get_event(self, block: bool = True) -> Event | None:
428464
continue
429465

430466
return None
467+
elif self.__vt_support:
468+
# If virtual terminal is enabled, scanning VT sequences
469+
self.event_queue.push(rec.Event.KeyEvent.uChar.UnicodeChar)
470+
continue
431471

432472
return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar)
473+
return self.event_queue.get()
433474

434475
def push_char(self, char: int | bytes) -> None:
435476
"""
@@ -551,6 +592,13 @@ class INPUT_RECORD(Structure):
551592
MOUSE_EVENT = 0x02
552593
WINDOW_BUFFER_SIZE_EVENT = 0x04
553594

595+
ENABLE_PROCESSED_INPUT = 0x0001
596+
ENABLE_LINE_INPUT = 0x0002
597+
ENABLE_ECHO_INPUT = 0x0004
598+
ENABLE_MOUSE_INPUT = 0x0010
599+
ENABLE_INSERT_MODE = 0x0020
600+
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
601+
554602
ENABLE_PROCESSED_OUTPUT = 0x01
555603
ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
556604
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
@@ -582,6 +630,10 @@ class INPUT_RECORD(Structure):
582630
]
583631
ScrollConsoleScreenBuffer.restype = BOOL
584632

633+
GetConsoleMode = _KERNEL32.GetConsoleMode
634+
GetConsoleMode.argtypes = [HANDLE, POINTER(DWORD)]
635+
GetConsoleMode.restype = BOOL
636+
585637
SetConsoleMode = _KERNEL32.SetConsoleMode
586638
SetConsoleMode.argtypes = [HANDLE, DWORD]
587639
SetConsoleMode.restype = BOOL
@@ -600,6 +652,7 @@ def _win_only(*args, **kwargs):
600652
GetStdHandle = _win_only
601653
GetConsoleScreenBufferInfo = _win_only
602654
ScrollConsoleScreenBuffer = _win_only
655+
GetConsoleMode = _win_only
603656
SetConsoleMode = _win_only
604657
ReadConsoleInput = _win_only
605658
OutHandle = 0

0 commit comments

Comments
 (0)