Spaces:
Sleeping
Sleeping
File size: 10,514 Bytes
2d876d1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
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
|