Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rich text support in console output #359

Closed
wants to merge 59 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
80814eb
Support for rendering a subset of HTML tags in status content
danschwarz Mar 24, 2023
3b4f46c
comments and formatting
danschwarz Apr 1, 2023
717a0e4
Removed unneeded import
danschwarz Apr 1, 2023
479907b
Support for rendering a subset of HTML tags in status content
danschwarz Apr 4, 2023
4ceb3e5
Ignore warning W503
danschwarz Apr 4, 2023
eb8033c
add get_lists method
danschwarz Apr 4, 2023
524115e
Make button widget unicode-aware (spacing)
danschwarz Apr 4, 2023
d65cac1
Add basic support for Mastodon Lists
danschwarz Apr 4, 2023
7189c6b
"toot list" console command added
danschwarz Apr 4, 2023
94315dd
added "toot list_accounts" command
danschwarz Apr 4, 2023
85c228b
Added "toot list_delete" and "toot list_create" commands
danschwarz Apr 4, 2023
678cc19
Added toot list_add_account command
danschwarz Apr 4, 2023
1164d0c
Added toot list_remove_account command
danschwarz Apr 4, 2023
7ad9e7c
minor improvement of feedback messages
danschwarz Apr 4, 2023
0430eed
Changed parameters for list cmds
danschwarz Apr 4, 2023
45cffb6
Give a more specfic error message if we can't add an account to list
danschwarz Apr 4, 2023
037f7f9
Break up integration tests
ihabunek Apr 4, 2023
1c9935e
Add a simple table printer and apply to lists
ihabunek Apr 4, 2023
4023bf0
Add integration tests for lists
ihabunek Apr 4, 2023
fe3044f
Fix tests
ihabunek Apr 4, 2023
006ad85
Extract fetching list ID
ihabunek Apr 4, 2023
d3ea1e5
Simplify integration tests by catching ConsoleError
ihabunek Apr 4, 2023
1fb0561
Slightly tightened up list number spacing
danschwarz Apr 4, 2023
c48501e
renamed get_style_name to get_urwid_attr_name for clarity
danschwarz Apr 4, 2023
dc9fa05
Make output match existing master branch status rendering exactly
danschwarz Apr 4, 2023
9ed0a47
fixed up palette constants a bit
danschwarz Apr 4, 2023
6e1d36f
Removed unneeded import
danschwarz Apr 4, 2023
0098371
Unicode normalize 'NKFC' incoming HTML text before rendering
danschwarz Apr 4, 2023
84663d6
Cleaned up type violations
danschwarz Apr 4, 2023
486cd6c
Make HTML class handling more sensible
danschwarz Apr 4, 2023
103767d
switch pre and code to render as light grey rather than dark
danschwarz Apr 4, 2023
eb64cb9
Handle nested B and I tags, also nested EM and STRONG tags
danschwarz Apr 4, 2023
e6b708f
Merge branch 'master' into richtext
danschwarz Apr 4, 2023
72aa934
make pytest work properly
danschwarz Apr 5, 2023
e9b4bfb
fix markup for br tag
danschwarz Apr 6, 2023
bb31054
elaborated comment
danschwarz Apr 7, 2023
739c009
Merge branch 'master' of https://github.com/danschwarz/toot
danschwarz Apr 7, 2023
0e6c726
Merge branch 'master' into richtext
danschwarz Apr 7, 2023
306cf45
README.rst: Fix image links
vitorgalvao Apr 18, 2023
5d47a0a
added urwidgets dependency
danschwarz May 1, 2023
9ccfd6b
Support OSC 8 underline of anchor links
danschwarz May 8, 2023
4a5db5b
added OSC 8 hyperlinks for media links and card links
danschwarz May 8, 2023
baa50ca
Added a workaround for statuses with malformed HTML
danschwarz May 13, 2023
de3461e
Hashtag highlighting now works correctly
danschwarz May 13, 2023
457facd
Fix for rendering HTML that starts with an inline tag
danschwarz May 13, 2023
fe88a40
Switched to released version of urwidgets (0.1.0)
danschwarz May 15, 2023
b3c654b
Added support for li value attribute, ol start and reverse attributes
danschwarz May 16, 2023
86304df
fix ws
danschwarz May 16, 2023
6109257
Removed followed_tags highlight feature (incompatible with HTML render)
danschwarz May 17, 2023
3b92466
HTML-to-markup conversion for status output to console/clipboard
danschwarz May 22, 2023
2563f42
leave URLs alone in copy to clipboard (don't use markup format)
danschwarz May 26, 2023
5c00edb
Strip extra newlines from status text output
danschwarz May 26, 2023
bd375de
Strip excess newlines from status when copied to clipboard
danschwarz May 26, 2023
93a5306
HTML-to-markup conversion for status output to console/clipboard
danschwarz May 26, 2023
290bac2
added test for timeline with (complex) HTML status message
danschwarz May 26, 2023
474d1f4
Login to servers that don't honor the uri spec for V1::Instance
danschwarz May 26, 2023
439aaa6
Anchor tags with class = hashtag or mention_hashtag are highlighted
danschwarz May 28, 2023
18ef70c
URLEncode hrefs; fixes crash bug with href urls in foreign scripts
danschwarz May 28, 2023
a770afc
Merge branch 'master' into console-output-enh
danschwarz Jun 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ publish :
twine upload dist/*.tar.gz dist/*.whl

test:
pytest -v
pytest tests/*.py -v
flake8
vermin --target=3.6 --no-tips --violations --exclude-regex venv/.* .

Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ requests>=2.13,<3.0
beautifulsoup4>=4.5.0,<5.0
wcwidth>=0.1.7
urwid>=2.0.0,<3.0

urwidgets>=0.1,<0.2
html2text>=2020.1.16
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"beautifulsoup4>=4.5.0,<5.0",
"wcwidth>=0.1.7",
"urwid>=2.0.0,<3.0",
"urwidgets>=0.1,<0.2",
"html2text>=2020.1.16"
],
entry_points={
'console_scripts': [
Expand Down
70 changes: 68 additions & 2 deletions tests/test_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,74 @@ def test_timeline(mock_get, monkeypatch, capsys):
assert err == ""


@mock.patch('toot.http.get')
def test_timeline_html_content(mock_get, monkeypatch, capsys):
mock_get.return_value = MockResponse([{
'id': '111111111111111111',
'account': {
'display_name': 'Frank Zappa 🎸',
'acct': 'fz'
},
'created_at': '2017-04-12T15:53:18.174Z',
'content': "<h2>HTML Render Test</h2><p><em>emphasized</em><br><u>underlined</u><br><strong>bold</strong><br><strong><em>bold and italic</em></strong><br><del>strikethrough</del><br>regular text</p><p>Code block:</p><pre><code>10 PRINT \"HELLO WORLD\"<br>20 GOTO 10<br></code></pre><blockquote><p>Something blockquoted here. The indentation is maintained as the text line wraps.</p></blockquote><ol><li>List item<ul><li>Nested item</li><li>Another nested </li></ul></li><li>Another list item. <ol><li>Something else nested</li><li>And a last nested</li></ol></li></ol><blockquote><p>Blockquote</p><ol><li>List in BQ</li><li>List item 2 in BQ</li></ol></blockquote><p><a href=\"https://babka.social/tags/hashtag\" class=\"mention hashtag\" rel=\"tag\">#<span>hashtag</span></a> <a href=\"https://babka.social/tags/test\" class=\"mention hashtag\" rel=\"tag\">#<span>test</span></a> <br><a href=\"https://a.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><span class=\"invisible\">https://</span><span class=\"\">a.com</span><span class=\"invisible\"></span></a> text after link</p>",
'reblog': None,
'in_reply_to_id': None,
'media_attachments': [],
}])

console.run_command(app, user, 'timeline', ['--once'])

mock_get.assert_called_once_with(app, user, '/api/v1/timelines/home', {'limit': 10})

out, err = capsys.readouterr()
lines = out.split("\n")
reference = [
"────────────────────────────────────────────────────────────────────────────────────────────────────",
"Frank Zappa 🎸 @fz 2017-04-12 15:53 UTC",
"",
"## HTML Render Test",
"",
" _emphasized_ ",
" _underlined_ ",
" **bold** ",
" ** _bold and italic_** ",
" ~~strikethrough~~ ",
"regular text",
"",
"Code block:",
"",
" ",
" 10 PRINT \"HELLO WORLD\" ",
" 20 GOTO 10 ",
" ",
"> Something blockquoted here. The indentation is maintained as the text line wraps.",
" 1. List item",
" • Nested item",
" • Another nested ",
" 2. Another list item. ",
" 1. Something else nested",
" 2. And a last nested",
"",
"> Blockquote",
"> 1. List in BQ",
"> 2. List item 2 in BQ",
">",
"",
"#hashtag #test ",
"https://a.com text after link",
"",
"ID 111111111111111111 ",
"────────────────────────────────────────────────────────────────────────────────────────────────────",
"",
]

assert len(lines) == len(reference)
for index, line in enumerate(lines):
assert line == reference[index], f"Line #{index}: Expected:\n{reference[index]}\nGot:\n{line}"

assert err == ""


@mock.patch('toot.http.get')
def test_timeline_with_re(mock_get, monkeypatch, capsys):
mock_get.return_value = MockResponse([{
Expand Down Expand Up @@ -585,8 +653,6 @@ def test_notifications(mock_get, capsys):
"────────────────────────────────────────────────────────────────────────────────────────────────────",
"",
])


@mock.patch('toot.http.get')
def test_notifications_empty(mock_get, capsys):
mock_get.return_value = MockResponse([])
Expand Down
17 changes: 16 additions & 1 deletion toot/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re
import sys
import textwrap
import html2text

from typing import List
from wcwidth import wcswidth
Expand Down Expand Up @@ -272,6 +273,20 @@ def print_search_results(results):
def print_status(status, width):
reblog = status['reblog']
content = reblog['content'] if reblog else status['content']

h2t = html2text.HTML2Text()

h2t.body_width = width
h2t.single_line_break = True
h2t.ignore_links = True
h2t.wrap_links = True
h2t.wrap_list_items = True
h2t.wrap_tables = True
h2t.unicode_snob = True
h2t.ul_item_mark = "\N{bullet}"

text_status = h2t.handle(content).strip()

media_attachments = reblog['media_attachments'] if reblog else status['media_attachments']
in_reply_to = status['in_reply_to_id']
poll = reblog.get('poll') if reblog else status.get('poll')
Expand All @@ -294,7 +309,7 @@ def print_status(status, width):
)

print_out("")
print_html(content, width)
print_out(highlight_hashtags(text_status))

if media_attachments:
print_out("\nMedia:")
Expand Down
44 changes: 23 additions & 21 deletions toot/tui/app.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import logging
import urwid
import html2text

from concurrent.futures import ThreadPoolExecutor

from toot import api, config, __version__
from toot.console import get_default_visibility
from toot.exceptions import ApiError


from .compose import StatusComposer
from .constants import PALETTE
from .entities import Status
from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource, StatusLinks, StatusZoom
from .overlays import StatusDeleteConfirmation, Account
from .poll import Poll
from .timeline import Timeline
from .utils import parse_content_links, show_media, copy_to_clipboard
from .utils import parse_content_links, show_media, copy_to_clipboard, parse_datetime

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -119,7 +121,6 @@ def __init__(self, app, user, args):
def run(self):
self.loop.set_alarm_in(0, lambda *args: self.async_load_instance())
self.loop.set_alarm_in(0, lambda *args: self.async_load_followed_accounts())
self.loop.set_alarm_in(0, lambda *args: self.async_load_followed_tags())
self.loop.set_alarm_in(0, lambda *args: self.async_load_timeline(
is_initial=True, timeline_name="home"))
self.loop.run()
Expand Down Expand Up @@ -315,22 +316,6 @@ def _done_accounts(accounts):

self.run_in_thread(_load_accounts, done_callback=_done_accounts)

def async_load_followed_tags(self):
def _load_tag_list():
try:
return api.followed_tags(self.app, self.user)
except ApiError:
# not supported by all Mastodon servers so fail silently if necessary
return []

def _done_tag_list(tags):
if len(tags) > 0:
self.followed_tags = [t["name"] for t in tags]
else:
self.followed_tags = []

self.run_in_thread(_load_tag_list, done_callback=_done_tag_list)

def refresh_footer(self, timeline):
"""Show status details in footer."""
status, index, count = timeline.get_focused_status_with_counts()
Expand Down Expand Up @@ -646,9 +631,26 @@ def _done(loop):
return self.run_in_thread(_delete, done_callback=_done)

def copy_status(self, status):
# TODO: copy a better version of status content
# including URLs
copy_to_clipboard(self.screen, status.original.data["content"])
h2t = html2text.HTML2Text()
h2t.body_width = 0 # nowrap
h2t.single_line_break = True
h2t.ignore_links = True
h2t.unicode_snob = True
h2t.ul_item_mark = "\N{bullet}"

time = parse_datetime(status.original.data['created_at'])
time = time.strftime('%Y-%m-%d %H:%M %Z')

text_status = (f"{status.original.data['url']}\n\n"
+ (status.original.author.display_name or "")
+ "\n"
+ (status.original.author.account or "")
+ "\n\n"
+ h2t.handle(status.original.data["content"]).strip()
+ "\n\n"
+ f"Created at: {time}")

copy_to_clipboard(self.screen, text_status)
self.footer.set_message(f"Status {status.original.id} copied")

# --- Overlay handling -----------------------------------------------------
Expand Down
24 changes: 23 additions & 1 deletion toot/tui/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,29 @@
('yellow_bold', 'yellow,bold', ''),
('red', 'dark red', ''),
('warning', 'light red', ''),
('white_bold', 'white,bold', '')
('white_bold', 'white,bold', ''),

# HTML tag styling
('a', ',italics', ''),
# em tag is mapped to i
('i', ',italics', ''),
# strong tag is mapped to b
('b', ',bold', ''),
# special case for bold + italic nested tags
('bi', ',bold,italics', ''),
('u', ',underline', ''),
('del', ',strikethrough', ''),
('code', 'light gray, standout', ''),
('pre', 'light gray, standout', ''),
('blockquote', 'light gray', ''),
('h1', ',bold', ''),
('h2', ',bold', ''),
('h3', ',bold', ''),
('h4', ',bold', ''),
('h5', ',bold', ''),
('h6', ',bold', ''),
('class_mention_hashtag', 'light cyan,bold', ''),
('class_hashtag', 'light cyan,bold', ''),
]

VISIBILITY_OPTIONS = [
Expand Down
21 changes: 14 additions & 7 deletions toot/tui/overlays.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import webbrowser

from toot import __version__
from toot.utils import format_content
from .utils import highlight_hashtags, highlight_keys
from .widgets import Button, EditBox, SelectableText
from toot import api
from .utils import highlight_keys
from .widgets import Button, EditBox, SelectableText
from .richtext import ContentParser


class StatusSource(urwid.Padding):
Expand Down Expand Up @@ -255,6 +255,8 @@ def setup_listbox(self):
super().__init__(walker)

def generate_contents(self, account, relationship=None, last_action=None):
parser = ContentParser()

if self.last_action and not self.last_action.startswith("Confirm"):
yield Button(f"Confirm {self.last_action}", on_press=take_action, user_data=self)
yield Button("Cancel", on_press=cancel_action, user_data=self)
Expand All @@ -279,8 +281,10 @@ def generate_contents(self, account, relationship=None, last_action=None):

if account["note"]:
yield urwid.Divider()
for line in format_content(account["note"]):
yield urwid.Text(highlight_hashtags(line, followed_tags=set()))

widgetlist = parser.html_to_widgets(account["note"])
for line in widgetlist:
yield (line)

yield urwid.Divider()
yield urwid.Text(["ID: ", ("green", f"{account['id']}")])
Expand Down Expand Up @@ -312,8 +316,11 @@ def generate_contents(self, account, relationship=None, last_action=None):
name = field["name"].title()
yield urwid.Divider()
yield urwid.Text([("yellow", f"{name.rstrip(':')}"), ":"])
for line in format_content(field["value"]):
yield urwid.Text(highlight_hashtags(line, followed_tags=set()))

widgetlist = parser.html_to_widgets(field["value"])
for line in widgetlist:
yield (line)

if field["verified_at"]:
yield urwid.Text(("green", "✓ Verified"))

Expand Down
12 changes: 8 additions & 4 deletions toot/tui/poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from toot import api
from toot.exceptions import ApiError
from toot.utils import format_content
from .utils import highlight_hashtags, parse_datetime
from .utils import parse_datetime
from .widgets import Button, CheckBox, RadioButton
from .richtext import ContentParser


class Poll(urwid.ListBox):
Expand Down Expand Up @@ -85,8 +85,12 @@ def generate_poll_detail(self):

def generate_contents(self, status):
yield urwid.Divider()
for line in format_content(status.data["content"]):
yield urwid.Text(highlight_hashtags(line, set()))

parser = ContentParser()
widgetlist = parser.html_to_widgets(status.data["content"])

for line in widgetlist:
yield (line)

yield urwid.Divider()
yield self.build_linebox(self.generate_poll_detail())
Expand Down
Loading