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, # Ambiguous 'H': 1, # Half-width 'W': 2, # Wide 'F': 2, # Fullwidth }
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 }
def get_matched_theme(self, match): try: return match['theme'] except KeyError: match['theme'] = Theme(theme_config=match['config'], main_theme_config=self.theme_config, **self.theme_kwargs) return match['theme']
def get_theme(self, matcher_info): for matcher in self.local_themes.keys(): if matcher(matcher_info): match = self.local_themes[matcher] try: return match['theme'] except KeyError: match['theme'] = Theme(theme_config=match['config'], top_theme_config=self.theme_config, **self.theme_kwargs) return match['theme'] else: return self.theme
def get_theme(self, matcher_info): if not matcher_info: return self.theme match = self.local_themes[matcher_info] try: return match['theme'] except KeyError: match['theme'] = Theme(theme_config=match['config'], main_theme_config=self.theme_config, **self.theme_kwargs) return match['theme']
def get_theme(self, matcher_info): if matcher_info == 'in': return self.theme else: match = self.local_themes[matcher_info] try: return match['theme'] except KeyError: match['theme'] = Theme(theme_config=match['config'], top_theme_config=self.theme_config, **self.theme_kwargs) return match['theme']
def __init__(self, theme_config, local_themes, theme_kwargs, colorscheme, pl, ambiwidth=1, **options): self.__dict__.update(options) self.theme_config = theme_config theme_kwargs['pl'] = pl self.pl = pl self.theme = Theme(theme_config=theme_config, **theme_kwargs) self.local_themes = local_themes self.theme_kwargs = theme_kwargs self.colorscheme = colorscheme self.width_data = { 'N': 1, # Neutral 'Na': 1, # Narrow 'A': ambiwidth, # Ambigious 'H': 1, # Half-width 'W': 2, # Wide 'F': 2, # Fullwidth }
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 }
def __init__(self, theme_config, local_themes, theme_kwargs, colorscheme, pl, **options): self.__dict__.update(options) self.theme_config = theme_config theme_kwargs['pl'] = pl self.pl = pl self.theme = Theme(theme_config=theme_config, **theme_kwargs) self.local_themes = local_themes self.theme_kwargs = theme_kwargs self.colorscheme = colorscheme self.width_data = { 'N': 1, # Neutral 'Na': 1, # Narrow 'A': getattr(self, 'ambiwidth', 1), # Ambigious 'H': 1, # Half-width 'W': 2, # Wide 'F': 2, # Fullwidth }
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 Colorscheme colorscheme: Colorscheme object that holds colors configuration. :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 ``.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``. ''' def __init__(self, theme_config, local_themes, theme_kwargs, colorscheme, pl, ambiwidth=1, **options): self.__dict__.update(options) self.theme_config = theme_config theme_kwargs['pl'] = pl self.pl = pl self.theme = Theme(theme_config=theme_config, **theme_kwargs) self.local_themes = local_themes self.theme_kwargs = theme_kwargs self.colorscheme = colorscheme self.width_data = { 'N': 1, # Neutral 'Na': 1, # Narrow 'A': ambiwidth, # Ambigious 'H': 1, # Half-width 'W': 2, # Wide 'F': 2, # Fullwidth } def strwidth(self, string): '''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. ''' return sum((0 if combining(symbol) else self.width_data[east_asian_width(symbol)] for symbol in string)) 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_highlighting(self, segment, mode): segment['highlight'] = self.colorscheme.get_highlighting( segment['highlight_group'], mode, segment.get('gradient_level')) if segment['divider_highlight_group']: segment['divider_highlight'] = self.colorscheme.get_highlighting( segment['divider_highlight_group'], mode) else: segment['divider_highlight'] = None return segment def get_segment_info(self, segment_info): '''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() if segment_info: r.update(segment_info) if 'PWD' in r['environ']: r['getcwd'] = lambda: r['environ']['PWD'] return r def render(self, mode=None, width=None, side=None, output_raw=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 filler segments 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 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 dict segment_info: Segment information. See also ``.get_segment_info()`` method. :param matcher_info: Matcher information. Is processed in ``.get_theme()`` method. ''' theme = self.get_theme(matcher_info) segments = theme.get_segments(side, self.get_segment_info(segment_info)) # Handle excluded/included segments for the current mode segments = [ self._get_highlighting(segment, mode) for segment in segments if mode not in segment['exclude_modes'] and (not segment['include_modes'] or mode in segment['include_modes']) ] segments = [ segment for segment in self._render_segments(theme, segments) ] if not width: # No width specified, so we don't need to crop or pad anything return construct_returned_value( ''.join([segment['_rendered_hl'] for segment in segments]) + self.hlstyle(), segments, output_raw) # 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) while sum([segment['_len'] for segment in segments ]) > width and len(segments_priority): segments.remove(segments_priority[0]) segments_priority.pop(0) # Distribute the remaining space on spacer segments segments_spacers = [ segment for segment in segments if segment['width'] == 'auto' ] if segments_spacers: distribute_len, distribute_len_remainder = divmod( width - sum([segment['_len'] for segment in segments]), len(segments_spacers)) for segment in segments_spacers: if segment['align'] == 'l': segment['_space_right'] += distribute_len elif segment['align'] == 'r': segment['_space_left'] += distribute_len elif segment['align'] == 'c': space_side, space_side_remainder = divmod( distribute_len, 2) segment['_space_left'] += space_side + space_side_remainder segment['_space_right'] += space_side segments_spacers[0]['_space_right'] += distribute_len_remainder rendered_highlighted = ''.join([ segment['_rendered_hl'] for segment in self._render_segments(theme, segments) ]) + self.hlstyle() return construct_returned_value(rendered_highlighted, segments, output_raw) 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) for index, segment in enumerate(segments): segment['_rendered_raw'] = '' segment['_rendered_hl'] = '' prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT next_segment = segments[ index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT compare_segment = next_segment if segment[ 'side'] == 'left' else prev_segment outer_padding = ' ' if (index == 0 and segment['side'] == 'left' ) or (index == segments_len - 1 and segment['side'] == 'right') else '' divider_type = 'soft' if compare_segment['highlight'][ 'bg'] == segment['highlight']['bg'] else 'hard' divider_raw = theme.get_divider(segment['side'], divider_type) divider_spaces = theme.get_spaces() divider_highlighted = '' contents_raw = segment['contents'] contents_highlighted = '' draw_divider = segment['draw_' + divider_type + '_divider'] # Pad segments first if draw_divider: if segment['side'] == 'left': contents_raw = outer_padding + ( segment['_space_left'] * ' ') + contents_raw + ( (divider_spaces + segment['_space_right']) * ' ') else: contents_raw = ( (divider_spaces + segment['_space_left']) * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding else: if segment['side'] == 'left': contents_raw = outer_padding + ( segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') else: contents_raw = ( segment['_space_left'] * ' ') + contents_raw + ( segment['_space_right'] * ' ') + outer_padding # Replace spaces with no-break spaces contents_raw = contents_raw.replace(' ', NBSP) divider_raw = divider_raw.replace(' ', NBSP) # Apply highlighting to padded dividers and contents if render_highlighted: 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'] divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False) contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) # Append padded raw and highlighted segments to the rendered segment variables if draw_divider: if segment['side'] == 'left': segment['_rendered_raw'] += contents_raw + divider_raw segment[ '_rendered_hl'] += contents_highlighted + divider_highlighted else: segment['_rendered_raw'] += divider_raw + contents_raw segment[ '_rendered_hl'] += divider_highlighted + contents_highlighted else: if segment['side'] == 'left': segment['_rendered_raw'] += contents_raw segment['_rendered_hl'] += contents_highlighted else: segment['_rendered_raw'] += contents_raw segment['_rendered_hl'] += contents_highlighted segment['_len'] = self.strwidth(segment['_rendered_raw']) yield segment @staticmethod def escape(string): '''Method that escapes segment contents. ''' return string def hlstyle(fg=None, bg=None, attr=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, attr=None): '''Output highlighted chunk. This implementation just outputs ``.hlstyle()`` joined with ``contents``. ''' return self.hlstyle(fg, bg, attr) + (contents or '')
class Renderer(object): segment_info = { 'environ': os.environ, 'getcwd': getattr(os, 'getcwdu', os.getcwd), 'home': os.environ.get('HOME'), } def __init__(self, theme_config, local_themes, theme_kwargs, colorscheme, pl, **options): self.__dict__.update(options) self.theme_config = theme_config theme_kwargs['pl'] = pl self.pl = pl self.theme = Theme(theme_config=theme_config, **theme_kwargs) self.local_themes = local_themes self.theme_kwargs = theme_kwargs self.colorscheme = colorscheme self.width_data = { 'N': 1, # Neutral 'Na': 1, # Narrow 'A': getattr(self, 'ambiwidth', 1), # Ambigious 'H': 1, # Half-width 'W': 2, # Wide 'F': 2, # Fullwidth } def strwidth(self, string): return sum((0 if combining(symbol) else self.width_data[east_asian_width(symbol)] for symbol in string)) def get_theme(self, matcher_info): return self.theme def shutdown(self): self.theme.shutdown() def get_highlighting(self, segment, mode): segment['highlight'] = self.colorscheme.get_highlighting(segment['highlight_group'], mode, segment.get('gradient_level')) if segment['divider_highlight_group']: segment['divider_highlight'] = self.colorscheme.get_highlighting(segment['divider_highlight_group'], mode) else: segment['divider_highlight'] = None return segment def get_segment_info(self, segment_info): r = self.segment_info.copy() if segment_info: r.update(segment_info) if 'PWD' in r['environ']: r['getcwd'] = lambda: r['environ']['PWD'] return r def render(self, mode=None, width=None, side=None, output_raw=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 filler segments are provided they will fill the remaining space until the desired width is reached. ''' theme = self.get_theme(matcher_info) segments = theme.get_segments(side, self.get_segment_info(segment_info)) # Handle excluded/included segments for the current mode segments = [self.get_highlighting(segment, mode) for segment in segments if mode not in segment['exclude_modes'] or (segment['include_modes'] and segment in segment['include_modes'])] segments = [segment for segment in self._render_segments(theme, segments)] if not width: # No width specified, so we don't need to crop or pad anything return construct_returned_value(''.join([segment['_rendered_hl'] for segment in segments]) + self.hlstyle(), segments, output_raw) # Create an ordered list of segments that can be dropped segments_priority = [segment for segment in sorted(segments, key=lambda segment: segment['priority'], reverse=True) if segment['priority'] > 0] while sum([segment['_len'] for segment in segments]) > width and len(segments_priority): segments.remove(segments_priority[0]) segments_priority.pop(0) # Distribute the remaining space on spacer segments segments_spacers = [segment for segment in segments if segment['width'] == 'auto'] if segments_spacers: distribute_len, distribute_len_remainder = divmod(width - sum([segment['_len'] for segment in segments]), len(segments_spacers)) for segment in segments_spacers: if segment['align'] == 'l': segment['_space_right'] += distribute_len elif segment['align'] == 'r': segment['_space_left'] += distribute_len elif segment['align'] == 'c': space_side, space_side_remainder = divmod(distribute_len, 2) segment['_space_left'] += space_side + space_side_remainder segment['_space_right'] += space_side segments_spacers[0]['_space_right'] += distribute_len_remainder rendered_highlighted = ''.join([segment['_rendered_hl'] for segment in self._render_segments(theme, segments)]) + self.hlstyle() return construct_returned_value(rendered_highlighted, segments, output_raw) 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) for index, segment in enumerate(segments): segment['_rendered_raw'] = '' segment['_rendered_hl'] = '' prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT next_segment = segments[index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT compare_segment = next_segment if segment['side'] == 'left' else prev_segment outer_padding = ' ' if (index == 0 and segment['side'] == 'left') or (index == segments_len - 1 and segment['side'] == 'right') else '' divider_type = 'soft' if compare_segment['highlight']['bg'] == segment['highlight']['bg'] else 'hard' divider_raw = theme.get_divider(segment['side'], divider_type) divider_spaces = theme.get_spaces() divider_highlighted = '' contents_raw = segment['contents'] contents_highlighted = '' draw_divider = segment['draw_' + divider_type + '_divider'] # Pad segments first if draw_divider: if segment['side'] == 'left': contents_raw = outer_padding + (segment['_space_left'] * ' ') + contents_raw + ((divider_spaces + segment['_space_right']) * ' ') else: contents_raw = ((divider_spaces + segment['_space_left']) * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding else: if segment['side'] == 'left': contents_raw = outer_padding + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') else: contents_raw = (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding # Replace spaces with no-break spaces contents_raw = contents_raw.replace(' ', NBSP) divider_raw = divider_raw.replace(' ', NBSP) # Apply highlighting to padded dividers and contents if render_highlighted: 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'] divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False) contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) # Append padded raw and highlighted segments to the rendered segment variables if draw_divider: if segment['side'] == 'left': segment['_rendered_raw'] += contents_raw + divider_raw segment['_rendered_hl'] += contents_highlighted + divider_highlighted else: segment['_rendered_raw'] += divider_raw + contents_raw segment['_rendered_hl'] += divider_highlighted + contents_highlighted else: if segment['side'] == 'left': segment['_rendered_raw'] += contents_raw segment['_rendered_hl'] += contents_highlighted else: segment['_rendered_raw'] += contents_raw segment['_rendered_hl'] += contents_highlighted segment['_len'] = self.strwidth(segment['_rendered_raw']) yield segment @staticmethod def escape(string): return string def hlstyle(fg=None, bg=None, attr=None): raise NotImplementedError def hl(self, contents, fg=None, bg=None, attr=None): return self.hlstyle(fg, bg, attr) + (contents or '')
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)) 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)) * " " 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 "")
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`` (Ambiguous). :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, # Ambiguous '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 # 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) ]) 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 '')
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 Colorscheme colorscheme: Colorscheme object that holds colors configuration. :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 ``.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. ''' np_character_translations = dict( ((i, '^' + chr(i + 0x40)) for i in range(0x20))) '''Non-printable character translations These are used to transform characters in range 0x00—0x1F into ``^@``, ``^A`` and so on. Unilke with ``.escape()`` method (and ``character_translations``) result is passed to ``.strwidth()`` method. Note: transforms tab into ``^I``. ''' def __init__(self, theme_config, local_themes, theme_kwargs, colorscheme, 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.colorscheme = colorscheme self.width_data = { 'N': 1, # Neutral 'Na': 1, # Narrow 'A': ambiwidth, # Ambigious 'H': 1, # Half-width 'W': 2, # Wide 'F': 2, # Fullwidth } def strwidth(self, string): '''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. ''' return sum((0 if combining(symbol) else self.width_data[east_asian_width(symbol)] for symbol in string)) 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 _set_highlighting(self, segment): segment['highlight'] = self.colorscheme.get_highlighting( segment['highlight_group'], segment['mode'], segment.get('gradient_level')) if segment['divider_highlight_group']: segment['divider_highlight'] = self.colorscheme.get_highlighting( segment['divider_highlight_group'], segment['mode']) else: segment['divider_highlight'] = None return segment 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 filler segments 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 ``.get_segment_info()`` method. :param matcher_info: Matcher information. Is processed in ``.get_theme()`` 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=segment_info, 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')), }, } 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 = [ self._set_highlighting(segment) for segment in (theme.get_segments( side, line, self.get_segment_info(segment_info, mode), mode)) ] current_width = 0 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( ''.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) for segment in segments_priority: current_width = self._render_length(theme, segments, divider_widths) if current_width <= width: break segments.remove(segment) # Distribute the remaining space on spacer segments segments_spacers = [ segment for segment in segments if segment['width'] == 'auto' ] if segments_spacers: if not segments_priority: # Update segment['_len'] and current_width if not already done # (is not done in shells where there is nothing to remove # because “priority” key is not specified) current_width = self._render_length(theme, segments, divider_widths) distribute_len, distribute_len_remainder = divmod( width - current_width, len(segments_spacers)) for segment in segments_spacers: if segment['align'] == 'l': segment['_space_right'] += distribute_len elif segment['align'] == 'r': segment['_space_left'] += distribute_len elif segment['align'] == 'c': space_side, space_side_remainder = divmod( distribute_len, 2) segment['_space_left'] += space_side + space_side_remainder segment['_space_right'] += space_side segments_spacers[0]['_space_right'] += distribute_len_remainder # `_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 = ''.join([ segment['_rendered_hl'] for segment in self._render_segments(theme, segments) ]) + self.hlstyle() return construct_returned_value(rendered_highlighted, segments, current_width, output_raw, output_width) 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() for index, segment in enumerate(segments): side = segment['side'] if segment['_contents_len'] is None: segment_len = segment['_contents_len'] = self.strwidth( segment['contents']) else: segment_len = segment['_contents_len'] prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT next_segment = segments[ index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT compare_segment = next_segment if side == 'left' else prev_segment divider_type = 'soft' if compare_segment['highlight'][ 'bg'] == segment['highlight']['bg'] else 'hard' outer_padding = int( bool((index == 0 and side == 'left') or (index == segments_len - 1 and side == 'right'))) draw_divider = segment['draw_' + divider_type + '_divider'] segment_len += segment['_space_left'] + segment[ '_space_right'] + outer_padding if draw_divider: segment_len += divider_widths[side][ divider_type] + divider_spaces 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() for index, segment in enumerate(segments): side = segment['side'] prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT next_segment = segments[ index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT compare_segment = next_segment if side == 'left' else prev_segment outer_padding = int( bool((index == 0 and side == 'left') or (index == segments_len - 1 and side == 'right'))) * ' ' 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'] # Pad segments first if draw_divider: divider_raw = self.escape(theme.get_divider( side, divider_type)) if side == 'left': contents_raw = ( outer_padding + (segment['_space_left'] * ' ') + contents_raw + ((divider_spaces + segment['_space_right']) * ' ')) else: contents_raw = ( ((divider_spaces + segment['_space_left']) * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding) else: if side == 'left': contents_raw = (outer_padding + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ')) else: contents_raw = ((segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding) # Replace spaces with no-break spaces contents_raw = contents_raw.translate( self.np_character_translations) # Apply highlighting to padded dividers and contents if render_highlighted: if draw_divider: 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'] divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False) contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) # Append padded raw and highlighted segments to the rendered segment variables if draw_divider: if side == 'left': segment['_rendered_raw'] = contents_raw + divider_raw segment[ '_rendered_hl'] = contents_highlighted + divider_highlighted else: segment['_rendered_raw'] = divider_raw + contents_raw segment[ '_rendered_hl'] = divider_highlighted + contents_highlighted else: if side == 'left': segment['_rendered_raw'] = contents_raw segment['_rendered_hl'] = contents_highlighted else: segment['_rendered_raw'] = contents_raw segment['_rendered_hl'] = contents_highlighted yield segment def escape(self, string): '''Method that escapes segment contents. ''' return string.translate(self.character_translations) def hlstyle(fg=None, bg=None, attr=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, attr=None): '''Output highlighted chunk. This implementation just outputs ``.hlstyle()`` joined with ``contents``. ''' return self.hlstyle(fg, bg, attr) + (contents or '')
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 Colorscheme colorscheme: Colorscheme object that holds colors configuration. :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 ``.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 = {ord(' '): NBSP} '''Character translations for use in escape() function. See documentation of ``unicode.translate`` for details. ''' np_character_translations = dict(((i, '^' + chr(i + 0x40)) for i in range(0x20))) '''Non-printable character translations These are used to transform characters in range 0x00—0x1F into ``^@``, ``^A`` and so on. Unilke with ``.escape()`` method (and ``character_translations``) result is passed to ``.strwidth()`` method. Note: transforms tab into ``^I``. ''' def __init__(self, theme_config, local_themes, theme_kwargs, colorscheme, pl, ambiwidth=1, **options): self.__dict__.update(options) self.theme_config = theme_config theme_kwargs['pl'] = pl self.pl = pl self.theme = Theme(theme_config=theme_config, **theme_kwargs) self.local_themes = local_themes self.theme_kwargs = theme_kwargs self.colorscheme = colorscheme self.width_data = { 'N': 1, # Neutral 'Na': 1, # Narrow 'A': ambiwidth, # Ambigious 'H': 1, # Half-width 'W': 2, # Wide 'F': 2, # Fullwidth } def strwidth(self, string): '''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. ''' return sum((0 if combining(symbol) else self.width_data[east_asian_width(symbol)] for symbol in string)) 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_highlighting(self, segment, mode): segment['highlight'] = self.colorscheme.get_highlighting(segment['highlight_group'], mode, segment.get('gradient_level')) if segment['divider_highlight_group']: segment['divider_highlight'] = self.colorscheme.get_highlighting(segment['divider_highlight_group'], mode) else: segment['divider_highlight'] = None return segment 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(self, mode=None, width=None, side=None, output_raw=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 filler segments 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 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 dict segment_info: Segment information. See also ``.get_segment_info()`` method. :param matcher_info: Matcher information. Is processed in ``.get_theme()`` method. ''' theme = self.get_theme(matcher_info) segments = theme.get_segments(side, self.get_segment_info(segment_info, mode)) # Handle excluded/included segments for the current mode segments = [self._get_highlighting(segment, mode) for segment in segments if mode not in segment['exclude_modes'] and (not segment['include_modes'] or mode in segment['include_modes'])] segments = [segment for segment in self._render_segments(theme, segments)] if not width: # No width specified, so we don't need to crop or pad anything return construct_returned_value(''.join([segment['_rendered_hl'] for segment in segments]) + self.hlstyle(), segments, output_raw) # 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) while sum([segment['_len'] for segment in segments]) > width and len(segments_priority): segments.remove(segments_priority[0]) segments_priority.pop(0) # Distribute the remaining space on spacer segments segments_spacers = [segment for segment in segments if segment['width'] == 'auto'] if segments_spacers: distribute_len, distribute_len_remainder = divmod(width - sum([segment['_len'] for segment in segments]), len(segments_spacers)) for segment in segments_spacers: if segment['align'] == 'l': segment['_space_right'] += distribute_len elif segment['align'] == 'r': segment['_space_left'] += distribute_len elif segment['align'] == 'c': space_side, space_side_remainder = divmod(distribute_len, 2) segment['_space_left'] += space_side + space_side_remainder segment['_space_right'] += space_side segments_spacers[0]['_space_right'] += distribute_len_remainder rendered_highlighted = ''.join([segment['_rendered_hl'] for segment in self._render_segments(theme, segments)]) + self.hlstyle() return construct_returned_value(rendered_highlighted, segments, output_raw) 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) for index, segment in enumerate(segments): segment['_rendered_raw'] = '' segment['_rendered_hl'] = '' prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT next_segment = segments[index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT compare_segment = next_segment if segment['side'] == 'left' else prev_segment outer_padding = ' ' if (index == 0 and segment['side'] == 'left') or (index == segments_len - 1 and segment['side'] == 'right') else '' divider_type = 'soft' if compare_segment['highlight']['bg'] == segment['highlight']['bg'] else 'hard' divider_raw = theme.get_divider(segment['side'], divider_type) divider_spaces = theme.get_spaces() divider_highlighted = '' contents_raw = segment['contents'] contents_highlighted = '' draw_divider = segment['draw_' + divider_type + '_divider'] # Pad segments first if draw_divider: if segment['side'] == 'left': contents_raw = outer_padding + (segment['_space_left'] * ' ') + contents_raw + ((divider_spaces + segment['_space_right']) * ' ') else: contents_raw = ((divider_spaces + segment['_space_left']) * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding else: if segment['side'] == 'left': contents_raw = outer_padding + (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') else: contents_raw = (segment['_space_left'] * ' ') + contents_raw + (segment['_space_right'] * ' ') + outer_padding # Replace spaces with no-break spaces divider_raw = divider_raw.replace(' ', NBSP) contents_raw = contents_raw.translate(self.np_character_translations) # Apply highlighting to padded dividers and contents if render_highlighted: 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'] divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False) contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) # Append padded raw and highlighted segments to the rendered segment variables if draw_divider: if segment['side'] == 'left': segment['_rendered_raw'] += contents_raw + divider_raw segment['_rendered_hl'] += contents_highlighted + divider_highlighted else: segment['_rendered_raw'] += divider_raw + contents_raw segment['_rendered_hl'] += divider_highlighted + contents_highlighted else: if segment['side'] == 'left': segment['_rendered_raw'] += contents_raw segment['_rendered_hl'] += contents_highlighted else: segment['_rendered_raw'] += contents_raw segment['_rendered_hl'] += contents_highlighted segment['_len'] = self.strwidth(segment['_rendered_raw']) yield segment @classmethod def escape(cls, string): '''Method that escapes segment contents. ''' return string.translate(cls.character_translations) def hlstyle(fg=None, bg=None, attr=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, attr=None): '''Output highlighted chunk. This implementation just outputs ``.hlstyle()`` joined with ``contents``. ''' return self.hlstyle(fg, bg, attr) + (contents or '')
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')), }, } 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(''.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 = ''.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: 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() for index, segment in enumerate(segments): side = segment['side'] segment_len = segment['_contents_len'] prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT next_segment = segments[index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT compare_segment = next_segment if side == 'left' else prev_segment divider_type = 'soft' if compare_segment['highlight']['bg'] == segment['highlight']['bg'] else 'hard' outer_padding = int(bool( (index == 0 and side == 'left') or (index == segments_len - 1 and side == 'right') )) draw_divider = segment['draw_' + divider_type + '_divider'] segment_len += outer_padding if draw_divider: segment_len += divider_widths[side][divider_type] + divider_spaces 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() for index, segment in enumerate(segments): side = segment['side'] prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT next_segment = segments[index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT compare_segment = next_segment if side == 'left' else prev_segment outer_padding = int(bool( (index == 0 and side == 'left') or (index == segments_len - 1 and side == 'right') )) * ' ' 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 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 '')