/usr/lib/python2.7/dist-packages/powerline/renderer.py is in python-powerline 2.6-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
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 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 | # vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
import sys
import os
import re
import operator
from itertools import chain
from powerline.theme import Theme
from powerline.lib.unicode import unichr, strwidth_ucs_2, strwidth_ucs_4
NBSP = ' '
np_control_character_translations = dict((
# Control characters: ^@ … ^Y
(i1, '^' + unichr(i1 + 0x40)) for i1 in range(0x20)
))
'''Control character translations
Dictionary that maps characters in range 0x00–0x1F (inclusive) to strings
``'^@'``, ``'^A'`` and so on.
.. note: maps tab to ``^I`` and newline to ``^J``.
'''
np_invalid_character_translations = dict((
# Invalid unicode characters obtained using 'surrogateescape' error
# handler.
(i2, '<{0:02x}>'.format(i2 - 0xDC00)) for i2 in range(0xDC80, 0xDD00)
))
'''Invalid unicode character translations
When using ``surrogateescape`` encoding error handling method characters in
range 0x80–0xFF (inclusive) are transformed into unpaired surrogate escape
unicode codepoints 0xDC80–0xDD00. This dictionary maps such characters to
``<80>``, ``<81>``, and so on: in Python-3 they cannot be printed or
converted to UTF-8 because UTF-8 standard does not allow surrogate escape
characters, not even paired ones. Python-2 contains a bug that allows such
action, but printing them in any case makes no sense.
'''
# XXX: not using `r` because it makes no sense.
np_invalid_character_re = re.compile('(?<![\uD800-\uDBFF])[\uDC80-\uDD00]')
'''Regex that finds unpaired surrogate escape characters
Search is only limited to the ones obtained from ``surrogateescape`` error
handling method. This regex is only used for UCS-2 Python variants because
in this case characters above 0xFFFF are represented as surrogate escapes
characters and are thus subject to partial transformation if
``np_invalid_character_translations`` translation table is used.
'''
np_character_translations = np_control_character_translations.copy()
'''Dictionary that contains non-printable character translations
In UCS-4 versions of Python this is a union of
``np_invalid_character_translations`` and ``np_control_character_translations``
dictionaries. In UCS-2 for technical reasons ``np_invalid_character_re`` is used
instead and this dictionary only contains items from
``np_control_character_translations``.
'''
translate_np = (
(
lambda s: (
np_invalid_character_re.subn(
lambda match: (
np_invalid_character_translations[ord(match.group(0))]
), s
)[0].translate(np_character_translations)
)
) if sys.maxunicode < 0x10FFFF else (
lambda s: (
s.translate(np_character_translations)
)
)
)
'''Function that translates non-printable characters into printable strings
Is used to translate control characters and surrogate escape characters
obtained from ``surrogateescape`` encoding errors handling method into some
printable sequences. See documentation for
``np_invalid_character_translations`` and
``np_control_character_translations`` for more details.
'''
def construct_returned_value(rendered_highlighted, segments, width, output_raw, output_width):
if not (output_raw or output_width):
return rendered_highlighted
else:
return (
(rendered_highlighted,)
+ ((''.join((segment['_rendered_raw'] for segment in segments)),) if output_raw else ())
+ ((width,) if output_width else ())
)
class Renderer(object):
'''Object that is responsible for generating the highlighted string.
:param dict theme_config:
Main theme configuration.
:param local_themes:
Local themes. Is to be used by subclasses from ``.get_theme()`` method,
base class only records this parameter to a ``.local_themes`` attribute.
:param dict theme_kwargs:
Keyword arguments for ``Theme`` class constructor.
:param PowerlineLogger pl:
Object used for logging.
:param int ambiwidth:
Width of the characters with east asian width unicode attribute equal to
``A`` (Ambigious).
:param dict options:
Various options. Are normally not used by base renderer, but all options
are recorded as attributes.
'''
segment_info = {
'environ': os.environ,
'getcwd': getattr(os, 'getcwdu', os.getcwd),
'home': os.environ.get('HOME'),
}
'''Basic segment info
Is merged with local segment information by :py:meth:`get_segment_info`
method. Keys:
``environ``
Object containing environment variables. Must define at least the
following methods: ``.__getitem__(var)`` that raises ``KeyError`` in
case requested environment variable is not present, ``.get(var,
default=None)`` that works like ``dict.get`` and be able to be passed to
``Popen``.
``getcwd``
Function that returns current working directory. Will be called without
any arguments, should return ``unicode`` or (in python-2) regular
string.
``home``
String containing path to home directory. Should be ``unicode`` or (in
python-2) regular string or ``None``.
'''
character_translations = {}
'''Character translations for use in escape() function.
See documentation of ``unicode.translate`` for details.
'''
def __init__(self,
theme_config,
local_themes,
theme_kwargs,
pl,
ambiwidth=1,
**options):
self.__dict__.update(options)
self.theme_config = theme_config
theme_kwargs['pl'] = pl
self.pl = pl
if theme_config.get('use_non_breaking_spaces', True):
self.character_translations = self.character_translations.copy()
self.character_translations[ord(' ')] = NBSP
self.theme = Theme(theme_config=theme_config, **theme_kwargs)
self.local_themes = local_themes
self.theme_kwargs = theme_kwargs
self.width_data = {
'N': 1, # Neutral
'Na': 1, # Narrow
'A': ambiwidth, # Ambigious
'H': 1, # Half-width
'W': 2, # Wide
'F': 2, # Fullwidth
}
strwidth = lambda self, s: (
(strwidth_ucs_2 if sys.maxunicode < 0x10FFFF else strwidth_ucs_4)(
self.width_data, s)
)
'''Function that returns string width.
Is used to calculate the place given string occupies when handling
``width`` argument to ``.render()`` method. Must take east asian width
into account.
:param unicode string:
String whose width will be calculated.
:return: unsigned integer.
'''
def get_theme(self, matcher_info):
'''Get Theme object.
Is to be overridden by subclasses to support local themes, this variant
only returns ``.theme`` attribute.
:param matcher_info:
Parameter ``matcher_info`` that ``.render()`` method received.
Unused.
'''
return self.theme
def shutdown(self):
'''Prepare for interpreter shutdown. The only job it is supposed to do
is calling ``.shutdown()`` method for all theme objects. Should be
overridden by subclasses in case they support local themes.
'''
self.theme.shutdown()
def get_segment_info(self, segment_info, mode):
'''Get segment information.
Must return a dictionary containing at least ``home``, ``environ`` and
``getcwd`` keys (see documentation for ``segment_info`` attribute). This
implementation merges ``segment_info`` dictionary passed to
``.render()`` method with ``.segment_info`` attribute, preferring keys
from the former. It also replaces ``getcwd`` key with function returning
``segment_info['environ']['PWD']`` in case ``PWD`` variable is
available.
:param dict segment_info:
Segment information that was passed to ``.render()`` method.
:return: dict with segment information.
'''
r = self.segment_info.copy()
r['mode'] = mode
if segment_info:
r.update(segment_info)
if 'PWD' in r['environ']:
r['getcwd'] = lambda: r['environ']['PWD']
return r
def render_above_lines(self, **kwargs):
'''Render all segments in the {theme}/segments/above list
Rendering happens in the reversed order. Parameters are the same as in
.render() method.
:yield: rendered line.
'''
theme = self.get_theme(kwargs.get('matcher_info', None))
for line in range(theme.get_line_number() - 1, 0, -1):
yield self.render(side=None, line=line, **kwargs)
def render(self, mode=None, width=None, side=None, line=0, output_raw=False, output_width=False, segment_info=None, matcher_info=None):
'''Render all segments.
When a width is provided, low-priority segments are dropped one at
a time until the line is shorter than the width, or only segments
with a negative priority are left. If one or more segments with
``"width": "auto"`` are provided they will fill the remaining space
until the desired width is reached.
:param str mode:
Mode string. Affects contents (colors and the set of segments) of
rendered string.
:param int width:
Maximum width text can occupy. May be exceeded if there are too much
non-removable segments.
:param str side:
One of ``left``, ``right``. Determines which side will be rendered.
If not present all sides are rendered.
:param int line:
Line number for which segments should be obtained. Is counted from
zero (botmost line).
:param bool output_raw:
Changes the output: if this parameter is ``True`` then in place of
one string this method outputs a pair ``(colored_string,
colorless_string)``.
:param bool output_width:
Changes the output: if this parameter is ``True`` then in place of
one string this method outputs a pair ``(colored_string,
string_width)``. Returns a three-tuple if ``output_raw`` is also
``True``: ``(colored_string, colorless_string, string_width)``.
:param dict segment_info:
Segment information. See also :py:meth:`get_segment_info` method.
:param matcher_info:
Matcher information. Is processed in :py:meth:`get_segment_info`
method.
'''
theme = self.get_theme(matcher_info)
return self.do_render(
mode=mode,
width=width,
side=side,
line=line,
output_raw=output_raw,
output_width=output_width,
segment_info=self.get_segment_info(segment_info, mode),
theme=theme,
)
def compute_divider_widths(self, theme):
return {
'left': {
'hard': self.strwidth(theme.get_divider('left', 'hard')),
'soft': self.strwidth(theme.get_divider('left', 'soft')),
},
'right': {
'hard': self.strwidth(theme.get_divider('right', 'hard')),
'soft': self.strwidth(theme.get_divider('right', 'soft')),
},
}
hl_join = staticmethod(''.join)
'''Join a list of rendered segments into a resulting string
This method exists to deal with non-string render outputs, so `segments`
may actually be not an iterable with strings.
:param list segments:
Iterable containing rendered segments. By “rendered segments”
:py:meth:`Renderer.hl` output is meant.
:return: Results of joining these segments.
'''
def do_render(self, mode, width, side, line, output_raw, output_width, segment_info, theme):
'''Like Renderer.render(), but accept theme in place of matcher_info
'''
segments = list(theme.get_segments(side, line, segment_info, mode))
current_width = 0
self._prepare_segments(segments, output_width or width)
if not width:
# No width specified, so we don’t need to crop or pad anything
if output_width:
current_width = self._render_length(theme, segments, self.compute_divider_widths(theme))
return construct_returned_value(self.hl_join([
segment['_rendered_hl']
for segment in self._render_segments(theme, segments)
]) + self.hlstyle(), segments, current_width, output_raw, output_width)
divider_widths = self.compute_divider_widths(theme)
# Create an ordered list of segments that can be dropped
segments_priority = sorted((segment for segment in segments if segment['priority'] is not None), key=lambda segment: segment['priority'], reverse=True)
no_priority_segments = filter(lambda segment: segment['priority'] is None, segments)
current_width = self._render_length(theme, segments, divider_widths)
if current_width > width:
for segment in chain(segments_priority, no_priority_segments):
if segment['truncate'] is not None:
segment['contents'] = segment['truncate'](self.pl, current_width - width, segment)
segments_priority = iter(segments_priority)
if current_width > width and len(segments) > 100:
# When there are too many segments use faster, but less correct
# algorythm for width computation
diff = current_width - width
for segment in segments_priority:
segments.remove(segment)
diff -= segment['_len']
if diff <= 0:
break
current_width = self._render_length(theme, segments, divider_widths)
if current_width > width:
# When there are not too much use more precise, but much slower
# width computation. It also finishes computations in case
# previous variant did not free enough space.
for segment in segments_priority:
segments.remove(segment)
current_width = self._render_length(theme, segments, divider_widths)
if current_width <= width:
break
del segments_priority
# Distribute the remaining space on spacer segments
segments_spacers = [segment for segment in segments if segment['expand'] is not None]
if segments_spacers:
distribute_len, distribute_len_remainder = divmod(width - current_width, len(segments_spacers))
for segment in segments_spacers:
segment['contents'] = (
segment['expand'](
self.pl,
distribute_len + (1 if distribute_len_remainder > 0 else 0),
segment))
distribute_len_remainder -= 1
# `_len` key is not needed anymore, but current_width should have an
# actual value for various bindings.
current_width = width
elif output_width:
current_width = self._render_length(theme, segments, divider_widths)
rendered_highlighted = self.hl_join([
segment['_rendered_hl']
for segment in self._render_segments(theme, segments)
])
if rendered_highlighted:
rendered_highlighted += self.hlstyle()
return construct_returned_value(rendered_highlighted, segments, current_width, output_raw, output_width)
def _prepare_segments(self, segments, calculate_contents_len):
'''Translate non-printable characters and calculate segment width
'''
for segment in segments:
segment['contents'] = translate_np(segment['contents'])
if calculate_contents_len:
for segment in segments:
if segment['literal_contents'][1]:
segment['_contents_len'] = segment['literal_contents'][0]
else:
segment['_contents_len'] = self.strwidth(segment['contents'])
def _render_length(self, theme, segments, divider_widths):
'''Update segments lengths and return them
'''
segments_len = len(segments)
ret = 0
divider_spaces = theme.get_spaces()
prev_segment = theme.EMPTY_SEGMENT
try:
first_segment = next(iter((
segment
for segment in segments
if not segment['literal_contents'][1]
)))
except StopIteration:
first_segment = None
try:
last_segment = next(iter((
segment
for segment in reversed(segments)
if not segment['literal_contents'][1]
)))
except StopIteration:
last_segment = None
for index, segment in enumerate(segments):
side = segment['side']
segment_len = segment['_contents_len']
if not segment['literal_contents'][1]:
if side == 'left':
if segment is not last_segment:
compare_segment = next(iter((
segment
for segment in segments[index + 1:]
if not segment['literal_contents'][1]
)))
else:
compare_segment = theme.EMPTY_SEGMENT
else:
compare_segment = prev_segment
divider_type = 'soft' if compare_segment['highlight']['bg'] == segment['highlight']['bg'] else 'hard'
outer_padding = int(bool(
segment is first_segment
if side == 'left' else
segment is last_segment
)) * theme.outer_padding
draw_divider = segment['draw_' + divider_type + '_divider']
segment_len += outer_padding
if draw_divider:
segment_len += divider_widths[side][divider_type] + divider_spaces
prev_segment = segment
segment['_len'] = segment_len
ret += segment_len
return ret
def _render_segments(self, theme, segments, render_highlighted=True):
'''Internal segment rendering method.
This method loops through the segment array and compares the
foreground/background colors and divider properties and returns the
rendered statusline as a string.
The method always renders the raw segment contents (i.e. without
highlighting strings added), and only renders the highlighted
statusline if render_highlighted is True.
'''
segments_len = len(segments)
divider_spaces = theme.get_spaces()
prev_segment = theme.EMPTY_SEGMENT
try:
first_segment = next(iter((
segment
for segment in segments
if not segment['literal_contents'][1]
)))
except StopIteration:
first_segment = None
try:
last_segment = next(iter((
segment
for segment in reversed(segments)
if not segment['literal_contents'][1]
)))
except StopIteration:
last_segment = None
for index, segment in enumerate(segments):
side = segment['side']
if not segment['literal_contents'][1]:
if side == 'left':
if segment is not last_segment:
compare_segment = next(iter((
segment
for segment in segments[index + 1:]
if not segment['literal_contents'][1]
)))
else:
compare_segment = theme.EMPTY_SEGMENT
else:
compare_segment = prev_segment
outer_padding = int(bool(
segment is first_segment
if side == 'left' else
segment is last_segment
)) * theme.outer_padding * ' '
divider_type = 'soft' if compare_segment['highlight']['bg'] == segment['highlight']['bg'] else 'hard'
divider_highlighted = ''
contents_raw = segment['contents']
contents_highlighted = ''
draw_divider = segment['draw_' + divider_type + '_divider']
# XXX Make sure self.hl() calls are called in the same order
# segments are displayed. This is needed for Vim renderer to work.
if draw_divider:
divider_raw = self.escape(theme.get_divider(side, divider_type))
if side == 'left':
contents_raw = outer_padding + contents_raw + (divider_spaces * ' ')
else:
contents_raw = (divider_spaces * ' ') + contents_raw + outer_padding
if divider_type == 'soft':
divider_highlight_group_key = 'highlight' if segment['divider_highlight_group'] is None else 'divider_highlight'
divider_fg = segment[divider_highlight_group_key]['fg']
divider_bg = segment[divider_highlight_group_key]['bg']
else:
divider_fg = segment['highlight']['bg']
divider_bg = compare_segment['highlight']['bg']
if side == 'left':
if render_highlighted:
contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight'])
divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False)
segment['_rendered_raw'] = contents_raw + divider_raw
segment['_rendered_hl'] = contents_highlighted + divider_highlighted
else:
if render_highlighted:
divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False)
contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight'])
segment['_rendered_raw'] = divider_raw + contents_raw
segment['_rendered_hl'] = divider_highlighted + contents_highlighted
else:
if side == 'left':
contents_raw = outer_padding + contents_raw
else:
contents_raw = contents_raw + outer_padding
contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight'])
segment['_rendered_raw'] = contents_raw
segment['_rendered_hl'] = contents_highlighted
prev_segment = segment
else:
segment['_rendered_raw'] = ' ' * segment['literal_contents'][0]
segment['_rendered_hl'] = segment['literal_contents'][1]
yield segment
def escape(self, string):
'''Method that escapes segment contents.
'''
return string.translate(self.character_translations)
def hlstyle(fg=None, bg=None, attrs=None):
'''Output highlight style string.
Assuming highlighted string looks like ``{style}{contents}`` this method
should output ``{style}``. If it is called without arguments this method
is supposed to reset style to its default.
'''
raise NotImplementedError
def hl(self, contents, fg=None, bg=None, attrs=None):
'''Output highlighted chunk.
This implementation just outputs :py:meth:`hlstyle` joined with
``contents``.
'''
return self.hlstyle(fg, bg, attrs) + (contents or '')
|