fixed subscription table
This commit is contained in:
@@ -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",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
153
.venv/lib/python3.12/site-packages/prompt_toolkit/input/base.py
Normal file
153
.venv/lib/python3.12/site-packages/prompt_toolkit/input/base.py
Normal 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
|
@@ -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()
|
@@ -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}"
|
@@ -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)
|
@@ -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] = []
|
309
.venv/lib/python3.12/site-packages/prompt_toolkit/input/vt100.py
Normal file
309
.venv/lib/python3.12/site-packages/prompt_toolkit/input/vt100.py
Normal 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
|
@@ -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()
|
886
.venv/lib/python3.12/site-packages/prompt_toolkit/input/win32.py
Normal file
886
.venv/lib/python3.12/site-packages/prompt_toolkit/input/win32.py
Normal 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)
|
@@ -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}"
|
Reference in New Issue
Block a user