win32_pipe.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. from __future__ import annotations
  2. import sys
  3. assert sys.platform == "win32"
  4. from contextlib import contextmanager
  5. from ctypes import windll
  6. from ctypes.wintypes import HANDLE
  7. from typing import Callable, ContextManager, Iterator
  8. from prompt_toolkit.eventloop.win32 import create_win32_event
  9. from ..key_binding import KeyPress
  10. from ..utils import DummyContext
  11. from .base import PipeInput
  12. from .vt100_parser import Vt100Parser
  13. from .win32 import _Win32InputBase, attach_win32_input, detach_win32_input
  14. __all__ = ["Win32PipeInput"]
  15. class Win32PipeInput(_Win32InputBase, PipeInput):
  16. """
  17. This is an input pipe that works on Windows.
  18. Text or bytes can be feed into the pipe, and key strokes can be read from
  19. the pipe. This is useful if we want to send the input programmatically into
  20. the application. Mostly useful for unit testing.
  21. Notice that even though it's Windows, we use vt100 escape sequences over
  22. the pipe.
  23. Usage::
  24. input = Win32PipeInput()
  25. input.send_text('inputdata')
  26. """
  27. _id = 0
  28. def __init__(self, _event: HANDLE) -> None:
  29. super().__init__()
  30. # Event (handle) for registering this input in the event loop.
  31. # This event is set when there is data available to read from the pipe.
  32. # Note: We use this approach instead of using a regular pipe, like
  33. # returned from `os.pipe()`, because making such a regular pipe
  34. # non-blocking is tricky and this works really well.
  35. self._event = create_win32_event()
  36. self._closed = False
  37. # Parser for incoming keys.
  38. self._buffer: list[KeyPress] = [] # Buffer to collect the Key objects.
  39. self.vt100_parser = Vt100Parser(lambda key: self._buffer.append(key))
  40. # Identifier for every PipeInput for the hash.
  41. self.__class__._id += 1
  42. self._id = self.__class__._id
  43. @classmethod
  44. @contextmanager
  45. def create(cls) -> Iterator[Win32PipeInput]:
  46. event = create_win32_event()
  47. try:
  48. yield Win32PipeInput(_event=event)
  49. finally:
  50. windll.kernel32.CloseHandle(event)
  51. @property
  52. def closed(self) -> bool:
  53. return self._closed
  54. def fileno(self) -> int:
  55. """
  56. The windows pipe doesn't depend on the file handle.
  57. """
  58. raise NotImplementedError
  59. @property
  60. def handle(self) -> HANDLE:
  61. "The handle used for registering this pipe in the event loop."
  62. return self._event
  63. def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]:
  64. """
  65. Return a context manager that makes this input active in the current
  66. event loop.
  67. """
  68. return attach_win32_input(self, input_ready_callback)
  69. def detach(self) -> ContextManager[None]:
  70. """
  71. Return a context manager that makes sure that this input is not active
  72. in the current event loop.
  73. """
  74. return detach_win32_input(self)
  75. def read_keys(self) -> list[KeyPress]:
  76. "Read list of KeyPress."
  77. # Return result.
  78. result = self._buffer
  79. self._buffer = []
  80. # Reset event.
  81. if not self._closed:
  82. # (If closed, the event should not reset.)
  83. windll.kernel32.ResetEvent(self._event)
  84. return result
  85. def flush_keys(self) -> list[KeyPress]:
  86. """
  87. Flush pending keys and return them.
  88. (Used for flushing the 'escape' key.)
  89. """
  90. # Flush all pending keys. (This is most important to flush the vt100
  91. # 'Escape' key early when nothing else follows.)
  92. self.vt100_parser.flush()
  93. # Return result.
  94. result = self._buffer
  95. self._buffer = []
  96. return result
  97. def send_bytes(self, data: bytes) -> None:
  98. "Send bytes to the input."
  99. self.send_text(data.decode("utf-8", "ignore"))
  100. def send_text(self, text: str) -> None:
  101. "Send text to the input."
  102. if self._closed:
  103. raise ValueError("Attempt to write into a closed pipe.")
  104. # Pass it through our vt100 parser.
  105. self.vt100_parser.feed(text)
  106. # Set event.
  107. windll.kernel32.SetEvent(self._event)
  108. def raw_mode(self) -> ContextManager[None]:
  109. return DummyContext()
  110. def cooked_mode(self) -> ContextManager[None]:
  111. return DummyContext()
  112. def close(self) -> None:
  113. "Close write-end of the pipe."
  114. self._closed = True
  115. windll.kernel32.SetEvent(self._event)
  116. def typeahead_hash(self) -> str:
  117. """
  118. This needs to be unique for every `PipeInput`.
  119. """
  120. return f"pipe-input-{self._id}"