fixed subscription table

This commit is contained in:
2025-02-02 00:02:31 -05:00
parent a1ab31acfe
commit ef5f57e678
5389 changed files with 686710 additions and 28 deletions

View File

@@ -0,0 +1,14 @@
from __future__ import annotations
from .base import DummyInput, Input, PipeInput
from .defaults import create_input, create_pipe_input
__all__ = [
# Base.
"Input",
"PipeInput",
"DummyInput",
# Defaults.
"create_input",
"create_pipe_input",
]

View File

@@ -0,0 +1,344 @@
"""
Mappings from VT100 (ANSI) escape sequences to the corresponding prompt_toolkit
keys.
We are not using the terminfo/termcap databases to detect the ANSI escape
sequences for the input. Instead, we recognize 99% of the most common
sequences. This works well, because in practice, every modern terminal is
mostly Xterm compatible.
Some useful docs:
- Mintty: https://github.com/mintty/mintty/blob/master/wiki/Keycodes.md
"""
from __future__ import annotations
from ..keys import Keys
__all__ = [
"ANSI_SEQUENCES",
"REVERSE_ANSI_SEQUENCES",
]
# Mapping of vt100 escape codes to Keys.
ANSI_SEQUENCES: dict[str, Keys | tuple[Keys, ...]] = {
# Control keys.
"\x00": Keys.ControlAt, # Control-At (Also for Ctrl-Space)
"\x01": Keys.ControlA, # Control-A (home)
"\x02": Keys.ControlB, # Control-B (emacs cursor left)
"\x03": Keys.ControlC, # Control-C (interrupt)
"\x04": Keys.ControlD, # Control-D (exit)
"\x05": Keys.ControlE, # Control-E (end)
"\x06": Keys.ControlF, # Control-F (cursor forward)
"\x07": Keys.ControlG, # Control-G
"\x08": Keys.ControlH, # Control-H (8) (Identical to '\b')
"\x09": Keys.ControlI, # Control-I (9) (Identical to '\t')
"\x0a": Keys.ControlJ, # Control-J (10) (Identical to '\n')
"\x0b": Keys.ControlK, # Control-K (delete until end of line; vertical tab)
"\x0c": Keys.ControlL, # Control-L (clear; form feed)
"\x0d": Keys.ControlM, # Control-M (13) (Identical to '\r')
"\x0e": Keys.ControlN, # Control-N (14) (history forward)
"\x0f": Keys.ControlO, # Control-O (15)
"\x10": Keys.ControlP, # Control-P (16) (history back)
"\x11": Keys.ControlQ, # Control-Q
"\x12": Keys.ControlR, # Control-R (18) (reverse search)
"\x13": Keys.ControlS, # Control-S (19) (forward search)
"\x14": Keys.ControlT, # Control-T
"\x15": Keys.ControlU, # Control-U
"\x16": Keys.ControlV, # Control-V
"\x17": Keys.ControlW, # Control-W
"\x18": Keys.ControlX, # Control-X
"\x19": Keys.ControlY, # Control-Y (25)
"\x1a": Keys.ControlZ, # Control-Z
"\x1b": Keys.Escape, # Also Control-[
"\x9b": Keys.ShiftEscape,
"\x1c": Keys.ControlBackslash, # Both Control-\ (also Ctrl-| )
"\x1d": Keys.ControlSquareClose, # Control-]
"\x1e": Keys.ControlCircumflex, # Control-^
"\x1f": Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hyphen.)
# ASCII Delete (0x7f)
# Vt220 (and Linux terminal) send this when pressing backspace. We map this
# to ControlH, because that will make it easier to create key bindings that
# work everywhere, with the trade-off that it's no longer possible to
# handle backspace and control-h individually for the few terminals that
# support it. (Most terminals send ControlH when backspace is pressed.)
# See: http://www.ibb.net/~anne/keyboard.html
"\x7f": Keys.ControlH,
# --
# Various
"\x1b[1~": Keys.Home, # tmux
"\x1b[2~": Keys.Insert,
"\x1b[3~": Keys.Delete,
"\x1b[4~": Keys.End, # tmux
"\x1b[5~": Keys.PageUp,
"\x1b[6~": Keys.PageDown,
"\x1b[7~": Keys.Home, # xrvt
"\x1b[8~": Keys.End, # xrvt
"\x1b[Z": Keys.BackTab, # shift + tab
"\x1b\x09": Keys.BackTab, # Linux console
"\x1b[~": Keys.BackTab, # Windows console
# --
# Function keys.
"\x1bOP": Keys.F1,
"\x1bOQ": Keys.F2,
"\x1bOR": Keys.F3,
"\x1bOS": Keys.F4,
"\x1b[[A": Keys.F1, # Linux console.
"\x1b[[B": Keys.F2, # Linux console.
"\x1b[[C": Keys.F3, # Linux console.
"\x1b[[D": Keys.F4, # Linux console.
"\x1b[[E": Keys.F5, # Linux console.
"\x1b[11~": Keys.F1, # rxvt-unicode
"\x1b[12~": Keys.F2, # rxvt-unicode
"\x1b[13~": Keys.F3, # rxvt-unicode
"\x1b[14~": Keys.F4, # rxvt-unicode
"\x1b[15~": Keys.F5,
"\x1b[17~": Keys.F6,
"\x1b[18~": Keys.F7,
"\x1b[19~": Keys.F8,
"\x1b[20~": Keys.F9,
"\x1b[21~": Keys.F10,
"\x1b[23~": Keys.F11,
"\x1b[24~": Keys.F12,
"\x1b[25~": Keys.F13,
"\x1b[26~": Keys.F14,
"\x1b[28~": Keys.F15,
"\x1b[29~": Keys.F16,
"\x1b[31~": Keys.F17,
"\x1b[32~": Keys.F18,
"\x1b[33~": Keys.F19,
"\x1b[34~": Keys.F20,
# Xterm
"\x1b[1;2P": Keys.F13,
"\x1b[1;2Q": Keys.F14,
# '\x1b[1;2R': Keys.F15, # Conflicts with CPR response.
"\x1b[1;2S": Keys.F16,
"\x1b[15;2~": Keys.F17,
"\x1b[17;2~": Keys.F18,
"\x1b[18;2~": Keys.F19,
"\x1b[19;2~": Keys.F20,
"\x1b[20;2~": Keys.F21,
"\x1b[21;2~": Keys.F22,
"\x1b[23;2~": Keys.F23,
"\x1b[24;2~": Keys.F24,
# --
# CSI 27 disambiguated modified "other" keys (xterm)
# Ref: https://invisible-island.net/xterm/modified-keys.html
# These are currently unsupported, so just re-map some common ones to the
# unmodified versions
"\x1b[27;2;13~": Keys.ControlM, # Shift + Enter
"\x1b[27;5;13~": Keys.ControlM, # Ctrl + Enter
"\x1b[27;6;13~": Keys.ControlM, # Ctrl + Shift + Enter
# --
# Control + function keys.
"\x1b[1;5P": Keys.ControlF1,
"\x1b[1;5Q": Keys.ControlF2,
# "\x1b[1;5R": Keys.ControlF3, # Conflicts with CPR response.
"\x1b[1;5S": Keys.ControlF4,
"\x1b[15;5~": Keys.ControlF5,
"\x1b[17;5~": Keys.ControlF6,
"\x1b[18;5~": Keys.ControlF7,
"\x1b[19;5~": Keys.ControlF8,
"\x1b[20;5~": Keys.ControlF9,
"\x1b[21;5~": Keys.ControlF10,
"\x1b[23;5~": Keys.ControlF11,
"\x1b[24;5~": Keys.ControlF12,
"\x1b[1;6P": Keys.ControlF13,
"\x1b[1;6Q": Keys.ControlF14,
# "\x1b[1;6R": Keys.ControlF15, # Conflicts with CPR response.
"\x1b[1;6S": Keys.ControlF16,
"\x1b[15;6~": Keys.ControlF17,
"\x1b[17;6~": Keys.ControlF18,
"\x1b[18;6~": Keys.ControlF19,
"\x1b[19;6~": Keys.ControlF20,
"\x1b[20;6~": Keys.ControlF21,
"\x1b[21;6~": Keys.ControlF22,
"\x1b[23;6~": Keys.ControlF23,
"\x1b[24;6~": Keys.ControlF24,
# --
# Tmux (Win32 subsystem) sends the following scroll events.
"\x1b[62~": Keys.ScrollUp,
"\x1b[63~": Keys.ScrollDown,
"\x1b[200~": Keys.BracketedPaste, # Start of bracketed paste.
# --
# Sequences generated by numpad 5. Not sure what it means. (It doesn't
# appear in 'infocmp'. Just ignore.
"\x1b[E": Keys.Ignore, # Xterm.
"\x1b[G": Keys.Ignore, # Linux console.
# --
# Meta/control/escape + pageup/pagedown/insert/delete.
"\x1b[3;2~": Keys.ShiftDelete, # xterm, gnome-terminal.
"\x1b[5;2~": Keys.ShiftPageUp,
"\x1b[6;2~": Keys.ShiftPageDown,
"\x1b[2;3~": (Keys.Escape, Keys.Insert),
"\x1b[3;3~": (Keys.Escape, Keys.Delete),
"\x1b[5;3~": (Keys.Escape, Keys.PageUp),
"\x1b[6;3~": (Keys.Escape, Keys.PageDown),
"\x1b[2;4~": (Keys.Escape, Keys.ShiftInsert),
"\x1b[3;4~": (Keys.Escape, Keys.ShiftDelete),
"\x1b[5;4~": (Keys.Escape, Keys.ShiftPageUp),
"\x1b[6;4~": (Keys.Escape, Keys.ShiftPageDown),
"\x1b[3;5~": Keys.ControlDelete, # xterm, gnome-terminal.
"\x1b[5;5~": Keys.ControlPageUp,
"\x1b[6;5~": Keys.ControlPageDown,
"\x1b[3;6~": Keys.ControlShiftDelete,
"\x1b[5;6~": Keys.ControlShiftPageUp,
"\x1b[6;6~": Keys.ControlShiftPageDown,
"\x1b[2;7~": (Keys.Escape, Keys.ControlInsert),
"\x1b[5;7~": (Keys.Escape, Keys.ControlPageDown),
"\x1b[6;7~": (Keys.Escape, Keys.ControlPageDown),
"\x1b[2;8~": (Keys.Escape, Keys.ControlShiftInsert),
"\x1b[5;8~": (Keys.Escape, Keys.ControlShiftPageDown),
"\x1b[6;8~": (Keys.Escape, Keys.ControlShiftPageDown),
# --
# Arrows.
# (Normal cursor mode).
"\x1b[A": Keys.Up,
"\x1b[B": Keys.Down,
"\x1b[C": Keys.Right,
"\x1b[D": Keys.Left,
"\x1b[H": Keys.Home,
"\x1b[F": Keys.End,
# Tmux sends following keystrokes when control+arrow is pressed, but for
# Emacs ansi-term sends the same sequences for normal arrow keys. Consider
# it a normal arrow press, because that's more important.
# (Application cursor mode).
"\x1bOA": Keys.Up,
"\x1bOB": Keys.Down,
"\x1bOC": Keys.Right,
"\x1bOD": Keys.Left,
"\x1bOF": Keys.End,
"\x1bOH": Keys.Home,
# Shift + arrows.
"\x1b[1;2A": Keys.ShiftUp,
"\x1b[1;2B": Keys.ShiftDown,
"\x1b[1;2C": Keys.ShiftRight,
"\x1b[1;2D": Keys.ShiftLeft,
"\x1b[1;2F": Keys.ShiftEnd,
"\x1b[1;2H": Keys.ShiftHome,
# Meta + arrow keys. Several terminals handle this differently.
# The following sequences are for xterm and gnome-terminal.
# (Iterm sends ESC followed by the normal arrow_up/down/left/right
# sequences, and the OSX Terminal sends ESCb and ESCf for "alt
# arrow_left" and "alt arrow_right." We don't handle these
# explicitly, in here, because would could not distinguish between
# pressing ESC (to go to Vi navigation mode), followed by just the
# 'b' or 'f' key. These combinations are handled in
# the input processor.)
"\x1b[1;3A": (Keys.Escape, Keys.Up),
"\x1b[1;3B": (Keys.Escape, Keys.Down),
"\x1b[1;3C": (Keys.Escape, Keys.Right),
"\x1b[1;3D": (Keys.Escape, Keys.Left),
"\x1b[1;3F": (Keys.Escape, Keys.End),
"\x1b[1;3H": (Keys.Escape, Keys.Home),
# Alt+shift+number.
"\x1b[1;4A": (Keys.Escape, Keys.ShiftDown),
"\x1b[1;4B": (Keys.Escape, Keys.ShiftUp),
"\x1b[1;4C": (Keys.Escape, Keys.ShiftRight),
"\x1b[1;4D": (Keys.Escape, Keys.ShiftLeft),
"\x1b[1;4F": (Keys.Escape, Keys.ShiftEnd),
"\x1b[1;4H": (Keys.Escape, Keys.ShiftHome),
# Control + arrows.
"\x1b[1;5A": Keys.ControlUp, # Cursor Mode
"\x1b[1;5B": Keys.ControlDown, # Cursor Mode
"\x1b[1;5C": Keys.ControlRight, # Cursor Mode
"\x1b[1;5D": Keys.ControlLeft, # Cursor Mode
"\x1b[1;5F": Keys.ControlEnd,
"\x1b[1;5H": Keys.ControlHome,
# Tmux sends following keystrokes when control+arrow is pressed, but for
# Emacs ansi-term sends the same sequences for normal arrow keys. Consider
# it a normal arrow press, because that's more important.
"\x1b[5A": Keys.ControlUp,
"\x1b[5B": Keys.ControlDown,
"\x1b[5C": Keys.ControlRight,
"\x1b[5D": Keys.ControlLeft,
"\x1bOc": Keys.ControlRight, # rxvt
"\x1bOd": Keys.ControlLeft, # rxvt
# Control + shift + arrows.
"\x1b[1;6A": Keys.ControlShiftDown,
"\x1b[1;6B": Keys.ControlShiftUp,
"\x1b[1;6C": Keys.ControlShiftRight,
"\x1b[1;6D": Keys.ControlShiftLeft,
"\x1b[1;6F": Keys.ControlShiftEnd,
"\x1b[1;6H": Keys.ControlShiftHome,
# Control + Meta + arrows.
"\x1b[1;7A": (Keys.Escape, Keys.ControlDown),
"\x1b[1;7B": (Keys.Escape, Keys.ControlUp),
"\x1b[1;7C": (Keys.Escape, Keys.ControlRight),
"\x1b[1;7D": (Keys.Escape, Keys.ControlLeft),
"\x1b[1;7F": (Keys.Escape, Keys.ControlEnd),
"\x1b[1;7H": (Keys.Escape, Keys.ControlHome),
# Meta + Shift + arrows.
"\x1b[1;8A": (Keys.Escape, Keys.ControlShiftDown),
"\x1b[1;8B": (Keys.Escape, Keys.ControlShiftUp),
"\x1b[1;8C": (Keys.Escape, Keys.ControlShiftRight),
"\x1b[1;8D": (Keys.Escape, Keys.ControlShiftLeft),
"\x1b[1;8F": (Keys.Escape, Keys.ControlShiftEnd),
"\x1b[1;8H": (Keys.Escape, Keys.ControlShiftHome),
# Meta + arrow on (some?) Macs when using iTerm defaults (see issue #483).
"\x1b[1;9A": (Keys.Escape, Keys.Up),
"\x1b[1;9B": (Keys.Escape, Keys.Down),
"\x1b[1;9C": (Keys.Escape, Keys.Right),
"\x1b[1;9D": (Keys.Escape, Keys.Left),
# --
# Control/shift/meta + number in mintty.
# (c-2 will actually send c-@ and c-6 will send c-^.)
"\x1b[1;5p": Keys.Control0,
"\x1b[1;5q": Keys.Control1,
"\x1b[1;5r": Keys.Control2,
"\x1b[1;5s": Keys.Control3,
"\x1b[1;5t": Keys.Control4,
"\x1b[1;5u": Keys.Control5,
"\x1b[1;5v": Keys.Control6,
"\x1b[1;5w": Keys.Control7,
"\x1b[1;5x": Keys.Control8,
"\x1b[1;5y": Keys.Control9,
"\x1b[1;6p": Keys.ControlShift0,
"\x1b[1;6q": Keys.ControlShift1,
"\x1b[1;6r": Keys.ControlShift2,
"\x1b[1;6s": Keys.ControlShift3,
"\x1b[1;6t": Keys.ControlShift4,
"\x1b[1;6u": Keys.ControlShift5,
"\x1b[1;6v": Keys.ControlShift6,
"\x1b[1;6w": Keys.ControlShift7,
"\x1b[1;6x": Keys.ControlShift8,
"\x1b[1;6y": Keys.ControlShift9,
"\x1b[1;7p": (Keys.Escape, Keys.Control0),
"\x1b[1;7q": (Keys.Escape, Keys.Control1),
"\x1b[1;7r": (Keys.Escape, Keys.Control2),
"\x1b[1;7s": (Keys.Escape, Keys.Control3),
"\x1b[1;7t": (Keys.Escape, Keys.Control4),
"\x1b[1;7u": (Keys.Escape, Keys.Control5),
"\x1b[1;7v": (Keys.Escape, Keys.Control6),
"\x1b[1;7w": (Keys.Escape, Keys.Control7),
"\x1b[1;7x": (Keys.Escape, Keys.Control8),
"\x1b[1;7y": (Keys.Escape, Keys.Control9),
"\x1b[1;8p": (Keys.Escape, Keys.ControlShift0),
"\x1b[1;8q": (Keys.Escape, Keys.ControlShift1),
"\x1b[1;8r": (Keys.Escape, Keys.ControlShift2),
"\x1b[1;8s": (Keys.Escape, Keys.ControlShift3),
"\x1b[1;8t": (Keys.Escape, Keys.ControlShift4),
"\x1b[1;8u": (Keys.Escape, Keys.ControlShift5),
"\x1b[1;8v": (Keys.Escape, Keys.ControlShift6),
"\x1b[1;8w": (Keys.Escape, Keys.ControlShift7),
"\x1b[1;8x": (Keys.Escape, Keys.ControlShift8),
"\x1b[1;8y": (Keys.Escape, Keys.ControlShift9),
}
def _get_reverse_ansi_sequences() -> dict[Keys, str]:
"""
Create a dictionary that maps prompt_toolkit keys back to the VT100 escape
sequences.
"""
result: dict[Keys, str] = {}
for sequence, key in ANSI_SEQUENCES.items():
if not isinstance(key, tuple):
if key not in result:
result[key] = sequence
return result
REVERSE_ANSI_SEQUENCES = _get_reverse_ansi_sequences()

