# 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('(? 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 # algorithm 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, hl_args) ]) if rendered_highlighted: rendered_highlighted += self.hlstyle(**hl_args) 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, hl_args, 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'] segment_hl_args = {} segment_hl_args.update(segment['highlight']) segment_hl_args.update(hl_args) # 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_hl_args) divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False, **hl_args) 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, **hl_args) contents_highlighted = self.hl(self.escape(contents_raw), **segment_hl_args) 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_hl_args) 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, **kwargs): '''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, **kwargs): '''Output highlighted chunk. This implementation just outputs :py:meth:`hlstyle` joined with ``contents``. ''' return self.hlstyle(fg, bg, attrs, **kwargs) + (contents or '')