From 875f8e98b1a9da6e4323a14e2b9b6066cc8b9768 Mon Sep 17 00:00:00 2001 From: ZyX Date: Sat, 30 Aug 2014 16:39:49 +0400 Subject: [PATCH 1/6] Update documentation --- docs/source/configuration/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/configuration/reference.rst b/docs/source/configuration/reference.rst index b224eb1e..326de4ba 100644 --- a/docs/source/configuration/reference.rst +++ b/docs/source/configuration/reference.rst @@ -441,7 +441,7 @@ ascii Theme without any unicode characters at all ``align`` Aligns the segments contents to the left (``l``), center (``c``) or - right (``r``). + right (``r``). Has no sense if ``width`` key was not specified. .. _config-themes-seg-width: From 0da40f08a59d5e40c0968c5515df84e038a98cb4 Mon Sep 17 00:00:00 2001 From: ZyX Date: Sat, 30 Aug 2014 16:50:23 +0400 Subject: [PATCH 2/6] Fix syntastic error --- tests/lib/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index bd271103..5399df1a 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -44,6 +44,7 @@ def urllib_read(query_url): else: raise NotImplementedError + class Process(object): def __init__(self, output, err): self.output = output From bfa335d96a12c53e84ce80d85a68b8610d1e8138 Mon Sep 17 00:00:00 2001 From: ZyX Date: Sat, 30 Aug 2014 17:08:32 +0400 Subject: [PATCH 3/6] Fix typo in test_configuration.py --- tests/test_configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 34e402c8..de15ab88 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -94,7 +94,7 @@ config = { highlighted_string('g', 'str2'), ], 'right': [ - highlighted_string('f', 'str2', width='auto', align='right'), + highlighted_string('f', 'str2', width='auto', align='r'), ], }, }, From 5b038dba8264085c4144de8bc4e673e39096c13d Mon Sep 17 00:00:00 2001 From: ZyX Date: Sat, 30 Aug 2014 17:10:30 +0400 Subject: [PATCH 4/6] Use `expand` key to fill segments --- powerline/renderer.py | 43 ++++++++++++------------------------------- powerline/segment.py | 6 ++---- powerline/theme.py | 27 +++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/powerline/renderer.py b/powerline/renderer.py index a94a51b4..ca62e387 100644 --- a/powerline/renderer.py +++ b/powerline/renderer.py @@ -269,7 +269,7 @@ class Renderer(object): segments.remove(segment) # Distribute the remaining space on spacer segments - segments_spacers = [segment for segment in segments if segment['width'] == 'auto'] + segments_spacers = [segment for segment in segments if segment['expand'] is not None] if segments_spacers: if not segments_priority: # Update segment['_len'] and current_width if not already done @@ -278,15 +278,12 @@ class Renderer(object): 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 + 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 @@ -321,7 +318,7 @@ class Renderer(object): )) draw_divider = segment['draw_' + divider_type + '_divider'] - segment_len += segment['_space_left'] + segment['_space_right'] + outer_padding + segment_len += outer_padding if draw_divider: segment_len += divider_widths[side][divider_type] + divider_spaces @@ -363,30 +360,14 @@ class Renderer(object): 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']) * ' ') - ) + contents_raw = outer_padding + contents_raw + (divider_spaces * ' ') else: - contents_raw = ( - ((divider_spaces + segment['_space_left']) * ' ') - + contents_raw - + (segment['_space_right'] * ' ') + outer_padding - ) + contents_raw = (divider_spaces * ' ') + contents_raw + outer_padding else: if side == 'left': - contents_raw = ( - outer_padding + (segment['_space_left'] * ' ') - + contents_raw - + (segment['_space_right'] * ' ') - ) + contents_raw = outer_padding + contents_raw else: - contents_raw = ( - (segment['_space_left'] * ' ') - + contents_raw - + (segment['_space_right'] * ' ') + outer_padding - ) + contents_raw = contents_raw + outer_padding # Replace spaces with no-break spaces contents_raw = contents_raw.translate(self.np_character_translations) diff --git a/powerline/segment.py b/powerline/segment.py index 6449a43a..00a48a30 100644 --- a/powerline/segment.py +++ b/powerline/segment.py @@ -285,6 +285,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge 'include_modes': segment.get('include_modes', []), 'width': None, 'align': None, + 'expand': None, 'startup': None, 'shutdown': None, 'mode': None, @@ -292,8 +293,6 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge '_rendered_hl': '', '_len': None, '_contents_len': None, - '_space_left': 0, - '_space_right': 0, } if segment_type == 'function': @@ -331,6 +330,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge 'include_modes': segment.get('include_modes', []), 'width': segment.get('width'), 'align': segment.get('align', 'l'), + 'expand': None, 'startup': startup_func, 'shutdown': shutdown_func, 'mode': None, @@ -338,8 +338,6 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge '_rendered_hl': '', '_len': None, '_contents_len': None, - '_space_left': 0, - '_space_right': 0, } return get diff --git a/powerline/theme.py b/powerline/theme.py index 3bf8483b..2fbdd79e 100644 --- a/powerline/theme.py +++ b/powerline/theme.py @@ -23,6 +23,26 @@ def new_empty_segment_line(): } +def add_spaces_left(pl, amount, segment): + return (' ' * amount) + segment['contents'] + + +def add_spaces_right(pl, amount, segment): + return segment['contents'] + (' ' * amount) + + +def add_spaces_center(pl, amount, segment): + amount, remainder = divmod(amount, 2) + return (' ' * (amount + remainder)) + segment['contents'] + (' ' * amount) + + +expand_functions = { + 'l': add_spaces_right, + 'r': add_spaces_left, + 'c': add_spaces_center, +} + + class Theme(object): def __init__(self, ext, @@ -128,6 +148,13 @@ class Theme(object): self.colorscheme, ) for segment in parsed_segments: + width = segment['width'] + align = segment['align'] + if width == 'auto': + segment['expand'] = expand_functions.get(align) + if segment['expand'] is None: + self.pl.error('Align argument must be “r”, “l” or “c”, not “{0}”', align) + segment['contents'] = segment['before'] + u(segment['contents'] if segment['contents'] is not None else '') + segment['after'] # Align segment contents if segment['width'] and segment['width'] != 'auto': From d6c603daf0a6b74b390d5a4ae909a5beff318961 Mon Sep 17 00:00:00 2001 From: ZyX Date: Sat, 30 Aug 2014 17:54:46 +0400 Subject: [PATCH 5/6] Add support for `segment.expand` Closes #154 --- docs/source/configuration/reference.rst | 12 ++- docs/source/develop/listers.rst | 7 +- docs/source/develop/segments.rst | 109 ++++++++++++++++++++++++ powerline/segment.py | 18 ++-- powerline/theme.py | 2 +- tests/test_configuration.py | 20 +++++ 6 files changed, 159 insertions(+), 9 deletions(-) diff --git a/docs/source/configuration/reference.rst b/docs/source/configuration/reference.rst index 326de4ba..c3096b9c 100644 --- a/docs/source/configuration/reference.rst +++ b/docs/source/configuration/reference.rst @@ -369,6 +369,8 @@ ascii Theme without any unicode characters at all Each segment dictionary has the following options: + .. _config-themes-seg-type: + ``type`` The segment type. Can be one of ``function`` (default), ``string`` or ``segments_list``: @@ -439,9 +441,13 @@ ascii Theme without any unicode characters at all ``args`` A dict of arguments to be passed to a ``function`` segment. + .. _config-themes-seg-align: + ``align`` Aligns the segments contents to the left (``l``), center (``c``) or - right (``r``). Has no sense if ``width`` key was not specified. + right (``r``). Has no sense if ``width`` key was not specified or if + segment provides its own function for ``auto`` ``width`` handling and + does not care about this option. .. _config-themes-seg-width: @@ -454,6 +460,8 @@ ascii Theme without any unicode characters at all either returned by a function or a static string, and the contents can be aligned with the ``align`` property. + .. _config-themes-seg-priority: + ``priority`` Optional segment priority. Segments with priority ``None`` (the default priority, represented by ``null`` in json) will always be included, @@ -482,6 +490,8 @@ ascii Theme without any unicode characters at all segments. Only applicable for functions returning multiple segments. Defaults to ``False``. + .. _config-themes-seg-exclude_modes: + ``exclude_modes`` A list of modes where this segment will be excluded: The segment is included in all modes, *except* for the modes in this list. diff --git a/docs/source/develop/listers.rst b/docs/source/develop/listers.rst index 8c7b439e..85513718 100644 --- a/docs/source/develop/listers.rst +++ b/docs/source/develop/listers.rst @@ -36,8 +36,11 @@ Listers must return a sequence of pairs. First item in the pair must contain a ``segment_info`` dictionary specific to one of the listed entities. Second item must contain another dictionary: it will be used to modify the -resulting segment. In addition to usual keys that describe segment the following -keys may be present (it is advised that *only* the following keys will be used): +resulting segment. In addition to :ref:`usual keys that describe segment +` the following keys may be present (it is advised that +*only* the following keys will be used): + +.. _dev-listers-mode: ``mode`` Segment-specific mode. Used to alter segment highlighting. diff --git a/docs/source/develop/segments.rst b/docs/source/develop/segments.rst index fe802818..22f88206 100644 --- a/docs/source/develop/segments.rst +++ b/docs/source/develop/segments.rst @@ -51,6 +51,8 @@ powerline: theme-specific values go directly to :ref:`top-level themes `. +.. _dev-segments-startup: + ``startup`` This attribute must be a callable which accepts the following keyword arguments: @@ -68,6 +70,8 @@ powerline: used (more specific: when :py:class:`powerline.Powerline` constructor received true ``run_once`` argument). +.. _dev-segments-shutdown: + ``shutdown`` This attribute must be a callable that accepts no arguments and shuts down threads and frees any other resources allocated in ``startup`` method of the @@ -75,6 +79,29 @@ powerline: This function is not called when ``startup`` method is not called. +.. _dev-segments-expand: + +``expand`` + This attribute must be a callable that accepts the following keyword + arguments: + + * ``pl``: :py:class:`powerline.PowerlineLogger` instance which is to be used + for logging. + * ``amount``: integer number representing amount of display cells result + must occupy. + + .. warning:: + “Amount of display cells” is *not* number of Unicode codepoints, string + length, or byte count. It is suggested that your function should look + something like ``return (' ' * amount) + segment['contents']`` where + ``' '`` may be replaced with anything that is known to occupy exactly + one display cell. + * ``segment``: :ref:`segment dictionary `. + * Any arguments found in user configuration for the given segment (i.e. + :ref:`args key `). + + It must return new value of :ref:`contents ` key. + This callable object should may return either a string (``unicode`` in Python2 or ``str`` in Python3, *not* ``str`` in Python2 or ``bytes`` in Python3) object or a list of dictionaries. String object is a short form of the following return @@ -87,6 +114,8 @@ value: 'highlight_group': [segment_name], }] +.. _dev-segments-return: + Returned list is a list of segments treated independently, except for :ref:`draw_inner_divider key `. @@ -158,6 +187,86 @@ Detailed description of used dictionary keys: No error occurs if segment has this key, but no used highlight groups use gradient color. +``_*`` + Keys starting with underscore are reserved for powerline and must not be + returned. + +``__*`` + Keys starting with two underscores are reserved for the segment functions, + specifically for :ref:`expand function `. + +.. _dev-segments-segment: + +Segment dictionary +================== + +Segment dictionary contains the following keys: + +* All keys returned by segment function (if it was used). + +* All of the following keys: + + ``name`` + Segment name: value of the :ref:`name key ` or + function name (last component of the :ref:`function key + `). May be ``None``. + + ``type`` + :ref:`Segment type `. Always represents actual type + and is never ``None``. + + ``highlight_group``, ``divider_highlight_group`` + Used highlight groups. May be ``None``. + + .. _dev-segments-seg-around: + + ``before``, ``after`` + Value of :ref:`before ` or :ref:`after + ` configuration options. May be ``None`` as well as + an empty string. + + ``contents_func`` + Function used to get segment contents. May be ``None``. + + .. _dev-segments-seg-contents: + + ``contents`` + Actual segment contents, excluding dividers and :ref:`before/after + `. May be ``None``. + + ``priority`` + :ref:`Segment priority `. May be ``None`` for no + priority (such segments are always shown). + + ``draw_soft_divider``, ``draw_hard_divider``, ``draw_inner_divider`` + :ref:`Divider control flags `. + + ``side`` + Segment side: ``right`` or ``left``. + + ``exclude_modes``, ``include_modes`` + :ref:`Mode display control lists `. May be + empty, but may not be ``None``. + + ``width``, ``align`` + :ref:`Width and align options `. May be ``None``. + + ``expand`` + Partially applied :ref:`expand function `. Accepts + ``pl``, ``amount`` and ``segment`` positional parameters, keyword parameters + from :ref:`args ` key were applied. + + ``startup`` + Partially applied :ref:`startup function `. Accepts + ``pl`` and ``shutdown_event`` positional parameters, keyword parameters from + :ref:`args ` key were applied. + + ``shutdown`` + :ref:`Shutdown function `. Accepts no argument. + + ``mode`` + :ref:`Segment-specific mode `. May be ``None``. + Segments layout =============== diff --git a/powerline/segment.py b/powerline/segment.py index 00a48a30..5cbe31dc 100644 --- a/powerline/segment.py +++ b/powerline/segment.py @@ -82,14 +82,20 @@ segment_getters = { } -def get_attr_func(contents_func, key, args): +def get_attr_func(contents_func, key, args, is_space_func=False): try: func = getattr(contents_func, key) except AttributeError: return None else: - if args is None: - return lambda: func() + if is_space_func: + def expand_func(pl, amount, segment): + try: + return func(pl=pl, amount=amount, segment=segment, **args) + except Exception as e: + pl.exception('Exception while computing segment expansion: {0}', str(e)) + return segment['contents'] + (' ' * amount) + return expand_func else: return lambda pl, shutdown_event: func(pl=pl, shutdown_event=shutdown_event, **args) @@ -297,7 +303,8 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge if segment_type == 'function': startup_func = get_attr_func(_contents_func, 'startup', args) - shutdown_func = get_attr_func(_contents_func, 'shutdown', None) + shutdown_func = getattr(_contents_func, 'shutdown', None) + expand_func = get_attr_func(_contents_func, 'expand', args, True) if hasattr(_contents_func, 'powerline_requires_filesystem_watcher'): create_watcher = lambda: create_file_watcher(pl, common_config['watcher']) @@ -311,6 +318,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge startup_func = None shutdown_func = None contents_func = None + expand_func = None return { 'name': name or function_name, @@ -330,7 +338,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge 'include_modes': segment.get('include_modes', []), 'width': segment.get('width'), 'align': segment.get('align', 'l'), - 'expand': None, + 'expand': expand_func, 'startup': startup_func, 'shutdown': shutdown_func, 'mode': None, diff --git a/powerline/theme.py b/powerline/theme.py index 2fbdd79e..2558ca0e 100644 --- a/powerline/theme.py +++ b/powerline/theme.py @@ -150,7 +150,7 @@ class Theme(object): for segment in parsed_segments: width = segment['width'] align = segment['align'] - if width == 'auto': + if width == 'auto' and segment['expand'] is None: segment['expand'] = expand_functions.get(align) if segment['expand'] is None: self.pl.error('Align argument must be “r”, “l” or “c”, not “{0}”', align) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index de15ab88..e4fc7f5f 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -474,6 +474,26 @@ class TestSegmentAttributes(TestRender): } self.assertRenderEqual(p, '{56} pl;{6-}>>{--}') + @add_args + def test_expand(self, p, config): + def m1(divider=',', **kwargs): + return divider.join(kwargs.keys()) + divider + + def expand(pl, amount, segment, **kwargs): + return ('-' * amount) + segment['contents'] + + m1.expand = expand + sys.modules['bar'] = Args(m1=m1) + config['themes/test/default']['segments'] = { + 'left': [ + { + 'function': 'bar.m1', + 'width': 'auto' + } + ] + } + self.assertRenderEqual(p, '{56} ----pl,{6-}>>{--}', width=10) + class TestSegmentData(TestRender): @add_args From 2505d0b827fb165ff3e39d5e7038289aba4eb411 Mon Sep 17 00:00:00 2001 From: ZyX Date: Sat, 30 Aug 2014 18:19:56 +0400 Subject: [PATCH 6/6] Implement segment truncation Closes #161 (requires implementation for specific segments though) --- docs/source/develop/segments.rst | 19 +++++++++++++++---- powerline/renderer.py | 23 ++++++++++++++++------- powerline/segment.py | 6 +++++- tests/test_configuration.py | 19 +++++++++++++++++++ 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/docs/source/develop/segments.rst b/docs/source/develop/segments.rst index 22f88206..e1dd1b64 100644 --- a/docs/source/develop/segments.rst +++ b/docs/source/develop/segments.rst @@ -102,6 +102,16 @@ powerline: It must return new value of :ref:`contents ` key. +.. _dev-segments-truncate: + +``truncate`` + Like :ref:`expand function `, but for truncating + segments. Here ``amount`` means the number of display cells which must be + freed. + + This function is called for all segments before powerline starts purging + them to free space. + This callable object should may return either a string (``unicode`` in Python2 or ``str`` in Python3, *not* ``str`` in Python2 or ``bytes`` in Python3) object or a list of dictionaries. String object is a short form of the following return @@ -251,10 +261,11 @@ Segment dictionary contains the following keys: ``width``, ``align`` :ref:`Width and align options `. May be ``None``. - ``expand`` - Partially applied :ref:`expand function `. Accepts - ``pl``, ``amount`` and ``segment`` positional parameters, keyword parameters - from :ref:`args ` key were applied. + ``expand``, ``truncate`` + Partially applied :ref:`expand ` or :ref:`truncate + ` function. Accepts ``pl``, ``amount`` and + ``segment`` positional parameters, keyword parameters from :ref:`args + ` key were applied. ``startup`` Partially applied :ref:`startup function `. Accepts diff --git a/powerline/renderer.py b/powerline/renderer.py index ca62e387..a1408b5e 100644 --- a/powerline/renderer.py +++ b/powerline/renderer.py @@ -1,9 +1,12 @@ # vim:fileencoding=utf-8:noet -from powerline.theme import Theme -from unicodedata import east_asian_width, combining import os +from unicodedata import east_asian_width, combining +from itertools import chain + +from powerline.theme import Theme + try: NBSP = unicode(' ', 'utf-8') except NameError: @@ -262,11 +265,17 @@ class Renderer(object): # 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) + 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) + for segment in segments_priority: + if current_width <= width: + break + segments.remove(segment) + current_width = self._render_length(theme, segments, divider_widths) # Distribute the remaining space on spacer segments segments_spacers = [segment for segment in segments if segment['expand'] is not None] diff --git a/powerline/segment.py b/powerline/segment.py index 5cbe31dc..4dd283e4 100644 --- a/powerline/segment.py +++ b/powerline/segment.py @@ -93,7 +93,7 @@ def get_attr_func(contents_func, key, args, is_space_func=False): try: return func(pl=pl, amount=amount, segment=segment, **args) except Exception as e: - pl.exception('Exception while computing segment expansion: {0}', str(e)) + pl.exception('Exception while computing {0} function: {1}', key, str(e)) return segment['contents'] + (' ' * amount) return expand_func else: @@ -292,6 +292,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge 'width': None, 'align': None, 'expand': None, + 'truncate': None, 'startup': None, 'shutdown': None, 'mode': None, @@ -305,6 +306,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge startup_func = get_attr_func(_contents_func, 'startup', args) shutdown_func = getattr(_contents_func, 'shutdown', None) expand_func = get_attr_func(_contents_func, 'expand', args, True) + truncate_func = get_attr_func(_contents_func, 'truncate', args, True) if hasattr(_contents_func, 'powerline_requires_filesystem_watcher'): create_watcher = lambda: create_file_watcher(pl, common_config['watcher']) @@ -319,6 +321,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge shutdown_func = None contents_func = None expand_func = None + truncate_func = None return { 'name': name or function_name, @@ -339,6 +342,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge 'width': segment.get('width'), 'align': segment.get('align', 'l'), 'expand': expand_func, + 'truncate': truncate_func, 'startup': startup_func, 'shutdown': shutdown_func, 'mode': None, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index e4fc7f5f..f6cbc975 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -494,6 +494,25 @@ class TestSegmentAttributes(TestRender): } self.assertRenderEqual(p, '{56} ----pl,{6-}>>{--}', width=10) + @add_args + def test_truncate(self, p, config): + def m1(divider=',', **kwargs): + return divider.join(kwargs.keys()) + divider + + def truncate(pl, amount, segment, **kwargs): + return segment['contents'][:-amount] + + m1.truncate = truncate + sys.modules['bar'] = Args(m1=m1) + config['themes/test/default']['segments'] = { + 'left': [ + { + 'function': 'bar.m1' + } + ] + } + self.assertRenderEqual(p, '{56} p{6-}>>{--}', width=4) + class TestSegmentData(TestRender): @add_args