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 -*- # -*- coding: utf-8 -*-
class Segment: class Powerline:
'''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))
'''
dividers = { dividers = {
'l': { 'l': {
'hard': '', 'hard': '',
@ -36,91 +13,16 @@ class Segment:
}, },
} }
ATTR_BOLD = 1 def __init__(self, segments):
ATTR_ITALIC = 2 '''Create a new Powerline.
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.
''' '''
self.parent = None self.segments = segments
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)]
def render(self, renderer, width=None): 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 This method loops through the segment array and compares the
a one-dimensional array. It then loops through this array and compares foreground/background colors and divider properties and returns the
the foreground/background colors and divider properties and returns the
rendered statusline as a string. rendered statusline as a string.
When a width is provided, low-priority segments are dropped one at 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 provided they will fill the remaining space until the desired width is
reached. 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): 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 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_raw = ''
rendered_highlighted = '' rendered_highlighted = ''
@ -207,29 +94,67 @@ class Segment:
'raw': rendered_raw, 'raw': rendered_raw,
} }
rendered = render_segments(segments) rendered = render_segments(self.segments)
if not width: if not width:
# No width specified, so we don't need to crop or pad anything # No width specified, so we don't need to crop or pad anything
return rendered['highlighted'] return rendered['highlighted']
# Create an ordered list of segments that can be dropped # 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): 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) 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 # 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: if segments_fillers:
segments_fillers_contents = ' ' * int((width - len(rendered['raw'].decode('utf-8'))) / len(segments_fillers)) segments_fillers_contents = ' ' * int((width - len(rendered['raw'].decode('utf-8'))) / len(segments_fillers))
for segment in segments_fillers: for segment in segments_fillers:
segment.contents = segments_fillers_contents segment.contents = segments_fillers_contents
# Do a final render now that we have handled the cropping and padding # 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'] 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. '''Powerline terminal prompt example.
''' '''
from lib.core import Segment from lib.core import Powerline, Segment
from lib.renderers import TerminalSegmentRenderer from lib.renderers import TerminalSegmentRenderer
powerline = Segment([ powerline = Powerline([
Segment('⭤ SSH', 220, 166, attr=Segment.ATTR_BOLD), Segment('⭤ SSH', 220, 166, attr=Segment.ATTR_BOLD),
Segment('username', 153, 31), Segment('username', 153, 31),
Segment([ Segment('~', 248, 239),
Segment('~'), Segment('projects', 248, 239),
Segment('projects'), Segment('powerline', 231, 239, attr=Segment.ATTR_BOLD),
Segment('powerline', 231, attr=Segment.ATTR_BOLD), Segment(filler=True),
], 248, 239),
]) ])
print(powerline.render(TerminalSegmentRenderer())) print(powerline.render(TerminalSegmentRenderer()))

View File

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