1616 import msvcrt
1717 from ctypes import windll
1818
19- from ctypes import Array , pointer
19+ from ctypes import Array , byref , pointer
2020from ctypes .wintypes import DWORD , HANDLE
2121from typing import Callable , ContextManager , Iterable , Iterator , TextIO
2222
3535
3636from .ansi_escape_sequences import REVERSE_ANSI_SEQUENCES
3737from .base import Input
38+ from .vt100_parser import Vt100Parser
3839
3940__all__ = [
4041 "Win32Input" ,
5253MOUSE_MOVED = 0x0001
5354MOUSE_WHEELED = 0x0004
5455
56+ # See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx
57+ ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
58+
5559
5660class _Win32InputBase (Input ):
5761 """
@@ -74,7 +78,12 @@ class Win32Input(_Win32InputBase):
7478
7579 def __init__ (self , stdin : TextIO | None = None ) -> None :
7680 super ().__init__ ()
77- self .console_input_reader = ConsoleInputReader ()
81+ self ._use_virtual_terminal_input = _is_win_vt100_input_enabled ()
82+
83+ if self ._use_virtual_terminal_input :
84+ self .console_input_reader = Vt100ConsoleInputReader ()
85+ else :
86+ self .console_input_reader = ConsoleInputReader ()
7887
7988 def attach (self , input_ready_callback : Callable [[], None ]) -> ContextManager [None ]:
8089 """
@@ -101,7 +110,9 @@ def closed(self) -> bool:
101110 return False
102111
103112 def raw_mode (self ) -> ContextManager [None ]:
104- return raw_mode ()
113+ return raw_mode (
114+ use_win10_virtual_terminal_input = self ._use_virtual_terminal_input
115+ )
105116
106117 def cooked_mode (self ) -> ContextManager [None ]:
107118 return cooked_mode ()
@@ -555,6 +566,102 @@ def _handle_mouse(self, ev: MOUSE_EVENT_RECORD) -> list[KeyPress]:
555566 return [KeyPress (Keys .WindowsMouseEvent , data )]
556567
557568
569+ class Vt100ConsoleInputReader :
570+ """
571+ Similar to `ConsoleInputReader`, but for usage when
572+ `ENABLE_VIRTUAL_TERMINAL_INPUT` is enabled. This assumes that Windows sends
573+ us the right vt100 escape sequences and we parse those with our vt100
574+ parser.
575+
576+ (Using this instead of `ConsoleInputReader` results in the "data" attribute
577+ from the `KeyPress` instances to be more correct in edge cases, because
578+ this responds to for instance the terminal being in application cursor keys
579+ mode.)
580+ """
581+
582+ def __init__ (self ) -> None :
583+ self ._fdcon = None
584+
585+ self ._buffer : list [KeyPress ] = [] # Buffer to collect the Key objects.
586+ self ._vt100_parser = Vt100Parser (
587+ lambda key_press : self ._buffer .append (key_press )
588+ )
589+
590+ # When stdin is a tty, use that handle, otherwise, create a handle from
591+ # CONIN$.
592+ self .handle : HANDLE
593+ if sys .stdin .isatty ():
594+ self .handle = HANDLE (windll .kernel32 .GetStdHandle (STD_INPUT_HANDLE ))
595+ else :
596+ self ._fdcon = os .open ("CONIN$" , os .O_RDWR | os .O_BINARY )
597+ self .handle = HANDLE (msvcrt .get_osfhandle (self ._fdcon ))
598+
599+ def close (self ) -> None :
600+ "Close fdcon."
601+ if self ._fdcon is not None :
602+ os .close (self ._fdcon )
603+
604+ def read (self ) -> Iterable [KeyPress ]:
605+ """
606+ Return a list of `KeyPress` instances. It won't return anything when
607+ there was nothing to read. (This function doesn't block.)
608+
609+ http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx
610+ """
611+ max_count = 2048 # Max events to read at the same time.
612+
613+ read = DWORD (0 )
614+ arrtype = INPUT_RECORD * max_count
615+ input_records = arrtype ()
616+
617+ # Check whether there is some input to read. `ReadConsoleInputW` would
618+ # block otherwise.
619+ # (Actually, the event loop is responsible to make sure that this
620+ # function is only called when there is something to read, but for some
621+ # reason this happened in the asyncio_win32 loop, and it's better to be
622+ # safe anyway.)
623+ if not wait_for_handles ([self .handle ], timeout = 0 ):
624+ return
625+
626+ # Get next batch of input event.
627+ windll .kernel32 .ReadConsoleInputW (
628+ self .handle , pointer (input_records ), max_count , pointer (read )
629+ )
630+
631+ # First, get all the keys from the input buffer, in order to determine
632+ # whether we should consider this a paste event or not.
633+ for key_data in self ._get_keys (read , input_records ):
634+ self ._vt100_parser .feed (key_data )
635+
636+ # Return result.
637+ result = self ._buffer
638+ self ._buffer = []
639+ return result
640+
641+ def _get_keys (
642+ self , read : DWORD , input_records : Array [INPUT_RECORD ]
643+ ) -> Iterator [str ]:
644+ """
645+ Generator that yields `KeyPress` objects from the input records.
646+ """
647+ for i in range (read .value ):
648+ ir = input_records [i ]
649+
650+ # Get the right EventType from the EVENT_RECORD.
651+ # (For some reason the Windows console application 'cmder'
652+ # [http://gooseberrycreative.com/cmder/] can return '0' for
653+ # ir.EventType. -- Just ignore that.)
654+ if ir .EventType in EventTypes :
655+ ev = getattr (ir .Event , EventTypes [ir .EventType ])
656+
657+ # Process if this is a key event. (We also have mouse, menu and
658+ # focus events.)
659+ if isinstance (ev , KEY_EVENT_RECORD ) and ev .KeyDown :
660+ u_char = ev .uChar .UnicodeChar
661+ if u_char != "\x00 " :
662+ yield u_char
663+
664+
558665class _Win32Handles :
559666 """
560667 Utility to keep track of which handles are connectod to which callbacks.
@@ -700,8 +807,11 @@ class raw_mode:
700807 `raw_input` method of `.vt100_input`.
701808 """
702809
703- def __init__ (self , fileno : int | None = None ) -> None :
810+ def __init__ (
811+ self , fileno : int | None = None , use_win10_virtual_terminal_input : bool = False
812+ ) -> None :
704813 self .handle = HANDLE (windll .kernel32 .GetStdHandle (STD_INPUT_HANDLE ))
814+ self .use_win10_virtual_terminal_input = use_win10_virtual_terminal_input
705815
706816 def __enter__ (self ) -> None :
707817 # Remember original mode.
@@ -717,12 +827,15 @@ def _patch(self) -> None:
717827 ENABLE_LINE_INPUT = 0x0002
718828 ENABLE_PROCESSED_INPUT = 0x0001
719829
720- windll .kernel32 .SetConsoleMode (
721- self .handle ,
722- self .original_mode .value
723- & ~ (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT ),
830+ new_mode = self .original_mode .value & ~ (
831+ ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT
724832 )
725833
834+ if self .use_win10_virtual_terminal_input :
835+ new_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT
836+
837+ windll .kernel32 .SetConsoleMode (self .handle , new_mode )
838+
726839 def __exit__ (self , * a : object ) -> None :
727840 # Restore original mode
728841 windll .kernel32 .SetConsoleMode (self .handle , self .original_mode )
@@ -747,3 +860,25 @@ def _patch(self) -> None:
747860 self .original_mode .value
748861 | (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT ),
749862 )
863+
864+
865+ def _is_win_vt100_input_enabled () -> bool :
866+ """
867+ Returns True when we're running Windows and VT100 escape sequences are
868+ supported.
869+ """
870+ hconsole = HANDLE (windll .kernel32 .GetStdHandle (STD_INPUT_HANDLE ))
871+
872+ # Get original console mode.
873+ original_mode = DWORD (0 )
874+ windll .kernel32 .GetConsoleMode (hconsole , byref (original_mode ))
875+
876+ try :
877+ # Try to enable VT100 sequences.
878+ result : int = windll .kernel32 .SetConsoleMode (
879+ hconsole , DWORD (ENABLE_VIRTUAL_TERMINAL_INPUT )
880+ )
881+
882+ return result == 1
883+ finally :
884+ windll .kernel32 .SetConsoleMode (hconsole , original_mode )
0 commit comments