Skip to content

Commit 6c8bb9f

Browse files
Use virtual terminal input on Windows when available.
1 parent 92b3a95 commit 6c8bb9f

File tree

1 file changed

+143
-8
lines changed

1 file changed

+143
-8
lines changed

Diff for: src/prompt_toolkit/input/win32.py

+143-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import msvcrt
1717
from ctypes import windll
1818

19-
from ctypes import Array, pointer
19+
from ctypes import Array, byref, pointer
2020
from ctypes.wintypes import DWORD, HANDLE
2121
from typing import Callable, ContextManager, Iterable, Iterator, TextIO
2222

@@ -35,6 +35,7 @@
3535

3636
from .ansi_escape_sequences import REVERSE_ANSI_SEQUENCES
3737
from .base import Input
38+
from .vt100_parser import Vt100Parser
3839

3940
__all__ = [
4041
"Win32Input",
@@ -52,6 +53,9 @@
5253
MOUSE_MOVED = 0x0001
5354
MOUSE_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

5660
class _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+
558665
class _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

Comments
 (0)