opencrabs 0.5.1

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Recommended: the 40MB prebuilt binary for macOS, Linux and Windows: https://github.com/adolfousier/opencrabs/releases
#!/usr/bin/env python3
"""Derive AnsiColors blocks for each preset from their rgb(0xRRGGBB) literals.

Mechanical codegen: reads presets.rs, parses each ThemeColors literal, applies
the rgb_to_ansi256 algorithm (mirroring src/tui/render/presets_test.rs::rgb_to_ansi256),
and emits a parallel `ansi: AnsiColors { ... }` block per preset.

Source of truth for the algorithm: Rust implementation in theme.rs. This script
duplicates it for codegen only — the Rust tests pin the behavior.

Run:  python3 scripts/gen_theme_ansi.py
Apply output to src/tui/render/presets.rs
"""
from __future__ import annotations
import re
import sys
from pathlib import Path

PRESETS_FILE = Path("src/tui/render/presets.rs")

# ANSI 256-color ramp: 6x6x6 RGB cube + 24 grayscale ramp + 16 system colors
GRAY_RAMP_START = 232  # indices 232..=255 = 24 shades of gray

def rgb_to_ansi256(r: int, g: int, b: int) -> int:
    """Mirror of Rust rgb_to_ansi256 in theme.rs. Returns 16..=255."""
    # Pure neutral? use grayscale ramp
    if r == g == b:
        # Map 0..255 -> 232..255 (24 shades)
        idx = int(round(r * 23.0 / 255.0))
        idx = max(0, min(23, idx))
        return GRAY_RAMP_START + idx
    # Otherwise: project onto 6x6x6 cube (indices 16..=231)
    def axis(v: int) -> int:
        # Nearest of {0, 95, 135, 175, 215, 255}
        levels = [0, 95, 135, 175, 215, 255]
        return min(range(6), key=lambda i: abs(v - levels[i]))
    ri, gi, bi = axis(r), axis(g), axis(b)
    return 16 + 36 * ri + 6 * gi + bi


# Parse a preset: find `pub static NAME: Theme = Theme { ... ThemeColors { field: rgb(0xRRGGBB), ... } ... }`
PRESET_RE = re.compile(
    r"pub static (\w+): Theme = Theme \{[^}]*?colors: ThemeColors \{([^}]+)\}",
    re.DOTALL,
)
RGB_FIELD_RE = re.compile(r"(\w+):\s*rgb\((0x[0-9A-Fa-f]+)\)")


def parse_preset(body: str) -> list[tuple[str, int, int, int]]:
    """Return list of (field_name, r, g, b) in source order."""
    out = []
    for m in RGB_FIELD_RE.finditer(body):
        name = m.group(1)
        hex_str = m.group(2)
        val = int(hex_str, 16)
        r = (val >> 16) & 0xFF
        g = (val >> 8) & 0xFF
        b = val & 0xFF
        out.append((name, r, g, b))
    return out


def format_ansi_block(fields: list[tuple[str, int, int, int]], indent: str = "    ") -> str:
    """Produce the `ansi: AnsiColors { ... }` source text."""
    lines = ["ansi: AnsiColors {"]
    for name, r, g, b in fields:
        idx = rgb_to_ansi256(r, g, b)
        lines.append(f"{indent}{indent}{name}: {idx},")
    lines.append(f"{indent}}},")
    return "\n".join(lines)


def main() -> int:
    src = PRESETS_FILE.read_text()
    matches = list(PRESET_RE.finditer(src))
    if not matches:
        print("no presets found", file=sys.stderr)
        return 1
    print(f"// Generated by {__file__} — do not edit by hand.")
    print(f"// Source of truth: src/tui/render/presets.rs (rgb literals)\n")
    for m in matches:
        name = m.group(1)
        fields = parse_preset(m.group(2))
        print(f"// {name}: {len(fields)} fields")
        print(format_ansi_block(fields, indent="    "))
        print()
    return 0


if __name__ == "__main__":
    sys.exit(main())