diff --git a/README.md b/README.md index 2247254..df0a79b 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ just your terminal, an OpenAI-compatible model, and a sharp set of tools. ```text - _ _ - | | ___ ___ ___ __| | ___ - | |/ _ \/ __/ _ \ / _` |/ _ \ - | | __/ (_| (_) | (_| | __/ - |_|\___|\___\___/ \__,_|\___| - by wowi42 +██╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗ +██║ ██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔════╝ +██║ █████╗ ██║ ██║ ██║██║ ██║█████╗ +██║ ██╔══╝ ██║ ██║ ██║██║ ██║██╔══╝ +███████╗███████╗╚██████╗╚██████╔╝██████╔╝███████╗ +╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ + v0.2.0 · by Kalvad — now with 100% more purple ╭────────────────────────── lecode — fix-auth ───────────────────────────╮ │ ✓ session fix-auth — new session │ diff --git a/src/lecode/tui/loading.py b/src/lecode/tui/loading.py index 3b97218..bc6a648 100644 --- a/src/lecode/tui/loading.py +++ b/src/lecode/tui/loading.py @@ -11,6 +11,7 @@ from __future__ import annotations import logging +import random from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING @@ -41,14 +42,67 @@ #: Label column width for progressive lines (the longest label). LABEL_WIDTH = len("permissions") -#: ASCII-art banner (figlet "standard") printed above the loading panel. -_BANNER = r""" _ _ -| | ___ ___ ___ __| | ___ -| |/ _ \/ __/ _ \ / _` |/ _ \ -| | __/ (_| (_) | (_| | __/ -|_|\___|\___\___/ \__,_|\___|""" - -_BYLINE = "by wowi42" +#: ASCII-art banner (pyfiglet "ansi_shadow") printed above the loading panel. +_BANNER = """\ +██╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗ +██║ ██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔════╝ +██║ █████╗ ██║ ██║ ██║██║ ██║█████╗ +██║ ██╔══╝ ██║ ██║ ██║██║ ██║██╔══╝ +███████╗███████╗╚██████╗╚██████╔╝██████╔╝███████╗ +╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝""" + +_BYLINE = "by Kalvad" + +#: Splash taglines — one rides along with the byline, picked at random. +_SPLASHES = ( + "now with 100% more purple", + "your keys, your models, your rules", + "handcrafted in the terminal", + "type fast, undo faster", + "Ctrl-C is always there for you", + "purple is a feature", + "runs on commits and caffeine", + "minds your scrollback", + "ships on a purple carpet", +) + + +def _gradient_stops(theme: Theme) -> tuple[str, ...]: + """Deep purple → brand violet → fuchsia, following the theme.""" + return ("#7c3aed", theme.accent, theme.permission) + + +def _lerp_color(a: str, b: str, t: float) -> str: + """Linear interpolation between two ``#rrggbb`` colors.""" + ar, ag, ab = (int(a[i : i + 2], 16) for i in (1, 3, 5)) + br, bg, bb = (int(b[i : i + 2], 16) for i in (1, 3, 5)) + r = round(ar + (br - ar) * t) + g = round(ag + (bg - ag) * t) + blue = round(ab + (bb - ab) * t) + return f"#{r:02x}{g:02x}{blue:02x}" + + +def _gradient_color(t: float, stops: tuple[str, ...]) -> str: + """Piecewise-linear color at position ``t`` (0..1) across ``stops``.""" + span = (len(stops) - 1) * min(max(t, 0.0), 1.0) + i = min(int(span), len(stops) - 2) + return _lerp_color(stops[i], stops[i + 1], span - i) + + +def _gradient_text(text: str, stops: tuple[str, ...]) -> Text: + """``text`` as Rich text with a horizontal gradient across ``stops``.""" + lines = text.splitlines() + width = max(len(line) for line in lines) + out = Text() + for row, line in enumerate(lines): + if row: + out.append("\n") + for col, char in enumerate(line): + if char == " ": + out.append(char) + else: + out.append(char, style=_gradient_color(col / (width - 1), stops)) + return out @dataclass(frozen=True) @@ -260,10 +314,25 @@ def build_load_report( return steps -def print_banner(console: Console, theme: Theme) -> None: +def _byline_text(theme: Theme, splash: str | None) -> Text: + """Version, ``by Kalvad`` link, and the splash tagline on one line.""" + from lecode import __version__ + + if splash is None: + splash = random.choice(_SPLASHES) + line = Text() + line.append(f"v{__version__}", style=theme.muted) + line.append(" · ", style=theme.muted) + line.append("by ", style=theme.muted) + line.append("Kalvad", style=f"bold {theme.accent} link https://kalvad.com") + line.append(f" — {splash}", style=f"italic {theme.permission}") + return line + + +def print_banner(console: Console, theme: Theme, *, splash: str | None = None) -> None: """The ASCII-art banner + byline — printed immediately at startup.""" - console.print(Text(_BANNER, style=theme.accent), justify="center") - console.print(Text(_BYLINE, style=theme.muted), justify="center") + console.print(_gradient_text(_BANNER, _gradient_stops(theme)), justify="center") + console.print(_byline_text(theme, splash), justify="center") console.print() diff --git a/tests/test_headless.py b/tests/test_headless.py index 4cc8ba1..d2d3e32 100644 --- a/tests/test_headless.py +++ b/tests/test_headless.py @@ -140,8 +140,6 @@ def test_headless_does_not_load_interactive_modules(tmp_path, case): for name in ("HERDR_ENV", "HERDR_BIN_PATH", "HERDR_PANE_ID"): os.environ.pop(name, None) config = Config() -config.mcp.enable_exa = False -config.mcp.enable_context7 = False cli.load_config = lambda: SimpleNamespace(config=config) turns = 20 if sys.argv[2] == "tools" else 0 provider = FakeProvider([ diff --git a/tests/test_tui_loading.py b/tests/test_tui_loading.py index a737106..3203764 100644 --- a/tests/test_tui_loading.py +++ b/tests/test_tui_loading.py @@ -6,6 +6,7 @@ import pytest from rich.console import Console +from rich.text import Text from lecode.agent.builder import build_runtime from lecode.config.loader import LoadedConfig, config_dir, load_config @@ -289,11 +290,11 @@ def test_render_prints_ascii_banner_and_byline(env): console = Console(file=out, force_terminal=False, no_color=True, width=100) render_loading_screen(console, THEME, session_name="demo", steps=steps, cwd=env) text = out.getvalue() - # figlet "standard" banner for "lecode", byline below it - assert "| | ___ ___ ___ __| | ___" in text - assert "|_|\\___|\\___\\___/ \\__,_|\\___|" in text - assert "by wowi42" in text - assert text.index("by wowi42") < text.index("session") # banner above the panel + # pyfiglet "ansi_shadow" banner for "lecode", byline below it + assert "██╗" in text + assert "╚══════╝╚══════╝" in text + assert "by Kalvad" in text + assert text.index("by Kalvad") < text.index("session") # banner above the panel def test_show_loading_screen_never_raises(env, monkeypatch): @@ -332,10 +333,10 @@ def test_interactive_startup_prints_loading_screen(env, monkeypatch, capsys): assert code == 0 out = capsys.readouterr().out # ASCII banner up front, then progressive step lines - assert "| | ___ ___ ___ __| | ___" in out - assert "by wowi42" in out + assert "██╗" in out + assert "by Kalvad" in out assert "config" in out and "provider" in out and "session" in out - assert out.index("| | ___") < out.index("provider") # banner before the steps + assert out.index("██╗") < out.index("provider") # banner before the steps async def _fake_name_prompt(store): @@ -411,8 +412,36 @@ def test_progressive_banner_prints_immediately(): console, out = _record_console() LoadingProgress(console).banner() text = out.getvalue() - assert "| | ___ ___ ___ __| | ___" in text - assert "by wowi42" in text + assert "██╗" in text + assert "by Kalvad" in text + + +def test_banner_shows_version_splash_and_gradient(): + from lecode import __version__ + from lecode.tui.loading import print_banner + + out = io.StringIO() + console = Console( + file=out, force_terminal=True, color_system="truecolor", no_color=False, width=100 + ) + print_banner(console, THEME, splash="test splash") + raw = out.getvalue() + text = Text.from_ansi(raw).plain + assert f"v{__version__}" in text + assert "by Kalvad" in text + assert "test splash" in text + # the banner is colored per column — leftmost glyph is the deep-purple endpoint + assert "38;2;124;58;237" in raw # #7c3aed as an ANSI truecolor escape + + +def test_banner_random_splash_is_one_of_the_known_ones(): + from lecode.tui.loading import _SPLASHES, print_banner + + out = io.StringIO() + console = Console(file=out, force_terminal=False, no_color=True, width=100) + print_banner(console, THEME) + text = out.getvalue() + assert any(splash in text for splash in _SPLASHES) def test_progressive_steps_append_in_order(): @@ -429,7 +458,7 @@ def test_progressive_steps_append_in_order(): assert "… models" in text # pending line shown while the fetch runs assert text.index("… models") < text.index("427 fetched") # banner comes before any step - assert text.index("| | ___") < text.index("✓ config") + assert text.index("██╗") < text.index("✓ config") def test_progressive_step_multiline_detail_indented():