View File

@@ -0,0 +1,153 @@
"""
Abstraction of CLI Input.
"""
from __future__ import annotations
from abc import ABCMeta, abstractmethod, abstractproperty
from contextlib import contextmanager
from typing import Callable, ContextManager, Generator
from prompt_toolkit.key_binding import KeyPress
__all__ = [
"Input",
"PipeInput",
"DummyInput",
]
class Input(metaclass=ABCMeta):
"""
Abstraction for any input.
An instance of this class can be given to the constructor of a
:class:`~prompt_toolkit.application.Application` and will also be
passed to the :class:`~prompt_toolkit.eventloop.base.EventLoop`.
"""
@abstractmethod
def fileno(self) -> int:
"""
Fileno for putting this in an event loop.
"""
@abstractmethod
def typeahead_hash(self) -> str:
"""
Identifier for storing type ahead key presses.
"""
@abstractmethod
def read_keys(self) -> list[KeyPress]:
"""
Return a list of Key objects which are read/parsed from the input.
"""
def flush_keys(self) -> list[KeyPress]:
"""
Flush the underlying parser. and return the pending keys.
(Used for vt100 input.)
"""
return []
def flush(self) -> None:
"The event loop can call this when the input has to be flushed."
pass
@abstractproperty
def closed(self) -> bool:
"Should be true when the input stream is closed."
return False
@abstractmethod
def raw_mode(self) -> ContextManager[None]:
"""
Context manager that turns the input into raw mode.
"""
@abstractmethod
def cooked_mode(self) -> ContextManager[None]:
"""
Context manager that turns the input into cooked mode.
"""
@abstractmethod
def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]:
"""
Return a context manager that makes this input active in the current
event loop.
"""
@abstractmethod
def detach(self) -> ContextManager[None]:
"""
Return a context manager that makes sure that this input is not active
in the current event loop.
"""
def close(self) -> None:
"Close input."
pass
class PipeInput(Input):
"""
Abstraction for pipe input.
"""
@abstractmethod
def send_bytes(self, data: bytes) -> None:
"""Feed byte string into the pipe"""
@abstractmethod
def send_text(self, data: str) -> None:
"""Feed a text string into the pipe"""
class DummyInput(Input):
"""
Input for use in a `DummyApplication`
If used in an actual application, it will make the application render
itself once and exit immediately, due to an `EOFError`.
"""
def fileno(self) -> int:
raise NotImplementedError
def typeahead_hash(self) -> str:
return f"dummy-{id(self)}"
def read_keys(self) -> list[KeyPress]:
return []
@property
def closed(self) -> bool:
# This needs to be true, so that the dummy input will trigger an
# `EOFError` immediately in the application.
return True
def raw_mode(self) -> ContextManager[None]:
return _dummy_context_manager()
def cooked_mode(self) -> ContextManager[None]:
return _dummy_context_manager()
def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]:
# Call the callback immediately once after attaching.
# This tells the callback to call `read_keys` and check the
# `input.closed` flag, after which it won't receive any keys, but knows
# that `EOFError` should be raised. This unblocks `read_from_input` in
# `application.py`.
input_ready_callback()
return _dummy_context_manager()
def detach(self) -> ContextManager[None]:
return _dummy_context_manager()
@contextmanager
def _dummy_context_manager() -> Generator[None, None, None]:
yield

