initial commit
This commit is contained in:
496
venv/lib/python3.11/site-packages/blessed/formatters.py
Normal file
496
venv/lib/python3.11/site-packages/blessed/formatters.py
Normal file
@@ -0,0 +1,496 @@
|
||||
"""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``."""
|
||||
return self._callable(*args) if args else 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)
|
||||
Reference in New Issue
Block a user