Files
fn_registry/python/functions/infra/export_hub_icons.py
T
egutierrez 7913116a8e chore: auto-commit (129 archivos)
- .claude/agents/fn-analizador/SKILL.md
- .claude/agents/fn-constructor/SKILL.md
- .claude/agents/fn-executor/SKILL.md
- .claude/agents/fn-mejorador/SKILL.md
- .claude/agents/fn-orquestador/SKILL.md
- .claude/agents/fn-recopilador/SKILL.md
- .claude/commands/app.md
- .claude/commands/compile.md
- .claude/commands/cpp-app.md
- .claude/commands/create_functions.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 22:23:12 +02:00

218 lines
7.4 KiB
Python

"""export_hub_icons — rasteriza iconos PNG para todas las apps C++/imgui del registry.
Para cada app cpp/imgui registrada en registry.db lee el bloque `icon:` de su
app.md (phosphor + accent), rasteriza un PNG cuadrado usando la misma logica
visual que generate_app_icon y lo escribe en out_dir/<name>.png.
El directorio de salida lo consume app_hub_launcher via fn::gl_texture_load.
"""
from __future__ import annotations
import os
import sqlite3
import sys
from pathlib import Path
from typing import Any
from infra.generate_app_icon import (
_find_registry_root,
_make_icon_image,
_make_icon_image_adaptive,
_make_icon_image_duotone,
WHITE_DUOTONE_GLYPH,
)
DEFAULT_PHOSPHOR = "app-window"
DEFAULT_ACCENT = "#64748b"
DEFAULT_SIZE = 64
DEFAULT_STYLE = "fill_white"
VALID_STYLES = ("fill_white", "adaptive_duotone", "white_duotone")
def _read_frontmatter(md_path: Path) -> dict[str, Any]:
"""Parse YAML frontmatter from a .md file. Returns {} on any error."""
try:
import yaml
text = md_path.read_text(encoding="utf-8")
if not text.startswith("---"):
return {}
end = text.find("\n---", 3)
if end == -1:
return {}
yaml_block = text[3:end].strip()
data = yaml.safe_load(yaml_block)
return data if isinstance(data, dict) else {}
except Exception as exc:
print(f"[export_hub_icons] WARN: could not parse {md_path}: {exc}", file=sys.stderr)
return {}
def export_hub_icons(
out_dir: str,
*,
size: int = DEFAULT_SIZE,
registry_root: str | None = None,
style: str = DEFAULT_STYLE,
) -> dict:
"""Rasteriza iconos PNG para todas las apps C++/imgui del registry.
Consulta registry.db buscando apps con lang='cpp' AND framework='imgui',
lee el bloque icon: de cada app.md (phosphor + accent), y escribe un PNG
cuadrado de size x size pixels en out_dir/<name>.png usando el mismo estilo
visual de generate_app_icon: fondo redondeado del color accent con glyph
Phosphor blanco centrado al 70%.
Si una app.md no existe, no tiene icono declarado o el SVG Phosphor no se
encuentra, el app se omite (skipped) y el proceso continua con las demas.
Args:
out_dir: Directorio de salida. Se crea si no existe. Los PNGs existentes
se sobreescriben.
size: Lado del cuadrado del PNG en pixels. Default 64.
registry_root: Ruta a la raiz del fn_registry. Si es None, usa la variable
de entorno FN_REGISTRY_ROOT o la raiz del repo derivada de la ubicacion del archivo.
Returns:
{
"ok": True,
"count": N, # PNGs escritos correctamente
"out_dir": "<abs>", # ruta absoluta del directorio de salida
"skipped": [ # apps omitidas con razon
{"name": "...", "reason": "..."},
...
]
}
"""
if style not in VALID_STYLES:
raise ValueError(f"style invalido: {style!r}. Valores: {' | '.join(VALID_STYLES)}")
root = _find_registry_root(registry_root)
db_path = root / "registry.db"
if not db_path.exists():
raise FileNotFoundError(f"registry.db not found at {db_path}")
con = sqlite3.connect(str(db_path))
con.row_factory = sqlite3.Row
try:
rows = con.execute(
"SELECT id, name, dir_path FROM apps"
" WHERE lang='cpp' AND framework='imgui'"
" ORDER BY name"
).fetchall()
finally:
con.close()
phosphor_assets = root / "sources" / "phosphor-core" / "assets"
out_path = Path(out_dir).resolve()
out_path.mkdir(parents=True, exist_ok=True)
count = 0
skipped: list[dict[str, str]] = []
for row in rows:
app_name: str = row["name"]
dir_path: str = row["dir_path"]
# --- Leer app.md ---
md_path = root / dir_path / "app.md"
if not md_path.exists():
msg = f"app.md missing at {md_path}"
print(f"[export_hub_icons] SKIP {app_name}: {msg}", file=sys.stderr)
skipped.append({"name": app_name, "reason": msg})
continue
fm = _read_frontmatter(md_path)
if not fm:
msg = "app.md missing or malformed frontmatter"
print(f"[export_hub_icons] SKIP {app_name}: {msg}", file=sys.stderr)
skipped.append({"name": app_name, "reason": msg})
continue
icon_block = fm.get("icon")
if isinstance(icon_block, dict):
phosphor_name = icon_block.get("phosphor") or DEFAULT_PHOSPHOR
accent_hex = icon_block.get("accent") or DEFAULT_ACCENT
else:
phosphor_name = DEFAULT_PHOSPHOR
accent_hex = DEFAULT_ACCENT
# --- Localizar SVG Phosphor (duotone preferido si style lo necesita) ---
svg_fill = phosphor_assets / "fill" / f"{phosphor_name}-fill.svg"
svg_duo = phosphor_assets / "duotone" / f"{phosphor_name}-duotone.svg"
if style == "fill_white":
if not svg_fill.exists():
msg = f"Phosphor SVG not found: {svg_fill}"
print(f"[export_hub_icons] SKIP {app_name}: {msg}", file=sys.stderr)
skipped.append({"name": app_name, "reason": msg})
continue
else:
if not svg_duo.exists() and not svg_fill.exists():
msg = f"Phosphor SVG not found: {svg_duo} ni {svg_fill}"
print(f"[export_hub_icons] SKIP {app_name}: {msg}", file=sys.stderr)
skipped.append({"name": app_name, "reason": msg})
continue
# --- Rasterizar y guardar PNG ---
try:
if style == "fill_white":
img = _make_icon_image(svg_fill, accent_hex, size)
elif style == "adaptive_duotone":
img = _make_icon_image_adaptive(phosphor_name, accent_hex, size, phosphor_assets)
else: # white_duotone
img = _make_icon_image_duotone(phosphor_name, accent_hex, size, phosphor_assets, WHITE_DUOTONE_GLYPH)
png_out = out_path / f"{app_name}.png"
img.save(str(png_out), format="PNG")
count += 1
except Exception as exc:
msg = f"render error: {exc}"
print(f"[export_hub_icons] SKIP {app_name}: {msg}", file=sys.stderr)
skipped.append({"name": app_name, "reason": msg})
continue
return {
"ok": True,
"count": count,
"out_dir": str(out_path),
"skipped": skipped,
}
if __name__ == "__main__":
import argparse
import json
parser = argparse.ArgumentParser(
description="Export PNG icons for all cpp/imgui apps to a directory."
)
parser.add_argument("out_dir", help="Destination directory for .png files")
parser.add_argument(
"--size",
type=int,
default=DEFAULT_SIZE,
help=f"Icon side in pixels (default {DEFAULT_SIZE})",
)
parser.add_argument(
"--registry-root",
default=None,
help="Path to fn_registry root (default: FN_REGISTRY_ROOT env or repo root derived from file location)",
)
parser.add_argument(
"--style",
default=DEFAULT_STYLE,
choices=VALID_STYLES,
help=f"Estilo del icono (default {DEFAULT_STYLE})",
)
args = parser.parse_args()
result = export_hub_icons(
args.out_dir,
size=args.size,
registry_root=args.registry_root,
style=args.style,
)
print(json.dumps(result, indent=2))