Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 │
Expand Down
91 changes: 80 additions & 11 deletions src/lecode/tui/loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()


Expand Down
2 changes: 0 additions & 2 deletions tests/test_headless.py
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
51 changes: 40 additions & 11 deletions tests/test_tui_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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():
Expand All @@ -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():
Expand Down
Loading