diff --git a/.flake8 b/.flake8 index ac93b71..8d01264 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -exclude=build,tests,tmp,venv,toot/tui/scroll.py +exclude=build,tests,tmp,venv,_env,toot/tui/scroll.py ignore=E128,W503,W504 max-line-length=120 diff --git a/CHANGELOG.md b/CHANGELOG.md index f151780..0d92e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ Changelog +**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)** * TUI: Support displaying images (thanks Dan Schwarz) diff --git a/README.rst b/README.rst index be20e19..1235c34 100644 --- a/README.rst +++ b/README.rst @@ -37,11 +37,18 @@ Terminal User Interface toot includes a terminal user interface (TUI). Run it with ``toot tui``. +TUI Features: +------------- + +* Block graphic image display (requires optional libraries `pillow `, `term-image `, and `urwidgets `) +* Bitmapped image display in `kitty ` terminal ``toot tui -f kitty`` +* Bitmapped image display in `iTerm2 `, or `WezTerm ` 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_compose.png - License ------- diff --git a/changelog.yaml b/changelog.yaml index 3bf04c6..60e75bd 100644 --- a/changelog.yaml +++ b/changelog.yaml @@ -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: - date: TBA + date: 2024-08-12 changes: - "**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: date: 2024-04-13 diff --git a/docs/changelog.md b/docs/changelog.md index f151780..0d92e46 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,20 @@ Changelog +**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)** * TUI: Support displaying images (thanks Dan Schwarz) diff --git a/docs/images/tui_compose.png b/docs/images/tui_compose.png index 0a5ca14..2baaf48 100644 Binary files a/docs/images/tui_compose.png and b/docs/images/tui_compose.png differ diff --git a/docs/images/tui_list.png b/docs/images/tui_list.png index db7e593..6dd4f7e 100644 Binary files a/docs/images/tui_list.png and b/docs/images/tui_list.png differ diff --git a/docs/images/tui_poll.png b/docs/images/tui_poll.png deleted file mode 100644 index 9886ec9..0000000 Binary files a/docs/images/tui_poll.png and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index d65bcc1..d7055b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,5 +81,4 @@ packages=[ [tool.setuptools_scm] [tool.pyright] -typeCheckingMode = "strict" -pythonVersion = "3.8" \ No newline at end of file +pythonVersion = "3.8" diff --git a/toot/api.py b/toot/api.py index 01dcc74..7bd2032 100644 --- a/toot/api.py +++ b/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: + status_id = _resolve_status_id(app, user, status_id) url = f"/api/v1/statuses/{status_id}/{action}" 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: url = f"/api/v1/tags/{tag_name}/{action}" return http.post(app, user, url) diff --git a/toot/cli/__init__.py b/toot/cli/__init__.py index a4698ff..f525841 100644 --- a/toot/cli/__init__.py +++ b/toot/cli/__init__.py @@ -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 auth # noqa +from toot.cli import diag # noqa from toot.cli import lists # noqa from toot.cli import post # noqa from toot.cli import read # noqa diff --git a/toot/cli/diag.py b/toot/cli/diag.py new file mode 100644 index 0000000..fed0079 --- /dev/null +++ b/toot/cli/diag.py @@ -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) diff --git a/toot/cli/post.py b/toot/cli/post.py index f1acb2b..820aab6 100644 --- a/toot/cli/post.py +++ b/toot/cli/post.py @@ -9,6 +9,8 @@ from typing import BinaryIO, Optional, Tuple from toot import api, config from toot.cli import AccountParamType, cli, json_option, pass_context, Context 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.entities import MediaAttachment, from_dict 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( "--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), ) @click.option( diff --git a/toot/output.py b/toot/output.py index e9b0812..8f2715f 100644 --- a/toot/output.py +++ b/toot/output.py @@ -1,13 +1,18 @@ import click +import platform import re import shutil import textwrap 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.wcstring import wc_wrap -from wcwidth import wcswidth DEFAULT_WIDTH = 80 @@ -154,8 +159,9 @@ def print_list_accounts(accounts): def print_search_results(results): - accounts = results["accounts"] - hashtags = results["hashtags"] + accounts = results.get("accounts") + hashtags = results.get("hashtags") + statuses = results.get("statuses") if accounts: click.echo("\nAccounts:") @@ -165,7 +171,12 @@ def print_search_results(results): click.echo("\nHashtags:") 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") @@ -314,6 +325,78 @@ def format_account_name(account: Account) -> str: 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 def blue(text: t.Any) -> str: diff --git a/toot/tui/timeline.py b/toot/tui/timeline.py index 018ca40..1aa4936 100644 --- a/toot/tui/timeline.py +++ b/toot/tui/timeline.py @@ -484,15 +484,16 @@ class StatusDetails(urwid.Pile): yield self.image_widget(m["url"], aspect=aspect) yield urwid.Divider() # video media may include a preview URL, show that as a fallback - elif m["preview_url"].lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp')): - yield urwid.Text("") - try: - aspect = float(m["meta"]["small"]["aspect"]) - except Exception: - aspect = None - if image_support_enabled(): - yield self.image_widget(m["preview_url"], aspect=aspect) - yield urwid.Divider() + elif m["preview_url"]: + if m["preview_url"].lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp')): + yield urwid.Text("") + try: + aspect = float(m["meta"]["small"]["aspect"]) + except Exception: + aspect = None + if image_support_enabled(): + yield self.image_widget(m["preview_url"], aspect=aspect) + yield urwid.Divider() yield ("pack", url_to_widget(m["url"])) poll = status.original.data.get("poll")