Improve the Segment class and rendering

The Segment class has been given two new properties: priority and
filler. The render method has been given a width argument.

If the width argument is specified when rendering a segment and the
rendered segments are too wide, segments are dropped in order of
priority, with the lowest priority (highest number) being dropped first
and any segment with priority < 0 is kept, regardless of the specified
width.

If the width argument is specified along with one or more filler
segments, the filler segments will pad the remaining space with space
characters until the desired width has been reached.

The handling of segment attributes has also been improved by lazily
looking up the correct attributes in the segment tree when it's being
rendered.

Finally, the rendering code itself has been improved a bit with less
code duplication and much more readable code.
This commit is contained in:
Kim Silkebækken 2012-11-14 11:53:39 +01:00
parent 21a48e997a
commit d3bace24ec
2 changed files with 172 additions and 173 deletions

View File

@ -37,142 +37,106 @@ class Segment:
ATTR_ITALIC = 2
ATTR_UNDERLINE = 4
def __init__(self, content='', fg=None, bg=None, attr=None, side=None, padding=None, divide=None):
'''Create a new segment.
No arguments are required when creating new segments, as
empty/colorless segments can be used e.g. as left/right dividers.
def __init__(self, contents=None, fg=None, bg=None, attr=None, side=None, padding=None, draw_divider=None, priority=None, filler=None):
'''Create a new Powerline segment.
'''
self.content = content
self.parent = None
self.prev = None
self.next = None
self.contents = contents or ''
self.fg = fg
self.bg = bg
self.attr = attr
self.side = side
self.padding = padding
self.draw_divider = draw_divider
self.priority = priority
self.filler = filler
if self.filler:
# Filler segments should never have any dividers
self.draw_divider = False
# Set the parent property for child segments
for segment in self.contents:
try:
segment.parent = self
except AttributeError:
# Not a Segment node
continue
def init_attributes(self):
'''Initialize the default attributes for this segment.
This method is intended to be run when all segments in the segment tree
have the correct parent segment set (i.e. after the root segment has
been instantiated).
'''
def lookup_attr(attr, default, obj=self):
'''Looks up attributes in the segment tree.
If the attribute isn't found anywhere, the default argument is used
for this segment.
'''
# Check if the current object has the attribute defined
obj_attr = getattr(obj, attr)
if obj_attr is None:
try:
# Check if the object's parent has the attribute defined
return lookup_attr(attr, default, obj.parent)
except AttributeError:
# Root node reached
return default
return obj_attr
# Set default attributes
self.fg = lookup_attr('fg', False)
self.bg = lookup_attr('bg', False)
self.attr = lookup_attr('attr', False)
self.side = lookup_attr('side', 'l')
self.padding = lookup_attr('padding', ' ')
self.draw_divider = lookup_attr('draw_divider', True)
self.priority = lookup_attr('priority', -1)
self.filler = lookup_attr('filler', False)
try:
if len(fg) == 2:
self._fg = fg
if len(self.fg) == 2:
self.fg = self.fg
except TypeError:
# Only the terminal color is defined, so we need to get the hex color
from lib.colors import cterm_to_hex
self._fg = [fg, cterm_to_hex(fg)]
self.fg = [self.fg, cterm_to_hex(self.fg)]
try:
if len(bg) == 2:
self._bg = bg
if len(self.bg) == 2:
self.bg = self.bg
except TypeError:
# Only the terminal color is defined, so we need to get the hex color
from lib.colors import cterm_to_hex
self._bg = [bg, cterm_to_hex(bg)]
self.bg = [self.bg, cterm_to_hex(self.bg)]
self._attr = attr
self._side = side
self._padding = padding
self._divide = divide
@property
def fg(self):
'''Segment foreground color property.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, the default foreground color is used.
'''
if self.parent and self._fg[0] is None:
return self.parent.fg
return self._fg if self._fg[0] is not None else False
@property
def bg(self):
'''Segment background color property.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, the default background color is used.
'''
if self.parent and self._bg[0] is None:
return self.parent.bg
return self._bg if self._bg[0] is not None else False
@property
def attr(self):
'''Segment attribute property.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, no attributes are applied.
'''
if self.parent and self._attr is None:
return self.parent.attr
return self._attr if self._attr is not None else False
@property
def side(self):
'''Segment side property.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, the left side is used for all segments.
'''
if self.parent and self._side is None:
return self.parent.side
return self._side if self._side is not None else 'l'
@property
def padding(self):
'''Segment padding property.
Return which string is used to pad the segment before and after the
divider symbol.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, a single space is used for padding.
'''
if self.parent and self._padding is None:
return self.parent.padding
return self._padding if self._padding is not None else ' '
@property
def divide(self):
'''Segment divider property.
Returns whether a divider symbol should be drawn before/after the
segment.
Recursively searches for the property or the parent segments' property
until one is found. If this property is not defined anywhere in the
tree, then dividers will be drawn around all segments.
'''
if self.parent and self._divide is None:
return self.parent.divide
return self._divide if self._divide is not None else True
@property
def divider(self):
'''Segment divider property.
Returns the divider symbol to be used, depending on which side this
segment is on.
'''
return self.dividers[self.side]
def render(self, renderer):
def render(self, renderer, width=None):
'''Render the segment and all child segments.
This method flattens the segment and all its child segments into
a one-dimensional array. It then loops through this array and compares
the foreground/background colors and divider/padding properties and
returns the rendered statusline as a string.
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.
'''
def flatten(segment):
'''Flattens the segment tree into a one-dimensional array.
'''Flatten the segment tree into a one-dimensional array.
'''
ret = []
for child_segment in segment.content:
child_segment.parent = segment
if isinstance(child_segment.content, str):
for child_segment in segment.contents:
if isinstance(child_segment.contents, str):
# If the contents of the child segment is a string then
# this is a tree node
child_segment.init_attributes()
ret.append(child_segment)
else:
# This is a segment group that should be flattened
@ -180,57 +144,92 @@ class Segment:
return ret
segments = flatten(self)
output = ''
# Loop through the segment array and create the segments, colors and
# dividers
#
# TODO Make this prettier
def render_segments(segments, render_raw=True, render_highlighted=True):
'''Render a one-dimensional segment array.
By default this function renders both raw (un-highlighted segments
used for calculating final width) and highlighted segments.
'''
rendered_raw = ''
rendered_highlighted = ''
for idx, segment in enumerate(segments):
# Ensure that we always have a previous/next segment, if we're at
# the beginning/end of the array an empty segment is used for the
# prev/next segment
segment.prev = segments[idx - 1] if idx > 0 else Segment()
segment.next = segments[idx + 1] if idx < len(segments) - 1 else Segment()
prev = segments[idx - 1] if idx > 0 else Segment()
next = segments[idx + 1] if idx < len(segments) - 1 else Segment()
compare_segment = next if segment.side == 'l' else prev
divider_type = 'soft' if compare_segment.bg == segment.bg else 'hard'
divider = self.dividers[segment.side][divider_type]
if segment.filler:
# Filler segments shouldn't be padded
segment_format = '{contents}'
elif segment.draw_divider and (divider_type == 'hard' or segment.side == compare_segment.side):
# Draw divider if specified, and if the next segment is on
# the opposite side only draw the divider if it's a hard
# divider
if segment.side == 'l':
output += renderer.hl(segment.fg, segment.bg, segment.attr)
output += segment.padding
output += segment.content
output += renderer.hl(attr=False)
if segment.content:
if segment.next.bg == segment.bg:
if segment.next.content and segment.divide:
output += segment.padding
if segment.next.side == segment.side:
# Only draw the soft divider if this segment is on the same side
# No need to draw the soft divider if there's e.g. a vim separation point in the next segment
output += segment.divider['soft']
# Don't draw a hard divider if the next segment is on
# the opposite side, it screws up the coloring
elif segment.next.side == segment.side:
output += segment.padding
output += renderer.hl(segment.bg, segment.next.bg)
output += segment.divider['hard']
segment_format = '{segment_hl}{padding}{contents}{padding}{divider_hl}{divider}'
else:
pad_pre = False
if segment.content:
if segment.prev.bg == segment.bg:
if segment.prev.content and segment.divide:
pad_pre = True
if segment.prev.side == segment.side:
# Only draw the soft divider if this segment is on the same side
# No need to draw the soft divider if there's e.g. a vim separation point in the previous segment
output += segment.divider['soft']
segment_format = '{divider_hl}{divider}{segment_hl}{padding}{contents}{padding}'
elif segment.contents:
# Soft divided segments
segment_format = '{segment_hl}{padding}{contents}{padding}'
else:
pad_pre = True
output += renderer.hl(segment.bg, segment.prev.bg)
output += segment.divider['hard']
output += renderer.hl(segment.fg, segment.bg, segment.attr)
if pad_pre:
output += segment.padding
output += segment.content
output += renderer.hl(attr=False)
output += segment.padding
# Unknown segment type, skip it
continue
return output
if render_raw is True and segment.filler is False:
# Filler segments must be empty when used e.g. in vim (the
# %=%< segment which disappears), so they will be skipped
# when calculating the width using the raw rendering
rendered_raw += segment_format.format(
padding=segment.padding,
divider=divider,
contents=segment.contents,
divider_hl='',
segment_hl='',
)
if render_highlighted is True:
rendered_highlighted += segment_format.format(
padding=segment.padding,
divider=divider,
contents=segment.contents,
divider_hl='' if divider_type == 'soft' else renderer.hl(segment.bg, compare_segment.bg),
segment_hl=renderer.hl(segment.fg, segment.bg, segment.attr),
)
return {
'highlighted': rendered_highlighted,
'raw': rendered_raw,
}
rendered = render_segments(segments)
if not width:
# No width specified, so we don't need to crop or pad anything
return rendered['highlighted']
import math
# 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 len(rendered['raw']) > width and len(segments_priority):
segments.remove(segments_priority[0])
segments_priority.pop(0)
rendered = render_segments(segments, render_highlighted=False)
# Distribute the remaining space on the filler segments
segments_fillers = [segment for segment in segments if segment.filler is True]
segments_fillers_contents = ' ' * math.floor((width - len(rendered['raw'])) / len(segments_fillers))
for segment in segments_fillers:
segment.contents = segments_fillers_contents
# Do a final render now that we have handled the cropping and padding
rendered = render_segments(segments, render_raw=False)
return rendered['highlighted']

