kopia lustrzana https://github.com/ihabunek/toot
Merge branch 'ihabunek:master' into master
commit
09980470d2
2
.flake8
2
.flake8
|
@ -1,4 +1,4 @@
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude=build,tests,tmp,venv,toot/tui/scroll.py
|
exclude=build,tests,tmp,venv,_env,toot/tui/scroll.py
|
||||||
ignore=E128,W503,W504
|
ignore=E128,W503,W504
|
||||||
max-line-length=120
|
max-line-length=120
|
||||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -3,6 +3,20 @@ Changelog
|
||||||
|
|
||||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||||
|
|
||||||
|
**0.44.1 (2024-08-12)**
|
||||||
|
|
||||||
|
* Make it possible to pass status URL as status_id, experimental (thanks
|
||||||
|
@nemobis)
|
||||||
|
* Show statuses in search results (thanks @nemobis)
|
||||||
|
|
||||||
|
**0.44.0 (2024-08-12)**
|
||||||
|
|
||||||
|
* **BREAKING:** Require Python 3.8+
|
||||||
|
* Add `toot diag` for displaying diagnostic info (thanks Dan Schwarz)
|
||||||
|
* TUI: Improve image support (thanks @AnonymouX47)
|
||||||
|
* TUI: Add support for indexed color image rendering (#483) (thanks Dan Schwarz)
|
||||||
|
* TUI: Fix crash bug (#483) (thanks Dan Schwarz)
|
||||||
|
|
||||||
**0.43.0 (2024-04-13)**
|
**0.43.0 (2024-04-13)**
|
||||||
|
|
||||||
* TUI: Support displaying images (thanks Dan Schwarz)
|
* TUI: Support displaying images (thanks Dan Schwarz)
|
||||||
|
|
|
@ -37,11 +37,18 @@ Terminal User Interface
|
||||||
|
|
||||||
toot includes a terminal user interface (TUI). Run it with ``toot tui``.
|
toot includes a terminal user interface (TUI). Run it with ``toot tui``.
|
||||||
|
|
||||||
|
TUI Features:
|
||||||
|
-------------
|
||||||
|
|
||||||
|
* Block graphic image display (requires optional libraries `pillow <https://pypi.org/project/pillow/>`, `term-image <https://pypi.org/project/term-image/>`, and `urwidgets <https://pypi.org/project/urwidgets/>`)
|
||||||
|
* Bitmapped image display in `kitty <https://sw.kovidgoyal.net/kitty/>` terminal ``toot tui -f kitty``
|
||||||
|
* Bitmapped image display in `iTerm2 <https://iterm2.com/>`, or `WezTerm <https://wezfurlong.org/wezterm/index.html>` terminal ``toot tui -f iterm``
|
||||||
|
|
||||||
|
|
||||||
.. image :: https://raw.githubusercontent.com/ihabunek/toot/master/docs/images/tui_list.png
|
.. image :: https://raw.githubusercontent.com/ihabunek/toot/master/docs/images/tui_list.png
|
||||||
|
|
||||||
.. image :: https://raw.githubusercontent.com/ihabunek/toot/master/docs/images/tui_compose.png
|
.. image :: https://raw.githubusercontent.com/ihabunek/toot/master/docs/images/tui_compose.png
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,17 @@
|
||||||
|
0.44.1:
|
||||||
|
date: 2024-08-12
|
||||||
|
changes:
|
||||||
|
- "Make it possible to pass status URL as status_id, experimental (thanks @nemobis)"
|
||||||
|
- "Show statuses in search results (thanks @nemobis)"
|
||||||
|
|
||||||
0.44.0:
|
0.44.0:
|
||||||
date: TBA
|
date: 2024-08-12
|
||||||
changes:
|
changes:
|
||||||
- "**BREAKING:** Require Python 3.8+"
|
- "**BREAKING:** Require Python 3.8+"
|
||||||
|
- "Add `toot diag` for displaying diagnostic info (thanks Dan Schwarz)"
|
||||||
|
- "TUI: Improve image support (thanks @AnonymouX47)"
|
||||||
|
- "TUI: Add support for indexed color image rendering (#483) (thanks Dan Schwarz)"
|
||||||
|
- "TUI: Fix crash bug (#483) (thanks Dan Schwarz)"
|
||||||
|
|
||||||
0.43.0:
|
0.43.0:
|
||||||
date: 2024-04-13
|
date: 2024-04-13
|
||||||
|
|
|
@ -3,6 +3,20 @@ Changelog
|
||||||
|
|
||||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||||
|
|
||||||
|
**0.44.1 (2024-08-12)**
|
||||||
|
|
||||||
|
* Make it possible to pass status URL as status_id, experimental (thanks
|
||||||
|
@nemobis)
|
||||||
|
* Show statuses in search results (thanks @nemobis)
|
||||||
|
|
||||||
|
**0.44.0 (2024-08-12)**
|
||||||
|
|
||||||
|
* **BREAKING:** Require Python 3.8+
|
||||||
|
* Add `toot diag` for displaying diagnostic info (thanks Dan Schwarz)
|
||||||
|
* TUI: Improve image support (thanks @AnonymouX47)
|
||||||
|
* TUI: Add support for indexed color image rendering (#483) (thanks Dan Schwarz)
|
||||||
|
* TUI: Fix crash bug (#483) (thanks Dan Schwarz)
|
||||||
|
|
||||||
**0.43.0 (2024-04-13)**
|
**0.43.0 (2024-04-13)**
|
||||||
|
|
||||||
* TUI: Support displaying images (thanks Dan Schwarz)
|
* TUI: Support displaying images (thanks Dan Schwarz)
|
||||||
|
|
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 108 KiB Po Szerokość: | Wysokość: | Rozmiar: 237 KiB |
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 192 KiB Po Szerokość: | Wysokość: | Rozmiar: 617 KiB |
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 209 KiB |
|
@ -81,5 +81,4 @@ packages=[
|
||||||
[tool.setuptools_scm]
|
[tool.setuptools_scm]
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
typeCheckingMode = "strict"
|
pythonVersion = "3.8"
|
||||||
pythonVersion = "3.8"
|
|
||||||
|
|
24
toot/api.py
24
toot/api.py
|
@ -44,10 +44,34 @@ def _account_action(app, user, account, action) -> Response:
|
||||||
|
|
||||||
|
|
||||||
def _status_action(app, user, status_id, action, data=None) -> Response:
|
def _status_action(app, user, status_id, action, data=None) -> Response:
|
||||||
|
status_id = _resolve_status_id(app, user, status_id)
|
||||||
url = f"/api/v1/statuses/{status_id}/{action}"
|
url = f"/api/v1/statuses/{status_id}/{action}"
|
||||||
return http.post(app, user, url, data=data)
|
return http.post(app, user, url, data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_status_id(app, user, id_or_url) -> str:
|
||||||
|
"""
|
||||||
|
If given an URL instead of status ID, attempt to resolve the status ID.
|
||||||
|
|
||||||
|
TODO: Not 100% sure this is the correct way of doing this, but it seems to
|
||||||
|
work for all test cases I've thrown at it. So leaving it undocumented until
|
||||||
|
we're happy it works.
|
||||||
|
"""
|
||||||
|
if re.match(r"^https?://", id_or_url):
|
||||||
|
response = search(app, user, id_or_url, resolve=True, type="statuses")
|
||||||
|
statuses = response.json().get("statuses")
|
||||||
|
|
||||||
|
if not statuses:
|
||||||
|
raise ConsoleError(f"Cannot find status matching URL {id_or_url}")
|
||||||
|
|
||||||
|
if len(statuses) > 1:
|
||||||
|
raise ConsoleError(f"Found multiple statuses mathcing URL {id_or_url}")
|
||||||
|
|
||||||
|
return statuses[0]["id"]
|
||||||
|
|
||||||
|
return id_or_url
|
||||||
|
|
||||||
|
|
||||||
def _tag_action(app, user, tag_name, action) -> Response:
|
def _tag_action(app, user, tag_name, action) -> Response:
|
||||||
url = f"/api/v1/tags/{tag_name}/{action}"
|
url = f"/api/v1/tags/{tag_name}/{action}"
|
||||||
return http.post(app, user, url)
|
return http.post(app, user, url)
|
||||||
|
|
|
@ -173,6 +173,7 @@ def cli(ctx: click.Context, max_width: int, color: bool, debug: bool, as_user: s
|
||||||
|
|
||||||
from toot.cli import accounts # noqa
|
from toot.cli import accounts # noqa
|
||||||
from toot.cli import auth # noqa
|
from toot.cli import auth # noqa
|
||||||
|
from toot.cli import diag # noqa
|
||||||
from toot.cli import lists # noqa
|
from toot.cli import lists # noqa
|
||||||
from toot.cli import post # noqa
|
from toot.cli import post # noqa
|
||||||
from toot.cli import read # noqa
|
from toot.cli import read # noqa
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
from typing import Optional
|
||||||
|
import click
|
||||||
|
from toot import api, config
|
||||||
|
from toot.entities import Data
|
||||||
|
from toot.output import print_diags
|
||||||
|
from toot.cli import cli
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"-f",
|
||||||
|
"--files",
|
||||||
|
is_flag=True,
|
||||||
|
help="Print contents of the config and settings files in diagnostic output",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-s",
|
||||||
|
"--server",
|
||||||
|
is_flag=True,
|
||||||
|
help="Print information about the curren server in diagnostic output",
|
||||||
|
)
|
||||||
|
def diag(files: bool, server: bool):
|
||||||
|
"""Display useful information for diagnosing problems"""
|
||||||
|
instance_dict: Optional[Data] = None
|
||||||
|
if server:
|
||||||
|
_, app = config.get_active_user_app()
|
||||||
|
if app:
|
||||||
|
instance_dict = api.get_instance(app.base_url).json()
|
||||||
|
|
||||||
|
print_diags(instance_dict, files)
|
|
@ -9,6 +9,8 @@ from typing import BinaryIO, Optional, Tuple
|
||||||
from toot import api, config
|
from toot import api, config
|
||||||
from toot.cli import AccountParamType, cli, json_option, pass_context, Context
|
from toot.cli import AccountParamType, cli, json_option, pass_context, Context
|
||||||
from toot.cli import DURATION_EXAMPLES, VISIBILITY_CHOICES
|
from toot.cli import DURATION_EXAMPLES, VISIBILITY_CHOICES
|
||||||
|
from toot.tui.constants import VISIBILITY_OPTIONS # move to top-level ?
|
||||||
|
|
||||||
from toot.cli.validators import validate_duration, validate_language
|
from toot.cli.validators import validate_duration, validate_language
|
||||||
from toot.entities import MediaAttachment, from_dict
|
from toot.entities import MediaAttachment, from_dict
|
||||||
from toot.utils import EOF_KEY, delete_tmp_status_file, editor_input, multiline_input
|
from toot.utils import EOF_KEY, delete_tmp_status_file, editor_input, multiline_input
|
||||||
|
@ -38,7 +40,10 @@ from toot.utils.datetime import parse_datetime
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--visibility", "-v",
|
"--visibility", "-v",
|
||||||
help="Post visibility",
|
help="Post visibility: " + "; "
|
||||||
|
.join("{} = {}".format(visibility, description)
|
||||||
|
for visibility, caption, description in VISIBILITY_OPTIONS),
|
||||||
|
default=VISIBILITY_CHOICES[0],
|
||||||
type=click.Choice(VISIBILITY_CHOICES),
|
type=click.Choice(VISIBILITY_CHOICES),
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import click
|
import click
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import textwrap
|
import textwrap
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from toot.entities import Account, Instance, Notification, Poll, Status, List
|
from datetime import datetime, timezone
|
||||||
|
from importlib.metadata import version
|
||||||
|
from wcwidth import wcswidth
|
||||||
|
|
||||||
|
from toot import __version__, config, settings
|
||||||
|
from toot.entities import Account, Data, Instance, Notification, Poll, Status, List
|
||||||
from toot.utils import get_text, html_to_paragraphs
|
from toot.utils import get_text, html_to_paragraphs
|
||||||
from toot.wcstring import wc_wrap
|
from toot.wcstring import wc_wrap
|
||||||
from wcwidth import wcswidth
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_WIDTH = 80
|
DEFAULT_WIDTH = 80
|
||||||
|
@ -154,8 +159,9 @@ def print_list_accounts(accounts):
|
||||||
|
|
||||||
|
|
||||||
def print_search_results(results):
|
def print_search_results(results):
|
||||||
accounts = results["accounts"]
|
accounts = results.get("accounts")
|
||||||
hashtags = results["hashtags"]
|
hashtags = results.get("hashtags")
|
||||||
|
statuses = results.get("statuses")
|
||||||
|
|
||||||
if accounts:
|
if accounts:
|
||||||
click.echo("\nAccounts:")
|
click.echo("\nAccounts:")
|
||||||
|
@ -165,7 +171,12 @@ def print_search_results(results):
|
||||||
click.echo("\nHashtags:")
|
click.echo("\nHashtags:")
|
||||||
click.echo(", ".join([format_tag_name(tag) for tag in hashtags]))
|
click.echo(", ".join([format_tag_name(tag) for tag in hashtags]))
|
||||||
|
|
||||||
if not accounts and not hashtags:
|
if statuses:
|
||||||
|
click.echo("\nStatuses:")
|
||||||
|
for status in statuses:
|
||||||
|
click.echo(f" * {green(status['id'])} {status['url']}")
|
||||||
|
|
||||||
|
if not accounts and not hashtags and not statuses:
|
||||||
click.echo("Nothing found")
|
click.echo("Nothing found")
|
||||||
|
|
||||||
|
|
||||||
|
@ -314,6 +325,78 @@ def format_account_name(account: Account) -> str:
|
||||||
return acct
|
return acct
|
||||||
|
|
||||||
|
|
||||||
|
def print_diags(instance_dict: t.Optional[Data], include_files: bool):
|
||||||
|
click.echo(f'{green("## Toot Diagnostics")}')
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
click.echo(f'{green("Current Date/Time:")} {now.strftime("%Y-%m-%d %H:%M:%S %Z")}')
|
||||||
|
|
||||||
|
click.echo(f'{green("Toot version:")} {__version__}')
|
||||||
|
click.echo(f'{green("Platform:")} {platform.platform()}')
|
||||||
|
|
||||||
|
# print distro - only call if available (python 3.10+)
|
||||||
|
fd_os_release = getattr(platform, "freedesktop_os_release", None) # novermin
|
||||||
|
if callable(fd_os_release): # novermin
|
||||||
|
try:
|
||||||
|
name = platform.freedesktop_os_release()['PRETTY_NAME']
|
||||||
|
click.echo(f'{green("Distro:")} {name}')
|
||||||
|
except: # noqa
|
||||||
|
pass
|
||||||
|
|
||||||
|
click.echo(f'{green("Python version:")} {platform.python_version()}')
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
click.echo(green("Dependency versions:"))
|
||||||
|
|
||||||
|
deps = sorted(['beautifulsoup4', 'click', 'requests', 'tomlkit', 'urwid', 'wcwidth',
|
||||||
|
'pillow', 'term-image', 'urwidgets', 'flake8', 'pytest', 'setuptools',
|
||||||
|
'vermin', 'typing-extensions'])
|
||||||
|
|
||||||
|
for dep in deps:
|
||||||
|
try:
|
||||||
|
ver = version(dep)
|
||||||
|
except: # noqa
|
||||||
|
ver = yellow("not installed")
|
||||||
|
|
||||||
|
click.echo(f" * {dep}: {ver}")
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
click.echo(f'{green("Settings file path:")} {settings.get_settings_path()}')
|
||||||
|
click.echo(f'{green("Config file path:")} {config.get_config_file_path()}')
|
||||||
|
|
||||||
|
if instance_dict:
|
||||||
|
click.echo(f'{green("Server URI:")} {instance_dict.get("uri")}')
|
||||||
|
click.echo(f'{green("Server version:")} {instance_dict.get("version")}')
|
||||||
|
|
||||||
|
if include_files:
|
||||||
|
click.echo(f'{green("Settings file contents:")}')
|
||||||
|
try:
|
||||||
|
with open(settings.get_settings_path(), 'r') as f:
|
||||||
|
print("```toml")
|
||||||
|
print(f.read())
|
||||||
|
print("```")
|
||||||
|
except: # noqa
|
||||||
|
click.echo(f'{yellow("Could not open settings file")}')
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
click.echo(f'{green("Config file contents:")}')
|
||||||
|
click.echo("```json")
|
||||||
|
try:
|
||||||
|
with open(config.get_config_file_path(), 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
# Do not output client secret or access token lines
|
||||||
|
if "client_" in line or "token" in line:
|
||||||
|
click.echo(f'{yellow("***CONTENTS REDACTED***")}')
|
||||||
|
else:
|
||||||
|
click.echo(line, nl=False)
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
except: # noqa
|
||||||
|
click.echo(f'{yellow("Could not open config file")}')
|
||||||
|
click.echo("```")
|
||||||
|
|
||||||
|
|
||||||
# Shorthand functions for coloring output
|
# Shorthand functions for coloring output
|
||||||
|
|
||||||
def blue(text: t.Any) -> str:
|
def blue(text: t.Any) -> str:
|
||||||
|
|
|
@ -484,15 +484,16 @@ class StatusDetails(urwid.Pile):
|
||||||
yield self.image_widget(m["url"], aspect=aspect)
|
yield self.image_widget(m["url"], aspect=aspect)
|
||||||
yield urwid.Divider()
|
yield urwid.Divider()
|
||||||
# video media may include a preview URL, show that as a fallback
|
# video media may include a preview URL, show that as a fallback
|
||||||
elif m["preview_url"].lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp')):
|
elif m["preview_url"]:
|
||||||
yield urwid.Text("")
|
if m["preview_url"].lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp')):
|
||||||
try:
|
yield urwid.Text("")
|
||||||
aspect = float(m["meta"]["small"]["aspect"])
|
try:
|
||||||
except Exception:
|
aspect = float(m["meta"]["small"]["aspect"])
|
||||||
aspect = None
|
except Exception:
|
||||||
if image_support_enabled():
|
aspect = None
|
||||||
yield self.image_widget(m["preview_url"], aspect=aspect)
|
if image_support_enabled():
|
||||||
yield urwid.Divider()
|
yield self.image_widget(m["preview_url"], aspect=aspect)
|
||||||
|
yield urwid.Divider()
|
||||||
yield ("pack", url_to_widget(m["url"]))
|
yield ("pack", url_to_widget(m["url"]))
|
||||||
|
|
||||||
poll = status.original.data.get("poll")
|
poll = status.original.data.get("poll")
|
||||||
|
|
Ładowanie…
Reference in New Issue