eris/vigil/termstr.py
Andrew Hamilton d40e0a8b59 Coding style.
- Started to add docstrings.
2018-07-05 14:22:40 +10:00

314 lines
11 KiB
Python

# Copyright (C) 2015-2018 Andrew Hamilton. All rights reserved.
# Licensed under the Artistic License 2.0.
"""Termstr strings contain characters that have color and style."""
import collections
import functools
import html
import itertools
import os
import weakref
import pygments.formatters.terminal256
import vigil.ColorMap
import vigil.terminal as terminal
xterm_colormap = vigil.ColorMap.XTermColorMap()
@functools.lru_cache()
def xterm_color_to_rgb(color_index):
"""Return the rgb color of an xterm color."""
return vigil.ColorMap._rgb(xterm_colormap.colors[color_index])
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:
"""A list of common colors."""
# https://en.wikipedia.org/wiki/Natural_Color_System
black = (0, 0, 0)
white = (255, 255, 255)
red = (196, 2, 51)
green = (0, 159, 107)
blue = (0, 135, 189)
lime = (0, 255, 0)
yellow = (255, 211, 0)
grey_50 = (50, 50, 50)
grey_100 = (100, 100, 100)
grey_150 = (150, 150, 150)
grey_200 = (200, 200, 200)
light_blue = (173, 216, 230)
purple = (200, 0, 200)
brown = (150, 75, 0)
orange = (255, 153, 0)
class CharStyle:
"""Characters have a foreground and background color, and styles.
The styles are bold, italic or underlined.
"""
_POOL = weakref.WeakValueDictionary()
_TERMINAL256_FORMATTER = \
pygments.formatters.terminal256.Terminal256Formatter()
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 __getstate__(self):
state = self.__dict__.copy()
if "_cache" in state:
del state["_cache"]
return state
def __setstate__(self, state):
self.__dict__ = state
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 (f"<CharStyle: fg:{self.fg_color} bg:{self.bg_color}"
f" attr:{','.join(attributes)}>")
def termcode_of_color(self, color, is_foreground):
if isinstance(color, int):
return terminal.color(color, is_foreground)
else: # true color
if os.environ["TERM"] == "xterm":
closest_color = self._TERMINAL256_FORMATTER._closest_color(
*color)
return terminal.color(closest_color, is_foreground)
else:
return terminal.rgb_color(color, is_foreground)
@_cache_first_result
def code_for_term(self):
fg_termcode = self.termcode_of_color(self.fg_color, True)
bg_termcode = self.termcode_of_color(self.bg_color, False)
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_termcode, bg_termcode, bold_code,
italic_code, underline_code])
def as_html(self):
bold_code = "font-weight:bold; " if self.is_bold else ""
italic_code = "font-style:italic; " if self.is_italic else ""
underline_code = ("text-decoration:underline; "
if self.is_underlined else "")
fg_color = (self.fg_color if type(self.fg_color) == tuple
else xterm_color_to_rgb(self.fg_color))
bg_color = (self.bg_color if type(self.bg_color) == tuple
else xterm_color_to_rgb(self.bg_color))
return (f"<style>.S{id(self)} {{font-size:80%%; color:rgb{fg_color!r};"
f" background-color:rgb{bg_color!r}; "
f"{bold_code}{italic_code}{underline_code}}}</style>")
def _join_lists(lists):
return list(itertools.chain.from_iterable(lists))
class TermStr(collections.UserString):
def __init__(self, data, style=CharStyle()):
try:
self.data, self.style = data.data, data.style
except AttributeError:
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 f"<TermStr: {self.data!r}>"
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):
lines_with_ends = self.data.splitlines(keepends=True)
lines_without_ends = self.data.splitlines()
result_parts = lines_with_ends if keepends else lines_without_ends
result = []
cursor = 0
for line, line_with_end in zip(result_parts, lines_with_ends):
style_part = self.style[cursor:cursor+len(line)]
result.append(self.__class__(line, style_part))
cursor += len(line_with_end)
return result
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)
def as_html(self):
result = []
styles = set()
for style, str_, position in self._partition_style():
styles.add(style)
encoded = str(html.escape(str_).encode(
"ascii", "xmlcharrefreplace"))[2:-1]
encoded = encoded.replace("\\\\", "\\")
result.append(f'<span class="S{id(style):d}">{encoded}</span>')
return "".join(result), styles