0bf4b2a21a
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.)
172 lines
5 KiB
Python
172 lines
5 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
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 toot.utils import format_content, get_text, pad
|
|
|
|
START_CODES = {
|
|
'red': '\033[31m',
|
|
'green': '\033[32m',
|
|
'yellow': '\033[33m',
|
|
'blue': '\033[34m',
|
|
'magenta': '\033[35m',
|
|
'cyan': '\033[36m',
|
|
}
|
|
|
|
END_CODE = '\033[0m'
|
|
|
|
START_PATTERN = "<(" + "|".join(START_CODES.keys()) + ")>"
|
|
|
|
END_PATTERN = "</(" + "|".join(START_CODES.keys()) + ")>"
|
|
|
|
|
|
def start_code(match):
|
|
name = match.group(1)
|
|
return START_CODES[name]
|
|
|
|
|
|
def colorize(text):
|
|
text = re.sub(START_PATTERN, start_code, text)
|
|
text = re.sub(END_PATTERN, END_CODE, text)
|
|
|
|
return text
|
|
|
|
|
|
def strip_tags(text):
|
|
text = re.sub(START_PATTERN, '', text)
|
|
text = re.sub(END_PATTERN, '', text)
|
|
|
|
return text
|
|
|
|
|
|
USE_ANSI_COLOR = "--no-color" not in sys.argv
|
|
QUIET = "--quiet" in sys.argv
|
|
|
|
|
|
def print_out(*args, **kwargs):
|
|
if not QUIET:
|
|
args = [colorize(a) if USE_ANSI_COLOR else strip_tags(a) for a in args]
|
|
print(*args, **kwargs)
|
|
|
|
|
|
def print_err(*args, **kwargs):
|
|
args = ["<red>{}</red>".format(a) for a in args]
|
|
args = [colorize(a) if USE_ANSI_COLOR else strip_tags(a) for a in args]
|
|
print(*args, file=sys.stderr, **kwargs)
|
|
|
|
|
|
def print_instance(instance):
|
|
print_out("<green>{}</green>".format(instance['title']))
|
|
print_out("<blue>{}</blue>".format(instance['uri']))
|
|
print_out("running Mastodon {}".format(instance['version']))
|
|
print_out("")
|
|
|
|
description = instance['description'].strip()
|
|
if not description:
|
|
return
|
|
|
|
lines = [line.strip() for line in format_content(description) if line.strip()]
|
|
for line in lines:
|
|
for l in wrap(line.strip()):
|
|
print_out(l)
|
|
print_out()
|
|
|
|
|
|
def print_account(account):
|
|
print_out("<green>@{}</green> {}".format(account['acct'], account['display_name']))
|
|
|
|
note = get_text(account['note'])
|
|
|
|
if note:
|
|
print_out("")
|
|
print_out("\n".join(wrap(note)))
|
|
|
|
print_out("")
|
|
print_out("ID: <green>{}</green>".format(account['id']))
|
|
print_out("Since: <green>{}</green>".format(account['created_at'][:19].replace('T', ' @ ')))
|
|
print_out("")
|
|
print_out("Followers: <yellow>{}</yellow>".format(account['followers_count']))
|
|
print_out("Following: <yellow>{}</yellow>".format(account['following_count']))
|
|
print_out("Statuses: <yellow>{}</yellow>".format(account['statuses_count']))
|
|
print_out("")
|
|
print_out(account['url'])
|
|
|
|
|
|
def print_search_results(results):
|
|
accounts = results['accounts']
|
|
hashtags = results['hashtags']
|
|
|
|
if accounts:
|
|
print_out("\nAccounts:")
|
|
for account in accounts:
|
|
print_out("* <green>@{}</green> {}".format(
|
|
account['acct'],
|
|
account['display_name']
|
|
))
|
|
|
|
if hashtags:
|
|
print_out("\nHashtags:")
|
|
print_out(", ".join(["<green>#{}</green>".format(t) for t in hashtags]))
|
|
|
|
if not accounts and not hashtags:
|
|
print_out("<yellow>Nothing found</yellow>")
|
|
|
|
|
|
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 timeline_rows(item):
|
|
display_name = item['account']['display_name']
|
|
username = "@" + item['account']['username']
|
|
time = item['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']))
|
|
|
|
if item['reply_to_toot'] is not None:
|
|
left_column.append('[RE]')
|
|
|
|
left_column.append("id: {}".format(item['id']))
|
|
|
|
right_column = wrap_text(item['text'], 80)
|
|
|
|
return zip_longest(left_column, right_column, fillvalue="")
|
|
|
|
for left, right in timeline_rows(item):
|
|
print_out("{} │ {}".format(pad(left, 30), right))
|
|
|
|
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)
|
|
for item in items:
|
|
_print_item(_parse_item(item))
|
|
print_out("─" * 31 + "┼" + "─" * 88)
|