diff --git a/toot/cli/base.py b/toot/cli/base.py index 83d235c..4a7362e 100644 --- a/toot/cli/base.py +++ b/toot/cli/base.py @@ -20,6 +20,17 @@ T = t.TypeVar("T") PRIVACY_CHOICES = ["public", "unlisted", "private"] VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"] +TUI_COLORS = { + "1": 1, + "16": 16, + "88": 88, + "256": 256, + "16777216": 16777216, + "24bit": 16777216, +} +TUI_COLORS_CHOICES = list(TUI_COLORS.keys()) +TUI_COLORS_VALUES = list(TUI_COLORS.values()) + DURATION_EXAMPLES = """e.g. "1 day", "2 hours 30 minutes", "5 minutes 30 seconds" or any combination of above. Shorthand: "1d", "2h30m", "5m30s\"""" diff --git a/toot/cli/tui.py b/toot/cli/tui.py index 423b12c..a0c0f0f 100644 --- a/toot/cli/tui.py +++ b/toot/cli/tui.py @@ -1,17 +1,44 @@ import click -from toot.cli.base import Context, cli, pass_context +from typing import Optional +from toot.cli.base import TUI_COLORS, Context, cli, pass_context +from toot.cli.validators import validate_tui_colors from toot.tui.app import TUI, TuiOptions +COLOR_OPTIONS = ", ".join(TUI_COLORS.keys()) + @cli.command() @click.option( - "--relative-datetimes", + "-r", "--relative-datetimes", is_flag=True, help="Show relative datetimes in status list" ) +@click.option( + "-m", "--media-viewer", + help="Program to invoke with media URLs to display the media files, such as 'feh'" +) +@click.option( + "-c", "--colors", + callback=validate_tui_colors, + help=f"""Number of colors to use, one of {COLOR_OPTIONS}, defaults to 16 if + using --color, and 1 if using --no-color.""" +) @pass_context -def tui(ctx: Context, relative_datetimes: bool): +def tui( + ctx: Context, + colors: Optional[int], + media_viewer: Optional[str], + relative_datetimes: bool, +): """Launches the toot terminal user interface""" - options = TuiOptions(relative_datetimes, ctx.color) - TUI.create(ctx.app, ctx.user, options).run() + if colors is None: + colors = 16 if ctx.color else 1 + + options = TuiOptions( + colors=colors, + media_viewer=media_viewer, + relative_datetimes=relative_datetimes, + ) + tui = TUI.create(ctx.app, ctx.user, options) + tui.run() diff --git a/toot/cli/validators.py b/toot/cli/validators.py index cfdd097..819fdf9 100644 --- a/toot/cli/validators.py +++ b/toot/cli/validators.py @@ -4,6 +4,8 @@ import re from click import Context from typing import Optional +from toot.cli.base import TUI_COLORS + def validate_language(ctx: Context, param: str, value: Optional[str]): if value is None: @@ -58,3 +60,16 @@ def validate_instance(ctx: click.Context, param: str, value: Optional[str]): value = value.rstrip("/") return value if value.startswith("http") else f"https://{value}" + + +def validate_tui_colors(ctx, param, value) -> Optional[int]: + if value is None: + return None + + if value in TUI_COLORS.values(): + return value + + if value in TUI_COLORS.keys(): + return TUI_COLORS[value] + + raise click.BadParameter(f"Invalid value: {value}. Expected one of: {', '.join(TUI_COLORS)}") diff --git a/toot/output.py b/toot/output.py index e08bec0..02f1083 100644 --- a/toot/output.py +++ b/toot/output.py @@ -25,6 +25,10 @@ def get_width() -> int: return min(get_terminal_width(), get_max_width()) +def print_warning(text: str): + click.secho(f"Warning: {text}", fg="yellow", err=True) + + def print_instance(instance: Instance): width = get_width() click.echo(instance_to_text(instance, width)) diff --git a/toot/tui/app.py b/toot/tui/app.py index 77cbaaf..bc712cd 100644 --- a/toot/tui/app.py +++ b/toot/tui/app.py @@ -3,7 +3,7 @@ import subprocess import urwid from concurrent.futures import ThreadPoolExecutor -from typing import NamedTuple +from typing import NamedTuple, Optional from toot import api, config, __version__, settings from toot import App, User @@ -28,8 +28,9 @@ DEFAULT_MAX_TOOT_CHARS = 500 class TuiOptions(NamedTuple): + colors: int + media_viewer: Optional[str] relative_datetimes: bool - color: bool class Header(urwid.WidgetWrap): @@ -89,7 +90,9 @@ class TUI(urwid.Frame): @staticmethod def create(app: App, user: User, args: TuiOptions): """Factory method, sets up TUI and an event loop.""" - screen = TUI.create_screen(args) + screen = urwid.raw_display.Screen() + screen.set_terminal_properties(args.colors) + tui = TUI(app, user, screen, args) palette = PALETTE.copy() @@ -108,23 +111,11 @@ class TUI(urwid.Frame): return tui - @staticmethod - def create_screen(args: TuiOptions): - screen = urwid.raw_display.Screen() - - # Determine how many colors to use - default_colors = 16 if args.color else 1 - colors = settings.get_setting("tui.colors", int, default_colors) - logger.debug(f"Setting colors to {colors}") - screen.set_terminal_properties(colors) - - return screen - - def __init__(self, app, user, screen, args: TuiOptions): + def __init__(self, app, user, screen, options: TuiOptions): self.app = app self.user = user - self.args = args self.config = config.load_config() + self.options = options self.loop = None # late init, set in `create` self.screen = screen @@ -146,7 +137,6 @@ class TUI(urwid.Frame): self.can_translate = False self.account = None self.followed_accounts = [] - self.media_viewer = settings.get_setting("tui.media_viewer", str) super().__init__(self.body, header=self.header, footer=self.footer) @@ -510,8 +500,15 @@ class TUI(urwid.Frame): if not urls: return - if self.media_viewer: - subprocess.run([self.media_viewer] + urls) + media_viewer = self.options.media_viewer + if media_viewer: + try: + subprocess.run([media_viewer] + urls) + except FileNotFoundError: + self.footer.set_error_message(f"Media viewer not found: '{media_viewer}'") + except Exception as ex: + self.exception = ex + self.footer.set_error_message("Failed invoking media viewer. Press X to see exception.") else: self.footer.set_error_message("Media viewer not configured") diff --git a/toot/tui/timeline.py b/toot/tui/timeline.py index 60dfeb0..63eb4b3 100644 --- a/toot/tui/timeline.py +++ b/toot/tui/timeline.py @@ -79,7 +79,7 @@ class Timeline(urwid.Columns): return urwid.ListBox(walker) def build_list_item(self, status): - item = StatusListItem(status, self.tui.args.relative_datetimes) + item = StatusListItem(status, self.tui.options.relative_datetimes) urwid.connect_signal(item, "click", lambda *args: self.tui.show_context_menu(status)) return urwid.AttrMap(item, None, focus_map={ @@ -95,7 +95,7 @@ class Timeline(urwid.Columns): return None poll = status.original.data.get("poll") - show_media = status.original.data["media_attachments"] and self.tui.media_viewer + show_media = status.original.data["media_attachments"] and self.tui.options.media_viewer options = [ "[A]ccount" if not status.is_mine else "", @@ -107,7 +107,6 @@ class Timeline(urwid.Columns): "[T]hread" if not self.is_thread else "", "L[i]nks", "[M]edia" if show_media else "", - self.tui.media_viewer, "[R]eply", "[P]oll" if poll and not poll["expired"] else "", "So[u]rce",