diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..e69de29b diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/colors.py b/lib/colors.py new file mode 100644 index 00000000..fb128762 --- /dev/null +++ b/lib/colors.py @@ -0,0 +1,54 @@ +def cterm_to_hex(cterm_color): + '''Translate a cterm color index into the corresponding hex/RGB color. + ''' + color_dict = { + 16: 0x000000, 17: 0x00005f, 18: 0x000087, 19: 0x0000af, 20: 0x0000d7, 21: 0x0000ff, + 22: 0x005f00, 23: 0x005f5f, 24: 0x005f87, 25: 0x005faf, 26: 0x005fd7, 27: 0x005fff, + 28: 0x008700, 29: 0x00875f, 30: 0x008787, 31: 0x0087af, 32: 0x0087d7, 33: 0x0087ff, + 34: 0x00af00, 35: 0x00af5f, 36: 0x00af87, 37: 0x00afaf, 38: 0x00afd7, 39: 0x00afff, + 40: 0x00d700, 41: 0x00d75f, 42: 0x00d787, 43: 0x00d7af, 44: 0x00d7d7, 45: 0x00d7ff, + 46: 0x00ff00, 47: 0x00ff5f, 48: 0x00ff87, 49: 0x00ffaf, 50: 0x00ffd7, 51: 0x00ffff, + 52: 0x5f0000, 53: 0x5f005f, 54: 0x5f0087, 55: 0x5f00af, 56: 0x5f00d7, 57: 0x5f00ff, + 58: 0x5f5f00, 59: 0x5f5f5f, 60: 0x5f5f87, 61: 0x5f5faf, 62: 0x5f5fd7, 63: 0x5f5fff, + 64: 0x5f8700, 65: 0x5f875f, 66: 0x5f8787, 67: 0x5f87af, 68: 0x5f87d7, 69: 0x5f87ff, + 70: 0x5faf00, 71: 0x5faf5f, 72: 0x5faf87, 73: 0x5fafaf, 74: 0x5fafd7, 75: 0x5fafff, + 76: 0x5fd700, 77: 0x5fd75f, 78: 0x5fd787, 79: 0x5fd7af, 80: 0x5fd7d7, 81: 0x5fd7ff, + 82: 0x5fff00, 83: 0x5fff5f, 84: 0x5fff87, 85: 0x5fffaf, 86: 0x5fffd7, 87: 0x5fffff, + 88: 0x870000, 89: 0x87005f, 90: 0x870087, 91: 0x8700af, 92: 0x8700d7, 93: 0x8700ff, + 94: 0x875f00, 95: 0x875f5f, 96: 0x875f87, 97: 0x875faf, 98: 0x875fd7, 99: 0x875fff, + 100: 0x878700, 101: 0x87875f, 102: 0x878787, 103: 0x8787af, 104: 0x8787d7, 105: 0x8787ff, + 106: 0x87af00, 107: 0x87af5f, 108: 0x87af87, 109: 0x87afaf, 110: 0x87afd7, 111: 0x87afff, + 112: 0x87d700, 113: 0x87d75f, 114: 0x87d787, 115: 0x87d7af, 116: 0x87d7d7, 117: 0x87d7ff, + 118: 0x87ff00, 119: 0x87ff5f, 120: 0x87ff87, 121: 0x87ffaf, 122: 0x87ffd7, 123: 0x87ffff, + 124: 0xaf0000, 125: 0xaf005f, 126: 0xaf0087, 127: 0xaf00af, 128: 0xaf00d7, 129: 0xaf00ff, + 130: 0xaf5f00, 131: 0xaf5f5f, 132: 0xaf5f87, 133: 0xaf5faf, 134: 0xaf5fd7, 135: 0xaf5fff, + 136: 0xaf8700, 137: 0xaf875f, 138: 0xaf8787, 139: 0xaf87af, 140: 0xaf87d7, 141: 0xaf87ff, + 142: 0xafaf00, 143: 0xafaf5f, 144: 0xafaf87, 145: 0xafafaf, 146: 0xafafd7, 147: 0xafafff, + 148: 0xafd700, 149: 0xafd75f, 150: 0xafd787, 151: 0xafd7af, 152: 0xafd7d7, 153: 0xafd7ff, + 154: 0xafff00, 155: 0xafff5f, 156: 0xafff87, 157: 0xafffaf, 158: 0xafffd7, 159: 0xafffff, + 160: 0xd70000, 161: 0xd7005f, 162: 0xd70087, 163: 0xd700af, 164: 0xd700d7, 165: 0xd700ff, + 166: 0xd75f00, 167: 0xd75f5f, 168: 0xd75f87, 169: 0xd75faf, 170: 0xd75fd7, 171: 0xd75fff, + 172: 0xd78700, 173: 0xd7875f, 174: 0xd78787, 175: 0xd787af, 176: 0xd787d7, 177: 0xd787ff, + 178: 0xd7af00, 179: 0xd7af5f, 180: 0xd7af87, 181: 0xd7afaf, 182: 0xd7afd7, 183: 0xd7afff, + 184: 0xd7d700, 185: 0xd7d75f, 186: 0xd7d787, 187: 0xd7d7af, 188: 0xd7d7d7, 189: 0xd7d7ff, + 190: 0xd7ff00, 191: 0xd7ff5f, 192: 0xd7ff87, 193: 0xd7ffaf, 194: 0xd7ffd7, 195: 0xd7ffff, + 196: 0xff0000, 197: 0xff005f, 198: 0xff0087, 199: 0xff00af, 200: 0xff00d7, 201: 0xff00ff, + 202: 0xff5f00, 203: 0xff5f5f, 204: 0xff5f87, 205: 0xff5faf, 206: 0xff5fd7, 207: 0xff5fff, + 208: 0xff8700, 209: 0xff875f, 210: 0xff8787, 211: 0xff87af, 212: 0xff87d7, 213: 0xff87ff, + 214: 0xffaf00, 215: 0xffaf5f, 216: 0xffaf87, 217: 0xffafaf, 218: 0xffafd7, 219: 0xffafff, + 220: 0xffd700, 221: 0xffd75f, 222: 0xffd787, 223: 0xffd7af, 224: 0xffd7d7, 225: 0xffd7ff, + 226: 0xffff00, 227: 0xffff5f, 228: 0xffff87, 229: 0xffffaf, 230: 0xffffd7, 231: 0xffffff, + 232: 0x080808, 233: 0x121212, 234: 0x1c1c1c, 235: 0x262626, 236: 0x303030, 237: 0x3a3a3a, + 238: 0x444444, 239: 0x4e4e4e, 240: 0x585858, 241: 0x626262, 242: 0x6c6c6c, 243: 0x767676, + 244: 0x808080, 245: 0x8a8a8a, 246: 0x949494, 247: 0x9e9e9e, 248: 0xa8a8a8, 249: 0xb2b2b2, + 250: 0xbcbcbc, 251: 0xc6c6c6, 252: 0xd0d0d0, 253: 0xdadada, 254: 0xe4e4e4, 255: 0xeeeeee, + } + if not cterm_color: + return None + + try: + return color_dict[cterm_color] + except KeyError: + import sys + sys.stderr.write('Invalid cterm color index: {0}\n'.format(cterm_color)) + return None diff --git a/lib/core.py b/lib/core.py new file mode 100644 index 00000000..a942311e --- /dev/null +++ b/lib/core.py @@ -0,0 +1,236 @@ +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)) + ''' + separators = { + 'l': { + 'hard': '⮀', + 'soft': '⮁', + }, + 'r': { + 'hard': '⮂', + 'soft': '⮃', + }, + } + + ATTR_BOLD = 1 + ATTR_ITALIC = 2 + ATTR_UNDERLINE = 4 + + def __init__(self, content='', fg=None, bg=None, attr=None, side=None, padding=None, separate=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 separators. + ''' + self.content = content + self.parent = None + self.prev = None + self.next = None + + try: + if len(fg) == 2: + self._fg = 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)] + + try: + if len(bg) == 2: + self._bg = 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._attr = attr + self._side = side + self._padding = padding + self._separate = separate + + @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 [None, None] + + @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 [None, None] + + @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 0 + + @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 + separator 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 separate(self): + '''Segment separation property. + + Returns whether a separator 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 separators will be drawn around all segments. + ''' + if self.parent and self._separate is None: + return self.parent.separate + return self._separate if self._separate is not None else True + + @property + def separator(self): + '''Segment separator property. + + Returns the separator symbol to be used, depending on which side this + segment is on. + ''' + return self.separators[self.side] + + def render(self, renderer): + '''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 separator/padding properties and + returns the rendered statusline as a string. + ''' + def flatten(segment): + '''Flattens 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): + # If the contents of the child segment is a string then + # this is a tree node + ret.append(child_segment) + else: + # This is a segment group that should be flattened + ret += flatten(child_segment) + return ret + + segments = flatten(self) + output = '' + + # Loop through the segment array and create the segments, colors and + # separators + # + # TODO Make this prettier + 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() + + if segment.side == 'l': + output += renderer.fg(segment.fg[0]) + output += renderer.bg(segment.bg[0]) + output += segment.padding + output += renderer.attr(segment.attr) + output += segment.content + output += renderer.attr(None) + if segment.content: + if segment.next.bg == segment.bg: + if segment.next.content and segment.separate: + output += segment.padding + output += segment.separator['soft'] + # Don't draw a hard separator 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.fg(segment.bg[0]) + output += renderer.bg(segment.next.bg[0]) + output += segment.separator['hard'] + else: + pad_pre = False + if segment.content: + if segment.prev.bg == segment.bg: + if segment.prev.content and segment.separate: + pad_pre = True + output += segment.separator['soft'] + else: + pad_pre = True + output += renderer.fg(segment.bg[0]) + output += renderer.bg(segment.prev.bg[0]) + output += segment.separator['hard'] + output += renderer.fg(segment.fg[0]) + output += renderer.bg(segment.bg[0]) + if pad_pre: + output += segment.padding + output += renderer.attr(segment.attr) + output += segment.content + output += renderer.attr(None) + output += segment.padding + + return output diff --git a/lib/renderers/__init__.py b/lib/renderers/__init__.py new file mode 100644 index 00000000..ee7568ee --- /dev/null +++ b/lib/renderers/__init__.py @@ -0,0 +1,8 @@ +class SegmentRenderer: + def fg(col): + raise NotImplementedError + + def bg(col): + raise NotImplementedError + +from lib.renderers.terminal import TerminalSegmentRenderer diff --git a/lib/renderers/terminal.py b/lib/renderers/terminal.py new file mode 100644 index 00000000..10eeb3d9 --- /dev/null +++ b/lib/renderers/terminal.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +from lib.core import Segment +from lib.renderers import SegmentRenderer + + +class TerminalSegmentRenderer(SegmentRenderer): + '''Powerline terminal segment renderer. + ''' + def fg(col): + '''Return ANSI escape code for foreground colors. + + If no color is provided, the color is reset to the terminal default. + ''' + if col: + return '[38;5;{0}m'.format(col) + else: + return '' + + def bg(col): + '''Return ANSI escape code for background colors. + + If no color is provided, the color is reset to the terminal default. + ''' + if col: + return '[48;5;{0}m'.format(col) + else: + return '' + + def attr(attrs): + '''Return ANSI escape code for attributes. + + Accepts a flag with attributes defined in Segment. + + If no attributes are provided, the attributes are reset to the terminal + defaults. + ''' + if not attrs: + return '' + + ansi_attrs = [] + if attrs & Segment.ATTR_BOLD: + ansi_attrs.append('1') + + if ansi_attrs: + return '[{0}m'.format(';'.join(ansi_attrs)) + + return '' diff --git a/powerline-terminal-example.py b/powerline-terminal-example.py new file mode 100755 index 00000000..2230e516 --- /dev/null +++ b/powerline-terminal-example.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +'''Powerline terminal prompt example. +''' + +from lib.core import Segment +from lib.renderers import TerminalSegmentRenderer + +powerline = Segment([ + 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), +]) + +print(powerline.render(TerminalSegmentRenderer))