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"
@@ -110,6 +113,12 @@ def __init__(self, err: int | None, descr: str | None = None) -> None:
110
113
class _error (Exception ):
111
114
pass
112
115
116
+ def _supports_vt ():
117
+ try :
118
+ import nt
119
+ return nt ._supports_virtual_terminal ()
120
+ except (ImportError , AttributeError ):
121
+ return False
113
122
114
123
class WindowsConsole (Console ):
115
124
def __init__ (
@@ -121,17 +130,29 @@ def __init__(
121
130
):
122
131
super ().__init__ (f_in , f_out , term , encoding )
123
132
133
+ self .__vt_support = _supports_vt ()
134
+
135
+ if self .__vt_support :
136
+ trace ('console supports virtual terminal' )
137
+
138
+ # Save original console modes so we can recover on cleanup.
139
+ original_input_mode = DWORD ()
140
+ GetConsoleMode (InHandle , original_input_mode )
141
+ trace (f'saved original input mode 0x{ original_input_mode .value :x} ' )
142
+ self .__original_input_mode = original_input_mode .value
143
+
124
144
SetConsoleMode (
125
145
OutHandle ,
126
146
ENABLE_WRAP_AT_EOL_OUTPUT
127
147
| ENABLE_PROCESSED_OUTPUT
128
148
| ENABLE_VIRTUAL_TERMINAL_PROCESSING ,
129
149
)
150
+
130
151
self .screen : list [str ] = []
131
152
self .width = 80
132
153
self .height = 25
133
154
self .__offset = 0
134
- self .event_queue : deque [ Event ] = deque ( )
155
+ self .event_queue = EventQueue ( encoding )
135
156
try :
136
157
self .out = io ._WindowsConsoleIO (self .output_fd , "w" ) # type: ignore[attr-defined]
137
158
except ValueError :
@@ -295,6 +316,12 @@ def _enable_blinking(self):
295
316
def _disable_blinking (self ):
296
317
self .__write ("\x1b [?12l" )
297
318
319
+ def _enable_bracketed_paste (self ) -> None :
320
+ self .__write ("\x1b [?2004h" )
321
+
322
+ def _disable_bracketed_paste (self ) -> None :
323
+ self .__write ("\x1b [?2004l" )
324
+
298
325
def __write (self , text : str ) -> None :
299
326
if "\x1a " in text :
300
327
text = '' .join (["^Z" if x == '\x1a ' else x for x in text ])
@@ -324,8 +351,15 @@ def prepare(self) -> None:
324
351
self .__gone_tall = 0
325
352
self .__offset = 0
326
353
354
+ if self .__vt_support :
355
+ SetConsoleMode (InHandle , self .__original_input_mode | ENABLE_VIRTUAL_TERMINAL_INPUT )
356
+ self ._enable_bracketed_paste ()
357
+
327
358
def restore (self ) -> None :
328
- pass
359
+ if self .__vt_support :
360
+ # Recover to original mode before running REPL
361
+ self ._disable_bracketed_paste ()
362
+ SetConsoleMode (InHandle , self .__original_input_mode )
329
363
330
364
def _move_relative (self , x : int , y : int ) -> None :
331
365
"""Moves relative to the current posxy"""
@@ -346,7 +380,7 @@ def move_cursor(self, x: int, y: int) -> None:
346
380
raise ValueError (f"Bad cursor position { x } , { y } " )
347
381
348
382
if y < self .__offset or y >= self .__offset + self .height :
349
- self .event_queue .insert (0 , Event ("scroll" , "" ))
383
+ self .event_queue .insert (Event ("scroll" , "" ))
350
384
else :
351
385
self ._move_relative (x , y )
352
386
self .posxy = x , y
@@ -394,10 +428,8 @@ def get_event(self, block: bool = True) -> Event | None:
394
428
"""Return an Event instance. Returns None if |block| is false
395
429
and there is no event pending, otherwise waits for the
396
430
completion of an event."""
397
- if self .event_queue :
398
- return self .event_queue .pop ()
399
431
400
- while True :
432
+ while self . event_queue . empty () :
401
433
rec = self ._read_input (block )
402
434
if rec is None :
403
435
return None
@@ -428,20 +460,25 @@ def get_event(self, block: bool = True) -> Event | None:
428
460
key = f"ctrl { key } "
429
461
elif key_event .dwControlKeyState & ALT_ACTIVE :
430
462
# queue the key, return the meta command
431
- self .event_queue .insert (0 , Event (evt = "key" , data = key , raw = key ))
463
+ self .event_queue .insert (Event (evt = "key" , data = key , raw = key ))
432
464
return Event (evt = "key" , data = "\033 " ) # keymap.py uses this for meta
433
465
return Event (evt = "key" , data = key , raw = key )
434
466
if block :
435
467
continue
436
468
437
469
return None
470
+ elif self .__vt_support :
471
+ # If virtual terminal is enabled, scanning VT sequences
472
+ self .event_queue .push (rec .Event .KeyEvent .uChar .UnicodeChar )
473
+ continue
438
474
439
475
if key_event .dwControlKeyState & ALT_ACTIVE :
440
476
# queue the key, return the meta command
441
- self .event_queue .insert (0 , Event (evt = "key" , data = key , raw = raw_key ))
477
+ self .event_queue .insert (Event (evt = "key" , data = key , raw = raw_key ))
442
478
return Event (evt = "key" , data = "\033 " ) # keymap.py uses this for meta
443
479
444
480
return Event (evt = "key" , data = key , raw = raw_key )
481
+ return self .event_queue .get ()
445
482
446
483
def push_char (self , char : int | bytes ) -> None :
447
484
"""
@@ -563,6 +600,13 @@ class INPUT_RECORD(Structure):
563
600
MOUSE_EVENT = 0x02
564
601
WINDOW_BUFFER_SIZE_EVENT = 0x04
565
602
603
+ ENABLE_PROCESSED_INPUT = 0x0001
604
+ ENABLE_LINE_INPUT = 0x0002
605
+ ENABLE_ECHO_INPUT = 0x0004
606
+ ENABLE_MOUSE_INPUT = 0x0010
607
+ ENABLE_INSERT_MODE = 0x0020
608
+ ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
609
+
566
610
ENABLE_PROCESSED_OUTPUT = 0x01
567
611
ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
568
612
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
@@ -594,6 +638,10 @@ class INPUT_RECORD(Structure):
594
638
]
595
639
ScrollConsoleScreenBuffer .restype = BOOL
596
640
641
+ GetConsoleMode = _KERNEL32 .GetConsoleMode
642
+ GetConsoleMode .argtypes = [HANDLE , POINTER (DWORD )]
643
+ GetConsoleMode .restype = BOOL
644
+
597
645
SetConsoleMode = _KERNEL32 .SetConsoleMode
598
646
SetConsoleMode .argtypes = [HANDLE , DWORD ]
599
647
SetConsoleMode .restype = BOOL
@@ -620,6 +668,7 @@ def _win_only(*args, **kwargs):
620
668
GetStdHandle = _win_only
621
669
GetConsoleScreenBufferInfo = _win_only
622
670
ScrollConsoleScreenBuffer = _win_only
671
+ GetConsoleMode = _win_only
623
672
SetConsoleMode = _win_only
624
673
ReadConsoleInput = _win_only
625
674
GetNumberOfConsoleInputEvents = _win_only
0 commit comments