From c91beda9b4bc18550573f8257ead72e5dc932140 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Sun, 25 Jul 2021 22:59:23 +1000 Subject: [PATCH] Display wide characters correctly. - Made termstr's length match the on-screen width. - Achieved by adding a zero width character after each wide character. - This fixes ljust and rjust, so that there is correct padding, otherwise Portal appearances aren't the correct width. - When getting a sub-string containing half a wide character, the half character is a space. - Some wide characters are still broken. e.g. country flags --- eris/termstr.py | 34 ++++++++++++++++++++++++++++------ install-dependencies | 2 +- tests/termstr_test.py | 5 +++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/eris/termstr.py b/eris/termstr.py index 4f72998..8564eb5 100644 --- a/eris/termstr.py +++ b/eris/termstr.py @@ -8,6 +8,7 @@ import os import weakref import pygments.formatters.terminal256 +import cwcwidth import eris.ColorMap import eris.terminal as terminal @@ -130,15 +131,26 @@ def _join_lists(lists): return list(itertools.chain.from_iterable(lists)) +_ZERO_WIDTH_SPACE = chr(0x00ad) + + +def _pad_wide_chars(str_): + return "".join(f"{char}{_ZERO_WIDTH_SPACE}" + if cwcwidth.wcwidth(char) == 2 else char for char in str_) + + class TermStr(collections.UserString): def __init__(self, data, style=CharStyle()): - try: - self.data, self.style = data.data, data.style - except AttributeError: + if isinstance(style, tuple): self.data = data - self.style = (style if isinstance(style, tuple) - else (style,) * len(data)) + self.style = style + else: + try: + self.data, self.style = data.data, data.style + except AttributeError: + self.data = _pad_wide_chars(data) + self.style = (style,) * len(self.data) @classmethod def from_term(cls, data): @@ -249,7 +261,17 @@ class TermStr(collections.UserString): __rmul__ = __mul__ def __getitem__(self, index): - return self.__class__(self.data[index], self.style[index]) + data = self.data[index] + if len(data) == 0: + result = "" + else: + first_char = " " if data[0] == _ZERO_WIDTH_SPACE else data[0] + if len(data) == 1: + result = first_char + else: + end_char = " " if cwcwidth.wcwidth(data[-1]) == 2 else data[-1] + result = first_char + data[1:-1] + end_char + return self.__class__(result, self.style[index]) def join(self, parts): parts = [TermStr(part) if isinstance(part, str) else part diff --git a/install-dependencies b/install-dependencies index b319532..c879c37 100755 --- a/install-dependencies +++ b/install-dependencies @@ -10,7 +10,7 @@ if [ $DIST_ID != "ubuntu" ]; then exit 1 fi echo "Installing the dependencies of the eris script…" -sudo apt --yes install python3-pip python3.9 util-linux +sudo apt --yes install python3-pip python3.9 util-linux python3-cwcwidth python3.9 -m pip install pyinotify pygments docopt pillow toml echo echo "Installing all the tools eris may need…" diff --git a/tests/termstr_test.py b/tests/termstr_test.py index 394d68c..9c2880b 100755 --- a/tests/termstr_test.py +++ b/tests/termstr_test.py @@ -101,6 +101,11 @@ class TermStrTests(unittest.TestCase): self.assertEqual(foo_bold.ljust(5), foo_bold + TermStr(" ")) self.assertEqual(foo_bold.rjust(0), foo_bold) self.assertEqual(foo_bold.rjust(5), TermStr(" ") + foo_bold) + baz = TermStr("b👋z") + self.assertEqual(len(baz), 4) + self.assertEqual(baz[3:], TermStr("z")) + self.assertEqual(baz[:2], TermStr("b ")) + self.assertEqual(baz[2:], TermStr(" z")) def test_from_term(self): def test_round_trip(term_str):