749 lines
22 KiB
Python
749 lines
22 KiB
Python
|
"""
|
||
|
Output for vt100 terminals.
|
||
|
|
||
|
A lot of thanks, regarding outputting of colors, goes to the Pygments project:
|
||
|
(We don't rely on Pygments anymore, because many things are very custom, and
|
||
|
everything has been highly optimized.)
|
||
|
http://pygments.org/
|
||
|
"""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
import io
|
||
|
import os
|
||
|
import sys
|
||
|
from typing import Callable, Dict, Hashable, Iterable, Sequence, TextIO, Tuple
|
||
|
|
||
|
from prompt_toolkit.cursor_shapes import CursorShape
|
||
|
from prompt_toolkit.data_structures import Size
|
||
|
from prompt_toolkit.output import Output
|
||
|
from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs
|
||
|
from prompt_toolkit.utils import is_dumb_terminal
|
||
|
|
||
|
from .color_depth import ColorDepth
|
||
|
from .flush_stdout import flush_stdout
|
||
|
|
||
|
__all__ = [
|
||
|
"Vt100_Output",
|
||
|
]
|
||
|
|
||
|
|
||
|
FG_ANSI_COLORS = {
|
||
|
"ansidefault": 39,
|
||
|
# Low intensity.
|
||
|
"ansiblack": 30,
|
||
|
"ansired": 31,
|
||
|
"ansigreen": 32,
|
||
|
"ansiyellow": 33,
|
||
|
"ansiblue": 34,
|
||
|
"ansimagenta": 35,
|
||
|
"ansicyan": 36,
|
||
|
"ansigray": 37,
|
||
|
# High intensity.
|
||
|
"ansibrightblack": 90,
|
||
|
"ansibrightred": 91,
|
||
|
"ansibrightgreen": 92,
|
||
|
"ansibrightyellow": 93,
|
||
|
"ansibrightblue": 94,
|
||
|
"ansibrightmagenta": 95,
|
||
|
"ansibrightcyan": 96,
|
||
|
"ansiwhite": 97,
|
||
|
}
|
||
|
|
||
|
BG_ANSI_COLORS = {
|
||
|
"ansidefault": 49,
|
||
|
# Low intensity.
|
||
|
"ansiblack": 40,
|
||
|
"ansired": 41,
|
||
|
"ansigreen": 42,
|
||
|
"ansiyellow": 43,
|
||
|
"ansiblue": 44,
|
||
|
"ansimagenta": 45,
|
||
|
"ansicyan": 46,
|
||
|
"ansigray": 47,
|
||
|
# High intensity.
|
||
|
"ansibrightblack": 100,
|
||
|
"ansibrightred": 101,
|
||
|
"ansibrightgreen": 102,
|
||
|
"ansibrightyellow": 103,
|
||
|
"ansibrightblue": 104,
|
||
|
"ansibrightmagenta": 105,
|
||
|
"ansibrightcyan": 106,
|
||
|
"ansiwhite": 107,
|
||
|
}
|
||
|
|
||
|
|
||
|
ANSI_COLORS_TO_RGB = {
|
||
|
"ansidefault": (
|
||
|
0x00,
|
||
|
0x00,
|
||
|
0x00,
|
||
|
), # Don't use, 'default' doesn't really have a value.
|
||
|
"ansiblack": (0x00, 0x00, 0x00),
|
||
|
"ansigray": (0xE5, 0xE5, 0xE5),
|
||
|
"ansibrightblack": (0x7F, 0x7F, 0x7F),
|
||
|
"ansiwhite": (0xFF, 0xFF, 0xFF),
|
||
|
# Low intensity.
|
||
|
"ansired": (0xCD, 0x00, 0x00),
|
||
|
"ansigreen": (0x00, 0xCD, 0x00),
|
||
|
"ansiyellow": (0xCD, 0xCD, 0x00),
|
||
|
"ansiblue": (0x00, 0x00, 0xCD),
|
||
|
"ansimagenta": (0xCD, 0x00, 0xCD),
|
||
|
"ansicyan": (0x00, 0xCD, 0xCD),
|
||
|
# High intensity.
|
||
|
"ansibrightred": (0xFF, 0x00, 0x00),
|
||
|
"ansibrightgreen": (0x00, 0xFF, 0x00),
|
||
|
"ansibrightyellow": (0xFF, 0xFF, 0x00),
|
||
|
"ansibrightblue": (0x00, 0x00, 0xFF),
|
||
|
"ansibrightmagenta": (0xFF, 0x00, 0xFF),
|
||
|
"ansibrightcyan": (0x00, 0xFF, 0xFF),
|
||
|
}
|
||
|
|
||
|
|
||
|
assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
|
||
|
assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
|
||
|
assert set(ANSI_COLORS_TO_RGB) == set(ANSI_COLOR_NAMES)
|
||
|
|
||
|
|
||
|
def _get_closest_ansi_color(r: int, g: int, b: int, exclude: Sequence[str] = ()) -> str:
|
||
|
"""
|
||
|
Find closest ANSI color. Return it by name.
|
||
|
|
||
|
:param r: Red (Between 0 and 255.)
|
||
|
:param g: Green (Between 0 and 255.)
|
||
|
:param b: Blue (Between 0 and 255.)
|
||
|
:param exclude: A tuple of color names to exclude. (E.g. ``('ansired', )``.)
|
||
|
"""
|
||
|
exclude = list(exclude)
|
||
|
|
||
|
# When we have a bit of saturation, avoid the gray-like colors, otherwise,
|
||
|
# too often the distance to the gray color is less.
|
||
|
saturation = abs(r - g) + abs(g - b) + abs(b - r) # Between 0..510
|
||
|
|
||
|
if saturation > 30:
|
||
|
exclude.extend(["ansilightgray", "ansidarkgray", "ansiwhite", "ansiblack"])
|
||
|
|
||
|
# Take the closest color.
|
||
|
# (Thanks to Pygments for this part.)
|
||
|
distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff)
|
||
|
match = "ansidefault"
|
||
|
|
||
|
for name, (r2, g2, b2) in ANSI_COLORS_TO_RGB.items():
|
||
|
if name != "ansidefault" and name not in exclude:
|
||
|
d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
|
||
|
|
||
|
if d < distance:
|
||
|
match = name
|
||
|
distance = d
|
||
|
|
||
|
return match
|
||
|
|
||
|
|
||
|
_ColorCodeAndName = Tuple[int, str]
|
||
|
|
||
|
|
||
|
class _16ColorCache:
|
||
|
"""
|
||
|
Cache which maps (r, g, b) tuples to 16 ansi colors.
|
||
|
|
||
|
:param bg: Cache for background colors, instead of foreground.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, bg: bool = False) -> None:
|
||
|
self.bg = bg
|
||
|
self._cache: dict[Hashable, _ColorCodeAndName] = {}
|
||
|
|
||
|
def get_code(
|
||
|
self, value: tuple[int, int, int], exclude: Sequence[str] = ()
|
||
|
) -> _ColorCodeAndName:
|
||
|
"""
|
||
|
Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for
|
||
|
a given (r,g,b) value.
|
||
|
"""
|
||
|
key: Hashable = (value, tuple(exclude))
|
||
|
cache = self._cache
|
||
|
|
||
|
if key not in cache:
|
||
|
cache[key] = self._get(value, exclude)
|
||
|
|
||
|
return cache[key]
|
||
|
|
||
|
def _get(
|
||
|
self, value: tuple[int, int, int], exclude: Sequence[str] = ()
|
||
|
) -> _ColorCodeAndName:
|
||
|
r, g, b = value
|
||
|
match = _get_closest_ansi_color(r, g, b, exclude=exclude)
|
||
|
|
||
|
# Turn color name into code.
|
||
|
if self.bg:
|
||
|
code = BG_ANSI_COLORS[match]
|
||
|
else:
|
||
|
code = FG_ANSI_COLORS[match]
|
||
|
|
||
|
return code, match
|
||
|
|
||
|
|
||
|
class _256ColorCache(Dict[Tuple[int, int, int], int]):
|
||
|
"""
|
||
|
Cache which maps (r, g, b) tuples to 256 colors.
|
||
|
"""
|
||
|
|
||
|
def __init__(self) -> None:
|
||
|
# Build color table.
|
||
|
colors: list[tuple[int, int, int]] = []
|
||
|
|
||
|
# colors 0..15: 16 basic colors
|
||
|
colors.append((0x00, 0x00, 0x00)) # 0
|
||
|
colors.append((0xCD, 0x00, 0x00)) # 1
|
||
|
colors.append((0x00, 0xCD, 0x00)) # 2
|
||
|
colors.append((0xCD, 0xCD, 0x00)) # 3
|
||
|
colors.append((0x00, 0x00, 0xEE)) # 4
|
||
|
colors.append((0xCD, 0x00, 0xCD)) # 5
|
||
|
colors.append((0x00, 0xCD, 0xCD)) # 6
|
||
|
colors.append((0xE5, 0xE5, 0xE5)) # 7
|
||
|
colors.append((0x7F, 0x7F, 0x7F)) # 8
|
||
|
colors.append((0xFF, 0x00, 0x00)) # 9
|
||
|
colors.append((0x00, 0xFF, 0x00)) # 10
|
||
|
colors.append((0xFF, 0xFF, 0x00)) # 11
|
||
|
colors.append((0x5C, 0x5C, 0xFF)) # 12
|
||
|
colors.append((0xFF, 0x00, 0xFF)) # 13
|
||
|
colors.append((0x00, 0xFF, 0xFF)) # 14
|
||
|
colors.append((0xFF, 0xFF, 0xFF)) # 15
|
||
|
|
||
|
# colors 16..232: the 6x6x6 color cube
|
||
|
valuerange = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF)
|
||
|
|
||
|
for i in range(217):
|
||
|
r = valuerange[(i // 36) % 6]
|
||
|
g = valuerange[(i // 6) % 6]
|
||
|
b = valuerange[i % 6]
|
||
|
colors.append((r, g, b))
|
||
|
|
||
|
# colors 233..253: grayscale
|
||
|
for i in range(1, 22):
|
||
|
v = 8 + i * 10
|
||
|
colors.append((v, v, v))
|
||
|
|
||
|
self.colors = colors
|
||
|
|
||
|
def __missing__(self, value: tuple[int, int, int]) -> int:
|
||
|
r, g, b = value
|
||
|
|
||
|
# Find closest color.
|
||
|
# (Thanks to Pygments for this!)
|
||
|
distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff)
|
||
|
match = 0
|
||
|
|
||
|
for i, (r2, g2, b2) in enumerate(self.colors):
|
||
|
if i >= 16: # XXX: We ignore the 16 ANSI colors when mapping RGB
|
||
|
# to the 256 colors, because these highly depend on
|
||
|
# the color scheme of the terminal.
|
||
|
d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
|
||
|
|
||
|
if d < distance:
|
||
|
match = i
|
||
|
distance = d
|
||
|
|
||
|
# Turn color name into code.
|
||
|
self[value] = match
|
||
|
return match
|
||
|
|
||
|
|
||
|
_16_fg_colors = _16ColorCache(bg=False)
|
||
|
_16_bg_colors = _16ColorCache(bg=True)
|
||
|
_256_colors = _256ColorCache()
|
||
|
|
||
|
|
||
|
class _EscapeCodeCache(Dict[Attrs, str]):
|
||
|
"""
|
||
|
Cache for VT100 escape codes. It maps
|
||
|
(fgcolor, bgcolor, bold, underline, strike, reverse) tuples to VT100
|
||
|
escape sequences.
|
||
|
|
||
|
:param true_color: When True, use 24bit colors instead of 256 colors.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, color_depth: ColorDepth) -> None:
|
||
|
self.color_depth = color_depth
|
||
|
|
||
|
def __missing__(self, attrs: Attrs) -> str:
|
||
|
(
|
||
|
fgcolor,
|
||
|
bgcolor,
|
||
|
bold,
|
||
|
underline,
|
||
|
strike,
|
||
|
italic,
|
||
|
blink,
|
||
|
reverse,
|
||
|
hidden,
|
||
|
) = attrs
|
||
|
parts: list[str] = []
|
||
|
|
||
|
parts.extend(self._colors_to_code(fgcolor or "", bgcolor or ""))
|
||
|
|
||
|
if bold:
|
||
|
parts.append("1")
|
||
|
if italic:
|
||
|
parts.append("3")
|
||
|
if blink:
|
||
|
parts.append("5")
|
||
|
if underline:
|
||
|
parts.append("4")
|
||
|
if reverse:
|
||
|
parts.append("7")
|
||
|
if hidden:
|
||
|
parts.append("8")
|
||
|
if strike:
|
||
|
parts.append("9")
|
||
|
|
||
|
if parts:
|
||
|
result = "\x1b[0;" + ";".join(parts) + "m"
|
||
|
else:
|
||
|
result = "\x1b[0m"
|
||
|
|
||
|
self[attrs] = result
|
||
|
return result
|
||
|
|
||
|
def _color_name_to_rgb(self, color: str) -> tuple[int, int, int]:
|
||
|
"Turn 'ffffff', into (0xff, 0xff, 0xff)."
|
||
|
try:
|
||
|
rgb = int(color, 16)
|
||
|
except ValueError:
|
||
|
raise
|
||
|
else:
|
||
|
r = (rgb >> 16) & 0xFF
|
||
|
g = (rgb >> 8) & 0xFF
|
||
|
b = rgb & 0xFF
|
||
|
return r, g, b
|
||
|
|
||
|
def _colors_to_code(self, fg_color: str, bg_color: str) -> Iterable[str]:
|
||
|
"""
|
||
|
Return a tuple with the vt100 values that represent this color.
|
||
|
"""
|
||
|
# When requesting ANSI colors only, and both fg/bg color were converted
|
||
|
# to ANSI, ensure that the foreground and background color are not the
|
||
|
# same. (Unless they were explicitly defined to be the same color.)
|
||
|
fg_ansi = ""
|
||
|
|
||
|
def get(color: str, bg: bool) -> list[int]:
|
||
|
nonlocal fg_ansi
|
||
|
|
||
|
table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS
|
||
|
|
||
|
if not color or self.color_depth == ColorDepth.DEPTH_1_BIT:
|
||
|
return []
|
||
|
|
||
|
# 16 ANSI colors. (Given by name.)
|
||
|
elif color in table:
|
||
|
return [table[color]]
|
||
|
|
||
|
# RGB colors. (Defined as 'ffffff'.)
|
||
|
else:
|
||
|
try:
|
||
|
rgb = self._color_name_to_rgb(color)
|
||
|
except ValueError:
|
||
|
return []
|
||
|
|
||
|
# When only 16 colors are supported, use that.
|
||
|
if self.color_depth == ColorDepth.DEPTH_4_BIT:
|
||
|
if bg: # Background.
|
||
|
if fg_color != bg_color:
|
||
|
exclude = [fg_ansi]
|
||
|
else:
|
||
|
exclude = []
|
||
|
code, name = _16_bg_colors.get_code(rgb, exclude=exclude)
|
||
|
return [code]
|
||
|
else: # Foreground.
|
||
|
code, name = _16_fg_colors.get_code(rgb)
|
||
|
fg_ansi = name
|
||
|
return [code]
|
||
|
|
||
|
# True colors. (Only when this feature is enabled.)
|
||
|
elif self.color_depth == ColorDepth.DEPTH_24_BIT:
|
||
|
r, g, b = rgb
|
||
|
return [(48 if bg else 38), 2, r, g, b]
|
||
|
|
||
|
# 256 RGB colors.
|
||
|
else:
|
||
|
return [(48 if bg else 38), 5, _256_colors[rgb]]
|
||
|
|
||
|
result: list[int] = []
|
||
|
result.extend(get(fg_color, False))
|
||
|
result.extend(get(bg_color, True))
|
||
|
|
||
|
return map(str, result)
|
||
|
|
||
|
|
||
|
def _get_size(fileno: int) -> tuple[int, int]:
|
||
|
"""
|
||
|
Get the size of this pseudo terminal.
|
||
|
|
||
|
:param fileno: stdout.fileno()
|
||
|
:returns: A (rows, cols) tuple.
|
||
|
"""
|
||
|
size = os.get_terminal_size(fileno)
|
||
|
return size.lines, size.columns
|
||
|
|
||
|
|
||
|
class Vt100_Output(Output):
|
||
|
"""
|
||
|
:param get_size: A callable which returns the `Size` of the output terminal.
|
||
|
:param stdout: Any object with has a `write` and `flush` method + an 'encoding' property.
|
||
|
:param term: The terminal environment variable. (xterm, xterm-256color, linux, ...)
|
||
|
:param enable_cpr: When `True` (the default), send "cursor position
|
||
|
request" escape sequences to the output in order to detect the cursor
|
||
|
position. That way, we can properly determine how much space there is
|
||
|
available for the UI (especially for drop down menus) to render. The
|
||
|
`Renderer` will still try to figure out whether the current terminal
|
||
|
does respond to CPR escapes. When `False`, never attempt to send CPR
|
||
|
requests.
|
||
|
"""
|
||
|
|
||
|
# For the error messages. Only display "Output is not a terminal" once per
|
||
|
# file descriptor.
|
||
|
_fds_not_a_terminal: set[int] = set()
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
stdout: TextIO,
|
||
|
get_size: Callable[[], Size],
|
||
|
term: str | None = None,
|
||
|
default_color_depth: ColorDepth | None = None,
|
||
|
enable_bell: bool = True,
|
||
|
enable_cpr: bool = True,
|
||
|
) -> None:
|
||
|
assert all(hasattr(stdout, a) for a in ("write", "flush"))
|
||
|
|
||
|
self._buffer: list[str] = []
|
||
|
self.stdout: TextIO = stdout
|
||
|
self.default_color_depth = default_color_depth
|
||
|
self._get_size = get_size
|
||
|
self.term = term
|
||
|
self.enable_bell = enable_bell
|
||
|
self.enable_cpr = enable_cpr
|
||
|
|
||
|
# Cache for escape codes.
|
||
|
self._escape_code_caches: dict[ColorDepth, _EscapeCodeCache] = {
|
||
|
ColorDepth.DEPTH_1_BIT: _EscapeCodeCache(ColorDepth.DEPTH_1_BIT),
|
||
|
ColorDepth.DEPTH_4_BIT: _EscapeCodeCache(ColorDepth.DEPTH_4_BIT),
|
||
|
ColorDepth.DEPTH_8_BIT: _EscapeCodeCache(ColorDepth.DEPTH_8_BIT),
|
||
|
ColorDepth.DEPTH_24_BIT: _EscapeCodeCache(ColorDepth.DEPTH_24_BIT),
|
||
|
}
|
||
|
|
||
|
# Keep track of whether the cursor shape was ever changed.
|
||
|
# (We don't restore the cursor shape if it was never changed - by
|
||
|
# default, we don't change them.)
|
||
|
self._cursor_shape_changed = False
|
||
|
|
||
|
@classmethod
|
||
|
def from_pty(
|
||
|
cls,
|
||
|
stdout: TextIO,
|
||
|
term: str | None = None,
|
||
|
default_color_depth: ColorDepth | None = None,
|
||
|
enable_bell: bool = True,
|
||
|
) -> Vt100_Output:
|
||
|
"""
|
||
|
Create an Output class from a pseudo terminal.
|
||
|
(This will take the dimensions by reading the pseudo
|
||
|
terminal attributes.)
|
||
|
"""
|
||
|
fd: int | None
|
||
|
# Normally, this requires a real TTY device, but people instantiate
|
||
|
# this class often during unit tests as well. For convenience, we print
|
||
|
# an error message, use standard dimensions, and go on.
|
||
|
try:
|
||
|
fd = stdout.fileno()
|
||
|
except io.UnsupportedOperation:
|
||
|
fd = None
|
||
|
|
||
|
if not stdout.isatty() and (fd is None or fd not in cls._fds_not_a_terminal):
|
||
|
msg = "Warning: Output is not a terminal (fd=%r).\n"
|
||
|
sys.stderr.write(msg % fd)
|
||
|
sys.stderr.flush()
|
||
|
if fd is not None:
|
||
|
cls._fds_not_a_terminal.add(fd)
|
||
|
|
||
|
def get_size() -> Size:
|
||
|
# If terminal (incorrectly) reports its size as 0, pick a
|
||
|
# reasonable default. See
|
||
|
# https://github.com/ipython/ipython/issues/10071
|
||
|
rows, columns = (None, None)
|
||
|
|
||
|
# It is possible that `stdout` is no longer a TTY device at this
|
||
|
# point. In that case we get an `OSError` in the ioctl call in
|
||
|
# `get_size`. See:
|
||
|
# https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1021
|
||
|
try:
|
||
|
rows, columns = _get_size(stdout.fileno())
|
||
|
except OSError:
|
||
|
pass
|
||
|
return Size(rows=rows or 24, columns=columns or 80)
|
||
|
|
||
|
return cls(
|
||
|
stdout,
|
||
|
get_size,
|
||
|
term=term,
|
||
|
default_color_depth=default_color_depth,
|
||
|
enable_bell=enable_bell,
|
||
|
)
|
||
|
|
||
|
def get_size(self) -> Size:
|
||
|
return self._get_size()
|
||
|
|
||
|
def fileno(self) -> int:
|
||
|
"Return file descriptor."
|
||
|
return self.stdout.fileno()
|
||
|
|
||
|
def encoding(self) -> str:
|
||
|
"Return encoding used for stdout."
|
||
|
return self.stdout.encoding
|
||
|
|
||
|
def write_raw(self, data: str) -> None:
|
||
|
"""
|
||
|
Write raw data to output.
|
||
|
"""
|
||
|
self._buffer.append(data)
|
||
|
|
||
|
def write(self, data: str) -> None:
|
||
|
"""
|
||
|
Write text to output.
|
||
|
(Removes vt100 escape codes. -- used for safely writing text.)
|
||
|
"""
|
||
|
self._buffer.append(data.replace("\x1b", "?"))
|
||
|
|
||
|
def set_title(self, title: str) -> None:
|
||
|
"""
|
||
|
Set terminal title.
|
||
|
"""
|
||
|
if self.term not in (
|
||
|
"linux",
|
||
|
"eterm-color",
|
||
|
): # Not supported by the Linux console.
|
||
|
self.write_raw(
|
||
|
"\x1b]2;{}\x07".format(title.replace("\x1b", "").replace("\x07", ""))
|
||
|
)
|
||
|
|
||
|
def clear_title(self) -> None:
|
||
|
self.set_title("")
|
||
|
|
||
|
def erase_screen(self) -> None:
|
||
|
"""
|
||
|
Erases the screen with the background color and moves the cursor to
|
||
|
home.
|
||
|
"""
|
||
|
self.write_raw("\x1b[2J")
|
||
|
|
||
|
def enter_alternate_screen(self) -> None:
|
||
|
self.write_raw("\x1b[?1049h\x1b[H")
|
||
|
|
||
|
def quit_alternate_screen(self) -> None:
|
||
|
self.write_raw("\x1b[?1049l")
|
||
|
|
||
|
def enable_mouse_support(self) -> None:
|
||
|
self.write_raw("\x1b[?1000h")
|
||
|
|
||
|
# Enable mouse-drag support.
|
||
|
self.write_raw("\x1b[?1003h")
|
||
|
|
||
|
# Enable urxvt Mouse mode. (For terminals that understand this.)
|
||
|
self.write_raw("\x1b[?1015h")
|
||
|
|
||
|
# Also enable Xterm SGR mouse mode. (For terminals that understand this.)
|
||
|
self.write_raw("\x1b[?1006h")
|
||
|
|
||
|
# Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr
|
||
|
# extensions.
|
||
|
|
||
|
def disable_mouse_support(self) -> None:
|
||
|
self.write_raw("\x1b[?1000l")
|
||
|
self.write_raw("\x1b[?1015l")
|
||
|
self.write_raw("\x1b[?1006l")
|
||
|
self.write_raw("\x1b[?1003l")
|
||
|
|
||
|
def erase_end_of_line(self) -> None:
|
||
|
"""
|
||
|
Erases from the current cursor position to the end of the current line.
|
||
|
"""
|
||
|
self.write_raw("\x1b[K")
|
||
|
|
||
|
def erase_down(self) -> None:
|
||
|
"""
|
||
|
Erases the screen from the current line down to the bottom of the
|
||
|
screen.
|
||
|
"""
|
||
|
self.write_raw("\x1b[J")
|
||
|
|
||
|
def reset_attributes(self) -> None:
|
||
|
self.write_raw("\x1b[0m")
|
||
|
|
||
|
def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None:
|
||
|
"""
|
||
|
Create new style and output.
|
||
|
|
||
|
:param attrs: `Attrs` instance.
|
||
|
"""
|
||
|
# Get current depth.
|
||
|
escape_code_cache = self._escape_code_caches[color_depth]
|
||
|
|
||
|
# Write escape character.
|
||
|
self.write_raw(escape_code_cache[attrs])
|
||
|
|
||
|
def disable_autowrap(self) -> None:
|
||
|
self.write_raw("\x1b[?7l")
|
||
|
|
||
|
def enable_autowrap(self) -> None:
|
||
|
self.write_raw("\x1b[?7h")
|
||
|
|
||
|
def enable_bracketed_paste(self) -> None:
|
||
|
self.write_raw("\x1b[?2004h")
|
||
|
|
||
|
def disable_bracketed_paste(self) -> None:
|
||
|
self.write_raw("\x1b[?2004l")
|
||
|
|
||
|
def reset_cursor_key_mode(self) -> None:
|
||
|
"""
|
||
|
For vt100 only.
|
||
|
Put the terminal in cursor mode (instead of application mode).
|
||
|
"""
|
||
|
# Put the terminal in cursor mode. (Instead of application mode.)
|
||
|
self.write_raw("\x1b[?1l")
|
||
|
|
||
|
def cursor_goto(self, row: int = 0, column: int = 0) -> None:
|
||
|
"""
|
||
|
Move cursor position.
|
||
|
"""
|
||
|
self.write_raw("\x1b[%i;%iH" % (row, column))
|
||
|
|
||
|
def cursor_up(self, amount: int) -> None:
|
||
|
if amount == 0:
|
||
|
pass
|
||
|
elif amount == 1:
|
||
|
self.write_raw("\x1b[A")
|
||
|
else:
|
||
|
self.write_raw("\x1b[%iA" % amount)
|
||
|
|
||
|
def cursor_down(self, amount: int) -> None:
|
||
|
if amount == 0:
|
||
|
pass
|
||
|
elif amount == 1:
|
||
|
# Note: Not the same as '\n', '\n' can cause the window content to
|
||
|
# scroll.
|
||
|
self.write_raw("\x1b[B")
|
||
|
else:
|
||
|
self.write_raw("\x1b[%iB" % amount)
|
||
|
|
||
|
def cursor_forward(self, amount: int) -> None:
|
||
|
if amount == 0:
|
||
|
pass
|
||
|
elif amount == 1:
|
||
|
self.write_raw("\x1b[C")
|
||
|
else:
|
||
|
self.write_raw("\x1b[%iC" % amount)
|
||
|
|
||
|
def cursor_backward(self, amount: int) -> None:
|
||
|
if amount == 0:
|
||
|
pass
|
||
|
elif amount == 1:
|
||
|
self.write_raw("\b") # '\x1b[D'
|
||
|
else:
|
||
|
self.write_raw("\x1b[%iD" % amount)
|
||
|
|
||
|
def hide_cursor(self) -> None:
|
||
|
self.write_raw("\x1b[?25l")
|
||
|
|
||
|
def show_cursor(self) -> None:
|
||
|
self.write_raw("\x1b[?12l\x1b[?25h") # Stop blinking cursor and show.
|
||
|
|
||
|
def set_cursor_shape(self, cursor_shape: CursorShape) -> None:
|
||
|
if cursor_shape == CursorShape._NEVER_CHANGE:
|
||
|
return
|
||
|
|
||
|
self._cursor_shape_changed = True
|
||
|
self.write_raw(
|
||
|
{
|
||
|
CursorShape.BLOCK: "\x1b[2 q",
|
||
|
CursorShape.BEAM: "\x1b[6 q",
|
||
|
CursorShape.UNDERLINE: "\x1b[4 q",
|
||
|
CursorShape.BLINKING_BLOCK: "\x1b[1 q",
|
||
|
CursorShape.BLINKING_BEAM: "\x1b[5 q",
|
||
|
CursorShape.BLINKING_UNDERLINE: "\x1b[3 q",
|
||
|
}.get(cursor_shape, "")
|
||
|
)
|
||
|
|
||
|
def reset_cursor_shape(self) -> None:
|
||
|
"Reset cursor shape."
|
||
|
# (Only reset cursor shape, if we ever changed it.)
|
||
|
if self._cursor_shape_changed:
|
||
|
self._cursor_shape_changed = False
|
||
|
|
||
|
# Reset cursor shape.
|
||
|
self.write_raw("\x1b[0 q")
|
||
|
|
||
|
def flush(self) -> None:
|
||
|
"""
|
||
|
Write to output stream and flush.
|
||
|
"""
|
||
|
if not self._buffer:
|
||
|
return
|
||
|
|
||
|
data = "".join(self._buffer)
|
||
|
self._buffer = []
|
||
|
|
||
|
flush_stdout(self.stdout, data)
|
||
|
|
||
|
def ask_for_cpr(self) -> None:
|
||
|
"""
|
||
|
Asks for a cursor position report (CPR).
|
||
|
"""
|
||
|
self.write_raw("\x1b[6n")
|
||
|
self.flush()
|
||
|
|
||
|
@property
|
||
|
def responds_to_cpr(self) -> bool:
|
||
|
if not self.enable_cpr:
|
||
|
return False
|
||
|
|
||
|
# When the input is a tty, we assume that CPR is supported.
|
||
|
# It's not when the input is piped from Pexpect.
|
||
|
if os.environ.get("PROMPT_TOOLKIT_NO_CPR", "") == "1":
|
||
|
return False
|
||
|
|
||
|
if is_dumb_terminal(self.term):
|
||
|
return False
|
||
|
try:
|
||
|
return self.stdout.isatty()
|
||
|
except ValueError:
|
||
|
return False # ValueError: I/O operation on closed file
|
||
|
|
||
|
def bell(self) -> None:
|
||
|
"Sound bell."
|
||
|
if self.enable_bell:
|
||
|
self.write_raw("\a")
|
||
|
self.flush()
|
||
|
|
||
|
def get_default_color_depth(self) -> ColorDepth:
|
||
|
"""
|
||
|
Return the default color depth for a vt100 terminal, according to the
|
||
|
our term value.
|
||
|
|
||
|
We prefer 256 colors almost always, because this is what most terminals
|
||
|
support these days, and is a good default.
|
||
|
"""
|
||
|
if self.default_color_depth is not None:
|
||
|
return self.default_color_depth
|
||
|
|
||
|
term = self.term
|
||
|
|
||
|
if term is None:
|
||
|
return ColorDepth.DEFAULT
|
||
|
|
||
|
if is_dumb_terminal(term):
|
||
|
return ColorDepth.DEPTH_1_BIT
|
||
|
|
||
|
if term in ("linux", "eterm-color"):
|
||
|
return ColorDepth.DEPTH_4_BIT
|
||
|
|
||
|
return ColorDepth.DEFAULT
|