42
42
from .console import Event , Console
43
43
from .trace import trace
44
44
from .utils import wlen
45
+ from .windows_eventqueue import EventQueue
45
46
46
47
try :
47
48
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:
94
95
0x83 : "f20" , # VK_F20
95
96
}
96
97
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
98
101
ERASE_IN_LINE = "\x1b [K"
99
102
MOVE_LEFT = "\x1b [{}D"
100
103
MOVE_RIGHT = "\x1b [{}C"
@@ -106,6 +109,12 @@ def __init__(self, err: int | None, descr: str | None = None) -> None:
106
109
class _error (Exception ):
107
110
pass
108
111
112
+ def _supports_vt ():
113
+ try :
114
+ import nt
115
+ return nt ._supports_virtual_terminal ()
116
+ except (ImportError , AttributeError ):
117
+ return False
109
118
110
119
class WindowsConsole (Console ):
111
120
def __init__ (
@@ -117,17 +126,36 @@ def __init__(
117
126
):
118
127
super ().__init__ (f_in , f_out , term , encoding )
119
128
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
+
120
147
SetConsoleMode (
121
148
OutHandle ,
122
149
ENABLE_WRAP_AT_EOL_OUTPUT
123
150
| ENABLE_PROCESSED_OUTPUT
124
151
| ENABLE_VIRTUAL_TERMINAL_PROCESSING ,
125
152
)
153
+
126
154
self .screen : list [str ] = []
127
155
self .width = 80
128
156
self .height = 25
129
157
self .__offset = 0
130
- self .event_queue : deque [ Event ] = deque ( )
158
+ self .event_queue = EventQueue ( encoding )
131
159
try :
132
160
self .out = io ._WindowsConsoleIO (self .output_fd , "w" ) # type: ignore[attr-defined]
133
161
except ValueError :
@@ -291,6 +319,12 @@ def _enable_blinking(self):
291
319
def _disable_blinking (self ):
292
320
self .__write ("\x1b [?12l" )
293
321
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
+
294
328
def __write (self , text : str ) -> None :
295
329
if "\x1a " in text :
296
330
text = '' .join (["^Z" if x == '\x1a ' else x for x in text ])
@@ -320,8 +354,17 @@ def prepare(self) -> None:
320
354
self .__gone_tall = 0
321
355
self .__offset = 0
322
356
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
+
323
362
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 ()
325
368
326
369
def _move_relative (self , x : int , y : int ) -> None :
327
370
"""Moves relative to the current __posxy"""
@@ -342,7 +385,7 @@ def move_cursor(self, x: int, y: int) -> None:
342
385
raise ValueError (f"Bad cursor position { x } , { y } " )
343
386
344
387
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" , "" ))
346
389
else :
347
390
self ._move_relative (x , y )
348
391
self .__posxy = x , y
@@ -386,10 +429,8 @@ def get_event(self, block: bool = True) -> Event | None:
386
429
"""Return an Event instance. Returns None if |block| is false
387
430
and there is no event pending, otherwise waits for the
388
431
completion of an event."""
389
- if self .event_queue :
390
- return self .event_queue .pop ()
391
432
392
- while True :
433
+ while self . event_queue . empty () :
393
434
rec = self ._read_input ()
394
435
if rec is None :
395
436
if block :
@@ -428,8 +469,13 @@ def get_event(self, block: bool = True) -> Event | None:
428
469
continue
429
470
430
471
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
431
476
432
477
return Event (evt = "key" , data = key , raw = rec .Event .KeyEvent .uChar .UnicodeChar )
478
+ return self .event_queue .get ()
433
479
434
480
def push_char (self , char : int | bytes ) -> None :
435
481
"""
@@ -551,6 +597,13 @@ class INPUT_RECORD(Structure):
551
597
MOUSE_EVENT = 0x02
552
598
WINDOW_BUFFER_SIZE_EVENT = 0x04
553
599
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
+
554
607
ENABLE_PROCESSED_OUTPUT = 0x01
555
608
ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
556
609
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
@@ -582,6 +635,10 @@ class INPUT_RECORD(Structure):
582
635
]
583
636
ScrollConsoleScreenBuffer .restype = BOOL
584
637
638
+ GetConsoleMode = _KERNEL32 .GetConsoleMode
639
+ GetConsoleMode .argtypes = [HANDLE , POINTER (DWORD )]
640
+ GetConsoleMode .restype = BOOL
641
+
585
642
SetConsoleMode = _KERNEL32 .SetConsoleMode
586
643
SetConsoleMode .argtypes = [HANDLE , DWORD ]
587
644
SetConsoleMode .restype = BOOL
@@ -600,6 +657,7 @@ def _win_only(*args, **kwargs):
600
657
GetStdHandle = _win_only
601
658
GetConsoleScreenBufferInfo = _win_only
602
659
ScrollConsoleScreenBuffer = _win_only
660
+ GetConsoleMode = _win_only
603
661
SetConsoleMode = _win_only
604
662
ReadConsoleInput = _win_only
605
663
OutHandle = 0
0 commit comments