diff --git a/tests/test_console.py b/tests/test_console.py index 7d46e38..57e2948 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -128,7 +128,8 @@ def test_timeline(mock_get, monkeypatch, capsys): 'created_at': '2017-04-12T15:53:18.174Z', 'content': "

The computer can't tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.

", 'reblog': None, - 'in_reply_to_id': None + 'in_reply_to_id': None, + 'media_attachments': [], }]) console.run_command(app, user, 'timeline', ['--once']) @@ -136,16 +137,18 @@ def test_timeline(mock_get, monkeypatch, capsys): mock_get.assert_called_once_with(app, user, '/api/v1/timelines/home?limit=10', None) out, err = capsys.readouterr() + lines = out.split("\n") + + assert "Frank Zappa 🎸" in lines[1] + assert "@fz" in lines[1] + assert "2017-04-12 15:53" in lines[1] + + assert ( + "The computer can't tell you the emotional story. It can give you the " + "exact mathematical design, but\nwhat's missing is the eyebrows." in out) + + assert "111111111111111111" in lines[-3] - expected = ( - "───────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────\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" - "───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────\n" - ) - assert out == expected assert err == "" @@ -153,14 +156,21 @@ def test_timeline(mock_get, monkeypatch, capsys): def test_timeline_with_re(mock_get, monkeypatch, capsys): mock_get.return_value = MockResponse([{ 'id': '111111111111111111', + 'created_at': '2017-04-12T15:53:18.174Z', 'account': { 'display_name': 'Frank Zappa', 'username': 'fz' }, - 'created_at': '2017-04-12T15:53:18.174Z', - 'content': "

The computer can't tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.

", - 'reblog': None, - 'in_reply_to_id': '111111111111111110' + 'reblog': { + 'account': { + 'display_name': 'Johnny Cash', + 'username': 'jc' + }, + 'content': "

The computer can't tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.

