From 00533c8bcfb63e5c6f0a1023af50838df0716405 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 2 Dec 2023 12:30:50 +0000 Subject: [PATCH] Start colourising the rendering --- mastodonochrome | 2 +- text.py | 84 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 70 insertions(+), 16 deletions(-) diff --git a/mastodonochrome b/mastodonochrome index 8621062..b5fa0e4 100755 --- a/mastodonochrome +++ b/mastodonochrome @@ -77,7 +77,7 @@ class MainUI(Client): p = Post(item) for thing in p.text(): for line in thing.render(80): - print(line) + print(line.ecma48()) def main(): parser = argparse.ArgumentParser( diff --git a/text.py b/text.py index faa3eb3..4821b57 100644 --- a/text.py +++ b/text.py @@ -1,12 +1,65 @@ # Represent colourised terminal text in a width-independent form. import html.parser +import io +import itertools import time import wcwidth +class ColouredString: + def __init__(self, string, colour=' '): + if isinstance(string, ColouredString): + self.s, self.c = string.s, string.c + else: + if len(colour) != len(string): + assert len(colour) == 1, "Colour ids are single characters" + colour = colour * len(string) + self.s, self.c = string, colour + + def __add__(self, rhs): + rhs = type(self)(rhs) + return type(self)(self.s + rhs.s, self.c + rhs.c) + + def __mul__(self, rhs): + return type(self)(self.s * rhs, self.c * rhs) + + def __len__(self): + return len(self.s) + + @property + def width(self): + return wcwidth.wcswidth(self.s) + + def __str__(self): + return self.s + + def __repr__(self): + return f"ColouredString({self.s!r}, {self.c!r})" + + def __eq__(self, rhs): + rhs = type(self)(rhs) + return (self.s, self.c) == (rhs.s, rhs.c) + def __ne__(self, rhs): + return not (self == rhs) + + def ecma48(self): + buf = io.StringIO() + colour = ' ' + for sc, cc in itertools.chain(zip(self.s, self.c), [('',' ')]): + if cc != colour: + buf.write({ + ' ': '\033[m', + 'S': '\033[0;1;7;44;37m', + 'D': '\033[0;7;44;37m', + 'F': '\033[0;1;32m', + }[cc]) + colour = cc + buf.write(sc) + return buf.getvalue() + class BlankLine: def render(self, width): - yield "" + yield ColouredString("") class SeparatorLine: def __init__(self, timestamp): @@ -15,9 +68,10 @@ class SeparatorLine: def render(self, width): date = time.strftime("%a %b %e %H:%M:%S %Y", time.localtime(self.timestamp)) - # FIXME: colours - suffix = "[" + date + "]--" - yield "-" * (width - 1 - len(suffix)) + suffix + suffix = (ColouredString("[", 'S') + + ColouredString(date, 'D') + + ColouredString("]--", 'S')) + yield ColouredString("-", 'S') * (width - 1 - suffix.width) + suffix class FromLine: def __init__(self, account, nameline): @@ -26,31 +80,31 @@ class FromLine: def render(self, width): # FIXME: truncate - - yield f"From: {self.nameline} ({self.account})" + yield (ColouredString("From: ") + + ColouredString(f"{self.nameline} ({self.account})", 'F')) class Paragraph: def __init__(self): self.words = [] - self.unfinished_word = '' + self.unfinished_word = ColouredString('') def render(self, width): # For the moment, greedy algorithm. We can worry about cleverness later - line, space = '', '' + line, space = ColouredString(''), ColouredString('') for word in self.words: - if line != "" and wcwidth.wcswidth(line + space + word) >= width: + if line != "" and (line + space + word).width >= width: yield line - line, space = '', '' + line, space = ColouredString(''), ColouredString('') line += space + word - space = ' ' + space = ColouredString(' ') - if wcwidth.wcswidth(line) >= width: + if line.width >= width: # FIXME: wrap explicitly? yield line - line, space = '', '' + line, space = ColouredString(''), ColouredString('') - if line != "": + if len(line) != 0: yield line def empty(self): @@ -59,7 +113,7 @@ class Paragraph: def end_word(self): if len(self.unfinished_word) > 0: self.words.append(self.unfinished_word) - self.unfinished_word = '' + self.unfinished_word = ColouredString('') def add(self, text): for c in text: -- 2.30.2