4242from .console import Event , Console
4343from .trace import trace
4444from .utils import wlen
45+ from .windows_eventqueue import EventQueue
4546
4647try :
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
98101ERASE_IN_LINE = "\x1b [K"
99102MOVE_LEFT = "\x1b [{}D"
100103MOVE_RIGHT = "\x1b [{}C"
@@ -110,6 +113,12 @@ def __init__(self, err: int | None, descr: str | None = None) -> None:
110113class _error (Exception ):
111114 pass
112115
116+ def _supports_vt ():
117+ try :
118+ import nt
119+ return nt ._supports_virtual_terminal ()
120+ except (ImportError , AttributeError ):
121+ return False
113122
114123class WindowsConsole (Console ):
115124 def __init__ (
@@ -121,17 +130,29 @@ def __init__(
121130 ):
122131 super ().__init__ (f_in , f_out , term , encoding )
123132
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+
124144 SetConsoleMode (
125145 OutHandle ,
126146 ENABLE_WRAP_AT_EOL_OUTPUT
127147 | ENABLE_PROCESSED_OUTPUT
128148 | ENABLE_VIRTUAL_TERMINAL_PROCESSING ,
129149 )
150+
130151 self .screen : list [str ] = []
131152 self .width = 80
132153 self .height = 25
133154 self .__offset = 0
134- self .event_queue : deque [ Event ] = deque ( )
155+ self .event_queue = EventQueue ( encoding )
135156 try :
136157 self .out = io ._WindowsConsoleIO (self .output_fd , "w" ) # type: ignore[attr-defined]
137158 except ValueError :
@@ -295,6 +316,12 @@ def _enable_blinking(self):
295316 def _disable_blinking (self ):
296317 self .__write ("\x1b [?12l" )
297318
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+
298325 def __write (self , text : str ) -> None :
299326 if "\x1a " in text :
300327 text = '' .join (["^Z" if x == '\x1a ' else x for x in text ])
@@ -324,8 +351,15 @@ def prepare(self) -> None:
324351 self .__gone_tall = 0
325352 self .__offset = 0
326353
354+ if self .__vt_support :
355+ SetConsoleMode (InHandle , self .__original_input_mode | ENABLE_VIRTUAL_TERMINAL_INPUT )
356+ self ._enable_bracketed_paste ()
357+
327358 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 )
329363
330364 def _move_relative (self , x : int , y : int ) -> None :
331365 """Moves relative to the current posxy"""
@@ -346,7 +380,7 @@ def move_cursor(self, x: int, y: int) -> None:
346380 raise ValueError (f"Bad cursor position { x } , { y } " )
347381
348382 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" , "" ))
350384 else :
351385 self ._move_relative (x , y )
352386 self .posxy = x , y
@@ -394,10 +428,8 @@ def get_event(self, block: bool = True) -> Event | None:
394428 """Return an Event instance. Returns None if |block| is false
395429 and there is no event pending, otherwise waits for the
396430 completion of an event."""
397- if self .event_queue :
398- return self .event_queue .pop ()
399431
400- while True :
432+ while self . event_queue . empty () :
401433 rec = self ._read_input (block )
402434 if rec is None :
403435 return None
@@ -428,20 +460,25 @@ def get_event(self, block: bool = True) -> Event | None:
428460 key = f"ctrl { key } "
429461 elif key_event .dwControlKeyState & ALT_ACTIVE :
430462 # 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 ))
432464 return Event (evt = "key" , data = "\033 " ) # keymap.py uses this for meta
433465 return Event (evt = "key" , data = key , raw = key )
434466 if block :
435467 continue
436468
437469 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
438474
439475 if key_event .dwControlKeyState & ALT_ACTIVE :
440476 # 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 ))
442478 return Event (evt = "key" , data = "\033 " ) # keymap.py uses this for meta
443479
444480 return Event (evt = "key" , data = key , raw = raw_key )
481+ return self .event_queue .get ()
445482
446483 def push_char (self , char : int | bytes ) -> None :
447484 """
@@ -563,6 +600,13 @@ class INPUT_RECORD(Structure):
563600MOUSE_EVENT = 0x02
564601WINDOW_BUFFER_SIZE_EVENT = 0x04
565602
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+
566610ENABLE_PROCESSED_OUTPUT = 0x01
567611ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
568612ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
@@ -594,6 +638,10 @@ class INPUT_RECORD(Structure):
594638 ]
595639 ScrollConsoleScreenBuffer .restype = BOOL
596640
641+ GetConsoleMode = _KERNEL32 .GetConsoleMode
642+ GetConsoleMode .argtypes = [HANDLE , POINTER (DWORD )]
643+ GetConsoleMode .restype = BOOL
644+
597645 SetConsoleMode = _KERNEL32 .SetConsoleMode
598646 SetConsoleMode .argtypes = [HANDLE , DWORD ]
599647 SetConsoleMode .restype = BOOL
@@ -620,6 +668,7 @@ def _win_only(*args, **kwargs):
620668 GetStdHandle = _win_only
621669 GetConsoleScreenBufferInfo = _win_only
622670 ScrollConsoleScreenBuffer = _win_only
671+ GetConsoleMode = _win_only
623672 SetConsoleMode = _win_only
624673 ReadConsoleInput = _win_only
625674 GetNumberOfConsoleInputEvents = _win_only
0 commit comments