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,14 @@ 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
+ self .console_input_reader : Vt100ConsoleInputReader | ConsoleInputReader
84
+
85
+ if self ._use_virtual_terminal_input :
86
+ self .console_input_reader = Vt100ConsoleInputReader ()
87
+ else :
88
+ self .console_input_reader = ConsoleInputReader ()
78
89
79
90
def attach (self , input_ready_callback : Callable [[], None ]) -> ContextManager [None ]:
80
91
"""
@@ -101,7 +112,9 @@ def closed(self) -> bool:
101
112
return False
102
113
103
114
def raw_mode (self ) -> ContextManager [None ]:
104
- return raw_mode ()
115
+ return raw_mode (
116
+ use_win10_virtual_terminal_input = self ._use_virtual_terminal_input
117
+ )
105
118
106
119
def cooked_mode (self ) -> ContextManager [None ]:
107
120
return cooked_mode ()
@@ -555,6 +568,102 @@ def _handle_mouse(self, ev: MOUSE_EVENT_RECORD) -> list[KeyPress]:
555
568
return [KeyPress (Keys .WindowsMouseEvent , data )]
556
569
557
570
571
+ class Vt100ConsoleInputReader :
572
+ """
573
+ Similar to `ConsoleInputReader`, but for usage when
574
+ `ENABLE_VIRTUAL_TERMINAL_INPUT` is enabled. This assumes that Windows sends
575
+ us the right vt100 escape sequences and we parse those with our vt100
576
+ parser.
577
+
578
+ (Using this instead of `ConsoleInputReader` results in the "data" attribute
579
+ from the `KeyPress` instances to be more correct in edge cases, because
580
+ this responds to for instance the terminal being in application cursor keys
581
+ mode.)
582
+ """
583
+
584
+ def __init__ (self ) -> None :
585
+ self ._fdcon = None
586
+
587
+ self ._buffer : list [KeyPress ] = [] # Buffer to collect the Key objects.
588
+ self ._vt100_parser = Vt100Parser (
589
+ lambda key_press : self ._buffer .append (key_press )
590
+ )
591
+
592
+ # When stdin is a tty, use that handle, otherwise, create a handle from
593
+ # CONIN$.
594
+ self .handle : HANDLE
595
+ if sys .stdin .isatty ():
596
+ self .handle = HANDLE (windll .kernel32 .GetStdHandle (STD_INPUT_HANDLE ))
597
+ else :
598
+ self ._fdcon = os .open ("CONIN$" , os .O_RDWR | os .O_BINARY )
599
+ self .handle = HANDLE (msvcrt .get_osfhandle (self ._fdcon ))
600
+
601
+ def close (self ) -> None :
602
+ "Close fdcon."
603
+ if self ._fdcon is not None :
604
+ os .close (self ._fdcon )
605
+
606
+ def read (self ) -> Iterable [KeyPress ]:
607
+ """
608
+ Return a list of `KeyPress` instances. It won't return anything when
609
+ there was nothing to read. (This function doesn't block.)
610
+
611
+ http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx
612
+ """
613
+ max_count = 2048 # Max events to read at the same time.
614
+
615
+ read = DWORD (0 )
616
+ arrtype = INPUT_RECORD * max_count
617
+ input_records = arrtype ()
618
+
619
+ # Check whether there is some input to read. `ReadConsoleInputW` would
620
+ # block otherwise.
621
+ # (Actually, the event loop is responsible to make sure that this
622
+ # function is only called when there is something to read, but for some
623
+ # reason this happened in the asyncio_win32 loop, and it's better to be
624
+ # safe anyway.)
625
+ if not wait_for_handles ([self .handle ], timeout = 0 ):
626
+ return []
627
+
628
+ # Get next batch of input event.
629
+ windll .kernel32 .ReadConsoleInputW (
630
+ self .handle , pointer (input_records ), max_count , pointer (read )
631
+ )
632
+
633
+ # First, get all the keys from the input buffer, in order to determine
634
+ # whether we should consider this a paste event or not.
635
+ for key_data in self ._get_keys (read , input_records ):
636
+ self ._vt100_parser .feed (key_data )
637
+
638
+ # Return result.
639
+ result = self ._buffer
640
+ self ._buffer = []
641
+ return result
642
+
643
+ def _get_keys (
644
+ self , read : DWORD , input_records : Array [INPUT_RECORD ]
645
+ ) -> Iterator [str ]:
646
+ """
647
+ Generator that yields `KeyPress` objects from the input records.
648
+ """
649
+ for i in range (read .value ):
650
+ ir = input_records [i ]
651
+
652
+ # Get the right EventType from the EVENT_RECORD.
653
+ # (For some reason the Windows console application 'cmder'
654
+ # [http://gooseberrycreative.com/cmder/] can return '0' for
655
+ # ir.EventType. -- Just ignore that.)
656
+ if ir .EventType in EventTypes :
657
+ ev = getattr (ir .Event , EventTypes [ir .EventType ])
658
+
659
+ # Process if this is a key event. (We also have mouse, menu and
660
+ # focus events.)
661
+ if isinstance (ev , KEY_EVENT_RECORD ) and ev .KeyDown :
662
+ u_char = ev .uChar .UnicodeChar
663
+ if u_char != "\x00 " :
664
+ yield u_char
665
+
666
+
558
667
class _Win32Handles :
559
668
"""
560
669
Utility to keep track of which handles are connectod to which callbacks.
@@ -700,8 +809,11 @@ class raw_mode:
700
809
`raw_input` method of `.vt100_input`.
701
810
"""
702
811
703
- def __init__ (self , fileno : int | None = None ) -> None :
812
+ def __init__ (
813
+ self , fileno : int | None = None , use_win10_virtual_terminal_input : bool = False
814
+ ) -> None :
704
815
self .handle = HANDLE (windll .kernel32 .GetStdHandle (STD_INPUT_HANDLE ))
816
+ self .use_win10_virtual_terminal_input = use_win10_virtual_terminal_input
705
817
706
818
def __enter__ (self ) -> None :
707
819
# Remember original mode.
@@ -717,12 +829,15 @@ def _patch(self) -> None:
717
829
ENABLE_LINE_INPUT = 0x0002
718
830
ENABLE_PROCESSED_INPUT = 0x0001
719
831
720
- windll .kernel32 .SetConsoleMode (
721
- self .handle ,
722
- self .original_mode .value
723
- & ~ (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT ),
832
+ new_mode = self .original_mode .value & ~ (
833
+ ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT
724
834
)
725
835
836
+ if self .use_win10_virtual_terminal_input :
837
+ new_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT
838
+
839
+ windll .kernel32 .SetConsoleMode (self .handle , new_mode )
840
+
726
841
def __exit__ (self , * a : object ) -> None :
727
842
# Restore original mode
728
843
windll .kernel32 .SetConsoleMode (self .handle , self .original_mode )
@@ -747,3 +862,25 @@ def _patch(self) -> None:
747
862
self .original_mode .value
748
863
| (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT ),
749
864
)
865
+
866
+
867
+ def _is_win_vt100_input_enabled () -> bool :
868
+ """
869
+ Returns True when we're running Windows and VT100 escape sequences are
870
+ supported.
871
+ """
872
+ hconsole = HANDLE (windll .kernel32 .GetStdHandle (STD_INPUT_HANDLE ))
873
+
874
+ # Get original console mode.
875
+ original_mode = DWORD (0 )
876
+ windll .kernel32 .GetConsoleMode (hconsole , byref (original_mode ))
877
+
878
+ try :
879
+ # Try to enable VT100 sequences.
880
+ result : int = windll .kernel32 .SetConsoleMode (
881
+ hconsole , DWORD (ENABLE_VIRTUAL_TERMINAL_INPUT )
882
+ )
883
+
884
+ return result == 1
885
+ finally :
886
+ windll .kernel32 .SetConsoleMode (hconsole , original_mode )
0 commit comments