0
0
.dotfiles/powerline-bin/powerline/renderer.py

595 lines
20 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 0x000x1F (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 0x800xFF (inclusive) are transformed into unpaired surrogate escape
unicode codepoints 0xDC800xDD00. 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 dont 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 '')