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:
parent
21a48e997a
commit
d3bace24ec
315
lib/core.py
315
lib/core.py
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue