From 0bf4b2a21a566f6bc0406effc6c2cf30608f6985 Mon Sep 17 00:00:00 2001 From: Denis Laxalde Date: Wed, 2 Jan 2019 13:22:06 +0100 Subject: [PATCH] Fix left column padding in timeline with wide characters When the left column contains wide characters (which occupy more than one cell when printed to screen), padding to 30-characters with "{:30}".format() does not work well. This happens for instance when the display name contains unicode characters such as emojis. We fix this by introducing a pad() function in utils module which uses the wcwidth library (https://pypi.org/project/wcwidth/) to compute the length of the text for the column. trunc() function is also adjusted to optionally compute the length of the text to be truncated since, when called from pad(), we now pre-compute this value. We update test for timeline rendering so that the display name now includes an emoji. (Without the fix, the test would not pass as left column would be misaligned.) --- requirements.txt | 1 + setup.py | 1 + tests/test_console.py | 4 ++-- tests/test_utils.py | 14 ++++++++++++++ toot/output.py | 4 ++-- toot/utils.py | 14 ++++++++++++-- 6 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 tests/test_utils.py diff --git a/requirements.txt b/requirements.txt index b2b6258..a18e432 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests>=2.13,<3.0 beautifulsoup4>=4.5.0,<5.0 +wcwidth>=0.1.7,<2.0 diff --git a/setup.py b/setup.py index bf274f1..6d73704 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ setup( install_requires=[ "requests>=2.13,<3.0", "beautifulsoup4>=4.5.0,<5.0", + "wcwidth>=0.1.7,<2.0", ], entry_points={ 'console_scripts': [ diff --git a/tests/test_console.py b/tests/test_console.py index 41b6efe..7d46e38 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -122,7 +122,7 @@ def test_timeline(mock_get, monkeypatch, capsys): mock_get.return_value = MockResponse([{ 'id': '111111111111111111', 'account': { - 'display_name': 'Frank Zappa', + 'display_name': 'Frank Zappa 🎸', 'username': 'fz' }, 'created_at': '2017-04-12T15:53:18.174Z', @@ -139,7 +139,7 @@ def test_timeline(mock_get, monkeypatch, capsys): expected = ( "───────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────\n" - "Frank Zappa │ The computer can't tell you the emotional story. It can give you the exact\n" + "Frank Zappa 🎸 │ The computer can't tell you the emotional story. It can give you the exact\n" "@fz │ mathematical design, but what's missing is the eyebrows.\n" "2017-04-12 15:53 │ \n" "id: 111111111111111111 │ \n" diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..ea42624 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,14 @@ +from toot import utils + + +def test_pad(): + text = 'Frank Zappa 🎸' + padded = utils.pad(text, 14) + assert padded == 'Frank Zappa 🎸' + # guitar symbol will occupy two cells, so padded text should be 1 + # character shorter + assert len(padded) == 13 + # when truncated, … occupies one cell, so we get full length + padded = utils.pad(text, 13) + assert padded == 'Frank Zappa …' + assert len(padded) == 13 diff --git a/toot/output.py b/toot/output.py index 60c034d..bfdd7e2 100644 --- a/toot/output.py +++ b/toot/output.py @@ -9,7 +9,7 @@ from itertools import chain from itertools import zip_longest from textwrap import wrap, TextWrapper -from toot.utils import format_content, get_text, trunc +from toot.utils import format_content, get_text, pad START_CODES = { 'red': '\033[31m', @@ -147,7 +147,7 @@ def print_timeline(items): return zip_longest(left_column, right_column, fillvalue="") for left, right in timeline_rows(item): - print_out("{:30} │ {}".format(trunc(left, 30), right)) + print_out("{} │ {}".format(pad(left, 30), right)) def _parse_item(item): content = item['reblog']['content'] if item['reblog'] else item['content'] diff --git a/toot/utils.py b/toot/utils.py index 2508e5a..8167889 100644 --- a/toot/utils.py +++ b/toot/utils.py @@ -7,6 +7,7 @@ import unicodedata import warnings from bs4 import BeautifulSoup +from wcwidth import wcswidth from toot.exceptions import ConsoleError @@ -75,14 +76,23 @@ def assert_domain_exists(domain): raise ConsoleError("Domain {} not found".format(domain)) -def trunc(text, length): +def trunc(text, length, text_length=None): """Trims text to given length, if trimmed appends ellipsis.""" - if len(text) <= length: + if text_length is None: + text_length = len(text) + if text_length <= length: return text return text[:length - 1] + '…' +def pad(text, length, fill=' '): + text_length = wcswidth(text) + text = trunc(text, length, text_length) + assert len(text) <= length + return text + fill * (length - text_length) + + EOF_KEY = "Ctrl-Z" if os.name == 'nt' else "Ctrl-D"