View File

@@ -0,0 +1,79 @@
from __future__ import annotations
import io
import sys
from typing import ContextManager, TextIO
from .base import DummyInput, Input, PipeInput
__all__ = [
"create_input",
"create_pipe_input",
]
def create_input(stdin: TextIO | None = None, always_prefer_tty: bool = False) -> Input:
"""
Create the appropriate `Input` object for the current os/environment.
:param always_prefer_tty: When set, if `sys.stdin` is connected to a Unix
`pipe`, check whether `sys.stdout` or `sys.stderr` are connected to a
pseudo terminal. If so, open the tty for reading instead of reading for
`sys.stdin`. (We can open `stdout` or `stderr` for reading, this is how
a `$PAGER` works.)
"""
if sys.platform == "win32":
from .win32 import Win32Input
# If `stdin` was assigned `None` (which happens with pythonw.exe), use
# a `DummyInput`. This triggers `EOFError` in the application code.
if stdin is None and sys.stdin is None:
return DummyInput()
return Win32Input(stdin or sys.stdin)
else:
from .vt100 import Vt100Input
# If no input TextIO is given, use stdin/stdout.
if stdin is None:
stdin = sys.stdin
if always_prefer_tty:
for obj in [sys.stdin, sys.stdout, sys.stderr]:
if obj.isatty():
stdin = obj
break
# If we can't access the file descriptor for the selected stdin, return
# a `DummyInput` instead. This can happen for instance in unit tests,
# when `sys.stdin` is patched by something that's not an actual file.
# (Instantiating `Vt100Input` would fail in this case.)
try:
stdin.fileno()
except io.UnsupportedOperation:
return DummyInput()
return Vt100Input(stdin)
def create_pipe_input() -> ContextManager[PipeInput]:
"""
Create an input pipe.
This is mostly useful for unit testing.
Usage::
with create_pipe_input() as input:
input.send_text('inputdata')
Breaking change: In prompt_toolkit 3.0.28 and earlier, this was returning
the `PipeInput` directly, rather than through a context manager.
"""
if sys.platform == "win32":
from .win32_pipe import Win32PipeInput
return Win32PipeInput.create()
else:
from .posix_pipe import PosixPipeInput
return PosixPipeInput.create()

View File

@@ -0,0 +1,118 @@
from __future__ import annotations
import sys
assert sys.platform != "win32"
import os
from contextlib import contextmanager
from typing import ContextManager, Iterator, TextIO, cast
from ..utils import DummyContext
from .base import PipeInput
from .vt100 import Vt100Input
__all__ = [
"PosixPipeInput",
]
class _Pipe:
"Wrapper around os.pipe, that ensures we don't double close any end."
def __init__(self) -> None:
self.read_fd, self.write_fd = os.pipe()
self._read_closed = False
self._write_closed = False
def close_read(self) -> None:
"Close read-end if not yet closed."
if self._read_closed:
return
os.close(self.read_fd)
self._read_closed = True
def close_write(self) -> None:
"Close write-end if not yet closed."
if self._write_closed:
return
os.close(self.write_fd)
self._write_closed = True
def close(self) -> None:
"Close both read and write ends."
self.close_read()
self.close_write()
class PosixPipeInput(Vt100Input, PipeInput):
"""
Input that is send through a pipe.
This is useful if we want to send the input programmatically into the
application. Mostly useful for unit testing.
Usage::
with PosixPipeInput.create() as input:
input.send_text('inputdata')
"""
_id = 0
def __init__(self, _pipe: _Pipe, _text: str = "") -> None:
# Private constructor. Users should use the public `.create()` method.
self.pipe = _pipe
class Stdin:
encoding = "utf-8"
def isatty(stdin) -> bool:
return True
def fileno(stdin) -> int:
return self.pipe.read_fd
super().__init__(cast(TextIO, Stdin()))
self.send_text(_text)
# Identifier for every PipeInput for the hash.
self.__class__._id += 1
self._id = self.__class__._id
@classmethod
@contextmanager
def create(cls, text: str = "") -> Iterator[PosixPipeInput]:
pipe = _Pipe()
try:
yield PosixPipeInput(_pipe=pipe, _text=text)
finally:
pipe.close()
def send_bytes(self, data: bytes) -> None:
os.write(self.pipe.write_fd, data)
def send_text(self, data: str) -> None:
"Send text to the input."
os.write(self.pipe.write_fd, data.encode("utf-8"))
def raw_mode(self) -> ContextManager[None]:
return DummyContext()
def cooked_mode(self) -> ContextManager[None]:
return DummyContext()
def close(self) -> None:
"Close pipe fds."
# Only close the write-end of the pipe. This will unblock the reader
# callback (in vt100.py > _attached_input), which eventually will raise
# `EOFError`. If we'd also close the read-end, then the event loop
# won't wake up the corresponding callback because of this.
self.pipe.close_write()
def typeahead_hash(self) -> str:
"""
This needs to be unique for every `PipeInput`.
"""
return f"pipe-input-{self._id}"

View File

@@ -0,0 +1,97 @@
from __future__ import annotations
import os
import select
from codecs import getincrementaldecoder
__all__ = [
"PosixStdinReader",
]
class PosixStdinReader:
"""
Wrapper around stdin which reads (nonblocking) the next available 1024
bytes and decodes it.
Note that you can't be sure that the input file is closed if the ``read``
function returns an empty string. When ``errors=ignore`` is passed,
``read`` can return an empty string if all malformed input was replaced by
an empty string. (We can't block here and wait for more input.) So, because
of that, check the ``closed`` attribute, to be sure that the file has been
closed.
:param stdin_fd: File descriptor from which we read.
:param errors: Can be 'ignore', 'strict' or 'replace'.
On Python3, this can be 'surrogateescape', which is the default.
'surrogateescape' is preferred, because this allows us to transfer
unrecognized bytes to the key bindings. Some terminals, like lxterminal
and Guake, use the 'Mxx' notation to send mouse events, where each 'x'
can be any possible byte.
"""
# By default, we want to 'ignore' errors here. The input stream can be full
# of junk. One occurrence of this that I had was when using iTerm2 on OS X,
# with "Option as Meta" checked (You should choose "Option as +Esc".)
def __init__(
self, stdin_fd: int, errors: str = "surrogateescape", encoding: str = "utf-8"
) -> None:
self.stdin_fd = stdin_fd
self.errors = errors
# Create incremental decoder for decoding stdin.
# We can not just do `os.read(stdin.fileno(), 1024).decode('utf-8')`, because
# it could be that we are in the middle of a utf-8 byte sequence.
self._stdin_decoder_cls = getincrementaldecoder(encoding)
self._stdin_decoder = self._stdin_decoder_cls(errors=errors)
#: True when there is nothing anymore to read.
self.closed = False
def read(self, count: int = 1024) -> str:
# By default we choose a rather small chunk size, because reading
# big amounts of input at once, causes the event loop to process
# all these key bindings also at once without going back to the
# loop. This will make the application feel unresponsive.
"""
Read the input and return it as a string.
Return the text. Note that this can return an empty string, even when
the input stream was not yet closed. This means that something went
wrong during the decoding.
"""
if self.closed:
return ""
# Check whether there is some input to read. `os.read` would block
# otherwise.
# (Actually, the event loop is responsible to make sure that this
# function is only called when there is something to read, but for some
# reason this happens in certain situations.)
try:
if not select.select([self.stdin_fd], [], [], 0)[0]:
return ""
except OSError:
# Happens for instance when the file descriptor was closed.
# (We had this in ptterm, where the FD became ready, a callback was
# scheduled, but in the meantime another callback closed it already.)
self.closed = True
# Note: the following works better than wrapping `self.stdin` like
# `codecs.getreader('utf-8')(stdin)` and doing `read(1)`.
# Somehow that causes some latency when the escape
# character is pressed. (Especially on combination with the `select`.)
try:
data = os.read(self.stdin_fd, count)
# Nothing more to read, stream is closed.
if data == b"":
self.closed = True
return ""
except OSError:
# In case of SIGWINCH
data = b""
return self._stdin_decoder.decode(data)

