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:
.. _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``).
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.

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.
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
<dev-segments-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.

View File

@ -51,6 +51,8 @@ powerline:
theme-specific values go directly to :ref:`top-level themes
<config-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,39 @@ 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 <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
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 +124,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 <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
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
===============

View File

@ -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,14 +265,20 @@ 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['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 +287,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 +327,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 +369,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)

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:
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 {0} function: {1}', key, str(e))
return segment['contents'] + (' ' * amount)
return expand_func
else:
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', []),
'width': None,
'align': None,
'expand': None,
'truncate': None,
'startup': None,
'shutdown': None,
'mode': None,
@ -292,13 +300,13 @@ 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':
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'):
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
shutdown_func = None
contents_func = None
expand_func = None
truncate_func = None
return {
'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', []),
'width': segment.get('width'),
'align': segment.get('align', 'l'),
'expand': expand_func,
'truncate': truncate_func,
'startup': startup_func,
'shutdown': shutdown_func,
'mode': None,
@ -338,8 +350,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

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):
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' 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']
# Align segment contents
if segment['width'] and segment['width'] != 'auto':

View File

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

View File

@ -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'),
],
},
},
@ -474,6 +474,45 @@ 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)
@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