mirror of
https://github.com/ihabunek/toot.git
synced 2024-11-03 04:17:21 -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"]
|
PRIVACY_CHOICES = ["public", "unlisted", "private"]
|
||||||
VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"]
|
VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"]
|
||||||
|
IMAGE_FORMAT_CHOICES = ["block", "iterm", "kitty"]
|
||||||
TUI_COLORS = {
|
TUI_COLORS = {
|
||||||
"1": 1,
|
"1": 1,
|
||||||
"16": 16,
|
"16": 16,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from typing import Optional
|
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.cli.validators import validate_tui_colors, validate_cache_size
|
||||||
from toot.tui.app import TUI, TuiOptions
|
from toot.tui.app import TUI, TuiOptions
|
||||||
|
|
||||||
@ -40,6 +40,11 @@ COLOR_OPTIONS = ", ".join(TUI_COLORS.keys())
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Expand toots with content warnings automatically"
|
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
|
@pass_context
|
||||||
def tui(
|
def tui(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
@ -48,7 +53,8 @@ def tui(
|
|||||||
always_show_sensitive: bool,
|
always_show_sensitive: bool,
|
||||||
relative_datetimes: bool,
|
relative_datetimes: bool,
|
||||||
cache_size: Optional[int],
|
cache_size: Optional[int],
|
||||||
default_visibility: Optional[str]
|
default_visibility: Optional[str],
|
||||||
|
image_format: Optional[str]
|
||||||
):
|
):
|
||||||
"""Launches the toot terminal user interface"""
|
"""Launches the toot terminal user interface"""
|
||||||
if colors is None:
|
if colors is None:
|
||||||
@ -61,6 +67,7 @@ def tui(
|
|||||||
cache_size=cache_size,
|
cache_size=cache_size,
|
||||||
default_visibility=default_visibility,
|
default_visibility=default_visibility,
|
||||||
always_show_sensitive=always_show_sensitive,
|
always_show_sensitive=always_show_sensitive,
|
||||||
|
image_format=image_format,
|
||||||
)
|
)
|
||||||
tui = TUI.create(ctx.app, ctx.user, options)
|
tui = TUI.create(ctx.app, ctx.user, options)
|
||||||
tui.run()
|
tui.run()
|
||||||
|
@ -41,6 +41,7 @@ class TuiOptions(NamedTuple):
|
|||||||
relative_datetimes: bool
|
relative_datetimes: bool
|
||||||
cache_size: int
|
cache_size: int
|
||||||
default_visibility: Optional[bool]
|
default_visibility: Optional[bool]
|
||||||
|
image_format: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class Header(urwid.WidgetWrap):
|
class Header(urwid.WidgetWrap):
|
||||||
@ -656,7 +657,7 @@ class TUI(urwid.Frame):
|
|||||||
account = api.whois(self.app, self.user, account_id)
|
account = api.whois(self.app, self.user, account_id)
|
||||||
relationship = api.get_relationship(self.app, self.user, account_id)
|
relationship = api.get_relationship(self.app, self.user, account_id)
|
||||||
self.open_overlay(
|
self.open_overlay(
|
||||||
widget=Account(self.app, self.user, account, relationship),
|
widget=Account(self.app, self.user, account, relationship, self.options),
|
||||||
title="Account",
|
title="Account",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
# If term_image is loaded use their screen implementation which handles images
|
# If term_image is loaded use their screen implementation which handles images
|
||||||
try:
|
try:
|
||||||
from term_image.widget import UrwidImageScreen
|
from term_image.widget import UrwidImageScreen
|
||||||
|
from term_image import disable_queries # prevent phantom keystrokes
|
||||||
TuiScreen = UrwidImageScreen
|
TuiScreen = UrwidImageScreen
|
||||||
|
disable_queries()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urwid.raw_display import Screen
|
from urwid.raw_display import Screen
|
||||||
TuiScreen = Screen
|
TuiScreen = Screen
|
||||||
|
@ -7,11 +7,10 @@ import webbrowser
|
|||||||
from toot import __version__
|
from toot import __version__
|
||||||
from toot import api
|
from toot import api
|
||||||
|
|
||||||
from toot.tui.utils import highlight_keys, add_corners
|
from toot.tui.utils import highlight_keys, add_corners, get_base_image
|
||||||
from toot.tui.widgets import Button, EditBox, SelectableText, EmojiText
|
from toot.tui.widgets import Button, EditBox, SelectableText
|
||||||
from toot.tui.richtext import html_to_widgets
|
from toot.tui.richtext import html_to_widgets
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from term_image.image import AutoImage
|
|
||||||
from term_image.widget import UrwidImage
|
from term_image.widget import UrwidImage
|
||||||
|
|
||||||
|
|
||||||
@ -247,11 +246,12 @@ class Help(urwid.Padding):
|
|||||||
|
|
||||||
class Account(urwid.ListBox):
|
class Account(urwid.ListBox):
|
||||||
"""Shows account data and provides various actions"""
|
"""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.app = app
|
||||||
self.user = user
|
self.user = user
|
||||||
self.account = account
|
self.account = account
|
||||||
self.relationship = relationship
|
self.relationship = relationship
|
||||||
|
self.options = options
|
||||||
self.last_action = None
|
self.last_action = None
|
||||||
self.setup_listbox()
|
self.setup_listbox()
|
||||||
|
|
||||||
@ -268,8 +268,8 @@ class Account(urwid.ListBox):
|
|||||||
img = img.convert("RGBA")
|
img = img.convert("RGBA")
|
||||||
aimg = urwid.BoxAdapter(
|
aimg = urwid.BoxAdapter(
|
||||||
UrwidImage(
|
UrwidImage(
|
||||||
AutoImage(
|
get_base_image(
|
||||||
add_corners(img, 10)), upscale=True),
|
add_corners(img, 10), self.options.image_format), upscale=True),
|
||||||
10)
|
10)
|
||||||
else:
|
else:
|
||||||
aimg = urwid.BoxAdapter(urwid.SolidFill(" "), 10)
|
aimg = urwid.BoxAdapter(urwid.SolidFill(" "), 10)
|
||||||
@ -279,20 +279,13 @@ class Account(urwid.ListBox):
|
|||||||
|
|
||||||
if img.format == 'PNG' and img.mode != 'RGBA':
|
if img.format == 'PNG' and img.mode != 'RGBA':
|
||||||
img = img.convert("RGBA")
|
img = img.convert("RGBA")
|
||||||
himg = (urwid.BoxAdapter(
|
himg = (urwid.BoxAdapter(UrwidImage(get_base_image(
|
||||||
UrwidImage(
|
add_corners(img, 10), self.options.image_format), upscale=True), 10))
|
||||||
AutoImage(
|
|
||||||
add_corners(img, 10)
|
|
||||||
), upscale=True),
|
|
||||||
10)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
himg = urwid.BoxAdapter(urwid.SolidFill(" "), 10)
|
himg = urwid.BoxAdapter(urwid.SolidFill(" "), 10)
|
||||||
|
|
||||||
atxt = urwid.Pile([urwid.Divider(),
|
atxt = urwid.Pile([urwid.Divider(),
|
||||||
urwid.AttrMap(
|
(urwid.Text(("account", account["display_name"]))),
|
||||||
EmojiText(account["display_name"], account["emojis"]),
|
|
||||||
"account"),
|
|
||||||
(urwid.Text(("highlight", "@" + self.account['acct'])))])
|
(urwid.Text(("highlight", "@" + self.account['acct'])))])
|
||||||
columns = urwid.Columns([aimg, ("weight", 9999, himg)], dividechars=2, min_width=20)
|
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 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.tui.richtext import html_to_widgets, url_to_widget
|
||||||
from toot.utils.datetime import parse_datetime, time_ago
|
from toot.utils.datetime import parse_datetime, time_ago
|
||||||
from toot.utils.language import language_name
|
from toot.utils.language import language_name
|
||||||
|
|
||||||
from toot.entities import Status
|
from toot.entities import Status
|
||||||
from toot.tui.scroll import Scrollable, ScrollBar
|
from toot.tui.scroll import Scrollable, ScrollBar
|
||||||
from toot.tui.utils import highlight_keys
|
from toot.tui.utils import highlight_keys, get_base_image, can_render_pixels
|
||||||
from toot.tui.widgets import SelectableText, SelectableColumns, EmojiText
|
from toot.tui.widgets import SelectableText, SelectableColumns
|
||||||
from term_image.image import AutoImage
|
|
||||||
from term_image.widget import UrwidImage
|
from term_image.widget import UrwidImage
|
||||||
|
|
||||||
logger = logging.getLogger("toot")
|
logger = logging.getLogger("toot")
|
||||||
@ -48,7 +47,7 @@ class Timeline(urwid.Columns):
|
|||||||
self.is_thread = is_thread
|
self.is_thread = is_thread
|
||||||
self.statuses = statuses
|
self.statuses = statuses
|
||||||
self.status_list = self.build_status_list(statuses, focus=focus)
|
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:
|
try:
|
||||||
focused_status = statuses[focus]
|
focused_status = statuses[focus]
|
||||||
@ -330,11 +329,11 @@ class Timeline(urwid.Columns):
|
|||||||
if img:
|
if img:
|
||||||
try:
|
try:
|
||||||
render_img = add_corners(img, 10) if self.can_render_pixels else img
|
render_img = add_corners(img, 10) if self.can_render_pixels else img
|
||||||
|
|
||||||
status.placeholders[placeholder_index]._set_original_widget(
|
status.placeholders[placeholder_index]._set_original_widget(
|
||||||
UrwidImage(
|
UrwidImage(get_base_image(render_img, self.tui.options.image_format), '<', upscale=True))
|
||||||
AutoImage(render_img),
|
# "<" means left-justify the image
|
||||||
"<", upscale=True),
|
|
||||||
) # "<" means left-justify the image
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# ignore IndexErrors.
|
# ignore IndexErrors.
|
||||||
pass
|
pass
|
||||||
@ -403,9 +402,7 @@ class StatusDetails(urwid.Pile):
|
|||||||
if img:
|
if img:
|
||||||
render_img = add_corners(img, 10) if self.timeline.can_render_pixels else img
|
render_img = add_corners(img, 10) if self.timeline.can_render_pixels else img
|
||||||
return (urwid.BoxAdapter(
|
return (urwid.BoxAdapter(
|
||||||
UrwidImage(
|
UrwidImage(get_base_image(render_img, self.timeline.tui.options.image_format), "<", upscale=True),
|
||||||
AutoImage(render_img),
|
|
||||||
"<", upscale=True),
|
|
||||||
rows))
|
rows))
|
||||||
else:
|
else:
|
||||||
placeholder = urwid.BoxAdapter(urwid.SolidFill(fill_char=" "), rows)
|
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
|
account_color = ("highlight" if self.status.original.author.account in
|
||||||
self.timeline.tui.followed_accounts else "account")
|
self.timeline.tui.followed_accounts else "account")
|
||||||
|
|
||||||
atxt = urwid.Pile([("pack",
|
atxt = urwid.Pile([("pack", urwid.Text(("bold", self.status.author.display_name))),
|
||||||
urwid.AttrMap(
|
|
||||||
EmojiText(self.status.author.display_name,
|
|
||||||
self.status.data["account"]["emojis"]),
|
|
||||||
"bold")),
|
|
||||||
("pack", urwid.Text((account_color, self.status.author.account)))])
|
("pack", urwid.Text((account_color, self.status.author.account)))])
|
||||||
|
|
||||||
columns = urwid.Columns([aimg, ("weight", 9999, atxt)], dividechars=1, min_width=5)
|
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
|
if reblogged_by.display_name
|
||||||
else reblogged_by.username)
|
else reblogged_by.username)
|
||||||
text = f"♺ {reblogger_name} boosted"
|
text = f"♺ {reblogger_name} boosted"
|
||||||
yield urwid.AttrMap(
|
yield urwid.Text(("dim", text))
|
||||||
EmojiText(text, status.data["account"]["emojis"], make_gray=True),
|
|
||||||
"dim"
|
|
||||||
)
|
|
||||||
yield ("pack", urwid.AttrMap(urwid.Divider("-"), "dim"))
|
yield ("pack", urwid.AttrMap(urwid.Divider("-"), "dim"))
|
||||||
|
|
||||||
yield self.author_header(reblogged_by)
|
yield self.author_header(reblogged_by)
|
||||||
|
@ -9,7 +9,7 @@ from html.parser import HTMLParser
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from PIL import Image, ImageDraw
|
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')
|
HASHTAG_PATTERN = re.compile(r'(?<!\w)(#\w+)\b')
|
||||||
|
|
||||||
@ -107,9 +107,19 @@ def add_corners(img, rad):
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
def can_render_pixels():
|
def can_render_pixels(image_format: str):
|
||||||
# subclasses of GraphicsImage render to pixels
|
return image_format in ['kitty', 'iterm']
|
||||||
return issubclass(auto_image_class(), GraphicsImage)
|
|
||||||
|
|
||||||
|
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):
|
def copy_to_clipboard(screen: urwid.raw_display.Screen, text: str):
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
from typing import List
|
|
||||||
import urwid
|
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
|
from wcwidth import wcswidth
|
||||||
|
|
||||||
|
|
||||||
@ -76,91 +69,6 @@ class RadioButton(urwid.AttrWrap):
|
|||||||
return super().__init__(padding, "button", "button_focused")
|
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):
|
class ModalBox(urwid.Frame):
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
text = urwid.Text(message)
|
text = urwid.Text(message)
|
||||||
|
Loading…
Reference in New Issue
Block a user