Merge branch 'master' into images
This commit is contained in:
commit
b94c500c9c
46
CHANGELOG.md
46
CHANGELOG.md
|
@ -3,20 +3,42 @@ Changelog
|
||||||
|
|
||||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||||
|
|
||||||
**0.40.0 (TBA)**
|
**0.41.1 (2024-01-02)**
|
||||||
|
|
||||||
This release includes a major rewrite to use
|
* Fix a crash in settings parsing code
|
||||||
[Click](https://click.palletsprojects.com/) for creating the command line
|
|
||||||
interface. This allows for some new features like nested commands, setting
|
**0.41.0 (2024-01-02)**
|
||||||
parameters via environment variables, and shell completion. See docs for
|
|
||||||
details. Backward compatibility should be mostly preserved, except for cases
|
* Honour user's default visibility set in Mastodon preferences instead of always
|
||||||
noted below please report any issues.
|
defaulting to public visibility (thanks Lexi Winter)
|
||||||
|
* TUI: Add editing toots (thanks Lexi Winter)
|
||||||
|
* TUI: Fix a bug which made pallette config in settings not work
|
||||||
|
* TUI: Show edit datetime in status detail (thanks Lexi Winter)
|
||||||
|
|
||||||
|
**0.40.2 (2023-12-28)**
|
||||||
|
|
||||||
|
* Reinstate `toot post --using` option.
|
||||||
|
* Add shell completion for instances.
|
||||||
|
|
||||||
|
**0.40.1 (2023-12-28)**
|
||||||
|
|
||||||
|
* Add `toot --as` option to replace `toot post --using`. This now works for all
|
||||||
|
commands.
|
||||||
|
|
||||||
|
**0.40.0 (2023-12-27)**
|
||||||
|
|
||||||
|
This release includes a rather extensive change to use the Click library
|
||||||
|
(https://click.palletsprojects.com/) for creating the command line interface.
|
||||||
|
This allows for some new features like nested commands, setting parameters via
|
||||||
|
environment variables, and shell completion. Backward compatibility should be
|
||||||
|
mostly preserved, except for cases noted below. Please report any issues.
|
||||||
|
|
||||||
* BREAKING: Remove deprecated `--disable-https` option for `login` and
|
* BREAKING: Remove deprecated `--disable-https` option for `login` and
|
||||||
`login_cli`, pass the base URL instead
|
`login_cli`, pass the base URL instead
|
||||||
* BREAKING: Options `--debug`, `--color`, `--quiet` must be specified after
|
* BREAKING: Options `--debug` and `--color` must be specified after `toot` but
|
||||||
`toot` but before the command
|
before the command
|
||||||
* Enable passing params via environment variables, see:
|
* BREAKING: Option `--quiet` has been removed. Redirect output instead.
|
||||||
|
* Add passing parameters via environment variables, see:
|
||||||
https://toot.bezdomni.net/environment_variables.html
|
https://toot.bezdomni.net/environment_variables.html
|
||||||
* Add shell completion, see: https://toot.bezdomni.net/shell_completion.html
|
* Add shell completion, see: https://toot.bezdomni.net/shell_completion.html
|
||||||
* Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature`
|
* Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature`
|
||||||
|
@ -26,11 +48,11 @@ noted below please report any issues.
|
||||||
* Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists
|
* Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists
|
||||||
list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`,
|
list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`,
|
||||||
`lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands.
|
`lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands.
|
||||||
* Add `--json` option to tags commands
|
* Add `--json` option to tags and lists commands
|
||||||
* Add `--json` option to lists commands
|
|
||||||
* Add `toot --width` option for setting your prefered terminal width
|
* Add `toot --width` option for setting your prefered terminal width
|
||||||
* Add `--media-viewer` and `--colors` options to `toot tui`. These were
|
* Add `--media-viewer` and `--colors` options to `toot tui`. These were
|
||||||
previously accessible only via settings.
|
previously accessible only via settings.
|
||||||
|
* TUI: Fix issue where UI did not render until first input (thanks Urwid devs)
|
||||||
|
|
||||||
**0.39.0 (2023-11-23)**
|
**0.39.0 (2023-11-23)**
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,49 @@
|
||||||
|
0.41.1:
|
||||||
|
date: 2024-01-02
|
||||||
|
changes:
|
||||||
|
- "Fix a crash in settings parsing code"
|
||||||
|
|
||||||
|
0.41.0:
|
||||||
|
date: 2024-01-02
|
||||||
|
changes:
|
||||||
|
- "Honour user's default visibility set in Mastodon preferences instead of always defaulting to public visibility (thanks Lexi Winter)"
|
||||||
|
- "TUI: Add editing toots (thanks Lexi Winter)"
|
||||||
|
- "TUI: Fix a bug which made pallette config in settings not work"
|
||||||
|
- "TUI: Show edit datetime in status detail (thanks Lexi Winter)"
|
||||||
|
|
||||||
|
0.40.2:
|
||||||
|
date: 2023-12-28
|
||||||
|
changes:
|
||||||
|
- "Reinstate `toot post --using` option."
|
||||||
|
- "Add shell completion for instances."
|
||||||
|
|
||||||
|
0.40.1:
|
||||||
|
date: 2023-12-28
|
||||||
|
changes:
|
||||||
|
- "Add `toot --as` option to replace `toot post --using`. This now works for all commands."
|
||||||
|
|
||||||
0.40.0:
|
0.40.0:
|
||||||
date: TBA
|
date: 2023-12-27
|
||||||
description: |
|
description: |
|
||||||
This release includes a major rewrite to use [Click](https://click.palletsprojects.com/) for
|
This release includes a rather extensive change to use the Click library
|
||||||
creating the command line interface. This allows for some new features like nested commands,
|
(https://click.palletsprojects.com/) for creating the command line
|
||||||
setting parameters via environment variables, and shell completion. See docs for details.
|
interface. This allows for some new features like nested commands, setting
|
||||||
Backward compatibility should be mostly preserved, except for cases noted below please report
|
parameters via environment variables, and shell completion. Backward
|
||||||
any issues.
|
compatibility should be mostly preserved, except for cases noted below.
|
||||||
|
Please report any issues.
|
||||||
changes:
|
changes:
|
||||||
- "BREAKING: Remove deprecated `--disable-https` option for `login` and `login_cli`, pass the base URL instead"
|
- "BREAKING: Remove deprecated `--disable-https` option for `login` and `login_cli`, pass the base URL instead"
|
||||||
- "BREAKING: Options `--debug`, `--color`, `--quiet` must be specified after `toot` but before the command"
|
- "BREAKING: Options `--debug` and `--color` must be specified after `toot` but before the command"
|
||||||
- "Enable passing params via environment variables, see: https://toot.bezdomni.net/environment_variables.html"
|
- "BREAKING: Option `--quiet` has been removed. Redirect output instead."
|
||||||
|
- "Add passing parameters via environment variables, see: https://toot.bezdomni.net/environment_variables.html"
|
||||||
- "Add shell completion, see: https://toot.bezdomni.net/shell_completion.html"
|
- "Add shell completion, see: https://toot.bezdomni.net/shell_completion.html"
|
||||||
- "Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature` commands"
|
- "Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature` commands"
|
||||||
- "Add `tags followed`, `tags follow`, and `tags unfollow` sub-commands, deprecate `tags_followed`, `tags_follow`, and `tags tags_unfollow`"
|
- "Add `tags followed`, `tags follow`, and `tags unfollow` sub-commands, deprecate `tags_followed`, `tags_follow`, and `tags tags_unfollow`"
|
||||||
- "Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`, `lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands."
|
- "Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`, `lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands."
|
||||||
- "Add `--json` option to tags commands"
|
- "Add `--json` option to tags and lists commands"
|
||||||
- "Add `--json` option to lists commands"
|
|
||||||
- "Add `toot --width` option for setting your prefered terminal width"
|
- "Add `toot --width` option for setting your prefered terminal width"
|
||||||
- "Add `--media-viewer` and `--colors` options to `toot tui`. These were previously accessible only via settings."
|
- "Add `--media-viewer` and `--colors` options to `toot tui`. These were previously accessible only via settings."
|
||||||
|
- "TUI: Fix issue where UI did not render until first input (thanks Urwid devs)"
|
||||||
|
|
||||||
0.39.0:
|
0.39.0:
|
||||||
date: 2023-11-23
|
date: 2023-11-23
|
||||||
|
|
|
@ -3,20 +3,42 @@ Changelog
|
||||||
|
|
||||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||||
|
|
||||||
**0.40.0 (TBA)**
|
**0.41.1 (2024-01-02)**
|
||||||
|
|
||||||
This release includes a major rewrite to use
|
* Fix a crash in settings parsing code
|
||||||
[Click](https://click.palletsprojects.com/) for creating the command line
|
|
||||||
interface. This allows for some new features like nested commands, setting
|
**0.41.0 (2024-01-02)**
|
||||||
parameters via environment variables, and shell completion. See docs for
|
|
||||||
details. Backward compatibility should be mostly preserved, except for cases
|
* Honour user's default visibility set in Mastodon preferences instead of always
|
||||||
noted below please report any issues.
|
defaulting to public visibility (thanks Lexi Winter)
|
||||||
|
* TUI: Add editing toots (thanks Lexi Winter)
|
||||||
|
* TUI: Fix a bug which made pallette config in settings not work
|
||||||
|
* TUI: Show edit datetime in status detail (thanks Lexi Winter)
|
||||||
|
|
||||||
|
**0.40.2 (2023-12-28)**
|
||||||
|
|
||||||
|
* Reinstate `toot post --using` option.
|
||||||
|
* Add shell completion for instances.
|
||||||
|
|
||||||
|
**0.40.1 (2023-12-28)**
|
||||||
|
|
||||||
|
* Add `toot --as` option to replace `toot post --using`. This now works for all
|
||||||
|
commands.
|
||||||
|
|
||||||
|
**0.40.0 (2023-12-27)**
|
||||||
|
|
||||||
|
This release includes a rather extensive change to use the Click library
|
||||||
|
(https://click.palletsprojects.com/) for creating the command line interface.
|
||||||
|
This allows for some new features like nested commands, setting parameters via
|
||||||
|
environment variables, and shell completion. Backward compatibility should be
|
||||||
|
mostly preserved, except for cases noted below. Please report any issues.
|
||||||
|
|
||||||
* BREAKING: Remove deprecated `--disable-https` option for `login` and
|
* BREAKING: Remove deprecated `--disable-https` option for `login` and
|
||||||
`login_cli`, pass the base URL instead
|
`login_cli`, pass the base URL instead
|
||||||
* BREAKING: Options `--debug`, `--color`, `--quiet` must be specified after
|
* BREAKING: Options `--debug` and `--color` must be specified after `toot` but
|
||||||
`toot` but before the command
|
before the command
|
||||||
* Enable passing params via environment variables, see:
|
* BREAKING: Option `--quiet` has been removed. Redirect output instead.
|
||||||
|
* Add passing parameters via environment variables, see:
|
||||||
https://toot.bezdomni.net/environment_variables.html
|
https://toot.bezdomni.net/environment_variables.html
|
||||||
* Add shell completion, see: https://toot.bezdomni.net/shell_completion.html
|
* Add shell completion, see: https://toot.bezdomni.net/shell_completion.html
|
||||||
* Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature`
|
* Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature`
|
||||||
|
@ -26,11 +48,11 @@ noted below please report any issues.
|
||||||
* Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists
|
* Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists
|
||||||
list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`,
|
list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`,
|
||||||
`lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands.
|
`lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands.
|
||||||
* Add `--json` option to tags commands
|
* Add `--json` option to tags and lists commands
|
||||||
* Add `--json` option to lists commands
|
|
||||||
* Add `toot --width` option for setting your prefered terminal width
|
* Add `toot --width` option for setting your prefered terminal width
|
||||||
* Add `--media-viewer` and `--colors` options to `toot tui`. These were
|
* Add `--media-viewer` and `--colors` options to `toot tui`. These were
|
||||||
previously accessible only via settings.
|
previously accessible only via settings.
|
||||||
|
* TUI: Fix issue where UI did not render until first input (thanks Urwid devs)
|
||||||
|
|
||||||
**0.39.0 (2023-11-23)**
|
**0.39.0 (2023-11-23)**
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ if dist_version != version:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
release_date = changelog_item["date"]
|
release_date = changelog_item["date"]
|
||||||
|
description = changelog_item.get("description")
|
||||||
changes = changelog_item["changes"]
|
changes = changelog_item["changes"]
|
||||||
|
|
||||||
if not isinstance(release_date, date):
|
if not isinstance(release_date, date):
|
||||||
|
@ -50,6 +51,11 @@ if not isinstance(release_date, date):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
commit_message = f"toot {version}\n\n"
|
commit_message = f"toot {version}\n\n"
|
||||||
|
|
||||||
|
if description:
|
||||||
|
lines = textwrap.wrap(description.strip(), 72)
|
||||||
|
commit_message += "\n".join(lines) + "\n\n"
|
||||||
|
|
||||||
for c in changes:
|
for c in changes:
|
||||||
lines = textwrap.wrap(c, 70)
|
lines = textwrap.wrap(c, 70)
|
||||||
initial = True
|
initial = True
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -12,7 +12,7 @@ and blocking accounts and other actions.
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='toot',
|
name='toot',
|
||||||
version='0.40.0',
|
version='0.41.1',
|
||||||
description='Mastodon CLI client',
|
description='Mastodon CLI client',
|
||||||
long_description=long_description.strip(),
|
long_description=long_description.strip(),
|
||||||
author='Ivan Habunek',
|
author='Ivan Habunek',
|
||||||
|
|
|
@ -9,15 +9,14 @@ your test server and database:
|
||||||
|
|
||||||
```
|
```
|
||||||
export TOOT_TEST_BASE_URL="localhost:3000"
|
export TOOT_TEST_BASE_URL="localhost:3000"
|
||||||
export TOOT_TEST_DATABASE_DSN="dbname=mastodon_development"
|
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
import psycopg2
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import re
|
||||||
|
import typing as t
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from click.testing import CliRunner, Result
|
from click.testing import CliRunner, Result
|
||||||
|
@ -31,8 +30,10 @@ def pytest_configure(config):
|
||||||
toot.settings.DISABLE_SETTINGS = True
|
toot.settings.DISABLE_SETTINGS = True
|
||||||
|
|
||||||
|
|
||||||
|
# Type alias for run commands
|
||||||
|
Run = t.Callable[..., Result]
|
||||||
|
|
||||||
# Mastodon database name, used to confirm user registration without having to click the link
|
# Mastodon database name, used to confirm user registration without having to click the link
|
||||||
DATABASE_DSN = os.getenv("TOOT_TEST_DATABASE_DSN")
|
|
||||||
TOOT_TEST_BASE_URL = os.getenv("TOOT_TEST_BASE_URL")
|
TOOT_TEST_BASE_URL = os.getenv("TOOT_TEST_BASE_URL")
|
||||||
|
|
||||||
# Toot logo used for testing image upload
|
# Toot logo used for testing image upload
|
||||||
|
@ -52,17 +53,9 @@ def register_account(app: App):
|
||||||
email = f"{username}@example.com"
|
email = f"{username}@example.com"
|
||||||
|
|
||||||
response = api.register_account(app, username, email, "password", "en")
|
response = api.register_account(app, username, email, "password", "en")
|
||||||
confirm_user(email)
|
|
||||||
return User(app.instance, username, response["access_token"])
|
return User(app.instance, username, response["access_token"])
|
||||||
|
|
||||||
|
|
||||||
def confirm_user(email):
|
|
||||||
conn = psycopg2.connect(DATABASE_DSN)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("UPDATE users SET confirmed_at = now() WHERE email = %s;", (email,))
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Fixtures
|
# Fixtures
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -3,7 +3,7 @@ from unittest import mock
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from toot import User, cli
|
from toot import User, cli
|
||||||
from toot.cli import Run
|
from tests.integration.conftest import Run
|
||||||
|
|
||||||
# TODO: figure out how to test login
|
# TODO: figure out how to test login
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from tests.utils import run_with_retries
|
||||||
from toot import api, cli
|
from toot import api, cli
|
||||||
from toot.exceptions import NotFoundError
|
from toot.exceptions import NotFoundError
|
||||||
|
|
||||||
|
@ -46,11 +46,11 @@ def test_favourite(app, user, run):
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert result.stdout.strip() == "✓ Status unfavourited"
|
assert result.stdout.strip() == "✓ Status unfavourited"
|
||||||
|
|
||||||
# A short delay is required before the server returns new data
|
def test_favourited():
|
||||||
time.sleep(0.2)
|
nonlocal status
|
||||||
|
status = api.fetch_status(app, user, status["id"]).json()
|
||||||
status = api.fetch_status(app, user, status["id"]).json()
|
assert not status["favourited"]
|
||||||
assert not status["favourited"]
|
run_with_retries(test_favourited)
|
||||||
|
|
||||||
|
|
||||||
def test_favourite_json(app, user, run):
|
def test_favourite_json(app, user, run):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from time import sleep
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from tests.utils import run_with_retries
|
||||||
|
|
||||||
from toot import api, cli
|
from toot import api, cli
|
||||||
from toot.entities import from_dict, Status
|
from toot.entities import from_dict, Status
|
||||||
|
@ -40,16 +40,14 @@ def test_timelines(app, user, other_user, friend_user, friend_list, run):
|
||||||
status2 = _post_status(app, other_user, "#bar")
|
status2 = _post_status(app, other_user, "#bar")
|
||||||
status3 = _post_status(app, friend_user, "#foo #bar")
|
status3 = _post_status(app, friend_user, "#foo #bar")
|
||||||
|
|
||||||
# Give mastodon time to process things :/
|
|
||||||
# Tests fail if this is removed, required delay depends on server speed
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# Home timeline
|
# Home timeline
|
||||||
result = run(cli.timelines.timeline)
|
def test_home():
|
||||||
assert result.exit_code == 0
|
result = run(cli.timelines.timeline)
|
||||||
assert status1.id in result.stdout
|
assert result.exit_code == 0
|
||||||
assert status2.id not in result.stdout
|
assert status1.id in result.stdout
|
||||||
assert status3.id in result.stdout
|
assert status2.id not in result.stdout
|
||||||
|
assert status3.id in result.stdout
|
||||||
|
run_with_retries(test_home)
|
||||||
|
|
||||||
# Public timeline
|
# Public timeline
|
||||||
result = run(cli.timelines.timeline, "--public")
|
result = run(cli.timelines.timeline, "--public")
|
||||||
|
@ -166,13 +164,14 @@ def test_notifications(app, user, other_user, run):
|
||||||
|
|
||||||
text = f"Paging doctor @{user.username}"
|
text = f"Paging doctor @{user.username}"
|
||||||
status = _post_status(app, other_user, text)
|
status = _post_status(app, other_user, text)
|
||||||
sleep(0.5) # grr
|
|
||||||
|
|
||||||
result = run(cli.timelines.notifications)
|
def test_notifications():
|
||||||
assert result.exit_code == 0
|
result = run(cli.timelines.notifications)
|
||||||
assert f"@{other_user.username} mentioned you" in result.stdout
|
assert result.exit_code == 0
|
||||||
assert status.id in result.stdout
|
assert f"@{other_user.username} mentioned you" in result.stdout
|
||||||
assert text in result.stdout
|
assert status.id in result.stdout
|
||||||
|
assert text in result.stdout
|
||||||
|
run_with_retries(test_notifications)
|
||||||
|
|
||||||
result = run(cli.timelines.notifications, "--mentions")
|
result = run(cli.timelines.notifications, "--mentions")
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
@ -186,7 +185,6 @@ def test_notifications_follow(app, user, friend_user, run_as):
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert f"@{user.username} now follows you" in result.stdout
|
assert f"@{user.username} now follows you" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
result = run_as(friend_user, cli.timelines.notifications, "--mentions")
|
result = run_as(friend_user, cli.timelines.notifications, "--mentions")
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "now follows you" not in result.stdout
|
assert "now follows you" not in result.stdout
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
Helpers for testing.
|
Helpers for testing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
|
||||||
class MockResponse:
|
class MockResponse:
|
||||||
def __init__(self, response_data={}, ok=True, is_redirect=False):
|
def __init__(self, response_data={}, ok=True, is_redirect=False):
|
||||||
|
@ -19,3 +22,23 @@ class MockResponse:
|
||||||
|
|
||||||
def retval(val):
|
def retval(val):
|
||||||
return lambda *args, **kwargs: val
|
return lambda *args, **kwargs: val
|
||||||
|
|
||||||
|
|
||||||
|
def run_with_retries(fn: Callable[..., Any]):
|
||||||
|
"""
|
||||||
|
Run the the given function repeatedly until it finishes without raising an
|
||||||
|
AssertionError. Sleep a bit between attempts. If the function doesn't
|
||||||
|
succeed in the given number of tries raises the AssertionError. Used for
|
||||||
|
tests which should eventually succeed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Wait upto 6 seconds with incrementally longer sleeps
|
||||||
|
delays = [0.1, 0.2, 0.3, 0.4, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
|
||||||
|
|
||||||
|
for delay in delays:
|
||||||
|
try:
|
||||||
|
return fn()
|
||||||
|
except AssertionError:
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
fn()
|
||||||
|
|
|
@ -4,7 +4,7 @@ import sys
|
||||||
from os.path import join, expanduser
|
from os.path import join, expanduser
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
__version__ = '0.40.0'
|
__version__ = '0.41.1'
|
||||||
|
|
||||||
|
|
||||||
class App(NamedTuple):
|
class App(NamedTuple):
|
||||||
|
|
61
toot/api.py
61
toot/api.py
|
@ -183,7 +183,7 @@ def post_status(
|
||||||
app,
|
app,
|
||||||
user,
|
user,
|
||||||
status,
|
status,
|
||||||
visibility='public',
|
visibility=None,
|
||||||
media_ids=None,
|
media_ids=None,
|
||||||
sensitive=False,
|
sensitive=False,
|
||||||
spoiler_text=None,
|
spoiler_text=None,
|
||||||
|
@ -230,6 +230,52 @@ def post_status(
|
||||||
return http.post(app, user, '/api/v1/statuses', json=data, headers=headers)
|
return http.post(app, user, '/api/v1/statuses', json=data, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
def edit_status(
|
||||||
|
app,
|
||||||
|
user,
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
visibility='public',
|
||||||
|
media_ids=None,
|
||||||
|
sensitive=False,
|
||||||
|
spoiler_text=None,
|
||||||
|
in_reply_to_id=None,
|
||||||
|
language=None,
|
||||||
|
content_type=None,
|
||||||
|
poll_options=None,
|
||||||
|
poll_expires_in=None,
|
||||||
|
poll_multiple=None,
|
||||||
|
poll_hide_totals=None,
|
||||||
|
) -> Response:
|
||||||
|
"""
|
||||||
|
Edit an existing status
|
||||||
|
https://docs.joinmastodon.org/methods/statuses/#edit
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Strip keys for which value is None
|
||||||
|
# Sending null values doesn't bother Mastodon, but it breaks Pleroma
|
||||||
|
data = drop_empty_values({
|
||||||
|
'status': status,
|
||||||
|
'media_ids': media_ids,
|
||||||
|
'visibility': visibility,
|
||||||
|
'sensitive': sensitive,
|
||||||
|
'in_reply_to_id': in_reply_to_id,
|
||||||
|
'language': language,
|
||||||
|
'content_type': content_type,
|
||||||
|
'spoiler_text': spoiler_text,
|
||||||
|
})
|
||||||
|
|
||||||
|
if poll_options:
|
||||||
|
data["poll"] = {
|
||||||
|
"options": poll_options,
|
||||||
|
"expires_in": poll_expires_in,
|
||||||
|
"multiple": poll_multiple,
|
||||||
|
"hide_totals": poll_hide_totals,
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.put(app, user, f"/api/v1/statuses/{id}", json=data)
|
||||||
|
|
||||||
|
|
||||||
def fetch_status(app, user, id):
|
def fetch_status(app, user, id):
|
||||||
"""
|
"""
|
||||||
Fetch a single status
|
Fetch a single status
|
||||||
|
@ -238,6 +284,15 @@ def fetch_status(app, user, id):
|
||||||
return http.get(app, user, f"/api/v1/statuses/{id}")
|
return http.get(app, user, f"/api/v1/statuses/{id}")
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_status_source(app, user, id):
|
||||||
|
"""
|
||||||
|
Fetch the source (original text) for a single status.
|
||||||
|
This only works on local toots.
|
||||||
|
https://docs.joinmastodon.org/methods/statuses/#source
|
||||||
|
"""
|
||||||
|
return http.get(app, user, f"/api/v1/statuses/{id}/source")
|
||||||
|
|
||||||
|
|
||||||
def scheduled_statuses(app, user):
|
def scheduled_statuses(app, user):
|
||||||
"""
|
"""
|
||||||
List scheduled statuses
|
List scheduled statuses
|
||||||
|
@ -618,6 +673,10 @@ def get_instance(base_url: str) -> Response:
|
||||||
return http.anon_get(url)
|
return http.anon_get(url)
|
||||||
|
|
||||||
|
|
||||||
|
def get_preferences(app, user) -> Response:
|
||||||
|
return http.get(app, user, '/api/v1/preferences')
|
||||||
|
|
||||||
|
|
||||||
def get_lists(app, user):
|
def get_lists(app, user):
|
||||||
return http.get(app, user, "/api/v1/lists").json()
|
return http.get(app, user, "/api/v1/lists").json()
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,12 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
from click.testing import Result
|
from click.shell_completion import CompletionItem
|
||||||
|
from click.types import StringParamType
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from toot import App, User, config, __version__
|
from toot import App, User, config, __version__
|
||||||
|
from toot.output import print_warning
|
||||||
from toot.settings import get_settings
|
from toot.settings import get_settings
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
|
@ -35,10 +38,6 @@ DURATION_EXAMPLES = """e.g. "1 day", "2 hours 30 minutes", "5 minutes 30
|
||||||
seconds" or any combination of above. Shorthand: "1d", "2h30m", "5m30s\""""
|
seconds" or any combination of above. Shorthand: "1d", "2h30m", "5m30s\""""
|
||||||
|
|
||||||
|
|
||||||
# Type alias for run commands
|
|
||||||
Run = t.Callable[..., Result]
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_visibility() -> str:
|
def get_default_visibility() -> str:
|
||||||
return os.getenv("TOOT_POST_VISIBILITY", "public")
|
return os.getenv("TOOT_POST_VISIBILITY", "public")
|
||||||
|
|
||||||
|
@ -47,6 +46,17 @@ def get_default_map():
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
common = settings.get("common", {})
|
common = settings.get("common", {})
|
||||||
commands = settings.get("commands", {})
|
commands = settings.get("commands", {})
|
||||||
|
|
||||||
|
# TODO: remove in version 1.0
|
||||||
|
tui_old = settings.get("tui", {}).copy()
|
||||||
|
if "palette" in tui_old:
|
||||||
|
del tui_old["palette"]
|
||||||
|
if tui_old:
|
||||||
|
# TODO: don't show the warning for [toot.palette]
|
||||||
|
print_warning("Settings section [tui] has been deprecated in favour of [commands.tui].")
|
||||||
|
tui_new = commands.get("tui", {})
|
||||||
|
commands["tui"] = {**tui_old, **tui_new}
|
||||||
|
|
||||||
return {**common, **commands}
|
return {**common, **commands}
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,18 +79,44 @@ class Context(t.NamedTuple):
|
||||||
user: t.Optional[User] = None
|
user: t.Optional[User] = None
|
||||||
color: bool = False
|
color: bool = False
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
quiet: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class TootObj(t.NamedTuple):
|
class TootObj(t.NamedTuple):
|
||||||
"""Data to add to Click context"""
|
"""Data to add to Click context"""
|
||||||
color: bool = True
|
color: bool = True
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
quiet: bool = False
|
as_user: t.Optional[str] = None
|
||||||
# Pass a context for testing purposes
|
# Pass a context for testing purposes
|
||||||
test_ctx: t.Optional[Context] = None
|
test_ctx: t.Optional[Context] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AccountParamType(StringParamType):
|
||||||
|
"""Custom type to add shell completion for account names"""
|
||||||
|
name = "account"
|
||||||
|
|
||||||
|
def shell_complete(self, ctx, param, incomplete: str):
|
||||||
|
users = config.load_config()["users"].keys()
|
||||||
|
return [
|
||||||
|
CompletionItem(u)
|
||||||
|
for u in users
|
||||||
|
if u.lower().startswith(incomplete.lower())
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceParamType(StringParamType):
|
||||||
|
"""Custom type to add shell completion for instance domains"""
|
||||||
|
name = "instance"
|
||||||
|
|
||||||
|
def shell_complete(self, ctx, param, incomplete: str):
|
||||||
|
apps = config.load_config()["apps"]
|
||||||
|
|
||||||
|
return [
|
||||||
|
CompletionItem(i)
|
||||||
|
for i in apps.keys()
|
||||||
|
if i.lower().startswith(incomplete.lower())
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def pass_context(f: "t.Callable[te.Concatenate[Context, P], R]") -> "t.Callable[P, R]":
|
def pass_context(f: "t.Callable[te.Concatenate[Context, P], R]") -> "t.Callable[P, R]":
|
||||||
"""Pass the toot Context as first argument."""
|
"""Pass the toot Context as first argument."""
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
|
@ -98,11 +134,16 @@ def get_context() -> Context:
|
||||||
if obj.test_ctx:
|
if obj.test_ctx:
|
||||||
return obj.test_ctx
|
return obj.test_ctx
|
||||||
|
|
||||||
user, app = config.get_active_user_app()
|
if obj.as_user:
|
||||||
if not user or not app:
|
user, app = config.get_user_app(obj.as_user)
|
||||||
raise click.ClickException("This command requires you to be logged in.")
|
if not user or not app:
|
||||||
|
raise click.ClickException(f"Account '{obj.as_user}' not found. Run `toot auth` to see available accounts.")
|
||||||
|
else:
|
||||||
|
user, app = config.get_active_user_app()
|
||||||
|
if not user or not app:
|
||||||
|
raise click.ClickException("This command requires you to be logged in.")
|
||||||
|
|
||||||
return Context(app, user, obj.color, obj.debug, obj.quiet)
|
return Context(app, user, obj.color, obj.debug)
|
||||||
|
|
||||||
|
|
||||||
json_option = click.option(
|
json_option = click.option(
|
||||||
|
@ -117,12 +158,12 @@ json_option = click.option(
|
||||||
@click.option("-w", "--max-width", type=int, default=80, help="Maximum width for content rendered by toot")
|
@click.option("-w", "--max-width", type=int, default=80, help="Maximum width for content rendered by toot")
|
||||||
@click.option("--debug/--no-debug", default=False, help="Log debug info to stderr")
|
@click.option("--debug/--no-debug", default=False, help="Log debug info to stderr")
|
||||||
@click.option("--color/--no-color", default=sys.stdout.isatty(), help="Use ANSI color in output")
|
@click.option("--color/--no-color", default=sys.stdout.isatty(), help="Use ANSI color in output")
|
||||||
@click.option("--quiet/--no-quiet", default=False, help="Don't print anything to stdout")
|
@click.option("--as", "as_user", type=AccountParamType(), help="The account to use, overrides the active account.")
|
||||||
@click.version_option(__version__, message="%(prog)s v%(version)s")
|
@click.version_option(__version__, message="%(prog)s v%(version)s")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx: click.Context, max_width: int, color: bool, debug: bool, quiet: bool):
|
def cli(ctx: click.Context, max_width: int, color: bool, debug: bool, as_user: str):
|
||||||
"""Toot is a Mastodon CLI"""
|
"""Toot is a Mastodon CLI"""
|
||||||
ctx.obj = TootObj(color, debug, quiet)
|
ctx.obj = TootObj(color, debug, as_user)
|
||||||
ctx.color = color
|
ctx.color = color
|
||||||
ctx.max_content_width = max_width
|
ctx.max_content_width = max_width
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,10 @@ import click
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from click.shell_completion import CompletionItem
|
|
||||||
|
|
||||||
from click.types import StringParamType
|
|
||||||
|
|
||||||
from toot import api, config, __version__
|
from toot import api, config, __version__
|
||||||
from toot.auth import get_or_create_app, login_auth_code, login_username_password
|
from toot.auth import get_or_create_app, login_auth_code, login_username_password
|
||||||
from toot.cli import cli
|
from toot.cli import AccountParamType, cli
|
||||||
from toot.cli.validators import validate_instance
|
from toot.cli.validators import validate_instance
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,18 +19,6 @@ instance_option = click.option(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AccountParamType(StringParamType):
|
|
||||||
"""Custom type to add shell completion for account names"""
|
|
||||||
|
|
||||||
def shell_complete(self, ctx, param, incomplete: str):
|
|
||||||
accounts = config.load_config()["users"].keys()
|
|
||||||
return [
|
|
||||||
CompletionItem(a)
|
|
||||||
for a in accounts
|
|
||||||
if a.lower().startswith(incomplete.lower())
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def auth():
|
def auth():
|
||||||
"""Show logged in accounts and instances"""
|
"""Show logged in accounts and instances"""
|
||||||
|
|
|
@ -11,7 +11,8 @@ from toot.output import print_list_accounts, print_lists, print_warning
|
||||||
def lists(ctx: click.Context):
|
def lists(ctx: click.Context):
|
||||||
"""Display and manage lists"""
|
"""Display and manage lists"""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
print_warning("`toot lists` is deprecated in favour of `toot lists list`")
|
print_warning("`toot lists` is deprecated in favour of `toot lists list`.\n" +
|
||||||
|
"Run `toot lists -h` to see other list-related commands.")
|
||||||
|
|
||||||
user, app = config.get_active_user_app()
|
user, app = config.get_active_user_app()
|
||||||
if not user or not app:
|
if not user or not app:
|
||||||
|
|
|
@ -6,8 +6,8 @@ from datetime import datetime, timedelta, timezone
|
||||||
from time import sleep, time
|
from time import sleep, time
|
||||||
from typing import BinaryIO, Optional, Tuple
|
from typing import BinaryIO, Optional, Tuple
|
||||||
|
|
||||||
from toot import api
|
from toot import api, config
|
||||||
from toot.cli import 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.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
|
||||||
|
@ -40,7 +40,6 @@ from toot.utils.datetime import parse_datetime
|
||||||
"--visibility", "-v",
|
"--visibility", "-v",
|
||||||
help="Post visibility",
|
help="Post visibility",
|
||||||
type=click.Choice(VISIBILITY_CHOICES),
|
type=click.Choice(VISIBILITY_CHOICES),
|
||||||
default="public",
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--sensitive", "-s",
|
"--sensitive", "-s",
|
||||||
|
@ -106,6 +105,11 @@ from toot.utils.datetime import parse_datetime
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"-u", "--using",
|
||||||
|
type=AccountParamType(),
|
||||||
|
help="The account to use, overrides the active account.",
|
||||||
|
)
|
||||||
@json_option
|
@json_option
|
||||||
@pass_context
|
@pass_context
|
||||||
def post(
|
def post(
|
||||||
|
@ -114,7 +118,7 @@ def post(
|
||||||
media: Tuple[str],
|
media: Tuple[str],
|
||||||
descriptions: Tuple[str],
|
descriptions: Tuple[str],
|
||||||
thumbnails: Tuple[str],
|
thumbnails: Tuple[str],
|
||||||
visibility: str,
|
visibility: Optional[str],
|
||||||
sensitive: bool,
|
sensitive: bool,
|
||||||
spoiler_text: Optional[str],
|
spoiler_text: Optional[str],
|
||||||
reply_to: Optional[str],
|
reply_to: Optional[str],
|
||||||
|
@ -127,12 +131,20 @@ def post(
|
||||||
poll_expires_in: int,
|
poll_expires_in: int,
|
||||||
poll_multiple: bool,
|
poll_multiple: bool,
|
||||||
poll_hide_totals: bool,
|
poll_hide_totals: bool,
|
||||||
json: bool
|
json: bool,
|
||||||
|
using: str
|
||||||
):
|
):
|
||||||
"""Post a new status"""
|
"""Post a new status"""
|
||||||
if len(media) > 4:
|
if len(media) > 4:
|
||||||
raise click.ClickException("Cannot attach more than 4 files.")
|
raise click.ClickException("Cannot attach more than 4 files.")
|
||||||
|
|
||||||
|
if using:
|
||||||
|
user, app = config.get_user_app(using)
|
||||||
|
if not user or not app:
|
||||||
|
raise click.ClickException(f"Account '{using}' not found. Run `toot auth` to see available accounts.")
|
||||||
|
else:
|
||||||
|
user, app = ctx.user, ctx.app
|
||||||
|
|
||||||
media_ids = _upload_media(ctx.app, ctx.user, media, descriptions, thumbnails)
|
media_ids = _upload_media(ctx.app, ctx.user, media, descriptions, thumbnails)
|
||||||
status_text = _get_status_text(text, editor, media)
|
status_text = _get_status_text(text, editor, media)
|
||||||
scheduled_at = _get_scheduled_at(scheduled_at, scheduled_in)
|
scheduled_at = _get_scheduled_at(scheduled_at, scheduled_in)
|
||||||
|
@ -141,8 +153,8 @@ def post(
|
||||||
raise click.ClickException("You must specify either text or media to post.")
|
raise click.ClickException("You must specify either text or media to post.")
|
||||||
|
|
||||||
response = api.post_status(
|
response = api.post_status(
|
||||||
ctx.app,
|
app,
|
||||||
ctx.user,
|
user,
|
||||||
status_text,
|
status_text,
|
||||||
visibility=visibility,
|
visibility=visibility,
|
||||||
media_ids=media_ids,
|
media_ids=media_ids,
|
||||||
|
|
|
@ -9,7 +9,7 @@ from toot.cli.validators import validate_instance
|
||||||
from toot.entities import Instance, Status, from_dict, Account
|
from toot.entities import Instance, Status, from_dict, Account
|
||||||
from toot.exceptions import ApiError, ConsoleError
|
from toot.exceptions import ApiError, ConsoleError
|
||||||
from toot.output import print_account, print_instance, print_search_results, print_status, print_timeline
|
from toot.output import print_account, print_instance, print_search_results, print_status, print_timeline
|
||||||
from toot.cli import cli, get_context, json_option, pass_context, Context
|
from toot.cli import InstanceParamType, cli, get_context, json_option, pass_context, Context
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@ -43,7 +43,7 @@ def whois(ctx: Context, account: str, json: bool):
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument("instance", callback=validate_instance, required=False)
|
@click.argument("instance", type=InstanceParamType(), callback=validate_instance, required=False)
|
||||||
@json_option
|
@json_option
|
||||||
def instance(instance: Optional[str], json: bool):
|
def instance(instance: Optional[str], json: bool):
|
||||||
"""Display instance details
|
"""Display instance details
|
||||||
|
|
|
@ -2,7 +2,7 @@ import sys
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from toot import api
|
from toot import api
|
||||||
from toot.cli import cli, get_context, pass_context, Context
|
from toot.cli import InstanceParamType, cli, get_context, pass_context, Context
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from toot.cli.validators import validate_instance
|
from toot.cli.validators import validate_instance
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ from toot.output import print_notifications, print_timeline
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"--instance", "-i",
|
"--instance", "-i",
|
||||||
|
type=InstanceParamType(),
|
||||||
callback=validate_instance,
|
callback=validate_instance,
|
||||||
help="""Domain or base URL of the instance from which to read,
|
help="""Domain or base URL of the instance from which to read,
|
||||||
e.g. 'mastodon.social' or 'https://mastodon.social'""",
|
e.g. 'mastodon.social' or 'https://mastodon.social'""",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from toot.cli import TUI_COLORS, Context, cli, pass_context
|
from toot.cli import TUI_COLORS, VISIBILITY_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
|
||||||
|
|
||||||
|
@ -30,13 +30,25 @@ COLOR_OPTIONS = ", ".join(TUI_COLORS.keys())
|
||||||
help="""Specify the image cache maximum size in megabytes. Default: 10MB.
|
help="""Specify the image cache maximum size in megabytes. Default: 10MB.
|
||||||
Minimum: 1MB."""
|
Minimum: 1MB."""
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"-v", "--default-visibility",
|
||||||
|
type=click.Choice(VISIBILITY_CHOICES),
|
||||||
|
help="Default visibility when posting new toots; overrides the server-side preference"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-S", "--always-show-sensitive",
|
||||||
|
is_flag=True,
|
||||||
|
help="Expand toots with content warnings automatically"
|
||||||
|
)
|
||||||
@pass_context
|
@pass_context
|
||||||
def tui(
|
def tui(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
colors: Optional[int],
|
colors: Optional[int],
|
||||||
media_viewer: Optional[str],
|
media_viewer: Optional[str],
|
||||||
|
always_show_sensitive: bool,
|
||||||
relative_datetimes: bool,
|
relative_datetimes: bool,
|
||||||
cache_size: Optional[int],
|
cache_size: Optional[int],
|
||||||
|
default_visibility: Optional[str]
|
||||||
):
|
):
|
||||||
"""Launches the toot terminal user interface"""
|
"""Launches the toot terminal user interface"""
|
||||||
if colors is None:
|
if colors is None:
|
||||||
|
@ -47,6 +59,8 @@ def tui(
|
||||||
media_viewer=media_viewer,
|
media_viewer=media_viewer,
|
||||||
relative_datetimes=relative_datetimes,
|
relative_datetimes=relative_datetimes,
|
||||||
cache_size=cache_size,
|
cache_size=cache_size,
|
||||||
|
default_visibility=default_visibility,
|
||||||
|
always_show_sensitive=always_show_sensitive,
|
||||||
)
|
)
|
||||||
tui = TUI.create(ctx.app, ctx.user, options)
|
tui = TUI.create(ctx.app, ctx.user, options)
|
||||||
tui.run()
|
tui.run()
|
||||||
|
|
18
toot/http.py
18
toot/http.py
|
@ -38,7 +38,7 @@ def _get_error_message(response):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return "Unknown error"
|
return f"Unknown error: {response.status_code} {response.reason}"
|
||||||
|
|
||||||
|
|
||||||
def process_response(response):
|
def process_response(response):
|
||||||
|
@ -81,6 +81,22 @@ def post(app, user, path, headers=None, files=None, data=None, json=None, allow_
|
||||||
return anon_post(url, headers=headers, files=files, data=data, json=json, allow_redirects=allow_redirects)
|
return anon_post(url, headers=headers, files=files, data=data, json=json, allow_redirects=allow_redirects)
|
||||||
|
|
||||||
|
|
||||||
|
def anon_put(url, headers=None, files=None, data=None, json=None, allow_redirects=True):
|
||||||
|
request = Request(method="PUT", url=url, headers=headers, files=files, data=data, json=json)
|
||||||
|
response = send_request(request, allow_redirects)
|
||||||
|
|
||||||
|
return process_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def put(app, user, path, headers=None, files=None, data=None, json=None, allow_redirects=True):
|
||||||
|
url = app.base_url + path
|
||||||
|
|
||||||
|
headers = headers or {}
|
||||||
|
headers["Authorization"] = f"Bearer {user.access_token}"
|
||||||
|
|
||||||
|
return anon_put(url, headers=headers, files=files, data=data, json=json, allow_redirects=allow_redirects)
|
||||||
|
|
||||||
|
|
||||||
def patch(app, user, path, headers=None, files=None, data=None, json=None):
|
def patch(app, user, path, headers=None, files=None, data=None, json=None):
|
||||||
url = app.base_url + path
|
url = app.base_url + path
|
||||||
|
|
||||||
|
|
|
@ -274,8 +274,9 @@ def print_notification(notification: Notification):
|
||||||
|
|
||||||
def print_notifications(notifications: List[Notification]):
|
def print_notifications(notifications: List[Notification]):
|
||||||
for notification in notifications:
|
for notification in notifications:
|
||||||
print_divider()
|
if notification.type not in ['pleroma:emoji_reaction']:
|
||||||
print_notification(notification)
|
print_divider()
|
||||||
|
print_notification(notification)
|
||||||
print_divider()
|
print_divider()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,13 @@ import warnings
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from typing import NamedTuple, Optional
|
from typing import NamedTuple, Optional
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from toot import api, config, __version__, settings
|
from toot import api, config, __version__, settings
|
||||||
from toot import App, User
|
from toot import App, User
|
||||||
from toot.cli import get_default_visibility
|
from toot.cli import get_default_visibility
|
||||||
from toot.exceptions import ApiError
|
from toot.exceptions import ApiError
|
||||||
|
from toot.utils.datetime import parse_datetime
|
||||||
|
|
||||||
from .compose import StatusComposer
|
from .compose import StatusComposer
|
||||||
from .constants import PALETTE
|
from .constants import PALETTE
|
||||||
|
@ -22,7 +24,8 @@ from .poll import Poll
|
||||||
from .timeline import Timeline
|
from .timeline import Timeline
|
||||||
from .utils import get_max_toot_chars, parse_content_links, copy_to_clipboard, ImageCache
|
from .utils import get_max_toot_chars, parse_content_links, copy_to_clipboard, ImageCache
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from .widgets import ModalBox
|
||||||
|
>>>>>>> master
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -35,9 +38,10 @@ DEFAULT_MAX_TOOT_CHARS = 500
|
||||||
class TuiOptions(NamedTuple):
|
class TuiOptions(NamedTuple):
|
||||||
colors: int
|
colors: int
|
||||||
media_viewer: Optional[str]
|
media_viewer: Optional[str]
|
||||||
|
always_show_sensitive: bool
|
||||||
relative_datetimes: bool
|
relative_datetimes: bool
|
||||||
cache_size: int
|
cache_size: int
|
||||||
|
default_visibility: Optional[bool]
|
||||||
|
|
||||||
class Header(urwid.WidgetWrap):
|
class Header(urwid.WidgetWrap):
|
||||||
def __init__(self, app, user):
|
def __init__(self, app, user):
|
||||||
|
@ -143,6 +147,7 @@ class TUI(urwid.Frame):
|
||||||
self.can_translate = False
|
self.can_translate = False
|
||||||
self.account = None
|
self.account = None
|
||||||
self.followed_accounts = []
|
self.followed_accounts = []
|
||||||
|
self.preferences = {}
|
||||||
|
|
||||||
if self.options.cache_size:
|
if self.options.cache_size:
|
||||||
self.cache_max = 1024 * 1024 * self.options.cache_size
|
self.cache_max = 1024 * 1024 * self.options.cache_size
|
||||||
|
@ -153,6 +158,7 @@ class TUI(urwid.Frame):
|
||||||
|
|
||||||
def run(self):
|
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_instance())
|
||||||
|
self.loop.set_alarm_in(0, lambda *args: self.async_load_preferences())
|
||||||
self.loop.set_alarm_in(0, lambda *args: self.async_load_timeline(
|
self.loop.set_alarm_in(0, lambda *args: self.async_load_timeline(
|
||||||
is_initial=True, timeline_name="home"))
|
is_initial=True, timeline_name="home"))
|
||||||
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_accounts())
|
||||||
|
@ -337,6 +343,19 @@ class TUI(urwid.Frame):
|
||||||
|
|
||||||
return self.run_in_thread(_load_instance, done_callback=_done)
|
return self.run_in_thread(_load_instance, done_callback=_done)
|
||||||
|
|
||||||
|
def async_load_preferences(self):
|
||||||
|
"""
|
||||||
|
Attempt to update user preferences from instance.
|
||||||
|
https://docs.joinmastodon.org/methods/preferences/
|
||||||
|
"""
|
||||||
|
def _load_preferences():
|
||||||
|
return api.get_preferences(self.app, self.user).json()
|
||||||
|
|
||||||
|
def _done(preferences):
|
||||||
|
self.preferences = preferences
|
||||||
|
|
||||||
|
return self.run_in_thread(_load_preferences, done_callback=_done)
|
||||||
|
|
||||||
def async_load_followed_accounts(self):
|
def async_load_followed_accounts(self):
|
||||||
def _load_accounts():
|
def _load_accounts():
|
||||||
try:
|
try:
|
||||||
|
@ -411,11 +430,45 @@ class TUI(urwid.Frame):
|
||||||
def _post(timeline, *args):
|
def _post(timeline, *args):
|
||||||
self.post_status(*args)
|
self.post_status(*args)
|
||||||
|
|
||||||
composer = StatusComposer(self.max_toot_chars, self.user.username, in_reply_to)
|
# If the user specified --default-visibility, use that; otherwise,
|
||||||
|
# try to use the server-side default visibility. If that fails, fall
|
||||||
|
# back to get_default_visibility().
|
||||||
|
visibility = (self.options.default_visibility or
|
||||||
|
self.preferences.get('posting:default:visibility',
|
||||||
|
get_default_visibility()))
|
||||||
|
|
||||||
|
composer = StatusComposer(self.max_toot_chars, self.user.username,
|
||||||
|
visibility, in_reply_to)
|
||||||
urwid.connect_signal(composer, "close", _close)
|
urwid.connect_signal(composer, "close", _close)
|
||||||
urwid.connect_signal(composer, "post", _post)
|
urwid.connect_signal(composer, "post", _post)
|
||||||
self.open_overlay(composer, title="Compose status")
|
self.open_overlay(composer, title="Compose status")
|
||||||
|
|
||||||
|
def async_edit(self, status):
|
||||||
|
def _fetch_source():
|
||||||
|
return api.fetch_status_source(self.app, self.user, status.id).json()
|
||||||
|
|
||||||
|
def _done(source):
|
||||||
|
self.close_overlay()
|
||||||
|
self.show_edit(status, source)
|
||||||
|
|
||||||
|
please_wait = ModalBox("Loading status...")
|
||||||
|
self.open_overlay(please_wait)
|
||||||
|
|
||||||
|
self.run_in_thread(_fetch_source, done_callback=_done)
|
||||||
|
|
||||||
|
def show_edit(self, status, source):
|
||||||
|
def _close(*args):
|
||||||
|
self.close_overlay()
|
||||||
|
|
||||||
|
def _edit(timeline, *args):
|
||||||
|
self.edit_status(status, *args)
|
||||||
|
|
||||||
|
composer = StatusComposer(self.max_toot_chars, self.user.username,
|
||||||
|
visibility=None, edit=status, source=source)
|
||||||
|
urwid.connect_signal(composer, "close", _close)
|
||||||
|
urwid.connect_signal(composer, "post", _edit)
|
||||||
|
self.open_overlay(composer, title="Edit status")
|
||||||
|
|
||||||
def show_goto_menu(self):
|
def show_goto_menu(self):
|
||||||
user_timelines = self.config.get("timelines", {})
|
user_timelines = self.config.get("timelines", {})
|
||||||
user_lists = api.get_lists(self.app, self.user) or []
|
user_lists = api.get_lists(self.app, self.user) or []
|
||||||
|
@ -563,6 +616,42 @@ class TUI(urwid.Frame):
|
||||||
self.footer.set_message("Status posted {} \\o/".format(status.id))
|
self.footer.set_message("Status posted {} \\o/".format(status.id))
|
||||||
self.close_overlay()
|
self.close_overlay()
|
||||||
|
|
||||||
|
def edit_status(self, status, content, warning, visibility, in_reply_to_id):
|
||||||
|
# We don't support editing polls (yet), so to avoid losing the poll
|
||||||
|
# data from the original toot, copy it to the edit request.
|
||||||
|
poll_args = {}
|
||||||
|
poll = status.original.data.get('poll', None)
|
||||||
|
|
||||||
|
if poll is not None:
|
||||||
|
poll_args['poll_options'] = [o['title'] for o in poll['options']]
|
||||||
|
poll_args['poll_multiple'] = poll['multiple']
|
||||||
|
|
||||||
|
# Convert absolute expiry time into seconds from now.
|
||||||
|
expires_at = parse_datetime(poll['expires_at'])
|
||||||
|
expires_in = int((expires_at - datetime.now(timezone.utc)).total_seconds())
|
||||||
|
poll_args['poll_expires_in'] = expires_in
|
||||||
|
|
||||||
|
if 'hide_totals' in poll:
|
||||||
|
poll_args['poll_hide_totals'] = poll['hide_totals']
|
||||||
|
|
||||||
|
data = api.edit_status(
|
||||||
|
self.app,
|
||||||
|
self.user,
|
||||||
|
status.id,
|
||||||
|
content,
|
||||||
|
spoiler_text=warning,
|
||||||
|
visibility=visibility,
|
||||||
|
**poll_args
|
||||||
|
).json()
|
||||||
|
|
||||||
|
new_status = self.make_status(data)
|
||||||
|
|
||||||
|
self.footer.set_message("Status edited {} \\o/".format(status.id))
|
||||||
|
self.close_overlay()
|
||||||
|
|
||||||
|
if self.timeline is not None:
|
||||||
|
self.timeline.update_status(new_status)
|
||||||
|
|
||||||
def show_account(self, account_id):
|
def show_account(self, account_id):
|
||||||
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)
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import urwid
|
import urwid
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from toot.cli import get_default_visibility
|
|
||||||
|
|
||||||
from .constants import VISIBILITY_OPTIONS
|
from .constants import VISIBILITY_OPTIONS
|
||||||
from .widgets import Button, EditBox
|
from .widgets import Button, EditBox
|
||||||
|
|
||||||
|
@ -11,21 +9,22 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class StatusComposer(urwid.Frame):
|
class StatusComposer(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
UI for compose and posting a status message.
|
UI for composing or editing a status message.
|
||||||
|
|
||||||
|
To edit a status, provide the original status in 'edit', and optionally
|
||||||
|
provide the status source (from the /status/:id/source API endpoint) in
|
||||||
|
'source'; this should have at least a 'text' member, and optionally
|
||||||
|
'spoiler_text'. If source is not provided, the formatted HTML will be
|
||||||
|
presented to the user for editing.
|
||||||
"""
|
"""
|
||||||
signals = ["close", "post"]
|
signals = ["close", "post"]
|
||||||
|
|
||||||
def __init__(self, max_chars, username, in_reply_to=None):
|
def __init__(self, max_chars, username, visibility, in_reply_to=None,
|
||||||
|
edit=None, source=None):
|
||||||
self.in_reply_to = in_reply_to
|
self.in_reply_to = in_reply_to
|
||||||
self.max_chars = max_chars
|
self.max_chars = max_chars
|
||||||
self.username = username
|
self.username = username
|
||||||
|
self.edit = edit
|
||||||
text = self.get_initial_text(in_reply_to)
|
|
||||||
self.content_edit = EditBox(
|
|
||||||
edit_text=text, edit_pos=len(text), multiline=True, allow_tab=True)
|
|
||||||
urwid.connect_signal(self.content_edit.edit, "change", self.text_changed)
|
|
||||||
|
|
||||||
self.char_count = urwid.Text(["0/{}".format(max_chars)])
|
|
||||||
|
|
||||||
self.cw_edit = None
|
self.cw_edit = None
|
||||||
self.cw_add_button = Button("Add content warning",
|
self.cw_add_button = Button("Add content warning",
|
||||||
|
@ -33,13 +32,34 @@ class StatusComposer(urwid.Frame):
|
||||||
self.cw_remove_button = Button("Remove content warning",
|
self.cw_remove_button = Button("Remove content warning",
|
||||||
on_press=self.remove_content_warning)
|
on_press=self.remove_content_warning)
|
||||||
|
|
||||||
self.visibility = (
|
if edit:
|
||||||
in_reply_to.visibility if in_reply_to else get_default_visibility()
|
if source is None:
|
||||||
)
|
text = edit.data["content"]
|
||||||
|
else:
|
||||||
|
text = source.get("text", edit.data["content"])
|
||||||
|
|
||||||
|
if 'spoiler_text' in source:
|
||||||
|
self.cw_edit = EditBox(multiline=True, allow_tab=True,
|
||||||
|
edit_text=source['spoiler_text'])
|
||||||
|
|
||||||
|
self.visibility = edit.data["visibility"]
|
||||||
|
|
||||||
|
else: # not edit
|
||||||
|
text = self.get_initial_text(in_reply_to)
|
||||||
|
self.visibility = (
|
||||||
|
in_reply_to.visibility if in_reply_to else visibility
|
||||||
|
)
|
||||||
|
|
||||||
|
self.content_edit = EditBox(
|
||||||
|
edit_text=text, edit_pos=len(text), multiline=True, allow_tab=True)
|
||||||
|
urwid.connect_signal(self.content_edit.edit, "change", self.text_changed)
|
||||||
|
|
||||||
|
self.char_count = urwid.Text(["0/{}".format(max_chars)])
|
||||||
|
|
||||||
self.visibility_button = Button("Visibility: {}".format(self.visibility),
|
self.visibility_button = Button("Visibility: {}".format(self.visibility),
|
||||||
on_press=self.choose_visibility)
|
on_press=self.choose_visibility)
|
||||||
|
|
||||||
self.post_button = Button("Post", on_press=self.post)
|
self.post_button = Button("Edit" if edit else "Post", on_press=self.post)
|
||||||
self.cancel_button = Button("Cancel", on_press=self.close)
|
self.cancel_button = Button("Cancel", on_press=self.close)
|
||||||
|
|
||||||
contents = list(self.generate_list_items())
|
contents = list(self.generate_list_items())
|
||||||
|
|
|
@ -53,6 +53,10 @@ class Status:
|
||||||
self.id = self.data["id"]
|
self.id = self.data["id"]
|
||||||
self.account = self._get_account()
|
self.account = self._get_account()
|
||||||
self.created_at = parse_datetime(data["created_at"])
|
self.created_at = parse_datetime(data["created_at"])
|
||||||
|
if data["edited_at"]:
|
||||||
|
self.edited_at = parse_datetime(data["edited_at"])
|
||||||
|
else:
|
||||||
|
self.edited_at = None
|
||||||
self.author = self._get_author()
|
self.author = self._get_author()
|
||||||
self.favourited = data.get("favourited", False)
|
self.favourited = data.get("favourited", False)
|
||||||
self.reblogged = data.get("reblogged", False)
|
self.reblogged = data.get("reblogged", False)
|
||||||
|
|
|
@ -231,6 +231,7 @@ class Help(urwid.Padding):
|
||||||
yield urwid.Text(h(" [N] - Translate status if possible (toggle)"))
|
yield urwid.Text(h(" [N] - Translate status if possible (toggle)"))
|
||||||
yield urwid.Text(h(" [R] - Reply to current status"))
|
yield urwid.Text(h(" [R] - Reply to current status"))
|
||||||
yield urwid.Text(h(" [S] - Show text marked as sensitive"))
|
yield urwid.Text(h(" [S] - Show text marked as sensitive"))
|
||||||
|
yield urwid.Text(h(" [M] - Show status media"))
|
||||||
yield urwid.Text(h(" [T] - Show status thread (replies)"))
|
yield urwid.Text(h(" [T] - Show status thread (replies)"))
|
||||||
yield urwid.Text(h(" [L] - Show the status links"))
|
yield urwid.Text(h(" [L] - Show the status links"))
|
||||||
yield urwid.Text(h(" [U] - Show the status data in JSON as received from the server"))
|
yield urwid.Text(h(" [U] - Show the status data in JSON as received from the server"))
|
||||||
|
|
|
@ -109,6 +109,7 @@ class Timeline(urwid.Columns):
|
||||||
"[A]ccount" if not status.is_mine else "",
|
"[A]ccount" if not status.is_mine else "",
|
||||||
"[B]oost",
|
"[B]oost",
|
||||||
"[D]elete" if status.is_mine else "",
|
"[D]elete" if status.is_mine else "",
|
||||||
|
"[E]dit" if status.is_mine else "",
|
||||||
"B[o]okmark",
|
"B[o]okmark",
|
||||||
"[F]avourite",
|
"[F]avourite",
|
||||||
"[V]iew",
|
"[V]iew",
|
||||||
|
@ -198,6 +199,11 @@ class Timeline(urwid.Columns):
|
||||||
self.tui.show_delete_confirmation(status)
|
self.tui.show_delete_confirmation(status)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if key in ("e", "E"):
|
||||||
|
if status.is_mine:
|
||||||
|
self.tui.async_edit(status)
|
||||||
|
return
|
||||||
|
|
||||||
if key in ("f", "F"):
|
if key in ("f", "F"):
|
||||||
self.tui.async_toggle_favourite(self, status)
|
self.tui.async_toggle_favourite(self, status)
|
||||||
return
|
return
|
||||||
|
@ -349,6 +355,7 @@ class StatusDetails(urwid.Pile):
|
||||||
if self.status:
|
if self.status:
|
||||||
self.status.placeholders = []
|
self.status.placeholders = []
|
||||||
self.followed_accounts = timeline.tui.followed_accounts
|
self.followed_accounts = timeline.tui.followed_accounts
|
||||||
|
self.options = timeline.tui.options
|
||||||
|
|
||||||
reblogged_by = status.author if status and status.reblog else None
|
reblogged_by = status.author if status and status.reblog else None
|
||||||
widget_list = list(self.content_generator(status.original, reblogged_by)
|
widget_list = list(self.content_generator(status.original, reblogged_by)
|
||||||
|
@ -447,9 +454,12 @@ class StatusDetails(urwid.Pile):
|
||||||
yield ("pack", urwid.Divider())
|
yield ("pack", urwid.Divider())
|
||||||
|
|
||||||
# Show content warning
|
# Show content warning
|
||||||
if status.data["spoiler_text"] and not status.show_sensitive:
|
if status.data["spoiler_text"] and not status.show_sensitive and not self.options.always_show_sensitive:
|
||||||
yield ("pack", urwid.Text(("content_warning", "Marked as sensitive. Press S to view.")))
|
yield ("pack", urwid.Text(("content_warning", "Marked as sensitive. Press S to view.")))
|
||||||
else:
|
else:
|
||||||
|
if status.data["spoiler_text"]:
|
||||||
|
yield ("pack", urwid.Text(("content_warning", "Marked as sensitive.")))
|
||||||
|
|
||||||
content = status.original.translation if status.original.show_translation else status.data["content"]
|
content = status.original.translation if status.original.show_translation else status.data["content"]
|
||||||
widgetlist = html_to_widgets(content)
|
widgetlist = html_to_widgets(content)
|
||||||
|
|
||||||
|
@ -516,6 +526,8 @@ class StatusDetails(urwid.Pile):
|
||||||
|
|
||||||
yield ("pack", urwid.Text([
|
yield ("pack", urwid.Text([
|
||||||
("status_detail_timestamp", f"{status.created_at.strftime('%Y-%m-%d %H:%M')} "),
|
("status_detail_timestamp", f"{status.created_at.strftime('%Y-%m-%d %H:%M')} "),
|
||||||
|
("status_detail_timestamp",
|
||||||
|
f"(edited {status.edited_at.strftime('%Y-%m-%d %H:%M')}) " if status.edited_at else ""),
|
||||||
("status_detail_bookmarked" if status.bookmarked else "dim", "b "),
|
("status_detail_bookmarked" if status.bookmarked else "dim", "b "),
|
||||||
("dim", f"⤶ {status.data['replies_count']} "),
|
("dim", f"⤶ {status.data['replies_count']} "),
|
||||||
("highlight" if status.reblogged else "dim", f"♺ {status.data['reblogs_count']} "),
|
("highlight" if status.reblogged else "dim", f"♺ {status.data['reblogs_count']} "),
|
||||||
|
@ -579,7 +591,7 @@ class StatusDetails(urwid.Pile):
|
||||||
|
|
||||||
class StatusListItem(SelectableColumns):
|
class StatusListItem(SelectableColumns):
|
||||||
def __init__(self, status, relative_datetimes):
|
def __init__(self, status, relative_datetimes):
|
||||||
edited_at = status.data.get("edited_at")
|
edited_at = status.original.edited_at
|
||||||
|
|
||||||
# TODO: hacky implementation to avoid creating conflicts for existing
|
# TODO: hacky implementation to avoid creating conflicts for existing
|
||||||
# pull reuqests, refactor when merged.
|
# pull reuqests, refactor when merged.
|
||||||
|
@ -593,7 +605,7 @@ class StatusListItem(SelectableColumns):
|
||||||
favourited = ("highlight", "★") if status.original.favourited else " "
|
favourited = ("highlight", "★") if status.original.favourited else " "
|
||||||
reblogged = ("highlight", "♺") if status.original.reblogged else " "
|
reblogged = ("highlight", "♺") if status.original.reblogged else " "
|
||||||
is_reblog = ("dim", "♺") if status.reblog else " "
|
is_reblog = ("dim", "♺") if status.reblog else " "
|
||||||
is_reply = ("dim", "⤶") if status.original.in_reply_to else " "
|
is_reply = ("dim", "⤶ ") if status.original.in_reply_to else " "
|
||||||
|
|
||||||
return super().__init__([
|
return super().__init__([
|
||||||
("pack", SelectableText(("status_list_timestamp", created_at), wrap="clip")),
|
("pack", SelectableText(("status_list_timestamp", created_at), wrap="clip")),
|
||||||
|
|
|
@ -159,3 +159,10 @@ class EmojiText(urwid.Padding):
|
||||||
columns.append(("weight", 9999, urwid.Text("")))
|
columns.append(("weight", 9999, urwid.Text("")))
|
||||||
column_widget = urwid.Columns(columns, dividechars=1, min_width=2)
|
column_widget = urwid.Columns(columns, dividechars=1, min_width=2)
|
||||||
super().__init__(column_widget)
|
super().__init__(column_widget)
|
||||||
|
|
||||||
|
class ModalBox(urwid.Frame):
|
||||||
|
def __init__(self, message):
|
||||||
|
text = urwid.Text(message)
|
||||||
|
filler = urwid.Filler(text, valign='top', top=1, bottom=1)
|
||||||
|
padding = urwid.Padding(filler, left=1, right=1)
|
||||||
|
return super().__init__(padding)
|
||||||
|
|
Loading…
Reference in New Issue