235 lines
7.9 KiB
Python
235 lines
7.9 KiB
Python
|
|
# Copyright (C) 2015 Andrew Hamilton. All rights reserved.
|
|
# Licensed under the Artistic License 2.0.
|
|
|
|
import collections
|
|
import weakref
|
|
|
|
import terminal
|
|
|
|
|
|
def cache_first_result(user_function):
|
|
def decorator(self, *args, **kwds):
|
|
try:
|
|
return self._cache
|
|
except AttributeError:
|
|
self._cache = user_function(self, *args, **kwds)
|
|
return self._cache
|
|
return decorator
|
|
|
|
|
|
class Color:
|
|
|
|
black = (0, 0, 0)
|
|
white = (255, 255, 255)
|
|
red = (255, 0, 0)
|
|
green = (0, 255, 0)
|
|
blue = (0, 0, 255)
|
|
yellow = (255, 255, 0)
|
|
grey_50 = (50, 50, 50)
|
|
grey_100 = (100, 100, 100)
|
|
|
|
|
|
class CharStyle:
|
|
|
|
_POOL = weakref.WeakValueDictionary()
|
|
|
|
def __new__(cls, fg_color=None, bg_color=None, is_bold=False,
|
|
is_italic=False, is_underlined=False):
|
|
if fg_color is None:
|
|
fg_color = Color.white
|
|
if bg_color is None:
|
|
bg_color = Color.black
|
|
key = (fg_color, bg_color, is_bold, is_italic, is_underlined)
|
|
try:
|
|
return CharStyle._POOL[key]
|
|
except KeyError:
|
|
obj = object.__new__(cls)
|
|
obj.fg_color, obj.bg_color, obj.is_bold, obj.is_italic, \
|
|
obj.is_underlined = key
|
|
return CharStyle._POOL.setdefault(key, obj)
|
|
|
|
def __getnewargs__(self):
|
|
return (self.fg_color, self.bg_color, self.is_bold, self.is_italic,
|
|
self.is_underlined)
|
|
|
|
def __repr__(self):
|
|
attributes = []
|
|
if self.is_bold:
|
|
attributes.append("b")
|
|
if self.is_italic:
|
|
attributes.append("i")
|
|
if self.is_underlined:
|
|
attributes.append("u")
|
|
return ("<CharStyle: fg:%s bg:%s attr:%s>" %
|
|
(self.fg_color, self.bg_color, ",".join(attributes)))
|
|
|
|
@cache_first_result
|
|
def code_for_term(self):
|
|
fg_func = (terminal.fg_color if isinstance(self.fg_color, int)
|
|
else terminal.fg_rgb_color)
|
|
bg_func = (terminal.bg_color if isinstance(self.bg_color, int)
|
|
else terminal.bg_rgb_color)
|
|
bold_code = terminal.bold if self.is_bold else ""
|
|
italic_code = terminal.italic if self.is_italic else ""
|
|
underline_code = terminal.underline if self.is_underlined else ""
|
|
return "".join([terminal.normal, fg_func(self.fg_color),
|
|
bg_func(self.bg_color), bold_code, italic_code,
|
|
underline_code])
|
|
|
|
|
|
def join_lists(lists):
|
|
result = []
|
|
for list_ in lists:
|
|
result.extend(list_)
|
|
return result
|
|
|
|
|
|
class TermStr(collections.UserString):
|
|
|
|
def __init__(self, data, style=CharStyle()):
|
|
if isinstance(data, self.__class__):
|
|
self.data = data.data
|
|
self.style = data.style
|
|
else:
|
|
self.data = data
|
|
self.style = (style if isinstance(style, tuple)
|
|
else (style,) * len(data))
|
|
|
|
def __eq__(self, other):
|
|
return (self is other or
|
|
(isinstance(other, self.__class__) and
|
|
self.data == other.data and self.style == other.style))
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __hash__(self):
|
|
return hash((self.data, self.style))
|
|
|
|
@cache_first_result
|
|
def _partition_style(self):
|
|
if self.data == "":
|
|
return []
|
|
last_style, last_index = None, 0
|
|
result = []
|
|
for index, style in enumerate(self.style):
|
|
if style != last_style:
|
|
if last_style is not None:
|
|
result.append(
|
|
(last_style, self.data[last_index:index], last_index))
|
|
last_style, last_index = style, index
|
|
result.append(
|
|
(last_style, self.data[last_index:len(self.style)], last_index))
|
|
return result
|
|
|
|
def __str__(self):
|
|
return "".join(join_lists(
|
|
[style.code_for_term(), str_]
|
|
for style, str_, position in self._partition_style()) +
|
|
[terminal.normal])
|
|
|
|
def __repr__(self):
|
|
return "<TermStr: %r>" % self.data
|
|
|
|
def __add__(self, other):
|
|
if isinstance(other, str):
|
|
other = TermStr(other)
|
|
return self.__class__(self.data + other.data, self.style + other.style)
|
|
|
|
def __radd__(self, other):
|
|
if isinstance(other, str):
|
|
other = TermStr(other)
|
|
return self.__class__(other.data + self.data, other.style + self.style)
|
|
|
|
def __mul__(self, n):
|
|
return self.__class__(self.data*n, self.style*n)
|
|
__rmul__ = __mul__
|
|
|
|
def __getitem__(self, index):
|
|
return self.__class__(self.data[index], self.style[index])
|
|
|
|
def join(self, parts):
|
|
parts = [TermStr(part) if isinstance(part, str) else part
|
|
for part in parts]
|
|
joined_style = join_lists(self.style + part.style for part in parts)
|
|
return self.__class__(self.data.join(part.data for part in parts),
|
|
tuple(joined_style[len(self.style):]))
|
|
|
|
def _split_style(self, parts, sep_length):
|
|
result = []
|
|
cursor = 0
|
|
for part in parts:
|
|
style_part = self.style[cursor:cursor+len(part)]
|
|
result.append(self.__class__(part, style_part))
|
|
cursor += (len(part) + sep_length)
|
|
return result
|
|
|
|
def split(self, sep=None, maxsplit=-1):
|
|
return self._split_style(self.data.split(sep, maxsplit), len(sep))
|
|
|
|
def splitlines(self, keepends=0):
|
|
# FIX. Fails when a line seperator isn't one character in length.. \r\n
|
|
sep_length = 0 if keepends else len("\n")
|
|
return self._split_style(self.data.splitlines(keepends), sep_length)
|
|
|
|
def capitalize(self):
|
|
return self.__class__(self.data.capitalize(), self.style)
|
|
|
|
def lower(self):
|
|
return self.__class__(self.data.lower(), self.style)
|
|
|
|
def swapcase(self):
|
|
return self.__class__(self.data.swapcase(), self.style)
|
|
|
|
def title(self):
|
|
return self.__class__(self.data.title(), self.style)
|
|
|
|
def upper(self):
|
|
return self.__class__(self.data.upper(), self.style)
|
|
|
|
def ljust(self, width, fillchar=" "):
|
|
return self + self.__class__(fillchar * (width - len(self.data)))
|
|
|
|
def rjust(self, width, fillchar=" "):
|
|
return self.__class__(fillchar * (width - len(self.data))) + self
|
|
|
|
def center(self, width, fillchar=" "):
|
|
left_width = (width - len(self.data)) // 2
|
|
if left_width < 1:
|
|
return self
|
|
return (self.__class__(fillchar * left_width) + self +
|
|
self.__class__(fillchar *
|
|
(width - left_width - len(self.data))))
|
|
|
|
# Below are extra methods useful for termstrs.
|
|
|
|
def transform_style(self, transform_func):
|
|
new_style = tuple(join_lists([transform_func(style)] * len(str_)
|
|
for style, str_, position
|
|
in self._partition_style()))
|
|
return self.__class__(self.data, new_style)
|
|
|
|
def bold(self):
|
|
def make_bold(style):
|
|
return CharStyle(style.fg_color, style.bg_color, is_bold=True,
|
|
is_underlined=style.is_underlined)
|
|
return self.transform_style(make_bold)
|
|
|
|
def underline(self):
|
|
def make_underlined(style):
|
|
return CharStyle(style.fg_color, style.bg_color,
|
|
is_bold=style.is_bold, is_underlined=True)
|
|
return self.transform_style(make_underlined)
|
|
|
|
def fg_color(self, fg_color):
|
|
def set_fgcolor(style):
|
|
return CharStyle(fg_color, style.bg_color, is_bold=style.is_bold,
|
|
is_underlined=style.is_underlined)
|
|
return self.transform_style(set_fgcolor)
|
|
|
|
def bg_color(self, bg_color):
|
|
def set_bgcolor(style):
|
|
return CharStyle(style.fg_color, bg_color, is_bold=style.is_bold,
|
|
is_underlined=style.is_underlined)
|
|
return self.transform_style(set_bgcolor)
|