hub/venv/lib/python3.7/site-packages/prompt_toolkit/output/vt100.py

686 lines
21 KiB
Python
Raw Normal View History

"""
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/
"""
import array
import errno
import sys
from typing import (
IO,
Callable,
Dict,
Hashable,
Iterable,
List,
Optional,
Sequence,
Set,
TextIO,
Tuple,
)
from prompt_toolkit.data_structures import Size
from prompt_toolkit.output import Output
from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs
from .color_depth import ColorDepth
__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, 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, 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 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]:
# Thanks to fabric (fabfile.org), and
# http://sqizit.bartletts.id.au/2011/02/14/pseudo-terminals-in-python/
"""
Get the size of this pseudo terminal.
:param fileno: stdout.fileno()
:returns: A (rows, cols) tuple.
"""
# Inline imports, because these modules are not available on Windows.
# (This file is used by ConEmuOutput, which is used on Windows.)
import fcntl
import termios
# Buffer for the C call
buf = array.array("h", [0, 0, 0, 0])
# Do TIOCGWINSZ (Get)
# Note: We should not pass 'True' as a fourth parameter to 'ioctl'. (True
# is the default.) This causes segmentation faults on some systems.
# See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/364
fcntl.ioctl(fileno, termios.TIOCGWINSZ, buf) # type: ignore
# Return rows, cols
return buf[0], buf[1]
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 write_binary: Encode the output before writing it. If `True` (the
default), the `stdout` object is supposed to expose an `encoding` attribute.
"""
# 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: Optional[str] = None,
write_binary: bool = True,
) -> None:
assert all(hasattr(stdout, a) for a in ("write", "flush"))
if write_binary:
assert hasattr(stdout, "encoding")
self._buffer: List[str] = []
self.stdout = stdout
self.write_binary = write_binary
self._get_size = get_size
self.term = term or "xterm"
# 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),
}
@classmethod
def from_pty(cls, stdout: TextIO, term: Optional[str] = None) -> "Vt100_Output":
"""
Create an Output class from a pseudo terminal.
(This will take the dimensions by reading the pseudo
terminal attributes.)
"""
# 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.
fd = stdout.fileno()
if not stdout.isatty() and 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()
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)
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;%s\x07" % title.replace("\x1b", "").replace("\x07", "")
)
def clear_title(self) -> None:
self.set_title("")
def erase_screen(self) -> None:
"""
Erases the screen with the background colour 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 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")
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 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 flush(self) -> None:
"""
Write to output stream and flush.
"""
if not self._buffer:
return
data = "".join(self._buffer)
try:
# (We try to encode ourself, because that way we can replace
# characters that don't exist in the character set, avoiding
# UnicodeEncodeError crashes. E.g. u'\xb7' does not appear in 'ascii'.)
# My Arch Linux installation of july 2015 reported 'ANSI_X3.4-1968'
# for sys.stdout.encoding in xterm.
out: IO
if self.write_binary:
if hasattr(self.stdout, "buffer"):
out = self.stdout.buffer # Py3.
else:
out = self.stdout
out.write(data.encode(self.stdout.encoding or "utf-8", "replace"))
else:
self.stdout.write(data)
self.stdout.flush()
except IOError as e:
if e.args and e.args[0] == errno.EINTR:
# Interrupted system call. Can happen in case of a window
# resize signal. (Just ignore. The resize handler will render
# again anyway.)
pass
elif e.args and e.args[0] == 0:
# This can happen when there is a lot of output and the user
# sends a KeyboardInterrupt by pressing Control-C. E.g. in
# a Python REPL when we execute "while True: print('test')".
# (The `ptpython` REPL uses this `Output` class instead of
# `stdout` directly -- in order to be network transparent.)
# So, just ignore.
pass
else:
raise
self._buffer = []
def ask_for_cpr(self) -> None:
"""
Asks for a cursor position report (CPR).
"""
self.write_raw("\x1b[6n")
self.flush()
def bell(self) -> None:
" Sound bell. "
self.write_raw("\a")
self.flush()