diff --git a/src/winml/modelkit/cli.py b/src/winml/modelkit/cli.py index 87ff98411..86695a456 100644 --- a/src/winml/modelkit/cli.py +++ b/src/winml/modelkit/cli.py @@ -41,31 +41,31 @@ logger = logging.getLogger(__name__) _COMMANDS_DIR = Path(__file__).parent / "commands" - -# 5-row block-letter art for "WinML CLI". '#' = filled pixel, ' ' = empty. -# All letters use the same █ character so identical shapes (i vs I) look -# consistent regardless of horizontal position. -_LETTER_ART: dict[str, list[str]] = { - "W": ["# #", "# #", "# # #", "## ##", "# #"], - "i": ["###", " # ", " # ", " # ", "###"], - "n": ["# #", "## #", "# # #", "# ##", "# #"], - "M": ["# #", "## ##", "# # #", "# #", "# #"], - "L": ["# ", "# ", "# ", "# ", "#####"], - "C": ["####", "# ", "# ", "# ", "####"], - "I": ["###", " # ", " # ", " # ", "###"], +_COMPACT_LETTER_ART = { + "W": ("# #", "# #", "# # #", "## ##", "# #"), + "I": ("###", " # ", " # ", " # ", "###"), + "N": ("# #", "## #", "# # #", "# ##", "# #"), + "M": ("# #", "## ##", "# # #", "# #", "# #"), + "L": ("# ", "# ", "# ", "# ", "#####"), } -# Two word segments; rendered with a wider gap between them. -_SEGMENTS: list[list[str]] = [list("WinML"), list("CLI")] -_LETTER_GAP = " " # between letters within a word -_WORD_GAP = " " # between words +_WORDMARK = "WINML" +_LETTER_GAP = " " +# Four 4-column gradient tiles arranged as a Microsoft-style 2x2 mark. +_MARK_ART = ( + "#### ####", + "#### ####", + "#### ####", + " ", + "#### ####", + "#### ####", + "#### ####", +) # Gradient stops (left → right across the full banner width). _GRADIENT: list[tuple[float, tuple[int, int, int]]] = [ - (0.00, (0, 230, 255)), # cyan - (0.25, (0, 100, 255)), # blue - (0.50, (130, 0, 255)), # purple - (0.75, (255, 0, 180)), # pink - (1.00, (255, 80, 80)), # red + (0.00, (45, 145, 255)), + (0.55, (0, 205, 255)), + (1.00, (130, 70, 255)), ] @@ -83,53 +83,123 @@ def _gradient_color(t: float) -> tuple[int, int, int]: return _GRADIENT[-1][1] -def _print_banner(version: str, *, _console: Console | None = None) -> None: - """Print the WinML CLI gradient banner to stderr using Rich.""" - from rich.console import Console # lazy import - keeps startup fast +def _print_banner( + version: str, + *, + _console: Console | None = None, +) -> None: + """Print the capsule WinML CLI banner to stderr using Rich.""" + from rich.console import Console, Group # lazy import - keeps startup fast from rich.text import Text - # Compute total art width across both word segments. - art_w = len(_WORD_GAP) * (len(_SEGMENTS) - 1) - for seg in _SEGMENTS: - art_w += len(_LETTER_GAP) * (len(seg) - 1) - art_w += sum(len(_LETTER_ART[ch][0]) for ch in seg) - bar_w = art_w + 4 margin = " " - con = _console or Console(stderr=True, highlight=False) - con.print() - - for row_idx in range(5): - line = Text(margin) - col = 0 - for seg_idx, seg in enumerate(_SEGMENTS): - if seg_idx > 0: - line.append(_WORD_GAP) - col += len(_WORD_GAP) - for letter_idx, letter in enumerate(seg): - if letter_idx > 0: - line.append(_LETTER_GAP) - col += len(_LETTER_GAP) - for ch in _LETTER_ART[letter][row_idx]: - if ch == "#": - r, g, b = _gradient_color(col / max(art_w - 1, 1)) - line.append("█", style=f"bold rgb({r},{g},{b})") - else: - line.append(" ") - col += 1 - con.print(line) - - con.print() - bar = Text(margin) - for i in range(bar_w): - r, g, b = _gradient_color(i / max(bar_w - 1, 1)) - bar.append("─", style=f"rgb({r},{g},{b})") - con.print(bar) - - con.print() - con.print(f"{margin}[bold rgb(160,100,255)]Windows ML · Model Conversion & Optimization[/]") - con.print(f"{margin}[dim]v{version} · CPU · GPU · NPU[/]") - con.print() + compact_rows = tuple( + _LETTER_GAP.join(_COMPACT_LETTER_ART[letter][row] for letter in _WORDMARK) + for row in range(5) + ) + compact_width = len(compact_rows[0]) * 2 + mark_width = len(_MARK_ART[0]) * 2 + show_mark = con.width >= compact_width + mark_width + 11 + + def gradient_line(value: str) -> Text: + line = Text() + for col, char in enumerate(value): + r, g, b = _gradient_color(col / max(len(value) - 1, 1)) + line.append(char, style=f"bold rgb({r},{g},{b})") + return line + + def footer(tagline: str, *, include_version: bool = True) -> list[Text]: + lines = [ + Text(), + Text.from_markup(f"{margin}[bold rgb(160,100,255)]Windows ML[/] · {tagline}"), + ] + if include_version: + lines.append(Text.from_markup(f"{margin}v{version} · CPU · GPU · NPU")) + lines.append(Text()) + return lines + + def wordmark() -> list[Text]: + lines = [] + patterns = compact_rows + width = compact_width + shadow_rows = (*patterns, " " * len(patterns[0])) + for row, pattern in enumerate(shadow_rows): + line = Text() + previous = shadow_rows[row - 1] if row else "" + for pixel_col, char in enumerate(pattern): + col = pixel_col * 2 + if char == "#": + r, g, b = _gradient_color(col / max(width - 1, 1)) + line.append("██", style=f"bold rgb({r},{g},{b})") + elif row and pixel_col and previous[pixel_col - 1] == "#": + line.append("▓▓", style="bold rgb(130,80,210)") + else: + line.append(" ") + lines.append(line) + return lines + + def mark() -> list[Text]: + pane_cols = len(_MARK_ART[0]) + lines = [] + for pattern in _MARK_ART: + line = Text() + for col, char in enumerate(pattern): + if char == "#": + r, g, b = _gradient_color(col / max(pane_cols - 1, 1)) + line.append("██", style=f"bold rgb({r},{g},{b})") + else: + line.append(" ") + lines.append(line) + return lines + + def capsule() -> Group: + logo_lines = wordmark() + version_text = Text.from_markup(f"v{version} · CPU · GPU · NPU") + version_line = Text(" " * ((compact_width - len(version_text)) // 2)) + version_line.append_text(version_text) + version_line.append(" " * (compact_width - len(version_line))) + logo_lines.extend([Text(" " * compact_width), version_line]) + mark_lines = mark() if show_mark else [] + content_width = compact_width + (mark_width + 3 if show_mark else 0) + frame_margin = margin if show_mark else "" + + def framed_line(content: Text | None = None) -> Text: + line = gradient_line(f"{frame_margin}┃ ") + if content is None: + line.append(" " * content_width) + else: + line.append_text(content) + line.append(" " * (content_width - len(content))) + line.append(" ┃", style="bold rgb(130,70,255)") + return line + + lines = [ + Text(), + Text.from_markup(f"{margin}Windows ML CLI"), + gradient_line(f"{frame_margin}┏{'━' * (content_width + 4)}┓"), + framed_line(), + ] + for row in range(max(len(logo_lines), len(mark_lines))): + content = Text() + if show_mark: + mark_line = mark_lines[row] if row < len(mark_lines) else Text(" " * mark_width) + content.append_text(mark_line) + content.append(" ") + logo_line = logo_lines[row] if row < len(logo_lines) else Text(" " * compact_width) + content.append_text(logo_line) + lines.append(framed_line(content)) + + lines.extend( + [ + framed_line(), + gradient_line(f"{frame_margin}┗{'━' * (content_width + 4)}┛"), + *footer("Model conversion & optimization", include_version=False), + ] + ) + return Group(*lines) + + con.print(capsule()) # Commands that are temporarily disabled from the CLI surface. @@ -321,7 +391,12 @@ def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> hidden=True, ) @click.pass_context -def main(ctx: click.Context, verbose: int, quiet: bool, debug: bool) -> None: +def main( + ctx: click.Context, + verbose: int, + quiet: bool, + debug: bool, +) -> None: """WinML CLI - Accelerate Model Deployment on WinML. Universal ONNX export with various WinML execution providers support. diff --git a/tests/cli/test_help_cli.py b/tests/cli/test_help_cli.py index 39dc28d4e..5a9ba1309 100644 --- a/tests/cli/test_help_cli.py +++ b/tests/cli/test_help_cli.py @@ -5,7 +5,7 @@ """CLI surface tests for ``winml`` (no args) and ``winml --help``. Both invocations follow the same contract: exit 0 and render the full -help page, which consists of the gradient banner on stderr and the Click +help page, which consists of the selected banner on stderr and the Click help text (Usage / Options / Commands) on stdout. The tests here pin the *observable output contract* of these two entry points — no mocks, no subcommand execution. @@ -35,16 +35,20 @@ from __future__ import annotations import textwrap +from io import StringIO from typing import TYPE_CHECKING +from unittest.mock import patch import pytest from click.testing import CliRunner, Result +from rich.console import Console from winml.modelkit import __version__ from winml.modelkit.cli import ( _COMMANDS_DIR, _DISABLED_COMMANDS, _parse_click_help, + _print_banner, main, ) @@ -146,6 +150,58 @@ def test_subcommand_help_has_no_banner(self) -> None: assert result.exit_code == 0 assert "Windows ML" not in result.stderr + def test_banner_animation_is_skipped_outside_interactive_terminals(self) -> None: + with patch("time.sleep") as mock_sleep: + _print_banner( + "1.2.3", + _console=Console( + file=StringIO(), + force_terminal=False, + color_system=None, + ), + ) + mock_sleep.assert_not_called() + + @pytest.mark.parametrize("width", [80, 81, 82, 100]) + def test_interactive_banner_is_static(self, width: int) -> None: + with patch("time.sleep") as mock_sleep: + _print_banner( + "1.2.3", + _console=Console( + file=StringIO(), + force_terminal=True, + color_system="truecolor", + width=width, + ), + ) + mock_sleep.assert_not_called() + + def test_removed_banner_style_is_rejected(self) -> None: + result = _invoke("--banner-style", "unknown", "--help") + assert result.exit_code != 0 + assert "No such option" in result.output + + # Each row contains two four-column gradient tiles separated by a gap. + _MARK_SIGNATURE = "████████ ████████" + + def test_capsule_shows_left_mark_on_wide_terminals(self) -> None: + console = Console(file=StringIO(), force_terminal=False, color_system=None, width=90) + _print_banner("1.2.3", _console=console) + assert self._MARK_SIGNATURE in console.file.getvalue() + + def test_capsule_hides_left_mark_on_narrow_terminals(self) -> None: + console = Console(file=StringIO(), force_terminal=False, color_system=None, width=80) + _print_banner("1.2.3", _console=console) + assert self._MARK_SIGNATURE not in console.file.getvalue() + + def test_capsule_places_version_inside_frame(self) -> None: + console = Console(file=StringIO(), force_terminal=False, color_system=None, width=110) + _print_banner("1.2.3", _console=console) + output = console.file.getvalue() + version_line = next(line for line in output.splitlines() if "v1.2.3" in line) + assert version_line.strip().startswith("┃") + assert version_line.strip().endswith("┃") + # =========================================================================== # Commands section