forked from jquast/blessed
-
Notifications
You must be signed in to change notification settings - Fork 0
/
formatters.py
426 lines (357 loc) · 16.4 KB
/
formatters.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
"""Sub-module providing sequence-formatting functions."""
# standard imports
import platform
# 3rd-party
import six
# 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
"""
derivatives = ('on', 'bright', 'on_bright',)
colors = set('black red green yellow blue magenta cyan white'.split())
return set('_'.join((_deravitive, _color))
for _deravitive in derivatives
for _color in colors) | colors
def _make_compoundables(colors):
"""
Return given set ``colors`` along with all "compoundable" attributes.
:arg set colors: set of color names as string.
:rtype: set
"""
_compoundables = set('bold underline reverse blink dim italic shadow '
'standout subscript superscript'.split())
return colors | _compoundables
#: Valid colors and their background (on), bright,
#: and bright-background derivatives.
COLORS = _make_colors()
#: Attributes and colors which may be compounded by underscore.
COMPOUNDABLES = _make_compoundables(COLORS)
class ParameterizingString(six.text_type):
r"""
A Unicode string which can be called as a parameterizing termcap.
For example::
>>> term = Terminal()
>>> color = ParameterizingString(term.color, term.normal, 'color')
>>> color(9)('color #9')
u'\x1b[91mcolor #9\x1b(B\x1b[m'
"""
def __new__(cls, *args):
"""
Class constructor accepting 3 positional arguments.
:arg cap: parameterized string suitable for curses.tparm()
:arg normal: terminating sequence for this capability (optional).
:arg name: name of this terminal capability (optional).
"""
assert args and len(args) < 4, args
new = six.text_type.__new__(cls, args[0])
new._normal = args[1] if len(args) > 1 else u''
new._name = args[2] if len(args) > 2 else u'<not specified>'
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.
:rtype: :class:`FormattingString` or :class:`NullCallableString`
"""
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(
"A native or nonexistent capability template, %r received"
" invalid argument %r: %s. You probably misspelled a"
" formatting call like `bright_red'" % (
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, *args):
"""
Class constructor accepting 4 positional arguments.
:arg fmt: format string suitable for displaying terminal sequences.
:arg callable: receives __call__ arguments for formatting fmt.
:arg normal: terminating sequence for this capability (optional).
:arg name: name of this terminal capability (optional).
"""
assert args and len(args) < 4, args
assert isinstance(args[0], tuple), args[0]
assert callable(args[0][1]), args[0][1]
new = six.text_type.__new__(cls, args[0][0])
new._fmt_args = args[0][1]
new._normal = args[1] if len(args) > 1 else u''
new._name = args[2] if len(args) > 2 else u'<not specified>'
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
"""
return FormattingString(self.format(*self._fmt_args(*args)),
self._normal)
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)
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::
>>> 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, *args):
"""
Class constructor accepting 2 positional arguments.
:arg sequence: terminal attribute sequence.
:arg normal: terminating sequence for this attribute (optional).
"""
assert 1 <= len(args) <= 2, args
new = six.text_type.__new__(cls, args[0])
new._normal = args[1] if len(args) > 1 else u''
return new
def __call__(self, *args):
"""Return ``text`` joined by ``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):
raise TypeError("Positional argument #{idx} is {is_type} "
"expected any of {expected_types}: "
"{ucs_part!r}".format(
idx=idx, ucs_part=ucs_part,
is_type=type(ucs_part),
expected_types=six.string_types,
))
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 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."""
new = six.text_type.__new__(cls, u'')
return new
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 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
"""
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
"""
# Decode sequences as latin1, as they are always 8-bit bytes, so when
# b'\xff' is returned, this must be decoded to u'\xff'.
if not term.does_styling:
return u''
val = curses.tigetstr(term._sugar.get(attr, attr))
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`
"""
if term.number_of_colors == 0:
return NullCallableString()
# NOTE(erikrose): Does curses automatically exchange red and blue and cyan
# and yellow when a terminal supports setf/setb rather than setaf/setab?
# I'll be blasted if I can find any documentation. The following
# assumes it does: to terminfo(5) describes color(1) as COLOR_RED when
# using setaf, but COLOR_BLUE when using setf.
color_cap = (term._background_color if 'on_' in color else
term._foreground_color)
# 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 = color_cap(getattr(curses, attr) + offset)
return FormattingString(fmt_attr, 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", respectively.
: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 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))
if proxy is not None:
return proxy
return ParameterizingString(tparm_capseq, term.normal, attr)