", + 'media_attachments': [], + }, + 'in_reply_to_id': '111111111111111110', + 'media_attachments': [], }]) console.run_command(app, user, 'timeline', ['--once']) @@ -168,12 +178,21 @@ def test_timeline_with_re(mock_get, monkeypatch, capsys): mock_get.assert_called_once_with(app, user, '/api/v1/timelines/home?limit=10', None) out, err = capsys.readouterr() - assert "The computer can't tell you the emotional story." in out - assert "but what's missing is the eyebrows." in out - assert "Frank Zappa" in out - assert "@fz" in out - assert "id: 111111111111111111" in out - assert "[RE]" in out + lines = out.split("\n") + + assert "Frank Zappa" in lines[1] + assert "@fz" in lines[1] + assert "2017-04-12 15:53" in lines[1] + + assert ( + "The computer can't tell you the emotional story. It can give you the " + "exact mathematical design, but\nwhat's missing is the eyebrows." in out) + + assert "111111111111111111" in lines[-3] + assert "↻ Reblogged \x1b[34m@jc\x1b[0m" in lines[-3] + + assert err == "" + @mock.patch('toot.http.get') def test_thread(mock_get, monkeypatch, capsys): @@ -187,7 +206,8 @@ def test_thread(mock_get, monkeypatch, capsys): 'created_at': '2017-04-12T15:53:18.174Z', 'content': "my response in the middle", 'reblog': None, - 'in_reply_to_id': '111111111111111110' + 'in_reply_to_id': '111111111111111110', + 'media_attachments': [], }), MockResponse({ 'ancestors': [{ @@ -198,6 +218,7 @@ def test_thread(mock_get, monkeypatch, capsys): }, 'created_at': '2017-04-12T15:53:18.174Z', 'content': "original content", + 'media_attachments': [], 'reblog': None, 'in_reply_to_id': None}], 'descendants': [{ @@ -208,6 +229,7 @@ def test_thread(mock_get, monkeypatch, capsys): }, 'created_at': '2017-04-12T15:53:18.174Z', 'content': "response message", + 'media_attachments': [], 'reblog': None, 'in_reply_to_id': '111111111111111111'}], }), @@ -223,6 +245,8 @@ def test_thread(mock_get, monkeypatch, capsys): out, err = capsys.readouterr() + assert not err + # Display order assert out.index('original content') < out.index('my response in the middle') assert out.index('my response in the middle') < out.index('response message') @@ -232,8 +256,8 @@ def test_thread(mock_get, monkeypatch, capsys): assert "response message" in out assert "Frank Zappa" in out assert "@fz" in out - assert "id: 111111111111111111" in out - assert "[RE]" in out + assert "111111111111111111" in out + assert "In reply to" in out @mock.patch('toot.http.post') def test_upload(mock_post, capsys): diff --git a/toot/output.py b/toot/output.py index 1729edf..171b36d 100644 --- a/toot/output.py +++ b/toot/output.py @@ -3,14 +3,12 @@ import sys import re -from bs4 import BeautifulSoup from datetime import datetime -from itertools import chain -from itertools import zip_longest -from textwrap import wrap, TextWrapper +from textwrap import wrap +from wcwidth import wcswidth -from toot.utils import format_content, get_text -from toot.wcstring import pad +from toot.utils import format_content, get_text, parse_html +from toot.wcstring import wc_wrap START_CODES = { @@ -101,6 +99,13 @@ def print_account(account): print_out(account['url']) +HASHTAG_PATTERN = re.compile(r'(?\\1', line) + + def print_search_results(results): accounts = results['accounts'] hashtags = results['hashtags'] @@ -121,54 +126,52 @@ def print_search_results(results): print_out("Nothing found") -def print_timeline(items): - def _print_item(item): - def wrap_text(text, width): - wrapper = TextWrapper(width=width, break_long_words=False, break_on_hyphens=False) - return chain(*[wrapper.wrap(l) for l in text.split("\n")]) +def print_status(status, width): + reblog = status['reblog'] + content = reblog['content'] if reblog else status['content'] + media_attachments = reblog['media_attachments'] if reblog else status['media_attachments'] + in_reply_to = status['in_reply_to_id'] - def timeline_rows(item): - display_name = item['account']['display_name'] - username = "@" + item['account']['username'] - time = item['time'].strftime('%Y-%m-%d %H:%M%Z') + time = status['created_at'] + time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S.%fZ") + time = time.strftime('%Y-%m-%d %H:%M%z') - left_column = [display_name] - if display_name != username: - left_column.append(username) - left_column.append(time) - if item['reblogged']: - left_column.append("Reblogged @{}".format(item['reblogged'])) + username = "@" + status['account']['username'] + spacing = width - wcswidth(username) - wcswidth(time) - if item['reply_to_toot'] is not None: - left_column.append('[RE]') + display_name = status['account']['display_name'] + if display_name: + spacing -= wcswidth(display_name) + 1 - left_column.append("id: {}".format(item['id'])) + print_out("{}{}{}{}".format( + "{} ".format(display_name) if display_name else "", + "{}".format(username), + " " * spacing, + "{}".format(time), + )) - right_column = wrap_text(item['text'], 80) + for paragraph in parse_html(content): + print_out("") + for line in paragraph: + for subline in wc_wrap(line, width): + print_out(highlight_hashtags(subline)) - return zip_longest(left_column, right_column, fillvalue="") + if media_attachments: + print_out("\nMedia:") + for attachment in media_attachments: + url = attachment['text_url'] or attachment['url'] + for line in wc_wrap(url, width): + print_out(line) - for left, right in timeline_rows(item): - print_out("{} │ {}".format(pad(left, 30), right)) + print_out("\n{}{}{}".format( + "ID {} ".format(status['id']), + "↲ In reply to {} ".format(in_reply_to) if in_reply_to else "", + "↻ Reblogged @{} ".format(reblog['account']['username']) if reblog else "", + )) - def _parse_item(item): - content = item['reblog']['content'] if item['reblog'] else item['content'] - reblogged = item['reblog']['account']['username'] if item['reblog'] else None - soup = BeautifulSoup(content.replace(''', "'"), "html.parser") - text = soup.get_text() - time = datetime.strptime(item['created_at'], "%Y-%m-%dT%H:%M:%S.%fZ") - - return { - "id": item['id'], - "account": item['account'], - "text": text, - "time": time, - "reblogged": reblogged, - "reply_to_toot": item['in_reply_to_id'] - } - - print_out("─" * 31 + "┬" + "─" * 88) +def print_timeline(items, width=100): + print_out("─" * width) for item in items: - _print_item(_parse_item(item)) - print_out("─" * 31 + "┼" + "─" * 88) + print_status(item, width) + print_out("─" * width)