Merge pull request #1043 from ZyX-I/truncate-expand

Add segment.truncate and segment.expand attributes support
This commit is contained in:
Nikolai Aleksandrovich Pavlov 2014-08-30 18:29:28 +04:00
commit a2b58370c4
8 changed files with 250 additions and 50 deletions

View File

@ -369,6 +369,8 @@ ascii Theme without any unicode characters at all
Each segment dictionary has the following options: Each segment dictionary has the following options:
.. _config-themes-seg-type:
``type`` ``type``
The segment type. Can be one of ``function`` (default), ``string`` or The segment type. Can be one of ``function`` (default), ``string`` or
``segments_list``: ``segments_list``:
@ -439,9 +441,13 @@ ascii Theme without any unicode characters at all
``args`` ``args``
A dict of arguments to be passed to a ``function`` segment. A dict of arguments to be passed to a ``function`` segment.
.. _config-themes-seg-align:
``align`` ``align``
Aligns the segments contents to the left (``l``), center (``c``) or Aligns the segments contents to the left (``l``), center (``c``) or
right (``r``). 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: .. _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 either returned by a function or a static string, and the contents
can be aligned with the ``align`` property. can be aligned with the ``align`` property.
.. _config-themes-seg-priority:
``priority`` ``priority``
Optional segment priority. Segments with priority ``None`` (the default Optional segment priority. Segments with priority ``None`` (the default
priority, represented by ``null`` in json) will always be included, 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. segments. Only applicable for functions returning multiple segments.
Defaults to ``False``. Defaults to ``False``.
.. _config-themes-seg-exclude_modes:
``exclude_modes`` ``exclude_modes``
A list of modes where this segment will be excluded: The segment is A list of modes where this segment will be excluded: The segment is
included in all modes, *except* for the modes in this list. included in all modes, *except* for the modes in this list.

View File

@ -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. a ``segment_info`` dictionary specific to one of the listed entities.
Second item must contain another dictionary: it will be used to modify the 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 resulting segment. In addition to :ref:`usual keys that describe segment
keys may be present (it is advised that *only* the following keys will be used): <dev-segments-segment>` the following keys may be present (it is advised that
*only* the following keys will be used):
.. _dev-listers-mode:
``mode`` ``mode``
Segment-specific mode. Used to alter segment highlighting. Segment-specific mode. Used to alter segment highlighting.

View File

@ -51,6 +51,8 @@ powerline:
theme-specific values go directly to :ref:`top-level themes theme-specific values go directly to :ref:`top-level themes
<config-themes>`. <config-themes>`.
.. _dev-segments-startup:
``startup`` ``startup``
This attribute must be a callable which accepts the following keyword This attribute must be a callable which accepts the following keyword
arguments: arguments:
@ -68,6 +70,8 @@ powerline:
used (more specific: when :py:class:`powerline.Powerline` constructor used (more specific: when :py:class:`powerline.Powerline` constructor
received true ``run_once`` argument). received true ``run_once`` argument).
.. _dev-segments-shutdown:
``shutdown`` ``shutdown``
This attribute must be a callable that accepts no arguments and shuts down 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 threads and frees any other resources allocated in ``startup`` method of the
@ -75,6 +79,39 @@ powerline:
This function is not called when ``startup`` method is not called. 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 <dev-segments-segment>`.
* Any arguments found in user configuration for the given segment (i.e.
:ref:`args key <config-themes-seg-args>`).
It must return new value of :ref:`contents <dev-segments-seg-contents>` key.
.. _dev-segments-truncate:
``truncate``
Like :ref:`expand function <dev-segments-expand>`, 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 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 ``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 or a list of dictionaries. String object is a short form of the following return
@ -87,6 +124,8 @@ value:
'highlight_group': [segment_name], 'highlight_group': [segment_name],
}] }]
.. _dev-segments-return:
Returned list is a list of segments treated independently, except for Returned list is a list of segments treated independently, except for
:ref:`draw_inner_divider key <dev-segments-draw_inner_divider>`. :ref:`draw_inner_divider key <dev-segments-draw_inner_divider>`.
@ -158,6 +197,87 @@ Detailed description of used dictionary keys:
No error occurs if segment has this key, but no used highlight groups use No error occurs if segment has this key, but no used highlight groups use
gradient color. 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-expand>`.
.. _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 <config-themes-seg-name>` or
function name (last component of the :ref:`function key
<config-themes-seg-function>`). May be ``None``.
``type``
:ref:`Segment type <config-themes-seg-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 <config-themes-seg-before>` or :ref:`after
<config-themes-seg-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
<dev-segments-seg-around>`. May be ``None``.
``priority``
:ref:`Segment priority <config-themes-seg-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 <dev-segments-draw_inner_divider>`.
``side``
Segment side: ``right`` or ``left``.
``exclude_modes``, ``include_modes``
:ref:`Mode display control lists <config-themes-seg-exclude_modes>`. May be
empty, but may not be ``None``.
``width``, ``align``
:ref:`Width and align options <config-themes-seg-align>`. May be ``None``.
``expand``, ``truncate``
Partially applied :ref:`expand <dev-segments-expand>` or :ref:`truncate
<dev-segments-truncate>` function. Accepts ``pl``, ``amount`` and
``segment`` positional parameters, keyword parameters from :ref:`args
<config-themes-seg-args>` key were applied.
``startup``
Partially applied :ref:`startup function <dev-segments-startup>`. Accepts
``pl`` and ``shutdown_event`` positional parameters, keyword parameters from
:ref:`args <config-themes-seg-args>` key were applied.
``shutdown``
:ref:`Shutdown function <dev-segments-shutdown>`. Accepts no argument.
``mode``
:ref:`Segment-specific mode <dev-listers-mode>`. May be ``None``.
Segments layout Segments layout
=============== ===============

View File

@ -1,9 +1,12 @@
# vim:fileencoding=utf-8:noet # vim:fileencoding=utf-8:noet
from powerline.theme import Theme
from unicodedata import east_asian_width, combining
import os import os
from unicodedata import east_asian_width, combining
from itertools import chain
from powerline.theme import Theme
try: try:
NBSP = unicode(' ', 'utf-8') NBSP = unicode(' ', 'utf-8')
except NameError: except NameError:
@ -262,14 +265,20 @@ class Renderer(object):
# Create an ordered list of segments that can be dropped # 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) 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: no_priority_segments = filter(lambda segment: segment['priority'] is None, segments)
current_width = self._render_length(theme, segments, divider_widths) current_width = self._render_length(theme, segments, divider_widths)
if current_width <= width: if current_width > width:
break for segment in chain(segments_priority, no_priority_segments):
segments.remove(segment) 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 # 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 segments_spacers:
if not segments_priority: if not segments_priority:
# Update segment['_len'] and current_width if not already done # Update segment['_len'] and current_width if not already done
@ -278,15 +287,12 @@ class Renderer(object):
current_width = self._render_length(theme, segments, divider_widths) current_width = self._render_length(theme, segments, divider_widths)
distribute_len, distribute_len_remainder = divmod(width - current_width, len(segments_spacers)) distribute_len, distribute_len_remainder = divmod(width - current_width, len(segments_spacers))
for segment in segments_spacers: for segment in segments_spacers:
if segment['align'] == 'l': segment['contents'] = (
segment['_space_right'] += distribute_len segment['expand'](
elif segment['align'] == 'r': self.pl,
segment['_space_left'] += distribute_len distribute_len + (1 if distribute_len_remainder > 0 else 0),
elif segment['align'] == 'c': segment))
space_side, space_side_remainder = divmod(distribute_len, 2) distribute_len_remainder -= 1
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 # `_len` key is not needed anymore, but current_width should have an
# actual value for various bindings. # actual value for various bindings.
current_width = width current_width = width
@ -321,7 +327,7 @@ class Renderer(object):
)) ))
draw_divider = segment['draw_' + divider_type + '_divider'] draw_divider = segment['draw_' + divider_type + '_divider']
segment_len += segment['_space_left'] + segment['_space_right'] + outer_padding segment_len += outer_padding
if draw_divider: if draw_divider:
segment_len += divider_widths[side][divider_type] + divider_spaces segment_len += divider_widths[side][divider_type] + divider_spaces
@ -363,30 +369,14 @@ class Renderer(object):
if draw_divider: if draw_divider:
divider_raw = self.escape(theme.get_divider(side, divider_type)) divider_raw = self.escape(theme.get_divider(side, divider_type))
if side == 'left': if side == 'left':
contents_raw = ( contents_raw = outer_padding + contents_raw + (divider_spaces * ' ')
outer_padding + (segment['_space_left'] * ' ')
+ contents_raw
+ ((divider_spaces + segment['_space_right']) * ' ')
)
else: else:
contents_raw = ( contents_raw = (divider_spaces * ' ') + contents_raw + outer_padding
((divider_spaces + segment['_space_left']) * ' ')
+ contents_raw
+ (segment['_space_right'] * ' ') + outer_padding
)
else: else:
if side == 'left': if side == 'left':
contents_raw = ( contents_raw = outer_padding + contents_raw
outer_padding + (segment['_space_left'] * ' ')
+ contents_raw
+ (segment['_space_right'] * ' ')
)
else: else:
contents_raw = ( contents_raw = contents_raw + outer_padding
(segment['_space_left'] * ' ')
+ contents_raw
+ (segment['_space_right'] * ' ') + outer_padding
)
# Replace spaces with no-break spaces # Replace spaces with no-break spaces
contents_raw = contents_raw.translate(self.np_character_translations) contents_raw = contents_raw.translate(self.np_character_translations)

View File

@ -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: try:
func = getattr(contents_func, key) func = getattr(contents_func, key)
except AttributeError: except AttributeError:
return None return None
else: else:
if args is None: if is_space_func:
return lambda: 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 {0} function: {1}', key, str(e))
return segment['contents'] + (' ' * amount)
return expand_func
else: else:
return lambda pl, shutdown_event: func(pl=pl, shutdown_event=shutdown_event, **args) return lambda pl, shutdown_event: func(pl=pl, shutdown_event=shutdown_event, **args)
@ -285,6 +291,8 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge
'include_modes': segment.get('include_modes', []), 'include_modes': segment.get('include_modes', []),
'width': None, 'width': None,
'align': None, 'align': None,
'expand': None,
'truncate': None,
'startup': None, 'startup': None,
'shutdown': None, 'shutdown': None,
'mode': None, 'mode': None,
@ -292,13 +300,13 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge
'_rendered_hl': '', '_rendered_hl': '',
'_len': None, '_len': None,
'_contents_len': None, '_contents_len': None,
'_space_left': 0,
'_space_right': 0,
} }
if segment_type == 'function': if segment_type == 'function':
startup_func = get_attr_func(_contents_func, 'startup', args) 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)
truncate_func = get_attr_func(_contents_func, 'truncate', args, True)
if hasattr(_contents_func, 'powerline_requires_filesystem_watcher'): if hasattr(_contents_func, 'powerline_requires_filesystem_watcher'):
create_watcher = lambda: create_file_watcher(pl, common_config['watcher']) create_watcher = lambda: create_file_watcher(pl, common_config['watcher'])
@ -312,6 +320,8 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge
startup_func = None startup_func = None
shutdown_func = None shutdown_func = None
contents_func = None contents_func = None
expand_func = None
truncate_func = None
return { return {
'name': name or function_name, 'name': name or function_name,
@ -331,6 +341,8 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge
'include_modes': segment.get('include_modes', []), 'include_modes': segment.get('include_modes', []),
'width': segment.get('width'), 'width': segment.get('width'),
'align': segment.get('align', 'l'), 'align': segment.get('align', 'l'),
'expand': expand_func,
'truncate': truncate_func,
'startup': startup_func, 'startup': startup_func,
'shutdown': shutdown_func, 'shutdown': shutdown_func,
'mode': None, 'mode': None,
@ -338,8 +350,6 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, ge
'_rendered_hl': '', '_rendered_hl': '',
'_len': None, '_len': None,
'_contents_len': None, '_contents_len': None,
'_space_left': 0,
'_space_right': 0,
} }
return get return get

View File

@ -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): class Theme(object):
def __init__(self, def __init__(self,
ext, ext,
@ -128,6 +148,13 @@ class Theme(object):
self.colorscheme, self.colorscheme,
) )
for segment in parsed_segments: for segment in parsed_segments:
width = segment['width']
align = segment['align']
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)
segment['contents'] = segment['before'] + u(segment['contents'] if segment['contents'] is not None else '') + segment['after'] segment['contents'] = segment['before'] + u(segment['contents'] if segment['contents'] is not None else '') + segment['after']
# Align segment contents # Align segment contents
if segment['width'] and segment['width'] != 'auto': if segment['width'] and segment['width'] != 'auto':

View File

@ -44,6 +44,7 @@ def urllib_read(query_url):
else: else:
raise NotImplementedError raise NotImplementedError
class Process(object): class Process(object):
def __init__(self, output, err): def __init__(self, output, err):
self.output = output self.output = output

View File

@ -94,7 +94,7 @@ config = {
highlighted_string('g', 'str2'), highlighted_string('g', 'str2'),
], ],
'right': [ 'right': [
highlighted_string('f', 'str2', width='auto', align='right'), highlighted_string('f', 'str2', width='auto', align='r'),
], ],
}, },
}, },
@ -474,6 +474,45 @@ class TestSegmentAttributes(TestRender):
} }
self.assertRenderEqual(p, '{56} pl;{6-}>>{--}') 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)
@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): class TestSegmentData(TestRender):
@add_args @add_args