Toot-Mastodon-CLI-TUI-clien.../toot/output.py

331 lines
9.3 KiB
Python
Raw Normal View History

2023-12-05 11:39:22 +01:00
import click
import re
2022-11-22 21:27:04 +01:00
import textwrap
2017-04-19 14:47:30 +02:00
from toot.entities import Account, Instance, Notification, Poll, Status
2023-12-05 11:39:22 +01:00
from toot.utils import get_text, html_to_paragraphs
2019-02-14 16:53:58 +01:00
from toot.wcstring import wc_wrap
2023-12-05 11:39:22 +01:00
from typing import Any, Generator, Iterable, List
from wcwidth import wcswidth
2017-12-29 14:26:40 +01:00
2023-12-05 11:39:22 +01:00
def print_instance(instance: Instance, width: int = 80):
click.echo(instance_to_text(instance, width))
2017-04-19 14:47:30 +02:00
2022-12-02 07:40:47 +01:00
2023-12-05 11:39:22 +01:00
def instance_to_text(instance: Instance, width: int) -> str:
return "\n".join(instance_lines(instance, width))
2022-12-02 07:40:47 +01:00
2023-12-05 11:39:22 +01:00
def instance_lines(instance: Instance, width: int) -> Generator[str, None, None]:
yield f"{green(instance.title)}"
yield f"{blue(instance.uri)}"
yield f"running Mastodon {instance.version}"
yield ""
2022-11-22 21:27:04 +01:00
if instance.description:
for paragraph in re.split(r"[\r\n]+", instance.description.strip()):
2022-11-22 21:27:04 +01:00
paragraph = get_text(paragraph)
2023-12-05 11:39:22 +01:00
yield textwrap.fill(paragraph, width=width)
yield ""
2022-11-22 21:27:04 +01:00
if instance.rules:
2023-12-05 11:39:22 +01:00
yield "Rules:"
for ordinal, rule in enumerate(instance.rules):
2022-11-22 21:27:04 +01:00
ordinal = f"{ordinal + 1}."
2023-12-05 11:39:22 +01:00
lines = textwrap.wrap(rule.text, width - len(ordinal))
2022-11-22 21:27:04 +01:00
first = True
for line in lines:
if first:
2023-12-05 11:39:22 +01:00
yield f"{ordinal} {line}"
2022-11-22 21:27:04 +01:00
first = False
else:
2023-12-05 11:39:22 +01:00
yield f"{' ' * len(ordinal)} {line}"
yield ""
contact = instance.contact_account
if contact:
2023-12-05 11:39:22 +01:00
yield f"Contact: {contact.display_name} @{contact.acct}"
2017-12-29 14:42:51 +01:00
2023-12-05 11:39:22 +01:00
def print_account(account: Account, width: int = 80) -> None:
click.echo(account_to_text(account, width))
2017-12-29 14:42:51 +01:00
2023-12-05 11:39:22 +01:00
def account_to_text(account: Account, width: int) -> str:
return "\n".join(account_lines(account, width))
2017-12-29 14:42:51 +01:00
2022-12-16 12:56:51 +01:00
2023-12-05 11:39:22 +01:00
def account_lines(account: Account, width: int) -> Generator[str, None, None]:
acct = f"@{account.acct}"
since = account.created_at.strftime("%Y-%m-%d")
2022-12-16 12:56:51 +01:00
2023-12-05 11:39:22 +01:00
yield f"{green(acct)} {account.display_name}"
2017-12-29 14:42:51 +01:00
2023-12-05 11:39:22 +01:00
if account.note:
yield ""
yield from html_lines(account.note, width)
2017-12-29 14:42:51 +01:00
2023-12-05 11:39:22 +01:00
yield ""
yield f"ID: {green(account.id)}"
yield f"Since: {green(since)}"
yield ""
yield f"Followers: {yellow(account.followers_count)}"
yield f"Following: {yellow(account.following_count)}"
yield f"Statuses: {yellow(account.statuses_count)}"
2019-02-14 16:53:58 +01:00
2023-12-05 11:39:22 +01:00
if account.fields:
for field in account.fields:
name = field.name.title()
yield f'\n{yellow(name)}:'
yield from html_lines(field.value, width)
if field.verified_at:
yield green("✓ Verified")
2019-02-14 16:53:58 +01:00
2023-12-05 11:39:22 +01:00
yield ""
yield account.url
2019-02-14 16:53:58 +01:00
2022-11-22 21:27:21 +01:00
def print_acct_list(accounts):
for account in accounts:
2023-12-05 11:39:22 +01:00
acct = green(f"@{account['acct']}")
click.echo(f"* {acct} {account['display_name']}")
def print_tag_list(tags):
2022-12-31 09:14:28 +01:00
if tags:
for tag in tags:
2023-12-05 11:39:22 +01:00
click.echo(f"* {format_tag_name(tag)}\t{tag['url']}")
2022-12-31 09:14:28 +01:00
else:
2023-12-05 11:39:22 +01:00
click.echo("You're not following any hashtags.")
def print_lists(lists):
headers = ["ID", "Title", "Replies"]
data = [[lst["id"], lst["title"], lst["replies_policy"]] for lst in lists]
print_table(headers, data)
def print_table(headers: List[str], data: List[List[str]]):
widths = [[len(cell) for cell in row] for row in data + [headers]]
widths = [max(width) for width in zip(*widths)]
2023-12-05 11:39:22 +01:00
def print_row(row):
for idx, cell in enumerate(row):
width = widths[idx]
2023-12-05 11:39:22 +01:00
click.echo(cell.ljust(width), nl=False)
click.echo(" ", nl=False)
click.echo()
underlines = ["-" * width for width in widths]
2023-12-05 11:39:22 +01:00
print_row(headers)
print_row(underlines)
for row in data:
print_row(row)
2023-03-15 00:14:58 +01:00
def print_list_accounts(accounts):
2023-03-15 00:14:58 +01:00
if accounts:
2023-12-05 11:39:22 +01:00
click.echo("Accounts in list:\n")
2023-03-15 00:14:58 +01:00
print_acct_list(accounts)
else:
2023-12-05 11:39:22 +01:00
click.echo("This list has no accounts.")
2023-03-15 00:14:58 +01:00
2017-12-29 14:42:51 +01:00
def print_search_results(results):
2023-12-05 11:39:22 +01:00
accounts = results["accounts"]
hashtags = results["hashtags"]
2017-12-29 14:42:51 +01:00
if accounts:
2023-12-05 11:39:22 +01:00
click.echo("\nAccounts:")
print_acct_list(accounts)
2017-12-29 14:42:51 +01:00
if hashtags:
2023-12-05 11:39:22 +01:00
click.echo("\nHashtags:")
click.echo(", ".join([format_tag_name(tag) for tag in hashtags]))
2017-12-29 14:42:51 +01:00
if not accounts and not hashtags:
2023-12-05 11:39:22 +01:00
click.echo("Nothing found")
2023-12-05 11:39:22 +01:00
def print_status(status: Status, width: int = 80) -> None:
click.echo(status_to_text(status, width))
def status_to_text(status: Status, width: int) -> str:
return "\n".join(status_lines(status, width))
def status_lines(status: Status, width: int = 80) -> Generator[str, None, None]:
status_id = status.id
in_reply_to_id = status.in_reply_to_id
reblogged_by = status.account if status.reblog else None
status = status.original
time = status.created_at.strftime('%Y-%m-%d %H:%M %Z')
username = "@" + status.account.acct
2022-12-12 12:46:24 +01:00
spacing = width - wcswidth(username) - wcswidth(time) - 2
display_name = status.account.display_name
2023-12-05 11:39:22 +01:00
2019-02-14 16:53:58 +01:00
if display_name:
2023-12-05 11:39:22 +01:00
author = f"{green(display_name)} {blue(username)}"
2019-02-14 16:53:58 +01:00
spacing -= wcswidth(display_name) + 1
2023-12-05 11:39:22 +01:00
else:
author = blue(username)
2023-12-05 11:39:22 +01:00
spaces = " " * spacing
yield f"{author} {spaces} {yellow(time)}"
2023-12-05 11:39:22 +01:00
yield ""
yield from html_lines(status.content, width)
2019-02-14 16:53:58 +01:00
if status.media_attachments:
2023-12-05 11:39:22 +01:00
yield ""
yield "Media:"
for attachment in status.media_attachments:
url = attachment.url
2019-02-14 16:53:58 +01:00
for line in wc_wrap(url, width):
2023-12-05 11:39:22 +01:00
yield line
2019-02-14 16:53:58 +01:00
if status.poll:
2023-12-05 11:39:22 +01:00
yield from poll_lines(status.poll)
2023-12-05 11:39:22 +01:00
reblogged_by_acct = f"@{reblogged_by.acct}" if reblogged_by else None
yield ""
2023-12-05 11:39:22 +01:00
reply = f"↲ In reply to {yellow(in_reply_to_id)} " if in_reply_to_id else ""
boost = f"{blue(reblogged_by_acct)} boosted " if reblogged_by else ""
yield f"ID {yellow(status_id)} {reply} {boost}"
2023-12-05 11:39:22 +01:00
def html_lines(html: str, width: int) -> Generator[str, None, None]:
2022-12-16 12:56:51 +01:00
first = True
2023-12-05 11:39:22 +01:00
for paragraph in html_to_paragraphs(html):
2022-12-16 12:56:51 +01:00
if not first:
2023-12-05 11:39:22 +01:00
yield ""
2022-12-16 12:56:51 +01:00
for line in paragraph:
for subline in wc_wrap(line, width):
2023-12-05 11:39:22 +01:00
yield subline
2022-12-16 12:56:51 +01:00
first = False
2023-12-05 11:39:22 +01:00
def poll_lines(poll: Poll) -> Generator[str, None, None]:
for idx, option in enumerate(poll.options):
perc = (round(100 * option.votes_count / poll.votes_count)
if poll.votes_count and option.votes_count is not None else 0)
if poll.voted and poll.own_votes and idx in poll.own_votes:
2023-12-05 11:39:22 +01:00
voted_for = yellow("")
2022-11-29 09:20:00 +01:00
else:
voted_for = ""
2023-12-05 11:39:22 +01:00
yield f"{option.title} - {perc}% {voted_for}"
poll_footer = f'Poll · {poll.votes_count} votes'
if poll.expired:
2022-11-29 09:20:00 +01:00
poll_footer += " · Closed"
if poll.expires_at:
expires_at = poll.expires_at.strftime("%Y-%m-%d %H:%M")
2022-11-29 09:20:00 +01:00
poll_footer += f" · Closes on {expires_at}"
2023-12-05 11:39:22 +01:00
yield ""
yield poll_footer
2019-02-14 16:53:58 +01:00
2023-12-05 11:39:22 +01:00
def print_timeline(items: Iterable[Status], width=80):
click.echo("" * width)
for item in items:
2019-02-14 16:53:58 +01:00
print_status(item, width)
2023-12-05 11:39:22 +01:00
click.echo("" * width)
notification_msgs = {
"follow": "{account} now follows you",
"mention": "{account} mentioned you in",
"reblog": "{account} reblogged your status",
"favourite": "{account} favourited your status",
}
2023-12-05 11:39:22 +01:00
def print_notification(notification: Notification, width=80):
print_notification_header(notification)
if notification.status:
2023-12-05 11:39:22 +01:00
click.echo("-" * width)
print_status(notification.status, width)
2023-12-05 11:39:22 +01:00
def print_notifications(notifications: List[Notification], width=80):
for notification in notifications:
2023-12-05 11:39:22 +01:00
click.echo("" * width)
print_notification(notification)
2023-12-05 11:39:22 +01:00
click.echo("" * width)
def print_notification_header(notification: Notification):
account_name = format_account_name(notification.account)
if (notification.type == "follow"):
click.echo(f"{account_name} now follows you")
elif (notification.type == "mention"):
click.echo(f"{account_name} mentioned you")
elif (notification.type == "reblog"):
click.echo(f"{account_name} reblogged your status")
elif (notification.type == "favourite"):
click.echo(f"{account_name} favourited your status")
elif (notification.type == "update"):
click.echo(f"{account_name} edited a post")
else:
click.secho(f"Unknown notification type: '{notification.type}'", err=True, fg="yellow")
click.secho("Please report an issue to toot.", err=True, fg="yellow")
notification_msgs = {
"follow": "{account} now follows you",
"mention": "{account} mentioned you in",
"reblog": "{account} reblogged your status",
"favourite": "{account} favourited your status",
}
def format_tag_name(tag):
return green(f"#{tag['name']}")
def format_account_name(account: Account) -> str:
acct = blue(f"@{account.acct}")
if account.display_name:
return f"{green(account.display_name)} {acct}"
else:
return acct
# Shorthand functions for coloring output
def blue(text: Any) -> str:
return click.style(text, fg="blue")
def bold(text: Any) -> str:
return click.style(text, bold=True)
def cyan(text: Any) -> str:
return click.style(text, fg="cyan")
def dim(text: Any) -> str:
return click.style(text, dim=True)
def green(text: Any) -> str:
return click.style(text, fg="green")
def yellow(text: Any) -> str:
return click.style(text, fg="yellow")