7913116a8e
- .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>
218 lines
7.4 KiB
Python
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))
|