mirror of
https://github.com/ihabunek/toot.git
synced 2025-02-02 15:07:51 -05:00
removed autodetection of image format; now uses cmd line option
--image-format='kitty'|'iterm'|'block' (default is block) autodetection was causing intermittent phantom character output to the terminal in some configurations, generally over SSH connections. Switching to a command line option eliminates the problematic autodetection code. As a side effect, EmojiText widget had to be removed.
This commit is contained in:
parent
d2ea1f0c77
commit
906cdd013b
@ -22,7 +22,7 @@ T = t.TypeVar("T")
|
||||
|
||||
PRIVACY_CHOICES = ["public", "unlisted", "private"]
|
||||
VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"]
|
||||
|
||||
IMAGE_FORMAT_CHOICES = ["block", "iterm", "kitty"]
|
||||
TUI_COLORS = {
|
||||
"1": 1,
|
||||
"16": 16,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import click
|
||||
|
||||
from typing import Optional
|
||||
from toot.cli import TUI_COLORS, VISIBILITY_CHOICES, Context, cli, pass_context
|
||||
from toot.cli import TUI_COLORS, VISIBILITY_CHOICES, IMAGE_FORMAT_CHOICES, Context, cli, pass_context
|
||||
from toot.cli.validators import validate_tui_colors, validate_cache_size
|
||||
from toot.tui.app import TUI, TuiOptions
|
||||
|
||||
@ -40,6 +40,11 @@ COLOR_OPTIONS = ", ".join(TUI_COLORS.keys())
|
||||
is_flag=True,
|
||||
help="Expand toots with content warnings automatically"
|
||||
)
|
||||
@click.option(
|
||||
"-f", "--image-format",
|
||||
type=click.Choice(IMAGE_FORMAT_CHOICES),
|
||||
help="Image output format; support varies across terminals. Default: block"
|
||||
)
|
||||
@pass_context
|
||||
def tui(
|
||||
ctx: Context,
|
||||
@ -48,7 +53,8 @@ def tui(
|
||||
always_show_sensitive: bool,
|
||||
relative_datetimes: bool,
|
||||
cache_size: Optional[int],
|
||||
default_visibility: Optional[str]
|
||||
default_visibility: Optional[str],
|
||||
image_format: Optional[str]
|
||||
):
|
||||
"""Launches the toot terminal user interface"""
|
||||
if colors is None:
|
||||
@ -61,6 +67,7 @@ def tui(
|
||||
cache_size=cache_size,
|
||||
default_visibility=default_visibility,
|
||||
always_show_sensitive=always_show_sensitive,
|
||||
image_format=image_format,
|
||||
)
|
||||
tui = TUI.create(ctx.app, ctx.user, options)
|
||||
tui.run()
|
||||
|
@ -41,6 +41,7 @@ class TuiOptions(NamedTuple):
|
||||
relative_datetimes: bool
|
||||
cache_size: int
|
||||
default_visibility: Optional[bool]
|
||||
image_format: Optional[str]
|
||||
|
||||
|
||||
class Header(urwid.WidgetWrap):
|
||||
@ -656,7 +657,7 @@ class TUI(urwid.Frame):
|
||||
account = api.whois(self.app, self.user, account_id)
|
||||
relationship = api.get_relationship(self.app, self.user, account_id)
|
||||
self.open_overlay(
|
||||
widget=Account(self.app, self.user, account, relationship),
|
||||
widget=Account(self.app, self.user, account, relationship, self.options),
|
||||
title="Account",
|
||||
)
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
# If term_image is loaded use their screen implementation which handles images
|
||||
try:
|
||||
from term_image.widget import UrwidImageScreen
|
||||
from term_image import disable_queries # prevent phantom keystrokes
|
||||
TuiScreen = UrwidImageScreen
|
||||
disable_queries()
|
||||
except ImportError:
|
||||
from urwid.raw_display import Screen
|
||||
TuiScreen = Screen
|
||||
|
@ -7,11 +7,10 @@ import webbrowser
|
||||
from toot import __version__
|
||||
from toot import api
|
||||
|
||||
from toot.tui.utils import highlight_keys, add_corners
|
||||
from toot.tui.widgets import Button, EditBox, SelectableText, EmojiText
|
||||
from toot.tui.utils import highlight_keys, add_corners, get_base_image
|
||||
from toot.tui.widgets import Button, EditBox, SelectableText
|
||||
from toot.tui.richtext import html_to_widgets
|
||||
from PIL import Image
|
||||
from term_image.image import AutoImage
|
||||
from term_image.widget import UrwidImage
|
||||
|
||||
|
||||
@ -247,11 +246,12 @@ class Help(urwid.Padding):
|
||||
|
||||
class Account(urwid.ListBox):
|
||||
"""Shows account data and provides various actions"""
|
||||
def __init__(self, app, user, account, relationship):
|
||||
def __init__(self, app, user, account, relationship, options):
|
||||
self.app = app
|
||||
self.user = user
|
||||
self.account = account
|
||||
self.relationship = relationship
|
||||
self.options = options
|
||||
self.last_action = None
|
||||
self.setup_listbox()
|
||||
|
||||
@ -268,8 +268,8 @@ class Account(urwid.ListBox):
|
||||
img = img.convert("RGBA")
|
||||
aimg = urwid.BoxAdapter(
|
||||
UrwidImage(
|
||||
AutoImage(
|
||||
add_corners(img, 10)), upscale=True),
|
||||
get_base_image(
|
||||
add_corners(img, 10), self.options.image_format), upscale=True),
|
||||
10)
|
||||
else:
|
||||
aimg = urwid.BoxAdapter(urwid.SolidFill(" "), 10)
|
||||
@ -279,20 +279,13 @@ class Account(urwid.ListBox):
|
||||
|
||||
if img.format == 'PNG' and img.mode != 'RGBA':
|
||||
img = img.convert("RGBA")
|
||||
himg = (urwid.BoxAdapter(
|
||||
UrwidImage(
|
||||
AutoImage(
|
||||
add_corners(img, 10)
|
||||
), upscale=True),
|
||||
10)
|
||||
)
|
||||
himg = (urwid.BoxAdapter(UrwidImage(get_base_image(
|
||||
add_corners(img, 10), self.options.image_format), upscale=True), 10))
|
||||
else:
|
||||
himg = urwid.BoxAdapter(urwid.SolidFill(" "), 10)
|
||||
|
||||
atxt = urwid.Pile([urwid.Divider(),
|
||||
urwid.AttrMap(
|
||||
EmojiText(account["display_name"], account["emojis"]),
|
||||
"account"),
|
||||
(urwid.Text(("account", account["display_name"]))),
|
||||
(urwid.Text(("highlight", "@" + self.account['acct'])))])
|
||||
columns = urwid.Columns([aimg, ("weight", 9999, himg)], dividechars=2, min_width=20)
|
||||
|
||||
|
@ -7,16 +7,15 @@ from typing import List, Optional
|
||||
|
||||
from toot.tui import app
|
||||
|
||||
from toot.tui.utils import can_render_pixels, add_corners
|
||||
from toot.tui.utils import add_corners
|
||||
from toot.tui.richtext import html_to_widgets, url_to_widget
|
||||
from toot.utils.datetime import parse_datetime, time_ago
|
||||
from toot.utils.language import language_name
|
||||
|
||||
from toot.entities import Status
|
||||
from toot.tui.scroll import Scrollable, ScrollBar
|
||||
from toot.tui.utils import highlight_keys
|
||||
from toot.tui.widgets import SelectableText, SelectableColumns, EmojiText
|
||||
from term_image.image import AutoImage
|
||||
from toot.tui.utils import highlight_keys, get_base_image, can_render_pixels
|
||||
from toot.tui.widgets import SelectableText, SelectableColumns
|
||||
from term_image.widget import UrwidImage
|
||||
|
||||
logger = logging.getLogger("toot")
|
||||
@ -48,7 +47,7 @@ class Timeline(urwid.Columns):
|
||||
self.is_thread = is_thread
|
||||
self.statuses = statuses
|
||||
self.status_list = self.build_status_list(statuses, focus=focus)
|
||||
self.can_render_pixels = can_render_pixels()
|
||||
self.can_render_pixels = can_render_pixels(self.tui.options.image_format)
|
||||
|
||||
try:
|
||||
focused_status = statuses[focus]
|
||||
@ -330,11 +329,11 @@ class Timeline(urwid.Columns):
|
||||
if img:
|
||||
try:
|
||||
render_img = add_corners(img, 10) if self.can_render_pixels else img
|
||||
|
||||
status.placeholders[placeholder_index]._set_original_widget(
|
||||
UrwidImage(
|
||||
AutoImage(render_img),
|
||||
"<", upscale=True),
|
||||
) # "<" means left-justify the image
|
||||
UrwidImage(get_base_image(render_img, self.tui.options.image_format), '<', upscale=True))
|
||||
# "<" means left-justify the image
|
||||
|
||||
except IndexError:
|
||||
# ignore IndexErrors.
|
||||
pass
|
||||
@ -403,9 +402,7 @@ class StatusDetails(urwid.Pile):
|
||||
if img:
|
||||
render_img = add_corners(img, 10) if self.timeline.can_render_pixels else img
|
||||
return (urwid.BoxAdapter(
|
||||
UrwidImage(
|
||||
AutoImage(render_img),
|
||||
"<", upscale=True),
|
||||
UrwidImage(get_base_image(render_img, self.timeline.tui.options.image_format), "<", upscale=True),
|
||||
rows))
|
||||
else:
|
||||
placeholder = urwid.BoxAdapter(urwid.SolidFill(fill_char=" "), rows)
|
||||
@ -424,11 +421,7 @@ class StatusDetails(urwid.Pile):
|
||||
account_color = ("highlight" if self.status.original.author.account in
|
||||
self.timeline.tui.followed_accounts else "account")
|
||||
|
||||
atxt = urwid.Pile([("pack",
|
||||
urwid.AttrMap(
|
||||
EmojiText(self.status.author.display_name,
|
||||
self.status.data["account"]["emojis"]),
|
||||
"bold")),
|
||||
atxt = urwid.Pile([("pack", urwid.Text(("bold", self.status.author.display_name))),
|
||||
("pack", urwid.Text((account_color, self.status.author.account)))])
|
||||
|
||||
columns = urwid.Columns([aimg, ("weight", 9999, atxt)], dividechars=1, min_width=5)
|
||||
@ -440,10 +433,7 @@ class StatusDetails(urwid.Pile):
|
||||
if reblogged_by.display_name
|
||||
else reblogged_by.username)
|
||||
text = f"♺ {reblogger_name} boosted"
|
||||
yield urwid.AttrMap(
|
||||
EmojiText(text, status.data["account"]["emojis"], make_gray=True),
|
||||
"dim"
|
||||
)
|
||||
yield urwid.Text(("dim", text))
|
||||
yield ("pack", urwid.AttrMap(urwid.Divider("-"), "dim"))
|
||||
|
||||
yield self.author_header(reblogged_by)
|
||||
|
@ -9,7 +9,7 @@ from html.parser import HTMLParser
|
||||
from typing import List
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from term_image.image import auto_image_class, GraphicsImage
|
||||
from term_image.image import BaseImage, KittyImage, ITerm2Image, BlockImage
|
||||
|
||||
HASHTAG_PATTERN = re.compile(r'(?<!\w)(#\w+)\b')
|
||||
|
||||
@ -107,9 +107,19 @@ def add_corners(img, rad):
|
||||
return img
|
||||
|
||||
|
||||
def can_render_pixels():
|
||||
# subclasses of GraphicsImage render to pixels
|
||||
return issubclass(auto_image_class(), GraphicsImage)
|
||||
def can_render_pixels(image_format: str):
|
||||
return image_format in ['kitty', 'iterm']
|
||||
|
||||
|
||||
def get_base_image(image, image_format: str) -> BaseImage:
|
||||
# we don't autodetect kitty, iterm, we force based on option switches
|
||||
BaseImage.forced_support = True
|
||||
if image_format == 'kitty':
|
||||
return KittyImage(image)
|
||||
elif image_format == 'iterm':
|
||||
return ITerm2Image(image)
|
||||
else:
|
||||
return BlockImage(image)
|
||||
|
||||
|
||||
def copy_to_clipboard(screen: urwid.raw_display.Screen, text: str):
|
||||
|
@ -1,11 +1,4 @@
|
||||
from typing import List
|
||||
import urwid
|
||||
import re
|
||||
import requests
|
||||
from PIL import Image, ImageOps
|
||||
from term_image.image import AutoImage
|
||||
from term_image.widget import UrwidImage
|
||||
from .utils import can_render_pixels
|
||||
from wcwidth import wcswidth
|
||||
|
||||
|
||||
@ -76,91 +69,6 @@ class RadioButton(urwid.AttrWrap):
|
||||
return super().__init__(padding, "button", "button_focused")
|
||||
|
||||
|
||||
class EmojiText(urwid.Padding):
|
||||
"""Widget to render text with embedded custom emojis
|
||||
|
||||
Note, these are Mastodon custom server emojis
|
||||
which are indicated by :shortcode: in the text
|
||||
and rendered as images on supporting clients.
|
||||
|
||||
For clients that do not support pixel rendering,
|
||||
they are rendered as plain text :shortcode:
|
||||
|
||||
This widget was designed for use with displaynames
|
||||
but could be used with any string of text.
|
||||
However, due to the internal use of columns,
|
||||
this widget will not wrap multi-line text
|
||||
correctly.
|
||||
|
||||
Note, you can embed this widget in AttrWrap to style
|
||||
the text as desired.
|
||||
|
||||
Parameters:
|
||||
|
||||
text -- text string (with or without embedded shortcodes)
|
||||
emojis -- list of emojis with nested lists of associated
|
||||
shortcodes and URLs
|
||||
make_gray -- if True, convert emojis to grayscale
|
||||
"""
|
||||
image_cache = {}
|
||||
|
||||
def __init__(self, text: str, emojis: List, make_gray=False):
|
||||
columns = []
|
||||
|
||||
if not can_render_pixels():
|
||||
return self.plain(text, columns)
|
||||
|
||||
# build a regex to find all available shortcodes
|
||||
regex = '|'.join(f':{emoji["shortcode"]}:' for emoji in emojis)
|
||||
|
||||
if 0 == len(regex):
|
||||
# if no shortcodes, just output plain Text
|
||||
return self.plain(text, columns)
|
||||
|
||||
regex = f"({regex})"
|
||||
|
||||
for word in re.split(regex, text):
|
||||
if word.startswith(":") and word.endswith(":"):
|
||||
shortcode = word[1:-1]
|
||||
found = False
|
||||
for emoji in emojis:
|
||||
if emoji["shortcode"] == shortcode:
|
||||
try:
|
||||
img = EmojiText.image_cache.get(str(hash(emoji["url"])))
|
||||
if not img:
|
||||
# TODO: consider asynchronous loading in future
|
||||
img = Image.open(requests.get(emoji["url"], stream=True).raw)
|
||||
EmojiText.image_cache[str(hash(emoji["url"]))] = img
|
||||
|
||||
if make_gray:
|
||||
img = ImageOps.grayscale(img)
|
||||
|
||||
image_widget = urwid.BoxAdapter(UrwidImage(AutoImage(img), upscale=True), 1)
|
||||
|
||||
columns.append(image_widget)
|
||||
except Exception:
|
||||
columns.append(("pack", urwid.Text(word)))
|
||||
finally:
|
||||
found = True
|
||||
break
|
||||
if found is False:
|
||||
columns.append(("pack", urwid.Text(word)))
|
||||
else:
|
||||
columns.append(("pack", urwid.Text(word)))
|
||||
|
||||
columns.append(("weight", 9999, urwid.Text("")))
|
||||
|
||||
column_widget = urwid.Columns(columns, dividechars=0, min_width=2)
|
||||
super().__init__(column_widget)
|
||||
|
||||
def plain(self, text, columns):
|
||||
# if can't render pixels, just output plain Text
|
||||
columns.append(("pack", urwid.Text(text)))
|
||||
columns.append(("weight", 9999, urwid.Text("")))
|
||||
column_widget = urwid.Columns(columns, dividechars=1, min_width=2)
|
||||
super().__init__(column_widget)
|
||||
|
||||
|
||||
class ModalBox(urwid.Frame):
|
||||
def __init__(self, message):
|
||||
text = urwid.Text(message)
|
||||
|
Loading…
x
Reference in New Issue
Block a user