diff --git a/src/youtube_kit/guide.py b/src/youtube_kit/guide.py index 471bb0c..c7fbb88 100644 --- a/src/youtube_kit/guide.py +++ b/src/youtube_kit/guide.py @@ -5,14 +5,13 @@ """ import sys +import textwrap import webbrowser from pathlib import Path -from rich.console import Console, Group +from rich.console import Console from rich.markup import escape -from rich.padding import Padding from rich.style import Style -from rich.table import Table from rich.text import Text from .core import InvalidRequest, auth @@ -56,26 +55,29 @@ def connected(channel: MyChannel) -> None: def _show(task: SetupTask, bullet: str, *, open_page: bool) -> None: - parts: list = [Text(task.do)] + indent = " " * (2 + len(bullet)) + _hang(task.do, " " + bullet) if task.url: - parts.append(Text.assemble((task.url, Style(color="cyan", link=task.url)))) + _hang(task.url, indent, Style(color="cyan", link=task.url)) if open_page: webbrowser.open(task.url) if task.values: - values = Table.grid(padding=(0, 2)) - values.add_column(style="dim", no_wrap=True) - values.add_column() + width = max(map(len, task.values)) for field, value in task.values.items(): - values.add_row(field, value) - parts.append(Padding(values, (0, 0, 0, 2))) - body = Group(*parts) - if bullet: # a grid keeps wrapped lines indented under the text, not the bullet - row = Table.grid(padding=(0, 1, 0, 0)) - row.add_column(no_wrap=True) - row.add_column() - row.add_row(bullet.strip(), body) - body = row - console.print(Padding(body, (0, 0, 0, 2))) + _hang(value, Text(f"{indent} {field:<{width}} ", "dim")) + + +def _hang(text: str, first: str | Text, style: str | Style = "") -> None: + """Print text after the prefix `first`, wrapping under it. + + Rich layouts cut a word wider than the line with "…". Here a long URL stays whole on one line + the terminal soft-wraps, so copying it gives the whole URL. + """ + rest = " " * len(first) + width = max(console.width - len(rest), 20) + lines = textwrap.wrap(text, width, break_long_words=False, break_on_hyphens=False) or [""] + for index, line in enumerate(lines): + console.print(Text.assemble(rest if index else first, (line, style)), soft_wrap=True) def _downloaded_or_asked() -> str: diff --git a/tests/test_login.py b/tests/test_login.py index 22b9352..672e2a6 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -5,6 +5,7 @@ import sys import pytest +from rich.text import Text from youtube_kit import guide from youtube_kit.cli import main @@ -119,6 +120,22 @@ def test_no_browser_prints_links_without_opening_them(capsys, google, person, do assert "https://console.cloud.google.com/projectcreate" in err +def test_links_stay_whole_in_a_narrow_terminal(capsys, monkeypatch, google, person, downloads): + (downloads / "client_secret_1.json").write_text(CLIENT) + monkeypatch.setattr(guide.console, "width", 40) + person(*[""] * STEPS, "") + + code, _, err = run(capsys, "--no-browser") + + shown = Text.from_ansi(err).plain # with colour on, a cut link would still carry its URL in the escape code + assert code == 0 + for step in guide.CLIENT_STEPS: + for task in step.tasks: + for text in [task.url, *(task.values or {}).values()]: + if text and text.startswith("https://"): + assert text in shown + + def test_client_secrets_flag_skips_the_steps_at_a_terminal(capsys, google, person): person() code, _, err = run(capsys, "--client-secrets", CLIENT)