View File

@@ -0,0 +1,78 @@
r"""
Store input key strokes if we did read more than was required.
The input classes `Vt100Input` and `Win32Input` read the input text in chunks
of a few kilobytes. This means that if we read input from stdin, it could be
that we read a couple of lines (with newlines in between) at once.
This creates a problem: potentially, we read too much from stdin. Sometimes
people paste several lines at once because they paste input in a REPL and
expect each input() call to process one line. Or they rely on type ahead
because the application can't keep up with the processing.
However, we need to read input in bigger chunks. We need this mostly to support
pasting of larger chunks of text. We don't want everything to become
unresponsive because we:
- read one character;
- parse one character;
- call the key binding, which does a string operation with one character;
- and render the user interface.
Doing text operations on single characters is very inefficient in Python, so we
prefer to work on bigger chunks of text. This is why we have to read the input
in bigger chunks.
Further, line buffering is also not an option, because it doesn't work well in
the architecture. We use lower level Posix APIs, that work better with the
event loop and so on. In fact, there is also nothing that defines that only \n
can accept the input, you could create a key binding for any key to accept the
input.
To support type ahead, this module will store all the key strokes that were
read too early, so that they can be feed into to the next `prompt()` call or to
the next prompt_toolkit `Application`.
"""
from __future__ import annotations
from collections import defaultdict
from ..key_binding import KeyPress
from .base import Input
__all__ = [
"store_typeahead",
"get_typeahead",
"clear_typeahead",
]
_buffer: dict[str, list[KeyPress]] = defaultdict(list)
def store_typeahead(input_obj: Input, key_presses: list[KeyPress]) -> None:
"""
Insert typeahead key presses for the given input.
"""
global _buffer
key = input_obj.typeahead_hash()
_buffer[key].extend(key_presses)
def get_typeahead(input_obj: Input) -> list[KeyPress]:
"""
Retrieve typeahead and reset the buffer for this input.
"""
global _buffer
key = input_obj.typeahead_hash()
result = _buffer[key]
_buffer[key] = []
return result
def clear_typeahead(input_obj: Input) -> None:
"""
Clear typeahead buffer.
"""
global _buffer
key = input_obj.typeahead_hash()
_buffer[key] = []

View File

