"""
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 unicode_literals
from prompt_toolkit.filters import to_simple_filter, Condition
from prompt_toolkit.layout.screen import Size
from prompt_toolkit.renderer import Output
from prompt_toolkit.styles import ANSI_COLOR_NAMES
from six.moves import range
import array
import errno
import os
import six
__all__ = (
'Vt100_Output',
)
FG_ANSI_COLORS = {
'ansidefault': 39,
# Low intensity.
'ansiblack': 30,
'ansidarkred': 31,
'ansidarkgreen': 32,
'ansibrown': 33,
'ansidarkblue': 34,
'ansipurple': 35,
'ansiteal': 36,
'ansilightgray': 37,
# High intensity.
'ansidarkgray': 90,
'ansired': 91,
'ansigreen': 92,
'ansiyellow': 93,
'ansiblue': 94,
'ansifuchsia': 95,
'ansiturquoise': 96,
'ansiwhite': 97,
}
BG_ANSI_COLORS = {
'ansidefault': 49,
# Low intensity.
'ansiblack': 40,
'ansidarkred': 41,
'ansidarkgreen': 42,
'ansibrown': 43,
'ansidarkblue': 44,
'ansipurple': 45,
'ansiteal': 46,
'ansilightgray': 47,
# High intensity.
'ansidarkgray': 100,
'ansired': 101,
'ansigreen': 102,
'ansiyellow': 103,
'ansiblue': 104,
'ansifuchsia': 105,
'ansiturquoise': 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),
'ansidarkgray': (0x7f, 0x7f, 0x7f),
'ansiwhite': (0xff, 0xff, 0xff),
'ansilightgray': (0xe5, 0xe5, 0xe5),
# Low intensity.
'ansidarkred': (0xcd, 0x00, 0x00),
'ansidarkgreen': (0x00, 0xcd, 0x00),
'ansibrown': (0xcd, 0xcd, 0x00),
'ansidarkblue': (0x00, 0x00, 0xcd),
'ansipurple': (0xcd, 0x00, 0xcd),
'ansiteal': (0x00, 0xcd, 0xcd),
# High intensity.
'ansired': (0xff, 0x00, 0x00),
'ansigreen': (0x00, 0xff, 0x00),
'ansiyellow': (0xff, 0xff, 0x00),
'ansiblue': (0x00, 0x00, 0xff),
'ansifuchsia': (0xff, 0x00, 0xff),
'ansiturquoise': (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, g, b, exclude=()):
"""
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', )``.)
"""
assert isinstance(exclude, tuple)
# 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 += ('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
class _16ColorCache(dict):
"""
Cache which maps (r, g, b) tuples to 16 ansi colors.
:param bg: Cache for background colors, instead of foreground.
"""
def __init__(self, bg=False):
assert isinstance(bg, bool)
self.bg = bg
def get_code(self, value, exclude=()):
"""
Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for
a given (r,g,b) value.
"""
key = (value, exclude)
if key not in self:
self[key] = self._get(value, exclude)
return self[key]
def _get(self, value, exclude=()):
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]
self[value] = code
return code, match
class _256ColorCache(dict):
"""
Cach which maps (r, g, b) tuples to 256 colors.
"""
def __init__(self):
# Build color table.
colors = []
# 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):
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):
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):
"""
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, true_color=False, ansi_colors_only=False):
assert isinstance(true_color, bool)
self.true_color = true_color
self.ansi_colors_only = to_simple_filter(ansi_colors_only)
def __missing__(self, attrs):
fgcolor, bgcolor, bold, underline, italic, blink, reverse = attrs
parts = []
parts.extend(self._colors_to_code(fgcolor, bgcolor))
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 parts:
result = '\x1b[0;' + ';'.join(parts) + 'm'
else:
result = '\x1b[0m'
self[attrs] = result
return result
def _color_name_to_rgb(self, color):
" 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, bg_color):
" 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 explicitely defined to be the same color.)
fg_ansi = [()]
def get(color, bg):
table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS
if color is None:
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.ansi_colors_only():
if bg: # Background.
if fg_color != bg_color:
exclude = (fg_ansi[0], )
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[0] = name
return (code, )
# True colors. (Only when this feature is enabled.)
elif self.true_color:
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 = []
result.extend(get(fg_color, False))
result.extend(get(bg_color, True))
return map(six.text_type, result)
def _get_size(fileno):
# 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(b'h' if six.PY2 else u'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)
# 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 true_color: Use 24bit color instead of 256 colors. (Can be a :class:`SimpleFilter`.)
When `ansi_colors_only` is set, only 16 colors are used.
:param ansi_colors_only: Restrict to 16 ANSI colors only.
: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.
"""
def __init__(self, stdout, get_size, true_color=False,
ansi_colors_only=None, term=None, write_binary=True):
assert callable(get_size)
assert term is None or isinstance(term, six.text_type)
assert all(hasattr(stdout, a) for a in ('write', 'flush'))
if write_binary:
assert hasattr(stdout, 'encoding')
self._buffer = []
self.stdout = stdout
self.write_binary = write_binary
self.get_size = get_size
self.true_color = to_simple_filter(true_color)
self.term = term or 'xterm'
# ANSI colors only?
if ansi_colors_only is None:
# When not given, use the following default.
ANSI_COLORS_ONLY = bool(os.environ.get(
'PROMPT_TOOLKIT_ANSI_COLORS_ONLY', False))
@Condition
def ansi_colors_only():
return ANSI_COLORS_ONLY or term in ('linux', 'eterm-color')
else:
ansi_colors_only = to_simple_filter(ansi_colors_only)
self.ansi_colors_only = ansi_colors_only
# Cache for escape codes.
self._escape_code_cache = _EscapeCodeCache(ansi_colors_only=ansi_colors_only)
self._escape_code_cache_true_color = _EscapeCodeCache(
true_color=True, ansi_colors_only=ansi_colors_only)
@classmethod
def from_pty(cls, stdout, true_color=False, ansi_colors_only=None, term=None):
"""
Create an Output class from a pseudo terminal.
(This will take the dimensions by reading the pseudo
terminal attributes.)
"""
assert stdout.isatty()
def get_size():
rows, columns = _get_size(stdout.fileno())
# If terminal (incorrectly) reports its size as 0, pick a reasonable default.
# See https://github.com/ipython/ipython/issues/10071
return Size(rows=(rows or 24), columns=(columns or 80))
return cls(stdout, get_size, true_color=true_color,
ansi_colors_only=ansi_colors_only, term=term)
def fileno(self):
" Return file descriptor. "
return self.stdout.fileno()
def encoding(self):
" Return encoding used for stdout. "
return self.stdout.encoding
def write_raw(self, data):
"""
Write raw data to output.
"""
self._buffer.append(data)
def write(self, data):
"""
Write text to output.
(Removes vt100 escape codes. -- used for safely writing text.)
"""
self._buffer.append(data.replace('\x1b', '?'))
def set_title(self, title):
"""
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):
self.set_title('')
def erase_screen(self):
"""
Erases the screen with the background colour and moves the cursor to
home.
"""
self.write_raw('\x1b[2J')
def enter_alternate_screen(self):
self.write_raw('\x1b[?1049h\x1b[H')
def quit_alternate_screen(self):
self.write_raw('\x1b[?1049l')
def enable_mouse_support(self):
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):
self.write_raw('\x1b[?1000l')
self.write_raw('\x1b[?1015l')
self.write_raw('\x1b[?1006l')
def erase_end_of_line(self):
"""
Erases from the current cursor position to the end of the current line.
"""
self.write_raw('\x1b[K')
def erase_down(self):
"""
Erases the screen from the current line down to the bottom of the
screen.
"""
self.write_raw('\x1b[J')
def reset_attributes(self):
self.write_raw('\x1b[0m')
def set_attributes(self, attrs):
"""
Create new style and output.
:param attrs: `Attrs` instance.
"""
if self.true_color() and not self.ansi_colors_only():
self.write_raw(self._escape_code_cache_true_color[attrs])
else:
self.write_raw(self._escape_code_cache[attrs])
def disable_autowrap(self):
self.write_raw('\x1b[?7l')
def enable_autowrap(self):
self.write_raw('\x1b[?7h')
def enable_bracketed_paste(self):
self.write_raw('\x1b[?2004h')
def disable_bracketed_paste(self):
self.write_raw('\x1b[?2004l')
def cursor_goto(self, row=0, column=0):
""" Move cursor position. """
self.write_raw('\x1b[%i;%iH' % (row, column))
def cursor_up(self, amount):
if amount == 0:
pass
elif amount == 1:
self.write_raw('\x1b[A')
else:
self.write_raw('\x1b[%iA' % amount)
def cursor_down(self, amount):
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):
if amount == 0:
pass
elif amount == 1:
self.write_raw('\x1b[C')
else:
self.write_raw('\x1b[%iC' % amount)
def cursor_backward(self, amount):
if amount == 0:
pass
elif amount == 1:
self.write_raw('\b') # '\x1b[D'
else:
self.write_raw('\x1b[%iD' % amount)
def hide_cursor(self):
self.write_raw('\x1b[?25l')
def show_cursor(self):
self.write_raw('\x1b[?12l\x1b[?25h') # Stop blinking cursor and show.
def flush(self):
"""
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.
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 happpen 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):
"""
Asks for a cursor position report (CPR).
"""
self.write_raw('\x1b[6n')
self.flush()
def bell(self):
" Sound bell. "
self.write_raw('\a')
self.flush()