16
16
import msvcrt
17
17
from ctypes import windll
18
18
19
- from ctypes import Array , pointer
19
+ from ctypes import Array , byref , pointer
20
20
from ctypes .wintypes import DWORD , HANDLE
21
21
from typing import Callable , ContextManager , Iterable , Iterator , TextIO
22
22
35
35
36
36
from .ansi_escape_sequences import REVERSE_ANSI_SEQUENCES
37
37
from .base import Input
38
+ from .vt100_parser import Vt100Parser
38
39
39
40
__all__ = [
40
41
"Win32Input" ,
52
53
MOUSE_MOVED = 0x0001
53
54
MOUSE_WHEELED = 0x0004
54
55
56
+ # See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx
57
+ ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
58
+
55
59
56
60
class _Win32InputBase (Input ):
57
61
"""
@@ -74,7 +78,12 @@ class Win32Input(_Win32InputBase):
74
78
75
79
def __init__ (self , stdin : TextIO | None = None ) -> None :
76
80
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 ()
78
87
79
88
def attach (self , input_ready_callback : Callable [[], None ]) -> ContextManager [None ]:
80
89
"""
@@ -101,7 +110,9 @@ def closed(self) -> bool:
101
110
return False
102
111
103
112
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
+ )
105
116
106
117
def cooked_mode (self ) -> ContextManager [None ]:
107
118
return cooked_mode ()
@@ -555,6 +566,102 @@ def _handle_mouse(self, ev: MOUSE_EVENT_RECORD) -> list[KeyPress]:
555
566
return [KeyPress (Keys .WindowsMouseEvent , data )]
556
567
557
568
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
+
558
665
class _Win32Handles :
559
666
"""
560
667
Utility to keep track of which handles are connectod to which callbacks.
@@ -700,8 +807,11 @@ class raw_mode:
700
807
`raw_input` method of `.vt100_input`.
701
808
"""
702
809
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 :
704
813
self .handle = HANDLE (windll .kernel32 .GetStdHandle (STD_INPUT_HANDLE ))
814
+ self .use_win10_virtual_terminal_input = use_win10_virtual_terminal_input
705
815
706
816
def __enter__ (self ) -> None :
707
817
# Remember original mode.
@@ -717,12 +827,15 @@ def _patch(self) -> None:
717
827
ENABLE_LINE_INPUT = 0x0002
718
828
ENABLE_PROCESSED_INPUT = 0x0001
719
829
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
724
832
)
725
833
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
+
726
839
def __exit__ (self , * a : object ) -> None :
727
840
# Restore original mode
728
841
windll .kernel32 .SetConsoleMode (self .handle , self .original_mode )
@@ -747,3 +860,25 @@ def _patch(self) -> None:
747
860
self .original_mode .value
748
861
| (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT ),
749
862
)
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