diff options
Diffstat (limited to 'third_party/python/blessed/blessed/formatters.py')
-rw-r--r-- | third_party/python/blessed/blessed/formatters.py | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/third_party/python/blessed/blessed/formatters.py b/third_party/python/blessed/blessed/formatters.py new file mode 100644 index 0000000000..ed1badc5ad --- /dev/null +++ b/third_party/python/blessed/blessed/formatters.py @@ -0,0 +1,498 @@ +"""Sub-module providing sequence-formatting functions.""" +# std imports +import platform + +# 3rd party +import six + +# local +from blessed.colorspace import CGA_COLORS, X11_COLORNAMES_TO_RGB + +# isort: off +# curses +if platform.system() == 'Windows': + import jinxed as curses # pylint: disable=import-error +else: + import curses + + +def _make_colors(): + """ + Return set of valid colors and their derivatives. + + :rtype: set + :returns: Color names with prefixes + """ + colors = set() + # basic CGA foreground color, background, high intensity, and bold + # background ('iCE colors' in my day). + for cga_color in CGA_COLORS: + colors.add(cga_color) + colors.add('on_' + cga_color) + colors.add('bright_' + cga_color) + colors.add('on_bright_' + cga_color) + + # foreground and background VGA color + for vga_color in X11_COLORNAMES_TO_RGB: + colors.add(vga_color) + colors.add('on_' + vga_color) + return colors + + +#: Valid colors and their background (on), bright, and bright-background +#: derivatives. +COLORS = _make_colors() + +#: Attributes that may be compounded with colors, by underscore, such as +#: 'reverse_indigo'. +COMPOUNDABLES = set('bold underline reverse blink italic standout'.split()) + + +class ParameterizingString(six.text_type): + r""" + A Unicode string which can be called as a parameterizing termcap. + + For example:: + + >>> from blessed import Terminal + >>> term = Terminal() + >>> color = ParameterizingString(term.color, term.normal, 'color') + >>> color(9)('color #9') + u'\x1b[91mcolor #9\x1b(B\x1b[m' + """ + + def __new__(cls, cap, normal=u'', name=u'<not specified>'): + # pylint: disable = missing-return-doc, missing-return-type-doc + """ + Class constructor accepting 3 positional arguments. + + :arg str cap: parameterized string suitable for curses.tparm() + :arg str normal: terminating sequence for this capability (optional). + :arg str name: name of this terminal capability (optional). + """ + new = six.text_type.__new__(cls, cap) + new._normal = normal + new._name = name + return new + + def __call__(self, *args): + """ + Returning :class:`FormattingString` instance for given parameters. + + Return evaluated terminal capability (self), receiving arguments + ``*args``, followed by the terminating sequence (self.normal) into + a :class:`FormattingString` capable of being called. + + :raises TypeError: Mismatch between capability and arguments + :raises curses.error: :func:`curses.tparm` raised an exception + :rtype: :class:`FormattingString` or :class:`NullCallableString` + :returns: Callable string for given parameters + """ + try: + # Re-encode the cap, because tparm() takes a bytestring in Python + # 3. However, appear to be a plain Unicode string otherwise so + # concats work. + attr = curses.tparm(self.encode('latin1'), *args).decode('latin1') + return FormattingString(attr, self._normal) + except TypeError as err: + # If the first non-int (i.e. incorrect) arg was a string, suggest + # something intelligent: + if args and isinstance(args[0], six.string_types): + raise TypeError( + "Unknown terminal capability, %r, or, TypeError " + "for arguments %r: %s" % (self._name, args, err)) + # Somebody passed a non-string; I don't feel confident + # guessing what they were trying to do. + raise + except curses.error as err: + # ignore 'tparm() returned NULL', you won't get any styling, + # even if does_styling is True. This happens on win32 platforms + # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed + if "tparm() returned NULL" not in six.text_type(err): + raise + return NullCallableString() + + +class ParameterizingProxyString(six.text_type): + r""" + A Unicode string which can be called to proxy missing termcap entries. + + This class supports the function :func:`get_proxy_string`, and mirrors + the behavior of :class:`ParameterizingString`, except that instead of + a capability name, receives a format string, and callable to filter the + given positional ``*args`` of :meth:`ParameterizingProxyString.__call__` + into a terminal sequence. + + For example:: + + >>> from blessed import Terminal + >>> term = Terminal('screen') + >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa') + >>> hpa(9) + u'' + >>> fmt = u'\x1b[{0}G' + >>> fmt_arg = lambda *arg: (arg[0] + 1,) + >>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa') + >>> hpa(9) + u'\x1b[10G' + """ + + def __new__(cls, fmt_pair, normal=u'', name=u'<not specified>'): + # pylint: disable = missing-return-doc, missing-return-type-doc + """ + Class constructor accepting 4 positional arguments. + + :arg tuple fmt_pair: Two element tuple containing: + - format string suitable for displaying terminal sequences + - callable suitable for receiving __call__ arguments for formatting string + :arg str normal: terminating sequence for this capability (optional). + :arg str name: name of this terminal capability (optional). + """ + assert isinstance(fmt_pair, tuple), fmt_pair + assert callable(fmt_pair[1]), fmt_pair[1] + new = six.text_type.__new__(cls, fmt_pair[0]) + new._fmt_args = fmt_pair[1] + new._normal = normal + new._name = name + return new + + def __call__(self, *args): + """ + Returning :class:`FormattingString` instance for given parameters. + + Arguments are determined by the capability. For example, ``hpa`` + (move_x) receives only a single integer, whereas ``cup`` (move) + receives two integers. See documentation in terminfo(5) for the + given capability. + + :rtype: FormattingString + :returns: Callable string for given parameters + """ + return FormattingString(self.format(*self._fmt_args(*args)), + self._normal) + + +class FormattingString(six.text_type): + r""" + A Unicode string which doubles as a callable. + + This is used for terminal attributes, so that it may be used both + directly, or as a callable. When used directly, it simply emits + the given terminal sequence. When used as a callable, it wraps the + given (string) argument with the 2nd argument used by the class + constructor:: + + >>> from blessed import Terminal + >>> term = Terminal() + >>> style = FormattingString(term.bright_blue, term.normal) + >>> print(repr(style)) + u'\x1b[94m' + >>> style('Big Blue') + u'\x1b[94mBig Blue\x1b(B\x1b[m' + """ + + def __new__(cls, sequence, normal=u''): + # pylint: disable = missing-return-doc, missing-return-type-doc + """ + Class constructor accepting 2 positional arguments. + + :arg str sequence: terminal attribute sequence. + :arg str normal: terminating sequence for this attribute (optional). + """ + new = six.text_type.__new__(cls, sequence) + new._normal = normal + return new + + def __call__(self, *args): + """ + Return ``text`` joined by ``sequence`` and ``normal``. + + :raises TypeError: Not a string type + :rtype: str + :returns: Arguments wrapped in sequence and normal + """ + # Jim Allman brings us this convenience of allowing existing + # unicode strings to be joined as a call parameter to a formatting + # string result, allowing nestation: + # + # >>> t.red('This is ', t.bold('extremely'), ' dangerous!') + for idx, ucs_part in enumerate(args): + if not isinstance(ucs_part, six.string_types): + expected_types = ', '.join(_type.__name__ for _type in six.string_types) + raise TypeError( + "TypeError for FormattingString argument, " + "%r, at position %s: expected type %s, " + "got %s" % (ucs_part, idx, expected_types, + type(ucs_part).__name__)) + postfix = u'' + if self and self._normal: + postfix = self._normal + _refresh = self._normal + self + args = [_refresh.join(ucs_part.split(self._normal)) + for ucs_part in args] + + return self + u''.join(args) + postfix + + +class FormattingOtherString(six.text_type): + r""" + A Unicode string which doubles as a callable for another sequence when called. + + This is used for the :meth:`~.Terminal.move_up`, ``down``, ``left``, and ``right()`` + family of functions:: + + >>> from blessed import Terminal + >>> term = Terminal() + >>> move_right = FormattingOtherString(term.cuf1, term.cuf) + >>> print(repr(move_right)) + u'\x1b[C' + >>> print(repr(move_right(666))) + u'\x1b[666C' + >>> print(repr(move_right())) + u'\x1b[C' + """ + + def __new__(cls, direct, target): + # pylint: disable = missing-return-doc, missing-return-type-doc + """ + Class constructor accepting 2 positional arguments. + + :arg str direct: capability name for direct formatting, eg ``('x' + term.right)``. + :arg str target: capability name for callable, eg ``('x' + term.right(99))``. + """ + new = six.text_type.__new__(cls, direct) + new._callable = target + return new + + def __getnewargs__(self): + # return arguments used for the __new__ method upon unpickling. + return six.text_type.__new__(six.text_type, self), self._callable + + def __call__(self, *args): + """Return ``text`` by ``target``.""" + if args: + return self._callable(*args) + return self + + +class NullCallableString(six.text_type): + """ + A dummy callable Unicode alternative to :class:`FormattingString`. + + This is used for colors on terminals that do not support colors, it is just a basic form of + unicode that may also act as a callable. + """ + + def __new__(cls): + """Class constructor.""" + return six.text_type.__new__(cls, u'') + + def __call__(self, *args): + """ + Allow empty string to be callable, returning given string, if any. + + When called with an int as the first arg, return an empty Unicode. An + int is a good hint that I am a :class:`ParameterizingString`, as there + are only about half a dozen string-returning capabilities listed in + terminfo(5) which accept non-int arguments, they are seldom used. + + When called with a non-int as the first arg (no no args at all), return + the first arg, acting in place of :class:`FormattingString` without + any attributes. + """ + if not args or isinstance(args[0], int): + # As a NullCallableString, even when provided with a parameter, + # such as t.color(5), we must also still be callable, fe: + # + # >>> t.color(5)('shmoo') + # + # is actually simplified result of NullCallable()() on terminals + # without color support, so turtles all the way down: we return + # another instance. + return NullCallableString() + return u''.join(args) + + +def get_proxy_string(term, attr): + """ + Proxy and return callable string for proxied attributes. + + :arg Terminal term: :class:`~.Terminal` instance. + :arg str attr: terminal capability name that may be proxied. + :rtype: None or :class:`ParameterizingProxyString`. + :returns: :class:`ParameterizingProxyString` for some attributes + of some terminal types that support it, where the terminfo(5) + database would otherwise come up empty, such as ``move_x`` + attribute for ``term.kind`` of ``screen``. Otherwise, None. + """ + # normalize 'screen-256color', or 'ansi.sys' to its basic names + term_kind = next(iter(_kind for _kind in ('screen', 'ansi',) + if term.kind.startswith(_kind)), term) + _proxy_table = { # pragma: no cover + 'screen': { + # proxy move_x/move_y for 'screen' terminal type, used by tmux(1). + 'hpa': ParameterizingProxyString( + (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'vpa': ParameterizingProxyString( + (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), + }, + 'ansi': { + # proxy show/hide cursor for 'ansi' terminal type. There is some + # demand for a richly working ANSI terminal type for some reason. + 'civis': ParameterizingProxyString( + (u'\x1b[?25l', lambda *arg: ()), term.normal, attr), + 'cnorm': ParameterizingProxyString( + (u'\x1b[?25h', lambda *arg: ()), term.normal, attr), + 'hpa': ParameterizingProxyString( + (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'vpa': ParameterizingProxyString( + (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'sc': '\x1b[s', + 'rc': '\x1b[u', + } + } + return _proxy_table.get(term_kind, {}).get(attr, None) + + +def split_compound(compound): + """ + Split compound formating string into segments. + + >>> split_compound('bold_underline_bright_blue_on_red') + ['bold', 'underline', 'bright_blue', 'on_red'] + + :arg str compound: a string that may contain compounds, separated by + underline (``_``). + :rtype: list + :returns: List of formating string segments + """ + merged_segs = [] + # These occur only as prefixes, so they can always be merged: + mergeable_prefixes = ['on', 'bright', 'on_bright'] + for segment in compound.split('_'): + if merged_segs and merged_segs[-1] in mergeable_prefixes: + merged_segs[-1] += '_' + segment + else: + merged_segs.append(segment) + return merged_segs + + +def resolve_capability(term, attr): + """ + Resolve a raw terminal capability using :func:`tigetstr`. + + :arg Terminal term: :class:`~.Terminal` instance. + :arg str attr: terminal capability name. + :returns: string of the given terminal capability named by ``attr``, + which may be empty (u'') if not found or not supported by the + given :attr:`~.Terminal.kind`. + :rtype: str + """ + if not term.does_styling: + return u'' + val = curses.tigetstr(term._sugar.get(attr, attr)) # pylint: disable=protected-access + # Decode sequences as latin1, as they are always 8-bit bytes, so when + # b'\xff' is returned, this is decoded as u'\xff'. + return u'' if val is None else val.decode('latin1') + + +def resolve_color(term, color): + """ + Resolve a simple color name to a callable capability. + + This function supports :func:`resolve_attribute`. + + :arg Terminal term: :class:`~.Terminal` instance. + :arg str color: any string found in set :const:`COLORS`. + :returns: a string class instance which emits the terminal sequence + for the given color, and may be used as a callable to wrap the + given string with such sequence. + :returns: :class:`NullCallableString` when + :attr:`~.Terminal.number_of_colors` is 0, + otherwise :class:`FormattingString`. + :rtype: :class:`NullCallableString` or :class:`FormattingString` + """ + # pylint: disable=protected-access + if term.number_of_colors == 0: + return NullCallableString() + + # fg/bg capabilities terminals that support 0-256+ colors. + vga_color_cap = (term._background_color if 'on_' in color else + term._foreground_color) + + base_color = color.rsplit('_', 1)[-1] + if base_color in CGA_COLORS: + # curses constants go up to only 7, so add an offset to get at the + # bright colors at 8-15: + offset = 8 if 'bright_' in color else 0 + base_color = color.rsplit('_', 1)[-1] + attr = 'COLOR_%s' % (base_color.upper(),) + fmt_attr = vga_color_cap(getattr(curses, attr) + offset) + return FormattingString(fmt_attr, term.normal) + + assert base_color in X11_COLORNAMES_TO_RGB, ( + 'color not known', base_color) + rgb = X11_COLORNAMES_TO_RGB[base_color] + + # downconvert X11 colors to CGA, EGA, or VGA color spaces + if term.number_of_colors <= 256: + fmt_attr = vga_color_cap(term.rgb_downconvert(*rgb)) + return FormattingString(fmt_attr, term.normal) + + # Modern 24-bit color terminals are written pretty basically. The + # foreground and background sequences are: + # - ^[38;2;<r>;<g>;<b>m + # - ^[48;2;<r>;<g>;<b>m + fgbg_seq = ('48' if 'on_' in color else '38') + assert term.number_of_colors == 1 << 24 + fmt_attr = u'\x1b[' + fgbg_seq + ';2;{0};{1};{2}m' + return FormattingString(fmt_attr.format(*rgb), term.normal) + + +def resolve_attribute(term, attr): + """ + Resolve a terminal attribute name into a capability class. + + :arg Terminal term: :class:`~.Terminal` instance. + :arg str attr: Sugary, ordinary, or compound formatted terminal + capability, such as "red_on_white", "normal", "red", or + "bold_on_black". + :returns: a string class instance which emits the terminal sequence + for the given terminal capability, or may be used as a callable to + wrap the given string with such sequence. + :returns: :class:`NullCallableString` when + :attr:`~.Terminal.number_of_colors` is 0, + otherwise :class:`FormattingString`. + :rtype: :class:`NullCallableString` or :class:`FormattingString` + """ + if attr in COLORS: + return resolve_color(term, attr) + + # A direct compoundable, such as `bold' or `on_red'. + if attr in COMPOUNDABLES: + sequence = resolve_capability(term, attr) + return FormattingString(sequence, term.normal) + + # Given `bold_on_red', resolve to ('bold', 'on_red'), RECURSIVE + # call for each compounding section, joined and returned as + # a completed completed FormattingString. + formatters = split_compound(attr) + if all((fmt in COLORS or fmt in COMPOUNDABLES) for fmt in formatters): + resolution = (resolve_attribute(term, fmt) for fmt in formatters) + return FormattingString(u''.join(resolution), term.normal) + + # otherwise, this is our end-game: given a sequence such as 'csr' + # (change scrolling region), return a ParameterizingString instance, + # that when called, performs and returns the final string after curses + # capability lookup is performed. + tparm_capseq = resolve_capability(term, attr) + if not tparm_capseq: + # and, for special terminals, such as 'screen', provide a Proxy + # ParameterizingString for attributes they do not claim to support, + # but actually do! (such as 'hpa' and 'vpa'). + proxy = get_proxy_string(term, + term._sugar.get(attr, attr)) # pylint: disable=protected-access + if proxy is not None: + return proxy + + return ParameterizingString(tparm_capseq, term.normal, attr) |