Spaces:
Sleeping
Sleeping
first-space
/
first-space-venv
/lib
/python3.12
/site-packages
/prompt_toolkit
/input
/posix_utils.py
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) | |