1"""
2Output for vt100 terminals.
3
4A lot of thanks, regarding outputting of colors, goes to the Pygments project:
5(We don't rely on Pygments anymore, because many things are very custom, and
6everything has been highly optimized.)
7http://pygments.org/
8"""
9from __future__ import unicode_literals
10
11import array
12import errno
13import sys
14
15import six
16from six.moves import range
17
18from prompt_toolkit.layout.screen import Size
19from prompt_toolkit.output import Output
20from prompt_toolkit.styles.base import ANSI_COLOR_NAMES
21
22from .color_depth import ColorDepth
23
24__all__ = [
25    'Vt100_Output',
26]
27
28
29FG_ANSI_COLORS = {
30    'ansidefault': 39,
31
32    # Low intensity.
33    'ansiblack':   30,
34    'ansired':     31,
35    'ansigreen':   32,
36    'ansiyellow':  33,
37    'ansiblue':    34,
38    'ansimagenta': 35,
39    'ansicyan':    36,
40    'ansigray':    37,
41
42    # High intensity.
43    'ansibrightblack':   90,
44    'ansibrightred':     91,
45    'ansibrightgreen':   92,
46    'ansibrightyellow':  93,
47    'ansibrightblue':    94,
48    'ansibrightmagenta': 95,
49    'ansibrightcyan':    96,
50    'ansiwhite':         97,
51}
52
53BG_ANSI_COLORS = {
54    'ansidefault':     49,
55
56    # Low intensity.
57    'ansiblack':   40,
58    'ansired':     41,
59    'ansigreen':   42,
60    'ansiyellow':  43,
61    'ansiblue':    44,
62    'ansimagenta': 45,
63    'ansicyan':    46,
64    'ansigray':    47,
65
66    # High intensity.
67    'ansibrightblack':   100,
68    'ansibrightred':     101,
69    'ansibrightgreen':   102,
70    'ansibrightyellow':  103,
71    'ansibrightblue':    104,
72    'ansibrightmagenta': 105,
73    'ansibrightcyan':    106,
74    'ansiwhite':         107,
75}
76
77
78ANSI_COLORS_TO_RGB = {
79    'ansidefault':     (0x00, 0x00, 0x00),  # Don't use, 'default' doesn't really have a value.
80    'ansiblack':       (0x00, 0x00, 0x00),
81    'ansigray':        (0xe5, 0xe5, 0xe5),
82    'ansibrightblack': (0x7f, 0x7f, 0x7f),
83    'ansiwhite':       (0xff, 0xff, 0xff),
84
85    # Low intensity.
86    'ansired':     (0xcd, 0x00, 0x00),
87    'ansigreen':   (0x00, 0xcd, 0x00),
88    'ansiyellow':  (0xcd, 0xcd, 0x00),
89    'ansiblue':    (0x00, 0x00, 0xcd),
90    'ansimagenta': (0xcd, 0x00, 0xcd),
91    'ansicyan':    (0x00, 0xcd, 0xcd),
92
93    # High intensity.
94    'ansibrightred':     (0xff, 0x00, 0x00),
95    'ansibrightgreen':   (0x00, 0xff, 0x00),
96    'ansibrightyellow':  (0xff, 0xff, 0x00),
97    'ansibrightblue':    (0x00, 0x00, 0xff),
98    'ansibrightmagenta': (0xff, 0x00, 0xff),
99    'ansibrightcyan':    (0x00, 0xff, 0xff),
100}
101
102
103assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
104assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
105assert set(ANSI_COLORS_TO_RGB) == set(ANSI_COLOR_NAMES)
106
107
108def _get_closest_ansi_color(r, g, b, exclude=()):
109    """
110    Find closest ANSI color. Return it by name.
111
112    :param r: Red (Between 0 and 255.)
113    :param g: Green (Between 0 and 255.)
114    :param b: Blue (Between 0 and 255.)
115    :param exclude: A tuple of color names to exclude. (E.g. ``('ansired', )``.)
116    """
117    assert isinstance(exclude, tuple)
118
119    # When we have a bit of saturation, avoid the gray-like colors, otherwise,
120    # too often the distance to the gray color is less.
121    saturation = abs(r - g) + abs(g - b) + abs(b - r)  # Between 0..510
122
123    if saturation > 30:
124        exclude += ('ansilightgray', 'ansidarkgray', 'ansiwhite', 'ansiblack')
125
126    # Take the closest color.
127    # (Thanks to Pygments for this part.)
128    distance = 257 * 257 * 3  # "infinity" (>distance from #000000 to #ffffff)
129    match = 'ansidefault'
130
131    for name, (r2, g2, b2) in ANSI_COLORS_TO_RGB.items():
132        if name != 'ansidefault' and name not in exclude:
133            d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
134
135            if d < distance:
136                match = name
137                distance = d
138
139    return match
140
141
142class _16ColorCache(dict):
143    """
144    Cache which maps (r, g, b) tuples to 16 ansi colors.
145
146    :param bg: Cache for background colors, instead of foreground.
147    """
148    def __init__(self, bg=False):
149        assert isinstance(bg, bool)
150        self.bg = bg
151
152    def get_code(self, value, exclude=()):
153        """
154        Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for
155        a given (r,g,b) value.
156        """
157        key = (value, exclude)
158        if key not in self:
159            self[key] = self._get(value, exclude)
160        return self[key]
161
162    def _get(self, value, exclude=()):
163        r, g, b = value
164        match = _get_closest_ansi_color(r, g, b, exclude=exclude)
165
166        # Turn color name into code.
167        if self.bg:
168            code = BG_ANSI_COLORS[match]
169        else:
170            code = FG_ANSI_COLORS[match]
171
172        self[value] = code
173        return code, match
174
175
176class _256ColorCache(dict):
177    """
178    Cache which maps (r, g, b) tuples to 256 colors.
179    """
180    def __init__(self):
181        # Build color table.
182        colors = []
183
184        # colors 0..15: 16 basic colors
185        colors.append((0x00, 0x00, 0x00))  # 0
186        colors.append((0xcd, 0x00, 0x00))  # 1
187        colors.append((0x00, 0xcd, 0x00))  # 2
188        colors.append((0xcd, 0xcd, 0x00))  # 3
189        colors.append((0x00, 0x00, 0xee))  # 4
190        colors.append((0xcd, 0x00, 0xcd))  # 5
191        colors.append((0x00, 0xcd, 0xcd))  # 6
192        colors.append((0xe5, 0xe5, 0xe5))  # 7
193        colors.append((0x7f, 0x7f, 0x7f))  # 8
194        colors.append((0xff, 0x00, 0x00))  # 9
195        colors.append((0x00, 0xff, 0x00))  # 10
196        colors.append((0xff, 0xff, 0x00))  # 11
197        colors.append((0x5c, 0x5c, 0xff))  # 12
198        colors.append((0xff, 0x00, 0xff))  # 13
199        colors.append((0x00, 0xff, 0xff))  # 14
200        colors.append((0xff, 0xff, 0xff))  # 15
201
202        # colors 16..232: the 6x6x6 color cube
203        valuerange = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff)
204
205        for i in range(217):
206            r = valuerange[(i // 36) % 6]
207            g = valuerange[(i // 6) % 6]
208            b = valuerange[i % 6]
209            colors.append((r, g, b))
210
211        # colors 233..253: grayscale
212        for i in range(1, 22):
213            v = 8 + i * 10
214            colors.append((v, v, v))
215
216        self.colors = colors
217
218    def __missing__(self, value):
219        r, g, b = value
220
221        # Find closest color.
222        # (Thanks to Pygments for this!)
223        distance = 257 * 257 * 3  # "infinity" (>distance from #000000 to #ffffff)
224        match = 0
225
226        for i, (r2, g2, b2) in enumerate(self.colors):
227            if i >= 16:  # XXX: We ignore the 16 ANSI colors when mapping RGB
228                         # to the 256 colors, because these highly depend on
229                         # the color scheme of the terminal.
230                d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
231
232                if d < distance:
233                    match = i
234                    distance = d
235
236        # Turn color name into code.
237        self[value] = match
238        return match
239
240
241_16_fg_colors = _16ColorCache(bg=False)
242_16_bg_colors = _16ColorCache(bg=True)
243_256_colors = _256ColorCache()
244
245
246class _EscapeCodeCache(dict):
247    """
248    Cache for VT100 escape codes. It maps
249    (fgcolor, bgcolor, bold, underline, reverse) tuples to VT100 escape sequences.
250
251    :param true_color: When True, use 24bit colors instead of 256 colors.
252    """
253    def __init__(self, color_depth):
254        assert color_depth in ColorDepth._ALL
255        self.color_depth = color_depth
256
257    def __missing__(self, attrs):
258        fgcolor, bgcolor, bold, underline, italic, blink, reverse, hidden = attrs
259        parts = []
260
261        parts.extend(self._colors_to_code(fgcolor, bgcolor))
262
263        if bold:
264            parts.append('1')
265        if italic:
266            parts.append('3')
267        if blink:
268            parts.append('5')
269        if underline:
270            parts.append('4')
271        if reverse:
272            parts.append('7')
273        if hidden:
274            parts.append('8')
275
276        if parts:
277            result = '\x1b[0;' + ';'.join(parts) + 'm'
278        else:
279            result = '\x1b[0m'
280
281        self[attrs] = result
282        return result
283
284    def _color_name_to_rgb(self, color):
285        " Turn 'ffffff', into (0xff, 0xff, 0xff). "
286        try:
287            rgb = int(color, 16)
288        except ValueError:
289            raise
290        else:
291            r = (rgb >> 16) & 0xff
292            g = (rgb >> 8) & 0xff
293            b = rgb & 0xff
294            return r, g, b
295
296    def _colors_to_code(self, fg_color, bg_color):
297        " Return a tuple with the vt100 values  that represent this color. "
298        # When requesting ANSI colors only, and both fg/bg color were converted
299        # to ANSI, ensure that the foreground and background color are not the
300        # same. (Unless they were explicitly defined to be the same color.)
301        fg_ansi = [()]
302
303        def get(color, bg):
304            table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS
305
306            if not color or self.color_depth == ColorDepth.DEPTH_1_BIT:
307                return ()
308
309            # 16 ANSI colors. (Given by name.)
310            elif color in table:
311                return (table[color], )
312
313            # RGB colors. (Defined as 'ffffff'.)
314            else:
315                try:
316                    rgb = self._color_name_to_rgb(color)
317                except ValueError:
318                    return ()
319
320                # When only 16 colors are supported, use that.
321                if self.color_depth == ColorDepth.DEPTH_4_BIT:
322                    if bg:  # Background.
323                        if fg_color != bg_color:
324                            exclude = (fg_ansi[0], )
325                        else:
326                            exclude = ()
327                        code, name = _16_bg_colors.get_code(rgb, exclude=exclude)
328                        return (code, )
329                    else:  # Foreground.
330                        code, name = _16_fg_colors.get_code(rgb)
331                        fg_ansi[0] = name
332                        return (code, )
333
334                # True colors. (Only when this feature is enabled.)
335                elif self.color_depth == ColorDepth.DEPTH_24_BIT:
336                    r, g, b = rgb
337                    return (48 if bg else 38, 2, r, g, b)
338
339                # 256 RGB colors.
340                else:
341                    return (48 if bg else 38, 5, _256_colors[rgb])
342
343        result = []
344        result.extend(get(fg_color, False))
345        result.extend(get(bg_color, True))
346
347        return map(six.text_type, result)
348
349
350def _get_size(fileno):
351    # Thanks to fabric (fabfile.org), and
352    # http://sqizit.bartletts.id.au/2011/02/14/pseudo-terminals-in-python/
353    """
354    Get the size of this pseudo terminal.
355
356    :param fileno: stdout.fileno()
357    :returns: A (rows, cols) tuple.
358    """
359    # Inline imports, because these modules are not available on Windows.
360    # (This file is used by ConEmuOutput, which is used on Windows.)
361    import fcntl
362    import termios
363
364    # Buffer for the C call
365    buf = array.array(b'h' if six.PY2 else u'h', [0, 0, 0, 0])
366
367    # Do TIOCGWINSZ (Get)
368    # Note: We should not pass 'True' as a fourth parameter to 'ioctl'. (True
369    #       is the default.) This causes segmentation faults on some systems.
370    #       See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/364
371    fcntl.ioctl(fileno, termios.TIOCGWINSZ, buf)
372
373    # Return rows, cols
374    return buf[0], buf[1]
375
376
377class Vt100_Output(Output):
378    """
379    :param get_size: A callable which returns the `Size` of the output terminal.
380    :param stdout: Any object with has a `write` and `flush` method + an 'encoding' property.
381    :param term: The terminal environment variable. (xterm, xterm-256color, linux, ...)
382    :param write_binary: Encode the output before writing it. If `True` (the
383        default), the `stdout` object is supposed to expose an `encoding` attribute.
384    """
385    _fds_not_a_terminal = set()  # For the error messages. Only display "Output
386                                 # is not a terminal" once per file descriptor.
387
388    def __init__(self, stdout, get_size, term=None, write_binary=True):
389        assert callable(get_size)
390        assert term is None or isinstance(term, six.text_type)
391        assert all(hasattr(stdout, a) for a in ('write', 'flush'))
392
393        if write_binary:
394            assert hasattr(stdout, 'encoding')
395
396        self._buffer = []
397        self.stdout = stdout
398        self.write_binary = write_binary
399        self.get_size = get_size
400        self.term = term or 'xterm'
401
402        # Cache for escape codes.
403        self._escape_code_caches = {
404            ColorDepth.DEPTH_1_BIT: _EscapeCodeCache(ColorDepth.DEPTH_1_BIT),
405            ColorDepth.DEPTH_4_BIT: _EscapeCodeCache(ColorDepth.DEPTH_4_BIT),
406            ColorDepth.DEPTH_8_BIT: _EscapeCodeCache(ColorDepth.DEPTH_8_BIT),
407            ColorDepth.DEPTH_24_BIT: _EscapeCodeCache(ColorDepth.DEPTH_24_BIT),
408        }
409
410    @classmethod
411    def from_pty(cls, stdout, term=None):
412        """
413        Create an Output class from a pseudo terminal.
414        (This will take the dimensions by reading the pseudo
415        terminal attributes.)
416        """
417        # Normally, this requires a real TTY device, but people instantiate
418        # this class often during unit tests as well. For convenience, we print
419        # an error message, use standard dimensions, and go on.
420        isatty = stdout.isatty()
421        fd = stdout.fileno()
422
423        if not isatty and fd not in cls._fds_not_a_terminal:
424            msg = 'Warning: Output is not to a terminal (fd=%r).\n'
425            sys.stderr.write(msg % fd)
426            cls._fds_not_a_terminal.add(fd)
427
428        def get_size():
429            # If terminal (incorrectly) reports its size as 0, pick a
430            # reasonable default.  See
431            # https://github.com/ipython/ipython/issues/10071
432            rows, columns = (None, None)
433
434            if isatty:
435                rows, columns = _get_size(stdout.fileno())
436            return Size(rows=rows or 24, columns=columns or 80)
437
438        return cls(stdout, get_size, term=term)
439
440    def fileno(self):
441        " Return file descriptor. "
442        return self.stdout.fileno()
443
444    def encoding(self):
445        " Return encoding used for stdout. "
446        return self.stdout.encoding
447
448    def write_raw(self, data):
449        """
450        Write raw data to output.
451        """
452        self._buffer.append(data)
453
454    def write(self, data):
455        """
456        Write text to output.
457        (Removes vt100 escape codes. -- used for safely writing text.)
458        """
459        self._buffer.append(data.replace('\x1b', '?'))
460
461    def set_title(self, title):
462        """
463        Set terminal title.
464        """
465        if self.term not in ('linux', 'eterm-color'):  # Not supported by the Linux console.
466            self.write_raw('\x1b]2;%s\x07' % title.replace('\x1b', '').replace('\x07', ''))
467
468    def clear_title(self):
469        self.set_title('')
470
471    def erase_screen(self):
472        """
473        Erases the screen with the background colour and moves the cursor to
474        home.
475        """
476        self.write_raw('\x1b[2J')
477
478    def enter_alternate_screen(self):
479        self.write_raw('\x1b[?1049h\x1b[H')
480
481    def quit_alternate_screen(self):
482        self.write_raw('\x1b[?1049l')
483
484    def enable_mouse_support(self):
485        self.write_raw('\x1b[?1000h')
486
487        # Enable urxvt Mouse mode. (For terminals that understand this.)
488        self.write_raw('\x1b[?1015h')
489
490        # Also enable Xterm SGR mouse mode. (For terminals that understand this.)
491        self.write_raw('\x1b[?1006h')
492
493        # Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr
494        #       extensions.
495
496    def disable_mouse_support(self):
497        self.write_raw('\x1b[?1000l')
498        self.write_raw('\x1b[?1015l')
499        self.write_raw('\x1b[?1006l')
500
501    def erase_end_of_line(self):
502        """
503        Erases from the current cursor position to the end of the current line.
504        """
505        self.write_raw('\x1b[K')
506
507    def erase_down(self):
508        """
509        Erases the screen from the current line down to the bottom of the
510        screen.
511        """
512        self.write_raw('\x1b[J')
513
514    def reset_attributes(self):
515        self.write_raw('\x1b[0m')
516
517    def set_attributes(self, attrs, color_depth):
518        """
519        Create new style and output.
520
521        :param attrs: `Attrs` instance.
522        """
523        # Get current depth.
524        escape_code_cache = self._escape_code_caches[color_depth]
525
526        # Write escape character.
527        self.write_raw(escape_code_cache[attrs])
528
529    def disable_autowrap(self):
530        self.write_raw('\x1b[?7l')
531
532    def enable_autowrap(self):
533        self.write_raw('\x1b[?7h')
534
535    def enable_bracketed_paste(self):
536        self.write_raw('\x1b[?2004h')
537
538    def disable_bracketed_paste(self):
539        self.write_raw('\x1b[?2004l')
540
541    def cursor_goto(self, row=0, column=0):
542        """ Move cursor position. """
543        self.write_raw('\x1b[%i;%iH' % (row, column))
544
545    def cursor_up(self, amount):
546        if amount == 0:
547            pass
548        elif amount == 1:
549            self.write_raw('\x1b[A')
550        else:
551            self.write_raw('\x1b[%iA' % amount)
552
553    def cursor_down(self, amount):
554        if amount == 0:
555            pass
556        elif amount == 1:
557            # Note: Not the same as '\n', '\n' can cause the window content to
558            #       scroll.
559            self.write_raw('\x1b[B')
560        else:
561            self.write_raw('\x1b[%iB' % amount)
562
563    def cursor_forward(self, amount):
564        if amount == 0:
565            pass
566        elif amount == 1:
567            self.write_raw('\x1b[C')
568        else:
569            self.write_raw('\x1b[%iC' % amount)
570
571    def cursor_backward(self, amount):
572        if amount == 0:
573            pass
574        elif amount == 1:
575            self.write_raw('\b')  # '\x1b[D'
576        else:
577            self.write_raw('\x1b[%iD' % amount)
578
579    def hide_cursor(self):
580        self.write_raw('\x1b[?25l')
581
582    def show_cursor(self):
583        self.write_raw('\x1b[?12l\x1b[?25h')  # Stop blinking cursor and show.
584
585    def flush(self):
586        """
587        Write to output stream and flush.
588        """
589        if not self._buffer:
590            return
591
592        data = ''.join(self._buffer)
593
594        try:
595            # (We try to encode ourself, because that way we can replace
596            # characters that don't exist in the character set, avoiding
597            # UnicodeEncodeError crashes. E.g. u'\xb7' does not appear in 'ascii'.)
598            # My Arch Linux installation of july 2015 reported 'ANSI_X3.4-1968'
599            # for sys.stdout.encoding in xterm.
600            if self.write_binary:
601                if hasattr(self.stdout, 'buffer'):
602                    out = self.stdout.buffer  # Py3.
603                else:
604                    out = self.stdout
605                out.write(data.encode(self.stdout.encoding or 'utf-8', 'replace'))
606            else:
607                self.stdout.write(data)
608
609            self.stdout.flush()
610        except IOError as e:
611            if e.args and e.args[0] == errno.EINTR:
612                # Interrupted system call. Can happen in case of a window
613                # resize signal. (Just ignore. The resize handler will render
614                # again anyway.)
615                pass
616            elif e.args and e.args[0] == 0:
617                # This can happen when there is a lot of output and the user
618                # sends a KeyboardInterrupt by pressing Control-C. E.g. in
619                # a Python REPL when we execute "while True: print('test')".
620                # (The `ptpython` REPL uses this `Output` class instead of
621                # `stdout` directly -- in order to be network transparent.)
622                # So, just ignore.
623                pass
624            else:
625                raise
626
627        self._buffer = []
628
629    def ask_for_cpr(self):
630        """
631        Asks for a cursor position report (CPR).
632        """
633        self.write_raw('\x1b[6n')
634        self.flush()
635
636    def bell(self):
637        " Sound bell. "
638        self.write_raw('\a')
639        self.flush()
640