Rewrite core code to only allow a single-dimensional segment array

The segment tree seemed like a good idea at the time, but for the core
code it's probably best to do a single-dimensional array and instead
have some duplicated data (common segment attributes). By removing the
tree structured segments the code is much simpler to understand and use,
and probably quite a bit faster.

If a tree structure is wanted for ease of configuration, it could easily
be supported in the JSON config files for vim statuslines, and then
letting the vim statusline code flatten the configuration into
a single-dimensional array for rendering.
This commit is contained in:
Kim Silkebækken 2012-11-16 12:08:55 +01:00
parent 5abfdaeef8
commit 2ea555b2dd
3 changed files with 74 additions and 156 deletions

View File

@ -1,30 +1,7 @@
# -*- coding: utf-8 -*-
class Segment:
'''Powerline segment renderer.
Powerline segments are initially structured as a tree of segments and sub
segments. This is to give the segments a sense of grouping and "scope", to
avoid having to define all the properties (fg, bg, etc.) for every single
segment. By grouping you can e.g. provide a common background color for
several segments.
Usage example:
from lib.core import Segment
from lib.renderers import TerminalSegmentRenderer
powerline = Segment([
Segment('First segment'),
Segment([
Segment('Grouped segment 1'),
Segment('Grouped segment 2'),
]),
])
print(powerline.render(TerminalSegmentRenderer))
'''
class Powerline:
dividers = {
'l': {
'hard': '',
@ -36,91 +13,16 @@ class Segment:
},
}
ATTR_BOLD = 1
ATTR_ITALIC = 2
ATTR_UNDERLINE = 4
def __init__(self, contents=None, fg=None, bg=None, attr=None, side=None, draw_divider=None, priority=None, filler=None):
'''Create a new Powerline segment.
def __init__(self, segments):
'''Create a new Powerline.
'''
self.parent = None
self.contents = contents or ''
self.fg = fg
self.bg = bg
self.attr = attr
self.side = side
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.draw_divider = lookup_attr('draw_divider', True)
self.priority = lookup_attr('priority', -1)
self.filler = lookup_attr('filler', False)
try:
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 = [self.fg, cterm_to_hex(self.fg)]
try:
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 = [self.bg, cterm_to_hex(self.bg)]
self.segments = segments
def render(self, renderer, width=None):
'''Render the segment and all child segments.
'''Render all the segments with the specified renderer.
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 properties and returns the
This method loops through the segment array and compares the
foreground/background colors and divider properties and returns the
rendered statusline as a string.
When a width is provided, low-priority segments are dropped one at
@ -129,28 +31,13 @@ class Segment:
provided they will fill the remaining space until the desired width is
reached.
'''
def flatten(segment):
'''Flatten the segment tree into a one-dimensional array.
'''
ret = []
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
ret += flatten(child_segment)
return ret
segments = flatten(self)
def render_segments(segments, render_raw=True, render_highlighted=True):
'''Render a one-dimensional segment array.
'''Render a segment array.
By default this function renders both raw (un-highlighted segments
used for calculating final width) and highlighted segments.
used for calculating final width) and highlighted segments. The raw
rendering is used for calculating the total width for dropping
low-priority segments.
'''
rendered_raw = ''
rendered_highlighted = ''
@ -207,29 +94,67 @@ class Segment:
'raw': rendered_raw,
}
rendered = render_segments(segments)
rendered = render_segments(self.segments)
if not width:
# No width specified, so we don't need to crop or pad anything
return rendered['highlighted']
# 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]
segments_priority = [segment for segment in sorted(self.segments, key=lambda segment: segment.priority, reverse=True) if segment.priority > 0]
while len(rendered['raw'].decode('utf-8')) > width and len(segments_priority):
segments.remove(segments_priority[0])
self.segments.remove(segments_priority[0])
segments_priority.pop(0)
rendered = render_segments(segments, render_highlighted=False)
rendered = render_segments(self.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 = [segment for segment in self.segments if segment.filler is True]
if segments_fillers:
segments_fillers_contents = ' ' * int((width - len(rendered['raw'].decode('utf-8'))) / 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)
rendered = render_segments(self.segments, render_raw=False)
return rendered['highlighted']
class Segment:
ATTR_BOLD = 1
ATTR_ITALIC = 2
ATTR_UNDERLINE = 4
def __init__(self, contents=None, fg=False, bg=False, attr=False, side='l', draw_divider=True, priority=-1, filler=False):
'''Create a new Powerline segment.
'''
self.contents = str(contents or '')
self.fg = fg
self.bg = bg
self.attr = attr
self.side = side
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
try:
if len(self.fg) != 2:
raise TypeError
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 = [self.fg, cterm_to_hex(self.fg)]
try:
if len(self.bg) != 2:
raise TypeError
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 = [self.bg, cterm_to_hex(self.bg)]

View File

@ -2,17 +2,16 @@
'''Powerline terminal prompt example.
'''
from lib.core import Segment
from lib.core import Powerline, Segment
from lib.renderers import TerminalSegmentRenderer
powerline = Segment([
powerline = Powerline([
Segment('⭤ SSH', 220, 166, attr=Segment.ATTR_BOLD),
Segment('username', 153, 31),
Segment([
Segment('~'),
Segment('projects'),
Segment('powerline', 231, attr=Segment.ATTR_BOLD),
], 248, 239),
Segment('~', 248, 239),
Segment('projects', 248, 239),
Segment('powerline', 231, 239, attr=Segment.ATTR_BOLD),
Segment(filler=True),
])
print(powerline.render(TerminalSegmentRenderer()))

View File

@ -12,7 +12,7 @@ import re
import sys
sys.path.append('.')
from lib.core import Segment
from lib.core import Powerline, Segment
from lib.renderers import VimSegmentRenderer
winwidth = int(vim.eval('winwidth(0)'))
@ -47,26 +47,20 @@ filepath = os.path.split(vim.eval('expand("%:~:.")'))
if filepath[0]:
filepath[0] += os.sep
powerline = Segment([
powerline = Powerline([
Segment(mode, 22, 148, attr=Segment.ATTR_BOLD),
Segment('⭠ ' + branch, 250, 240, priority=10),
Segment([
Segment(filepath[0], draw_divider=False, priority=5),
Segment(filepath[1], 231, attr=Segment.ATTR_BOLD),
], 250, 240),
Segment(filler=True),
Segment([
Segment(vim.eval('&ff'), priority=50),
Segment(vim.eval('&fenc'), priority=50),
Segment(vim.eval('&ft'), priority=50),
Segment(str(line_percent).rjust(3) + '%', line_percent_color, 240, priority=30),
Segment([
Segment('⭡ ', 239),
Segment(str(line_current).rjust(3), attr=Segment.ATTR_BOLD, draw_divider=False),
Segment(':' + str(col_current).ljust(2), 244, priority=30, draw_divider=False),
], 235, 252),
], 247, side='r'),
], fg=236, bg=236)
Segment(filepath[0], 250, 240, draw_divider=False, priority=5),
Segment(filepath[1], 231, 240, attr=Segment.ATTR_BOLD),
Segment(filler=True, fg=236, bg=236),
Segment(vim.eval('&ff'), 247, 236, side='r', priority=50),
Segment(vim.eval('&fenc'), 247, 236, side='r', priority=50),
Segment(vim.eval('&ft'), 247, 236, side='r', priority=50),
Segment(str(line_percent).rjust(3) + '%', line_percent_color, 240, side='r', priority=30),
Segment('⭡ ', 239, 252, side='r'),
Segment(str(line_current).rjust(3), 235, 252, attr=Segment.ATTR_BOLD, side='r', draw_divider=False),
Segment(':' + str(col_current).ljust(2), 244, 252, side='r', priority=30, draw_divider=False),
])
renderer = VimSegmentRenderer()
stl = powerline.render(renderer, winwidth)