@@ -0,0 +1,309 @@
from __future__ import annotations
import sys
assert sys.platform != "win32"
import contextlib
import io
import termios
import tty
from asyncio import AbstractEventLoop, get_running_loop
from typing import Callable, ContextManager, Generator, TextIO
from ..key_binding import KeyPress
from .base import Input
from .posix_utils import PosixStdinReader
from .vt100_parser import Vt100Parser
__all__ = [
"Vt100Input",
"raw_mode",
"cooked_mode",
]
class Vt100Input(Input):
"""
Vt100 input for Posix systems.
(This uses a posix file descriptor that can be registered in the event loop.)
"""
# For the error messages. Only display "Input is not a terminal" once per
# file descriptor.
_fds_not_a_terminal: set[int] = set()
def __init__(self, stdin: TextIO) -> None:
# Test whether the given input object has a file descriptor.
# (Idle reports stdin to be a TTY, but fileno() is not implemented.)
try:
# This should not raise, but can return 0.
stdin.fileno()
except io.UnsupportedOperation as e:
if "idlelib.run" in sys.modules:
raise io.UnsupportedOperation(
"Stdin is not a terminal. Running from Idle is not supported."
) from e
else:
raise io.UnsupportedOperation("Stdin is not a terminal.") from e
# Even when we have a file descriptor, it doesn't mean it's a TTY.
# Normally, this requires a real TTY device, but people instantiate
# this class often during unit tests as well. They use for instance
# pexpect to pipe data into an application. For convenience, we print
# an error message and go on.
isatty = stdin.isatty()
fd = stdin.fileno()
if not isatty and fd not in Vt100Input._fds_not_a_terminal:
msg = "Warning: Input is not a terminal (fd=%r).\n"
sys.stderr.write(msg % fd)
sys.stderr.flush()
Vt100Input._fds_not_a_terminal.add(fd)
#
self.stdin = stdin
# Create a backup of the fileno(). We want this to work even if the
# underlying file is closed, so that `typeahead_hash()` keeps working.
self._fileno = stdin.fileno()
self._buffer: list[KeyPress] = [] # Buffer to collect the Key objects.
self.stdin_reader = PosixStdinReader(self._fileno, encoding=stdin.encoding)
self.vt100_parser = Vt100Parser(
lambda key_press: self._buffer.append(key_press)
)
def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]:
"""
Return a context manager that makes this input active in the current
event loop.
"""
return _attached_input(self, input_ready_callback)
def detach(self) -> ContextManager[None]:
"""
Return a context manager that makes sure that this input is not active
in the current event loop.
"""
return _detached_input(self)
def read_keys(self) -> list[KeyPress]:
"Read list of KeyPress."
# Read text from stdin.
data = self.stdin_reader.read()
# Pass it through our vt100 parser.
self.vt100_parser.feed(data)
# Return result.
result = self._buffer
self._buffer = []
return result
def flush_keys(self) -> list[KeyPress]:
"""
Flush pending keys and return them.
(Used for flushing the 'escape' key.)
"""
# Flush all pending keys. (This is most important to flush the vt100
# 'Escape' key early when nothing else follows.)
self.vt100_parser.flush()
# Return result.
result = self._buffer
self._buffer = []
return result
@property
def closed(self) -> bool:
return self.stdin_reader.closed
def raw_mode(self) -> ContextManager[None]:
return raw_mode(self.stdin.fileno())
def cooked_mode(self) -> ContextManager[None]:
return cooked_mode(self.stdin.fileno())
def fileno(self) -> int:
return self.stdin.fileno()
def typeahead_hash(self) -> str:
return f"fd-{self._fileno}"
_current_callbacks: dict[
tuple[AbstractEventLoop, int], Callable[[], None] | None
] = {} # (loop, fd) -> current callback
@contextlib.contextmanager
def _attached_input(
input: Vt100Input, callback: Callable[[], None]
) -> Generator[None, None, None]:
"""
Context manager that makes this input active in the current event loop.
:param input: :class:`~prompt_toolkit.input.Input` object.
:param callback: Called when the input is ready to read.
"""
loop = get_running_loop()
fd = input.fileno()
previous = _current_callbacks.get((loop, fd))
def callback_wrapper() -> None:
"""Wrapper around the callback that already removes the reader when
the input is closed. Otherwise, we keep continuously calling this
callback, until we leave the context manager (which can happen a bit
later). This fixes issues when piping /dev/null into a prompt_toolkit
application."""
if input.closed:
loop.remove_reader(fd)
callback()
try:
loop.add_reader(fd, callback_wrapper)
except PermissionError:
# For `EPollSelector`, adding /dev/null to the event loop will raise
# `PermissionError` (that doesn't happen for `SelectSelector`
# apparently). Whenever we get a `PermissionError`, we can raise
# `EOFError`, because there's not more to be read anyway. `EOFError` is
# an exception that people expect in
# `prompt_toolkit.application.Application.run()`.
# To reproduce, do: `ptpython 0< /dev/null 1< /dev/null`
raise EOFError
_current_callbacks[loop, fd] = callback
try:
yield
finally:
loop.remove_reader(fd)
if previous:
loop.add_reader(fd, previous)
_current_callbacks[loop, fd] = previous
else:
del _current_callbacks[loop, fd]
@contextlib.contextmanager
def _detached_input(input: Vt100Input) -> Generator[None, None, None]:
loop = get_running_loop()
fd = input.fileno()
previous = _current_callbacks.get((loop, fd))
if previous:
loop.remove_reader(fd)
_current_callbacks[loop, fd] = None
try:
yield
finally:
if previous:
loop.add_reader(fd, previous)
_current_callbacks[loop, fd] = previous
class raw_mode:
"""
::
with raw_mode(stdin):
''' the pseudo-terminal stdin is now used in raw mode '''
We ignore errors when executing `tcgetattr` fails.
"""
# There are several reasons for ignoring errors:
# 1. To avoid the "Inappropriate ioctl for device" crash if somebody would
# execute this code (In a Python REPL, for instance):
#
# import os; f = open(os.devnull); os.dup2(f.fileno(), 0)
#
# The result is that the eventloop will stop correctly, because it has
# to logic to quit when stdin is closed. However, we should not fail at
# this point. See:
# https://github.com/jonathanslenders/python-prompt-toolkit/pull/393
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/392
# 2. Related, when stdin is an SSH pipe, and no full terminal was allocated.
# See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/165
def __init__(self, fileno: int) -> None:
self.fileno = fileno
self.attrs_before: list[int | list[bytes | int]] | None
try:
self.attrs_before = termios.tcgetattr(fileno)
except termios.error:
# Ignore attribute errors.
self.attrs_before = None
def __enter__(self) -> None:
# NOTE: On os X systems, using pty.setraw() fails. Therefor we are using this:
try:
newattr = termios.tcgetattr(self.fileno)
except termios.error:
pass
else:
newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG])
newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG])
# VMIN defines the number of characters read at a time in
# non-canonical mode. It seems to default to 1 on Linux, but on
# Solaris and derived operating systems it defaults to 4. (This is
# because the VMIN slot is the same as the VEOF slot, which
# defaults to ASCII EOT = Ctrl-D = 4.)
newattr[tty.CC][termios.VMIN] = 1
termios.tcsetattr(self.fileno, termios.TCSANOW, newattr)
@classmethod
def _patch_lflag(cls, attrs: int) -> int:
return attrs & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
@classmethod
def _patch_iflag(cls, attrs: int) -> int:
return attrs & ~(
# Disable XON/XOFF flow control on output and input.
# (Don't capture Ctrl-S and Ctrl-Q.)
# Like executing: "stty -ixon."
termios.IXON
| termios.IXOFF
|
# Don't translate carriage return into newline on input.
termios.ICRNL
| termios.INLCR
| termios.IGNCR
)
def __exit__(self, *a: object) -> None:
if self.attrs_before is not None:
try:
termios.tcsetattr(self.fileno, termios.TCSANOW, self.attrs_before)
except termios.error:
pass
# # Put the terminal in application mode.
# self._stdout.write('\x1b[?1h')
class cooked_mode(raw_mode):
"""
The opposite of ``raw_mode``, used when we need cooked mode inside a
`raw_mode` block. Used in `Application.run_in_terminal`.::
with cooked_mode(stdin):
''' the pseudo-terminal stdin is now used in cooked mode. '''
"""
@classmethod
def _patch_lflag(cls, attrs: int) -> int:
return attrs | (termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
@classmethod
def _patch_iflag(cls, attrs: int) -> int:
# Turn the ICRNL flag back on. (Without this, calling `input()` in
# run_in_terminal doesn't work and displays ^M instead. Ptpython
# evaluates commands using `run_in_terminal`, so it's important that
# they translate ^M back into ^J.)
return attrs | termios.ICRNL

View File

@@ -0,0 +1,250 @@
"""
Parser for VT100 input stream.
"""
from __future__ import annotations
import re
from typing import Callable, Dict, Generator
from ..key_binding.key_processor import KeyPress
from ..keys import Keys
from .ansi_escape_sequences import ANSI_SEQUENCES
__all__ = [
"Vt100Parser",
]
# Regex matching any CPR response
# (Note that we use '\Z' instead of '$', because '$' could include a trailing
# newline.)
_cpr_response_re = re.compile("^" + re.escape("\x1b[") + r"\d+;\d+R\Z")
# Mouse events:
# Typical: "Esc[MaB*" Urxvt: "Esc[96;14;13M" and for Xterm SGR: "Esc[<64;85;12M"
_mouse_event_re = re.compile("^" + re.escape("\x1b[") + r"(<?[\d;]+[mM]|M...)\Z")
# Regex matching any valid prefix of a CPR response.
# (Note that it doesn't contain the last character, the 'R'. The prefix has to
# be shorter.)
_cpr_response_prefix_re = re.compile("^" + re.escape("\x1b[") + r"[\d;]*\Z")
_mouse_event_prefix_re = re.compile("^" + re.escape("\x1b[") + r"(<?[\d;]*|M.{0,2})\Z")
class _Flush:
"""Helper object to indicate flush operation to the parser."""
pass
class _IsPrefixOfLongerMatchCache(Dict[str, bool]):
"""
Dictionary that maps input sequences to a boolean indicating whether there is
any key that start with this characters.
"""
def __missing__(self, prefix: str) -> bool:
# (hard coded) If this could be a prefix of a CPR response, return
# True.
if _cpr_response_prefix_re.match(prefix) or _mouse_event_prefix_re.match(
prefix
):
result = True
else:
# If this could be a prefix of anything else, also return True.
result = any(
v
for k, v in ANSI_SEQUENCES.items()
if k.startswith(prefix) and k != prefix
)
self[prefix] = result
return result
_IS_PREFIX_OF_LONGER_MATCH_CACHE = _IsPrefixOfLongerMatchCache()
class Vt100Parser:
"""
Parser for VT100 input stream.
Data can be fed through the `feed` method and the given callback will be
called with KeyPress objects.
::
def callback(key):
pass
i = Vt100Parser(callback)
i.feed('data\x01...')
:attr feed_key_callback: Function that will be called when a key is parsed.
"""
# Lookup table of ANSI escape sequences for a VT100 terminal
# Hint: in order to know what sequences your terminal writes to stdin, run
# "od -c" and start typing.
def __init__(self, feed_key_callback: Callable[[KeyPress], None]) -> None:
self.feed_key_callback = feed_key_callback
self.reset()
def reset(self, request: bool = False) -> None:
self._in_bracketed_paste = False
self._start_parser()
def _start_parser(self) -> None:
"""
Start the parser coroutine.
"""
self._input_parser = self._input_parser_generator()
self._input_parser.send(None) # type: ignore
def _get_match(self, prefix: str) -> None | Keys | tuple[Keys, ...]:
"""
Return the key (or keys) that maps to this prefix.
"""
# (hard coded) If we match a CPR response, return Keys.CPRResponse.
# (This one doesn't fit in the ANSI_SEQUENCES, because it contains
# integer variables.)
if _cpr_response_re.match(prefix):
return Keys.CPRResponse
elif _mouse_event_re.match(prefix):
return Keys.Vt100MouseEvent
# Otherwise, use the mappings.
try:
return ANSI_SEQUENCES[prefix]
except KeyError:
return None
def _input_parser_generator(self) -> Generator[None, str | _Flush, None]:
"""
Coroutine (state machine) for the input parser.
"""
prefix = ""
retry = False
flush = False
while True:
flush = False
if retry:
retry = False
else:
# Get next character.
c = yield
if isinstance(c, _Flush):
flush = True
else:
prefix += c
# If we have some data, check for matches.
if prefix:
is_prefix_of_longer_match = _IS_PREFIX_OF_LONGER_MATCH_CACHE[prefix]
match = self._get_match(prefix)
# Exact matches found, call handlers..
if (flush or not is_prefix_of_longer_match) and match:
self._call_handler(match, prefix)
prefix = ""
# No exact match found.
elif (flush or not is_prefix_of_longer_match) and not match:
found = False
retry = True
# Loop over the input, try the longest match first and
# shift.
for i in range(len(prefix), 0, -1):
match = self._get_match(prefix[:i])
if match:
self._call_handler(match, prefix[:i])
prefix = prefix[i:]
found = True
if not found:
self._call_handler(prefix[0], prefix[0])
prefix = prefix[1:]
def _call_handler(
self, key: str | Keys | tuple[Keys, ...], insert_text: str
) -> None:
"""
Callback to handler.
"""
if isinstance(key, tuple):
# Received ANSI sequence that corresponds with multiple keys
# (probably alt+something). Handle keys individually, but only pass
# data payload to first KeyPress (so that we won't insert it
# multiple times).
for i, k in enumerate(key):
self._call_handler(k, insert_text if i == 0 else "")
else:
if key == Keys.BracketedPaste:
self._in_bracketed_paste = True
self._paste_buffer = ""
else:
self.feed_key_callback(KeyPress(key, insert_text))
def feed(self, data: str) -> None:
"""
Feed the input stream.
:param data: Input string (unicode).
"""
# Handle bracketed paste. (We bypass the parser that matches all other
# key presses and keep reading input until we see the end mark.)
# This is much faster then parsing character by character.
if self._in_bracketed_paste:
self._paste_buffer += data
end_mark = "\x1b[201~"
if end_mark in self._paste_buffer:
end_index = self._paste_buffer.index(end_mark)
# Feed content to key bindings.
paste_content = self._paste_buffer[:end_index]
self.feed_key_callback(KeyPress(Keys.BracketedPaste, paste_content))
# Quit bracketed paste mode and handle remaining input.
self._in_bracketed_paste = False
remaining = self._paste_buffer[end_index + len(end_mark) :]
self._paste_buffer = ""
self.feed(remaining)
# Handle normal input character by character.
else:
for i, c in enumerate(data):
if self._in_bracketed_paste:
# Quit loop and process from this position when the parser
# entered bracketed paste.
self.feed(data[i:])
break
else:
self._input_parser.send(c)
def flush(self) -> None:
"""
Flush the buffer of the input stream.
This will allow us to handle the escape key (or maybe meta) sooner.
The input received by the escape key is actually the same as the first
characters of e.g. Arrow-Up, so without knowing what follows the escape
sequence, we don't know whether escape has been pressed, or whether
it's something else. This flush function should be called after a
timeout, and processes everything that's still in the buffer as-is, so
without assuming any characters will follow.
"""
self._input_parser.send(_Flush())
def feed_and_flush(self, data: str) -> None:
"""
Wrapper around ``feed`` and ``flush``.
"""
self.feed(data)
self.flush()

View File

@@ -0,0 +1,886 @@
from __future__ import annotations
import os
import sys
from abc import abstractmethod
from asyncio import get_running_loop
from contextlib import contextmanager
from ..utils import SPHINX_AUTODOC_RUNNING
assert sys.platform == "win32"
# Do not import win32-specific stuff when generating documentation.
# Otherwise RTD would be unable to generate docs for this module.
if not SPHINX_AUTODOC_RUNNING:
import msvcrt
from ctypes import windll
from ctypes import Array, byref, pointer
from ctypes.wintypes import DWORD, HANDLE
from typing import Callable, ContextManager, Iterable, Iterator, TextIO
from prompt_toolkit.eventloop import run_in_executor_with_context
from prompt_toolkit.eventloop.win32 import create_win32_event, wait_for_handles
from prompt_toolkit.key_binding.key_processor import KeyPress
from prompt_toolkit.keys import Keys
from prompt_toolkit.mouse_events import MouseButton, MouseEventType
from prompt_toolkit.win32_types import (
INPUT_RECORD,
KEY_EVENT_RECORD,
MOUSE_EVENT_RECORD,
STD_INPUT_HANDLE,
EventTypes,
)
from .ansi_escape_sequences import REVERSE_ANSI_SEQUENCES
from .base import Input
from .vt100_parser import Vt100Parser
__all__ = [
"Win32Input",
"ConsoleInputReader",
"raw_mode",
"cooked_mode",
"attach_win32_input",
"detach_win32_input",
]
# Win32 Constants for MOUSE_EVENT_RECORD.
# See: https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str
FROM_LEFT_1ST_BUTTON_PRESSED = 0x1
RIGHTMOST_BUTTON_PRESSED = 0x2
MOUSE_MOVED = 0x0001
MOUSE_WHEELED = 0x0004
# See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
class _Win32InputBase(Input):
"""
Base class for `Win32Input` and `Win32PipeInput`.
"""
def __init__(self) -> None:
self.win32_handles = _Win32Handles()
@property
@abstractmethod
def handle(self) -> HANDLE:
pass
class Win32Input(_Win32InputBase):
"""
`Input` class that reads from the Windows console.
"""
def __init__(self, stdin: TextIO | None = None) -> None:
super().__init__()
self._use_virtual_terminal_input = _is_win_vt100_input_enabled()
self.console_input_reader: Vt100ConsoleInputReader | ConsoleInputReader
if self._use_virtual_terminal_input:
self.console_input_reader = Vt100ConsoleInputReader()
else:
self.console_input_reader = ConsoleInputReader()
def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]:
"""
Return a context manager that makes this input active in the current
event loop.
"""
return attach_win32_input(self, input_ready_callback)
def detach(self) -> ContextManager[None]:
"""
Return a context manager that makes sure that this input is not active
in the current event loop.
"""
return detach_win32_input(self)
def read_keys(self) -> list[KeyPress]:
return list(self.console_input_reader.read())
def flush(self) -> None:
pass
@property
def closed(self) -> bool:
return False
def raw_mode(self) -> ContextManager[None]:
return raw_mode(
use_win10_virtual_terminal_input=self._use_virtual_terminal_input
)
def cooked_mode(self) -> ContextManager[None]:
return cooked_mode()
def fileno(self) -> int:
# The windows console doesn't depend on the file handle, so
# this is not used for the event loop (which uses the
# handle instead). But it's used in `Application.run_system_command`
# which opens a subprocess with a given stdin/stdout.
return sys.stdin.fileno()
def typeahead_hash(self) -> str:
return "win32-input"
def close(self) -> None:
self.console_input_reader.close()
@property
def handle(self) -> HANDLE:
return self.console_input_reader.handle
class ConsoleInputReader:
"""
:param recognize_paste: When True, try to discover paste actions and turn
the event into a BracketedPaste.
"""
# Keys with character data.
mappings = {
b"\x1b": Keys.Escape,
b"\x00": Keys.ControlSpace, # Control-Space (Also for Ctrl-@)
b"\x01": Keys.ControlA, # Control-A (home)
b"\x02": Keys.ControlB, # Control-B (emacs cursor left)
b"\x03": Keys.ControlC, # Control-C (interrupt)
b"\x04": Keys.ControlD, # Control-D (exit)
b"\x05": Keys.ControlE, # Control-E (end)
b"\x06": Keys.ControlF, # Control-F (cursor forward)
b"\x07": Keys.ControlG, # Control-G
b"\x08": Keys.ControlH, # Control-H (8) (Identical to '\b')
b"\x09": Keys.ControlI, # Control-I (9) (Identical to '\t')
b"\x0a": Keys.ControlJ, # Control-J (10) (Identical to '\n')
b"\x0b": Keys.ControlK, # Control-K (delete until end of line; vertical tab)
b"\x0c": Keys.ControlL, # Control-L (clear; form feed)
b"\x0d": Keys.ControlM, # Control-M (enter)
b"\x0e": Keys.ControlN, # Control-N (14) (history forward)
b"\x0f": Keys.ControlO, # Control-O (15)
b"\x10": Keys.ControlP, # Control-P (16) (history back)
b"\x11": Keys.ControlQ, # Control-Q
b"\x12": Keys.ControlR, # Control-R (18) (reverse search)
b"\x13": Keys.ControlS, # Control-S (19) (forward search)
b"\x14": Keys.ControlT, # Control-T
b"\x15": Keys.ControlU, # Control-U
b"\x16": Keys.ControlV, # Control-V
b"\x17": Keys.ControlW, # Control-W
b"\x18": Keys.ControlX, # Control-X
b"\x19": Keys.ControlY, # Control-Y (25)
b"\x1a": Keys.ControlZ, # Control-Z
b"\x1c": Keys.ControlBackslash, # Both Control-\ and Ctrl-|
b"\x1d": Keys.ControlSquareClose, # Control-]
b"\x1e": Keys.ControlCircumflex, # Control-^
b"\x1f": Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hyphen.)
b"\x7f": Keys.Backspace, # (127) Backspace (ASCII Delete.)
}
# Keys that don't carry character data.
keycodes = {
# Home/End
33: Keys.PageUp,
34: Keys.PageDown,
35: Keys.End,
36: Keys.Home,
# Arrows
37: Keys.Left,
38: Keys.Up,
39: Keys.Right,
40: Keys.Down,
45: Keys.Insert,
46: Keys.Delete,
# F-keys.
112: Keys.F1,
113: Keys.F2,
114: Keys.F3,
115: Keys.F4,
116: Keys.F5,
117: Keys.F6,
118: Keys.F7,
119: Keys.F8,
120: Keys.F9,
121: Keys.F10,
122: Keys.F11,
123: Keys.F12,
}
LEFT_ALT_PRESSED = 0x0002
RIGHT_ALT_PRESSED = 0x0001
SHIFT_PRESSED = 0x0010
LEFT_CTRL_PRESSED = 0x0008
RIGHT_CTRL_PRESSED = 0x0004
def __init__(self, recognize_paste: bool = True) -> None:
self._fdcon = None
self.recognize_paste = recognize_paste
# When stdin is a tty, use that handle, otherwise, create a handle from
# CONIN$.
self.handle: HANDLE
if sys.stdin.isatty():
self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
else:
self._fdcon = os.open("CONIN$", os.O_RDWR | os.O_BINARY)
self.handle = HANDLE(msvcrt.get_osfhandle(self._fdcon))
def close(self) -> None:
"Close fdcon."
if self._fdcon is not None:
os.close(self._fdcon)
def read(self) -> Iterable[KeyPress]:
"""
Return a list of `KeyPress` instances. It won't return anything when
there was nothing to read. (This function doesn't block.)
http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx
"""
max_count = 2048 # Max events to read at the same time.
read = DWORD(0)
arrtype = INPUT_RECORD * max_count
input_records = arrtype()
# Check whether there is some input to read. `ReadConsoleInputW` would
# block otherwise.
# (Actually, the event loop is responsible to make sure that this
# function is only called when there is something to read, but for some
# reason this happened in the asyncio_win32 loop, and it's better to be
# safe anyway.)
if not wait_for_handles([self.handle], timeout=0):
return
# Get next batch of input event.
windll.kernel32.ReadConsoleInputW(
self.handle, pointer(input_records), max_count, pointer(read)
)
# First, get all the keys from the input buffer, in order to determine
# whether we should consider this a paste event or not.
all_keys = list(self._get_keys(read, input_records))
# Fill in 'data' for key presses.
all_keys = [self._insert_key_data(key) for key in all_keys]
# Correct non-bmp characters that are passed as separate surrogate codes
all_keys = list(self._merge_paired_surrogates(all_keys))
if self.recognize_paste and self._is_paste(all_keys):
gen = iter(all_keys)
k: KeyPress | None
for k in gen:
# Pasting: if the current key consists of text or \n, turn it
# into a BracketedPaste.
data = []
while k and (
not isinstance(k.key, Keys)
or k.key in {Keys.ControlJ, Keys.ControlM}
):
data.append(k.data)
try:
k = next(gen)
except StopIteration:
k = None
if data:
yield KeyPress(Keys.BracketedPaste, "".join(data))
if k is not None:
yield k
else:
yield from all_keys
def _insert_key_data(self, key_press: KeyPress) -> KeyPress:
"""
Insert KeyPress data, for vt100 compatibility.
"""
if key_press.data:
return key_press
if isinstance(key_press.key, Keys):
data = REVERSE_ANSI_SEQUENCES.get(key_press.key, "")
else:
data = ""
return KeyPress(key_press.key, data)
def _get_keys(
self, read: DWORD, input_records: Array[INPUT_RECORD]
) -> Iterator[KeyPress]:
"""
Generator that yields `KeyPress` objects from the input records.
"""
for i in range(read.value):
ir = input_records[i]
# Get the right EventType from the EVENT_RECORD.
# (For some reason the Windows console application 'cmder'
# [http://gooseberrycreative.com/cmder/] can return '0' for
# ir.EventType. -- Just ignore that.)
if ir.EventType in EventTypes:
ev = getattr(ir.Event, EventTypes[ir.EventType])
# Process if this is a key event. (We also have mouse, menu and
# focus events.)
if isinstance(ev, KEY_EVENT_RECORD) and ev.KeyDown:
yield from self._event_to_key_presses(ev)
elif isinstance(ev, MOUSE_EVENT_RECORD):
yield from self._handle_mouse(ev)
@staticmethod
def _merge_paired_surrogates(key_presses: list[KeyPress]) -> Iterator[KeyPress]:
"""
Combines consecutive KeyPresses with high and low surrogates into
single characters
"""
buffered_high_surrogate = None
for key in key_presses:
is_text = not isinstance(key.key, Keys)
is_high_surrogate = is_text and "\ud800" <= key.key <= "\udbff"
is_low_surrogate = is_text and "\udc00" <= key.key <= "\udfff"
if buffered_high_surrogate:
if is_low_surrogate:
# convert high surrogate + low surrogate to single character
fullchar = (
(buffered_high_surrogate.key + key.key)
.encode("utf-16-le", "surrogatepass")
.decode("utf-16-le")
)
key = KeyPress(fullchar, fullchar)
else:
yield buffered_high_surrogate
buffered_high_surrogate = None
if is_high_surrogate:
buffered_high_surrogate = key
else:
yield key
if buffered_high_surrogate:
yield buffered_high_surrogate
@staticmethod
def _is_paste(keys: list[KeyPress]) -> bool:
"""
Return `True` when we should consider this list of keys as a paste
event. Pasted text on windows will be turned into a
`Keys.BracketedPaste` event. (It's not 100% correct, but it is probably
the best possible way to detect pasting of text and handle that
correctly.)
"""
# Consider paste when it contains at least one newline and at least one
# other character.
text_count = 0
newline_count = 0
for k in keys:
if not isinstance(k.key, Keys):
text_count += 1
if k.key == Keys.ControlM:
newline_count += 1
return newline_count >= 1 and text_count >= 1
def _event_to_key_presses(self, ev: KEY_EVENT_RECORD) -> list[KeyPress]:
"""
For this `KEY_EVENT_RECORD`, return a list of `KeyPress` instances.
"""
assert isinstance(ev, KEY_EVENT_RECORD) and ev.KeyDown
result: KeyPress | None = None
control_key_state = ev.ControlKeyState
u_char = ev.uChar.UnicodeChar
# Use surrogatepass because u_char may be an unmatched surrogate
ascii_char = u_char.encode("utf-8", "surrogatepass")
# NOTE: We don't use `ev.uChar.AsciiChar`. That appears to be the
# unicode code point truncated to 1 byte. See also:
# https://github.com/ipython/ipython/issues/10004
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/389
if u_char == "\x00":
if ev.VirtualKeyCode in self.keycodes:
result = KeyPress(self.keycodes[ev.VirtualKeyCode], "")
else:
if ascii_char in self.mappings:
if self.mappings[ascii_char] == Keys.ControlJ:
u_char = (
"\n" # Windows sends \n, turn into \r for unix compatibility.
)
result = KeyPress(self.mappings[ascii_char], u_char)
else:
result = KeyPress(u_char, u_char)
# First we handle Shift-Control-Arrow/Home/End (need to do this first)
if (
(
control_key_state & self.LEFT_CTRL_PRESSED
or control_key_state & self.RIGHT_CTRL_PRESSED
)
and control_key_state & self.SHIFT_PRESSED
and result
):
mapping: dict[str, str] = {
Keys.Left: Keys.ControlShiftLeft,
Keys.Right: Keys.ControlShiftRight,
Keys.Up: Keys.ControlShiftUp,
Keys.Down: Keys.ControlShiftDown,
Keys.Home: Keys.ControlShiftHome,
Keys.End: Keys.ControlShiftEnd,
Keys.Insert: Keys.ControlShiftInsert,
Keys.PageUp: Keys.ControlShiftPageUp,
Keys.PageDown: Keys.ControlShiftPageDown,
}
result.key = mapping.get(result.key, result.key)
# Correctly handle Control-Arrow/Home/End and Control-Insert/Delete keys.
if (
control_key_state & self.LEFT_CTRL_PRESSED
or control_key_state & self.RIGHT_CTRL_PRESSED
) and result:
mapping = {
Keys.Left: Keys.ControlLeft,
Keys.Right: Keys.ControlRight,
Keys.Up: Keys.ControlUp,
Keys.Down: Keys.ControlDown,
Keys.Home: Keys.ControlHome,
Keys.End: Keys.ControlEnd,
Keys.Insert: Keys.ControlInsert,
Keys.Delete: Keys.ControlDelete,
Keys.PageUp: Keys.ControlPageUp,
Keys.PageDown: Keys.ControlPageDown,
}
result.key = mapping.get(result.key, result.key)
# Turn 'Tab' into 'BackTab' when shift was pressed.
# Also handle other shift-key combination
if control_key_state & self.SHIFT_PRESSED and result:
mapping = {
Keys.Tab: Keys.BackTab,
Keys.Left: Keys.ShiftLeft,
Keys.Right: Keys.ShiftRight,
Keys.Up: Keys.ShiftUp,
Keys.Down: Keys.ShiftDown,
Keys.Home: Keys.ShiftHome,
Keys.End: Keys.ShiftEnd,
Keys.Insert: Keys.ShiftInsert,
Keys.Delete: Keys.ShiftDelete,
Keys.PageUp: Keys.ShiftPageUp,
Keys.PageDown: Keys.ShiftPageDown,
}
result.key = mapping.get(result.key, result.key)
# Turn 'Space' into 'ControlSpace' when control was pressed.
if (
(
control_key_state & self.LEFT_CTRL_PRESSED
or control_key_state & self.RIGHT_CTRL_PRESSED
)
and result
and result.data == " "
):
result = KeyPress(Keys.ControlSpace, " ")
# Turn Control-Enter into META-Enter. (On a vt100 terminal, we cannot
# detect this combination. But it's really practical on Windows.)
if (
(
control_key_state & self.LEFT_CTRL_PRESSED
or control_key_state & self.RIGHT_CTRL_PRESSED
)
and result
and result.key == Keys.ControlJ
):
return [KeyPress(Keys.Escape, ""), result]
# Return result. If alt was pressed, prefix the result with an
# 'Escape' key, just like unix VT100 terminals do.
# NOTE: Only replace the left alt with escape. The right alt key often
# acts as altgr and is used in many non US keyboard layouts for
# typing some special characters, like a backslash. We don't want
# all backslashes to be prefixed with escape. (Esc-\ has a
# meaning in E-macs, for instance.)
if result:
meta_pressed = control_key_state & self.LEFT_ALT_PRESSED
if meta_pressed:
return [KeyPress(Keys.Escape, ""), result]
else:
return [result]
else:
return []
def _handle_mouse(self, ev: MOUSE_EVENT_RECORD) -> list[KeyPress]:
"""
Handle mouse events. Return a list of KeyPress instances.
"""
event_flags = ev.EventFlags
button_state = ev.ButtonState
event_type: MouseEventType | None = None
button: MouseButton = MouseButton.NONE
# Scroll events.
if event_flags & MOUSE_WHEELED:
if button_state > 0:
event_type = MouseEventType.SCROLL_UP
else:
event_type = MouseEventType.SCROLL_DOWN
else:
# Handle button state for non-scroll events.
if button_state == FROM_LEFT_1ST_BUTTON_PRESSED:
button = MouseButton.LEFT
elif button_state == RIGHTMOST_BUTTON_PRESSED:
button = MouseButton.RIGHT
# Move events.
if event_flags & MOUSE_MOVED:
event_type = MouseEventType.MOUSE_MOVE
# No key pressed anymore: mouse up.
if event_type is None:
if button_state > 0:
# Some button pressed.
event_type = MouseEventType.MOUSE_DOWN
else:
# No button pressed.
event_type = MouseEventType.MOUSE_UP
data = ";".join(
[
button.value,
event_type.value,
str(ev.MousePosition.X),
str(ev.MousePosition.Y),
]
)
return [KeyPress(Keys.WindowsMouseEvent, data)]
class Vt100ConsoleInputReader:
"""
Similar to `ConsoleInputReader`, but for usage when
`ENABLE_VIRTUAL_TERMINAL_INPUT` is enabled. This assumes that Windows sends
us the right vt100 escape sequences and we parse those with our vt100
parser.
(Using this instead of `ConsoleInputReader` results in the "data" attribute
from the `KeyPress` instances to be more correct in edge cases, because
this responds to for instance the terminal being in application cursor keys
mode.)
"""
def __init__(self) -> None:
self._fdcon = None
self._buffer: list[KeyPress] = [] # Buffer to collect the Key objects.
self._vt100_parser = Vt100Parser(
lambda key_press: self._buffer.append(key_press)
)
# When stdin is a tty, use that handle, otherwise, create a handle from
# CONIN$.
self.handle: HANDLE
if sys.stdin.isatty():
self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
else:
self._fdcon = os.open("CONIN$", os.O_RDWR | os.O_BINARY)
self.handle = HANDLE(msvcrt.get_osfhandle(self._fdcon))
def close(self) -> None:
"Close fdcon."
if self._fdcon is not None:
os.close(self._fdcon)
def read(self) -> Iterable[KeyPress]:
"""
Return a list of `KeyPress` instances. It won't return anything when
there was nothing to read. (This function doesn't block.)
http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx
"""
max_count = 2048 # Max events to read at the same time.
read = DWORD(0)
arrtype = INPUT_RECORD * max_count
input_records = arrtype()
# Check whether there is some input to read. `ReadConsoleInputW` would
# block otherwise.
# (Actually, the event loop is responsible to make sure that this
# function is only called when there is something to read, but for some
# reason this happened in the asyncio_win32 loop, and it's better to be
# safe anyway.)
if not wait_for_handles([self.handle], timeout=0):
return []
# Get next batch of input event.
windll.kernel32.ReadConsoleInputW(
self.handle, pointer(input_records), max_count, pointer(read)
)
# First, get all the keys from the input buffer, in order to determine
# whether we should consider this a paste event or not.
for key_data in self._get_keys(read, input_records):
self._vt100_parser.feed(key_data)
# Return result.
result = self._buffer
self._buffer = []
return result
def _get_keys(
self, read: DWORD, input_records: Array[INPUT_RECORD]
) -> Iterator[str]:
"""
Generator that yields `KeyPress` objects from the input records.
"""
for i in range(read.value):
ir = input_records[i]
# Get the right EventType from the EVENT_RECORD.
# (For some reason the Windows console application 'cmder'
# [http://gooseberrycreative.com/cmder/] can return '0' for
# ir.EventType. -- Just ignore that.)
if ir.EventType in EventTypes:
ev = getattr(ir.Event, EventTypes[ir.EventType])
# Process if this is a key event. (We also have mouse, menu and
# focus events.)
if isinstance(ev, KEY_EVENT_RECORD) and ev.KeyDown:
u_char = ev.uChar.UnicodeChar
if u_char != "\x00":
yield u_char
class _Win32Handles:
"""
Utility to keep track of which handles are connectod to which callbacks.
`add_win32_handle` starts a tiny event loop in another thread which waits
for the Win32 handle to become ready. When this happens, the callback will
be called in the current asyncio event loop using `call_soon_threadsafe`.
`remove_win32_handle` will stop this tiny event loop.
NOTE: We use this technique, so that we don't have to use the
`ProactorEventLoop` on Windows and we can wait for things like stdin
in a `SelectorEventLoop`. This is important, because our inputhook
mechanism (used by IPython), only works with the `SelectorEventLoop`.
"""
def __init__(self) -> None:
self._handle_callbacks: dict[int, Callable[[], None]] = {}
# Windows Events that are triggered when we have to stop watching this
# handle.
self._remove_events: dict[int, HANDLE] = {}
def add_win32_handle(self, handle: HANDLE, callback: Callable[[], None]) -> None:
"""
Add a Win32 handle to the event loop.
"""
handle_value = handle.value
if handle_value is None:
raise ValueError("Invalid handle.")
# Make sure to remove a previous registered handler first.
self.remove_win32_handle(handle)
loop = get_running_loop()
self._handle_callbacks[handle_value] = callback
# Create remove event.
remove_event = create_win32_event()
self._remove_events[handle_value] = remove_event
# Add reader.
def ready() -> None:
# Tell the callback that input's ready.
try:
callback()
finally:
run_in_executor_with_context(wait, loop=loop)
# Wait for the input to become ready.
# (Use an executor for this, the Windows asyncio event loop doesn't
# allow us to wait for handles like stdin.)
def wait() -> None:
# Wait until either the handle becomes ready, or the remove event
# has been set.
result = wait_for_handles([remove_event, handle])
if result is remove_event:
windll.kernel32.CloseHandle(remove_event)
return
else:
loop.call_soon_threadsafe(ready)
run_in_executor_with_context(wait, loop=loop)
def remove_win32_handle(self, handle: HANDLE) -> Callable[[], None] | None:
"""
Remove a Win32 handle from the event loop.
Return either the registered handler or `None`.
"""
if handle.value is None:
return None # Ignore.
# Trigger remove events, so that the reader knows to stop.
try:
event = self._remove_events.pop(handle.value)
except KeyError:
pass
else:
windll.kernel32.SetEvent(event)
try:
return self._handle_callbacks.pop(handle.value)
except KeyError:
return None
@contextmanager
def attach_win32_input(
input: _Win32InputBase, callback: Callable[[], None]
) -> Iterator[None]:
"""
Context manager that makes this input active in the current event loop.
:param input: :class:`~prompt_toolkit.input.Input` object.
:param input_ready_callback: Called when the input is ready to read.
"""
win32_handles = input.win32_handles
handle = input.handle
if handle.value is None:
raise ValueError("Invalid handle.")
# Add reader.
previous_callback = win32_handles.remove_win32_handle(handle)
win32_handles.add_win32_handle(handle, callback)
try:
yield
finally:
win32_handles.remove_win32_handle(handle)
if previous_callback:
win32_handles.add_win32_handle(handle, previous_callback)
@contextmanager
def detach_win32_input(input: _Win32InputBase) -> Iterator[None]:
win32_handles = input.win32_handles
handle = input.handle
if handle.value is None:
raise ValueError("Invalid handle.")
previous_callback = win32_handles.remove_win32_handle(handle)
try:
yield
finally:
if previous_callback:
win32_handles.add_win32_handle(handle, previous_callback)
class raw_mode:
"""
::
with raw_mode(stdin):
''' the windows terminal is now in 'raw' mode. '''
The ``fileno`` attribute is ignored. This is to be compatible with the
`raw_input` method of `.vt100_input`.
"""
def __init__(
self, fileno: int | None = None, use_win10_virtual_terminal_input: bool = False
) -> None:
self.handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
self.use_win10_virtual_terminal_input = use_win10_virtual_terminal_input
def __enter__(self) -> None:
# Remember original mode.
original_mode = DWORD()
windll.kernel32.GetConsoleMode(self.handle, pointer(original_mode))
self.original_mode = original_mode
self._patch()
def _patch(self) -> None:
# Set raw
ENABLE_ECHO_INPUT = 0x0004
ENABLE_LINE_INPUT = 0x0002
ENABLE_PROCESSED_INPUT = 0x0001
new_mode = self.original_mode.value & ~(
ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT
)
if self.use_win10_virtual_terminal_input:
new_mode |= ENABLE_VIRTUAL_TERMINAL_INPUT
windll.kernel32.SetConsoleMode(self.handle, new_mode)
def __exit__(self, *a: object) -> None:
# Restore original mode
windll.kernel32.SetConsoleMode(self.handle, self.original_mode)
class cooked_mode(raw_mode):
"""
::
with cooked_mode(stdin):
''' The pseudo-terminal stdin is now used in cooked mode. '''
"""
def _patch(self) -> None:
# Set cooked.
ENABLE_ECHO_INPUT = 0x0004
ENABLE_LINE_INPUT = 0x0002
ENABLE_PROCESSED_INPUT = 0x0001
windll.kernel32.SetConsoleMode(
self.handle,
self.original_mode.value
| (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT),
)
def _is_win_vt100_input_enabled() -> bool:
"""
Returns True when we're running Windows and VT100 escape sequences are
supported.
"""
hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE))
# Get original console mode.
original_mode = DWORD(0)
windll.kernel32.GetConsoleMode(hconsole, byref(original_mode))
try:
# Try to enable VT100 sequences.
result: int = windll.kernel32.SetConsoleMode(
hconsole, DWORD(ENABLE_VIRTUAL_TERMINAL_INPUT)
)
return result == 1
finally:
windll.kernel32.SetConsoleMode(hconsole, original_mode)

View File

@@ -0,0 +1,156 @@
from __future__ import annotations
import sys
assert sys.platform == "win32"
from contextlib import contextmanager
from ctypes import windll
from ctypes.wintypes import HANDLE
from typing import Callable, ContextManager, Iterator
from prompt_toolkit.eventloop.win32 import create_win32_event
from ..key_binding import KeyPress
from ..utils import DummyContext
from .base import PipeInput
from .vt100_parser import Vt100Parser
from .win32 import _Win32InputBase, attach_win32_input, detach_win32_input
__all__ = ["Win32PipeInput"]
class Win32PipeInput(_Win32InputBase, PipeInput):
"""
This is an input pipe that works on Windows.
Text or bytes can be feed into the pipe, and key strokes can be read from
the pipe. This is useful if we want to send the input programmatically into
the application. Mostly useful for unit testing.
Notice that even though it's Windows, we use vt100 escape sequences over
the pipe.
Usage::
input = Win32PipeInput()
input.send_text('inputdata')
"""
_id = 0
def __init__(self, _event: HANDLE) -> None:
super().__init__()
# Event (handle) for registering this input in the event loop.
# This event is set when there is data available to read from the pipe.
# Note: We use this approach instead of using a regular pipe, like
# returned from `os.pipe()`, because making such a regular pipe
# non-blocking is tricky and this works really well.
self._event = create_win32_event()
self._closed = False
# Parser for incoming keys.
self._buffer: list[KeyPress] = [] # Buffer to collect the Key objects.
self.vt100_parser = Vt100Parser(lambda key: self._buffer.append(key))
# Identifier for every PipeInput for the hash.
self.__class__._id += 1
self._id = self.__class__._id
@classmethod
@contextmanager
def create(cls) -> Iterator[Win32PipeInput]:
event = create_win32_event()
try:
yield Win32PipeInput(_event=event)
finally:
windll.kernel32.CloseHandle(event)
@property
def closed(self) -> bool:
return self._closed
def fileno(self) -> int:
"""
The windows pipe doesn't depend on the file handle.
"""
raise NotImplementedError
@property
def handle(self) -> HANDLE:
"The handle used for registering this pipe in the event loop."
return self._event
def attach(self, input_ready_callback: Callable[[], None]) -> ContextManager[None]:
"""
Return a context manager that makes this input active in the current
event loop.
"""
return attach_win32_input(self, input_ready_callback)
def detach(self) -> ContextManager[None]:
"""
Return a context manager that makes sure that this input is not active
in the current event loop.
"""
return detach_win32_input(self)
def read_keys(self) -> list[KeyPress]:
"Read list of KeyPress."
# Return result.
result = self._buffer
self._buffer = []
# Reset event.
if not self._closed:
# (If closed, the event should not reset.)
windll.kernel32.ResetEvent(self._event)
return result
def flush_keys(self) -> list[KeyPress]:
"""
Flush pending keys and return them.
(Used for flushing the 'escape' key.)
"""
# Flush all pending keys. (This is most important to flush the vt100
# 'Escape' key early when nothing else follows.)
self.vt100_parser.flush()
# Return result.
result = self._buffer
self._buffer = []
return result
def send_bytes(self, data: bytes) -> None:
"Send bytes to the input."
self.send_text(data.decode("utf-8", "ignore"))
def send_text(self, text: str) -> None:
"Send text to the input."
if self._closed:
raise ValueError("Attempt to write into a closed pipe.")
# Pass it through our vt100 parser.
self.vt100_parser.feed(text)
# Set event.
windll.kernel32.SetEvent(self._event)
def raw_mode(self) -> ContextManager[None]:
return DummyContext()
def cooked_mode(self) -> ContextManager[None]:
return DummyContext()
def close(self) -> None:
"Close write-end of the pipe."
self._closed = True
windll.kernel32.SetEvent(self._event)
def typeahead_hash(self) -> str:
"""
This needs to be unique for every `PipeInput`.
"""
return f"pipe-input-{self._id}"