"""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
''):
# 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
''):
# 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)