summaryrefslogtreecommitdiffstats
path: root/third_party/python/blessed/blessed/formatters.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/blessed/blessed/formatters.py')
-rw-r--r--third_party/python/blessed/blessed/formatters.py498
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)