View File

@ -7,22 +7,22 @@ from lib.renderers import VimSegmentRenderer
powerline = Segment([
Segment('NORMAL', 22, 148, attr=Segment.ATTR_BOLD),
Segment('⭠ develop', 247, 240),
Segment('⭠ develop', 247, 240, priority=10),
Segment([
Segment(' ~/projects/powerline/lib/'),
Segment(' ~/projects/powerline/lib/', draw_divider=False),
Segment('core.py ', 231, attr=Segment.ATTR_BOLD),
], 250, 240, divide=False, padding=''),
Segment('%<%='),
], 250, 240, padding=''),
Segment('%=%<', filler=True),
Segment([
Segment('unix'),
Segment('utf-8'),
Segment('python'),
Segment(' 83%%', 247, 240),
Segment('unix', priority=50),
Segment('utf-8', priority=50),
Segment('python', priority=50),
Segment('83%', 247, 240, priority=30),
Segment([
Segment('', 239),
Segment('23', attr=Segment.ATTR_BOLD),
Segment(':1 ', 244),
], 235, 252, divide=False, padding=''),
Segment('23', attr=Segment.ATTR_BOLD, padding='', draw_divider=False),
Segment(':1', 244, priority=30, padding='', draw_divider=False),
], 235, 252),
], 245, side='r'),
], bg=236)