diff --git a/qualytics/cli/__init__.py b/qualytics/cli/__init__.py index 47f068b..9b780bb 100644 --- a/qualytics/cli/__init__.py +++ b/qualytics/cli/__init__.py @@ -10,6 +10,7 @@ from rich import print from ..config import __version__ +from .logo import MIN_LOGO_WIDTH, compact_logo, logo_lines # Qualytics brand color BRAND = "#FF9933" @@ -52,21 +53,6 @@ def resolve_command(self, ctx: click.Context, args: list[str]): raise -# fmt: off -# Wordmark traced from official SVG (qualytics-word-mark.svg). -# Each letter rendered independently to guarantee vertical alignment. -LOGO = [ - " ▄▄███▀ ▄▄▄▄", - " ██▀ ▀██▄ ██ ███ ▀█", - " ██ ██ ██ ██ ▄█▀▀▀▀███ ██ ▀█▄ ▄██▀▀███▀▀ ██ ██▀▀▀██▄ ▄██▀▀██▄", - " ██▄ ▄██ ██ ██ ██ ██ ██ ██▄ ██ ███ ██ ██ ▀▀ ▀██▄▄▄▄", - " ▀██▄▄▄▄▄▄▄██▀ ▀█▄▄ ▄██ ▀█▄▄ ▄▄██ ██ ██▄██ ███ ██ ▀█▄▄ ▄██ ██▄ ▄██", - " ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀ ▀▀ ▀██▀ ▀▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀▀▀▀▀", - " ▄█▀", -] -# fmt: on - - def print_banner(subtitle: str | None = None) -> None: """Print the Qualytics logo banner. @@ -96,12 +82,16 @@ def print_banner(subtitle: str | None = None) -> None: display_url = url.rstrip("/") if url else "" subtitle = f"[{BRAND}]✓[/{BRAND}] [dim]Connected to[/dim] {display_url}" - print() - for line in LOGO: - print(f"[bold {BRAND}]{line}[/bold {BRAND}]") - print() - print(f" [bold]v{__version__}[/bold] [dim]·[/dim] {subtitle}") - print() + from rich.console import Console + + console = Console() + + console.print() + for line in logo_lines() if console.width >= MIN_LOGO_WIDTH else compact_logo(): + console.print(line) + console.print() + console.print(f" [bold]v{__version__}[/bold] [dim]·[/dim] {subtitle}") + console.print() def add_suggestion_callback(app: typer.Typer, group_name: str) -> None: diff --git a/qualytics/cli/logo.py b/qualytics/cli/logo.py new file mode 100644 index 0000000..a42a31b --- /dev/null +++ b/qualytics/cli/logo.py @@ -0,0 +1,94 @@ +"""Qualytics ASCII logo.""" + +from rich.text import Text + +# Column where the Q icon ends and the wordmark begins. +_SPLIT = 18 + +# fmt: off +# Wordmark traced from official logo, compact half-block rendering. +_LINES = [ + " ▄█████ ▄██▄ ▄ ▄ ▄", + " ██▀ ▀██ ██ ██ ▀▀", + " ██ ██ ██ ██ ▄████▄██ ██ ██ ██ ██████ ██ ▄██▀▀██▄ ▄█████▄", + " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ▀▀ ██▄▄▄", + " ██▄ ▄██ ██ ██ ██ ██ ██ ██▄▄██ ██ ██ ██ ▄▄ ▀▀▀██", + " ▀██████████▄▄ ▀█████▀██ ▀████▀██ ██ ▄█▀ ██ ██ ▀██▄▄██▀ ▀█████▀", + " ▄█▀", + " ▀▀", +] +# fmt: on + +# Qualytics brand color +BRAND = "#FF9933" + +# Horizontal gradient stops for the Q icon (left → right). +# #B83200 → #F96719 → brand → near-white to suggest fade into terminal default. +_STOPS = [ + (0xB8, 0x32, 0x00), # #B83200 + (0xF9, 0x67, 0x19), # #F96719 + (0xFF, 0x99, 0x33), # #FF9933 (brand) + (0xFF, 0xCC, 0x88), # light warm tone – bridges brand to default +] + + +def _gradient_color(t: float) -> str: + """Map t (0..1) to a color along the multi-stop gradient.""" + if t <= 0: + r, g, b = _STOPS[0] + elif t >= 1: + r, g, b = _STOPS[-1] + else: + # Scale t to the number of segments. + seg = t * (len(_STOPS) - 1) + i = int(seg) + f = seg - i + a, b_ = _STOPS[i], _STOPS[min(i + 1, len(_STOPS) - 1)] + r = int(a[0] + (b_[0] - a[0]) * f) + g = int(a[1] + (b_[1] - a[1]) * f) + b = int(a[2] + (b_[2] - a[2]) * f) + return f"#{r:02x}{g:02x}{b:02x}" + + +# Minimum terminal width needed for the full ASCII logo. +MIN_LOGO_WIDTH = max(len(line) for line in _LINES) + 2 + + +def logo_lines() -> list[Text]: + """Return the logo with a horizontal gradient on the Q icon. + + Q icon: left-to-right #B83200 → #F96719 → #FF9933 → light warm tone. + Wordmark: terminal default foreground. + """ + result = [] + for line in _LINES: + t = Text(line) + for col in range(min(_SPLIT, len(line))): + color = _gradient_color(col / max(_SPLIT - 1, 1)) + t.stylize(f"bold {color}", col, col + 1) + result.append(t) + return result + + +# fmt: off +# Compact block-letter wordmark (~37 cols wide). +_COMPACT_Q_SPLIT = 5 # width of the "Q" character +_COMPACT_LINES = [ + " ▄▀▀▄ █ █ ▄▀▀█ █ █ █ ▄█▄ ▀ ▄▀▀ ▄▀▀", + " █ █ █ █ █ █ █ ▀▄▀ █ █ █ ▀▄", + " ▀▄▄█▄ ▀▄▄█ ▀▄▄█ █ █ █ █ ▀▄▄ ▄▄▀", +] +# fmt: on + + +def compact_logo() -> list[Text]: + """Return a compact block-letter logo for narrow terminals.""" + result = [] + for line in _COMPACT_LINES: + t = Text(line) + for col in range(min(_COMPACT_Q_SPLIT, len(line))): + color = _gradient_color(col / max(_COMPACT_Q_SPLIT - 1, 1)) + t.stylize(f"bold {color}", col, col + 1) + t.stylize("bold", _COMPACT_Q_SPLIT) + result.append(t) + return result diff --git a/tests/test_cli.py b/tests/test_cli.py index 4853c07..1d123b1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -260,10 +260,8 @@ def test_banner_shows_logo_and_version(self, cli_runner, monkeypatch): result = cli_runner.invoke(app, []) assert result.exit_code == 0 output = _strip_ansi(result.output) - # SVG-traced wordmark: Q logomark + lowercase ualytics - assert "▄▄███▀" in output # Q top - assert "▀██▄▄▄▄▄▄▄██▀" in output # Q bottom - assert "▀▀▀▀▀▀▀▀" in output # baseline + # Logo present (full or compact depending on terminal width) + assert "▄█████" in output or "▄▀▀▄" in output # Version below wordmark assert f"v{__version__}" in output @@ -510,4 +508,4 @@ def test_doctor_shows_banner_with_doctor_label( result = cli_runner.invoke(app, ["doctor"]) output = _strip_ansi(result.output) assert "Doctor" in output - assert "▄▄███▀" in output # Wordmark present + assert "▄█████" in output or "▄▀▀▄" in output # Logo present