Files
fn_registry/.claude/rules/cpp_apps.md
T

12 KiB

Estandarizacion de apps C++ del registry

Fuentes autoritativas:

  • cpp/PATTERNS.md — checklist y esqueleto del app shell (fn::run_app, AppConfig, panels, layouts, Settings, About).
  • cpp/DESIGN_SYSTEM.md — identidad visual (fn_tokens, ThemeMode, equivalencias @fn_library ↔ C++).

Esta regla NO duplica esos documentos — los señala como obligatorios y añade convenciones estructurales que no aparecen alli.

Scaffolder canonico — OBLIGATORIO

REGLA DURA: crear apps C++ nuevas SIEMPRE con fn run init_cpp_app <name> [--project <p>] [--desc "..."]. NUNCA escribir main.cpp + CMakeLists.txt + app.md desde cero a mano en cpp/apps/ ni projects/*/apps/. Tampoco copiar otra app y renombrar — la deriva entre patrones es lo que estamos eliminando.

Si el scaffolder no cubre un caso (ej. necesitas plantilla diferente, layout custom desde el primer dia), modificas el scaffolder, no escribes la app a mano. La plantilla canonica es codigo, no decoracion.

Razones:

  • Garantiza cfg.about + cfg.log + cfg.panels + framework defaults aplicados.
  • Genera frontmatter app.md valido (framework, dir_path, repo_url) para fn index.
  • Registra add_subdirectory en cpp/CMakeLists.txt (raiz o bloque _DIR para projects).
  • Crea repo Gitea dataforge/<name> con master + commit inicial.

Pipeline: init_cpp_app_bash_pipelines. Slash command equivalente: /new-cpp-app. Auditoria: fn doctor cpp-apps.

1. Ubicacion

Caso Donde vive
App independiente cpp/apps/<nombre>/
App de un proyecto projects/<proyecto>/apps/<nombre>/

NUNCA en cpp/apps/<nombre>/ si pertenece a un proyecto, NUNCA fuera de apps/ directamente. Ver apps_location en memoria + regla apps_vs_functions.md.

2. Estructura minima

<app_dir>/
  CMakeLists.txt        # usa add_imgui_app(target ...)
  app.md                # frontmatter de registro (ver §4)
  main.cpp              # entry: parseo de args + fn::run_app + render()
  [data.{h,cpp}]        # opcional: capa de datos (DB / HTTP / archivos)
  [views.{h,cpp}]       # opcional: composicion de paneles
  [<modulo>.{h,cpp}]    # opcional: dominio especifico
  [vendor/]             # opcional: deps no comunes (se prefieren las globales en cpp/vendor/)
  [.git/]               # cada app es su propio repo Gitea (ver §6)

Reglas de split:

  • main.cpp SIEMPRE — punto de entrada con int main() + fn::run_app(...) + funcion render().
  • Si la app supera ~400 lineas en main.cpp, partir en data.{h,cpp} (carga/persistencia) + views.{h,cpp} (UI por panel).
  • Modulos especificos del dominio en archivos propios (compiler.cpp en shaders_lab, data_http.cpp en registry_dashboard).
  • NO crear archivos de "utilidades genericas" dentro de la app — eso va al registry como funcion (cpp/functions/...).

3. CMakeLists.txt

Patron canonico:

add_imgui_app(<target>
    main.cpp
    [extra_modules.cpp]
    # Funciones del registry usadas (paths absolutos):
    ${CMAKE_SOURCE_DIR}/functions/<dominio>/<funcion>.cpp
    ...
)
target_include_directories(<target> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(<target> PRIVATE [SQLite::SQLite3] [imgui_node_editor] ...)

if(WIN32)
    set_target_properties(<target> PROPERTIES WIN32_EXECUTABLE TRUE)
endif()

Reglas:

  • Usar SIEMPRE la macro add_imgui_app(target ...) — gestiona enlace con fn_framework y copia de TTFs.
  • Listar explicitamente cada .cpp del registry usado (no glob). Hace visible el grafo de dependencias.
  • NO listar tokens.cpp, icon_font.cpp, app_settings.cpp, app_about.cpp, fps_overlay.cpp, panel_menu.cpp, app_menubar.cpp, layouts_menu.cpp, gl_loader.cpp, layout_storage.cpp — viven en fn_framework y dan multiple-definition si se duplican.
  • En WIN32, marcar WIN32_EXECUTABLE TRUE para apps GUI (sin consola).

4. app.md (frontmatter)

Plantilla minima para apps C++:

---
name: <name>
lang: cpp
domain: <gfx|tui|tools|infra|...>
description: "Frase corta — lo que hace y por que existe."
tags: [imgui, ...]                    # si es service, anadir 'service'
uses_functions:                       # IDs del registry — el indexer NO deduce C++
  - <nombre>_cpp_<dominio>
  - ...
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/<name>" o "projects/<proyecto>/apps/<name>"
repo_url: "https://gitea-.../dataforge/<name>"
---

Reglas:

  • uses_functions se rellena a mano con los IDs de las funciones del registry usadas en CMakeLists.txt. Auditar con: sqlite3 registry.db "SELECT id FROM apps WHERE id='<id>';" + revisar diffs.
  • framework: "imgui" siempre que use fn::run_app. Otros valores solo si la app NO usa el shell (raro).
  • tags: incluir service si es daemon de larga duracion (ver function_tags.md).
  • repo_url apunta al sub-repo en Gitea (ver §6).

5. Registro en cpp/CMakeLists.txt

Cada app nueva se registra al final de cpp/CMakeLists.txt:

# --- <app_name> ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/<name>/CMakeLists.txt)
    add_subdirectory(apps/<name>)
endif()

Para apps en proyectos (fuera del arbol cpp/):

# --- <app_name> (lives in projects/<proj>/apps/) ---
set(_<NAME>_DIR ${CMAKE_SOURCE_DIR}/../projects/<proj>/apps/<name>)
if(EXISTS ${_<NAME>_DIR}/CMakeLists.txt)
    add_subdirectory(${_<NAME>_DIR} ${CMAKE_BINARY_DIR}/apps/<name>)
endif()

El if(EXISTS ...) hace el registro tolerante a apps no clonadas (cada app es sub-repo separado).

6. Sub-repo Gitea (TBD obligatorio)

Cada app C++ es su propio repo en dataforge/<name> con branch master. Esto significa:

  • El directorio <app_dir>/ esta en el .gitignore de fn_registry (excepto app.md).
  • El propio directorio tiene .git/ apuntando al sub-repo.
  • TBD obligatorio mientras se desarrolla la app: ver apps_tbd.md. Trabajar en issue/<NNNN>-<slug> o quick/<slug>, mergear a master con --no-ff.
  • Sync entre PCs y push/pull se gestionan con /full-git-push y /full-git-pull.

7. Convencion local_files/ — separacion de distribuible vs estado local

OBLIGATORIO: TODA app coloca sus archivos escribibles bajo <exe_dir>/local_files/. Los archivos distribuibles (.exe, .dll, .ttf, enrichers/, runtime/) viven directos en <exe_dir>/.

<exe_dir>/
├── <app>.exe
├── duckdb.dll, *.ttf, runtime/, enrichers/    ← read-only, ships con el zip
└── local_files/                                ← writable, per-PC
    ├── imgui.ini                               ← gestionado por fn::run_app
    ├── app_settings.ini                        ← gestionado por fn_ui::settings_*
    └── <lo que la app escriba>                 ← usar fn::local_path("nombre")

fn::run_app lo gestiona automaticamente para imgui.ini y app_settings.ini y migra desde <exe_dir>/ o cwd si vienen de una version previa.

Apps que escriban archivos extra (DBs, caches, proyectos del usuario) DEBEN usar fn::local_path("nombre") al construir sus paths. Ejemplo:

// MAL
sqlite3_open("graph_explorer.db", &db);
fopen("graph_explorer.ini", "r");

// BIEN
sqlite3_open(fn::local_path("graph_explorer.db"), &db);
fopen(fn::local_path("graph_explorer.ini"), "r");

API en cpp/framework/app_base.h:

  • fn::exe_dir() — directorio del ejecutable.
  • fn::local_dir()<exe_dir>/local_files/, creado on-demand.
  • fn::local_path(name)<local_dir>/<name>.
  • fn::migrate_to_local_files(names, n) — mueve archivos viejos.

Beneficios:

  • Carpeta del .exe limpia para distribuir (zip portable).
  • Reset trivial (basta borrar local_files/).
  • Separacion clara para backup/sync (solo local_files/ es propio del PC).

7.1 Anti-jitter automatico (AltSnap, tiling WMs)

fn::run_app aplica tres capas de proteccion contra jitter al mover la ventana con herramientas externas (AltSnap en Windows, snap-assist, tiling WMs). Activado por defecto, sin opt-in:

  1. GLFW pos/size callbacksvp->Pos/Size se sincronizan al instante con glfwSetWindowPos/Size (no espera al siguiente NewFrame).
  2. Per-frame viewport sync al inicio del main loop — cubre viewports secundarios (paneles drag-out) que la backend crea dinamicamente.
  3. Win32 WndProc subclass (#ifdef _WIN32) — observa WM_ENTERSIZEMOVE / WM_EXITSIZEMOVE que AltSnap fakea alrededor de cada drag. Mientras el bracket esta abierto el main loop SKIPEA render_fn + glfwSwapBuffers, replicando el contrato del title-bar drag native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer existente).

Tests: cpp/apps/altsnap_jitter_test/ corre dos fases:

  • p1.sync (cross-platform): drives glfwSetWindowPos cada frame, asserta vp->Pos sigue OS dentro de 1px.
  • p2.altsnap (Windows): worker thread fakea WM_ENTERSIZEMOVE + burst de SetWindowPos(SWP_ASYNCWINDOWPOS) + WM_EXITSIZEMOVE, asserta que render() no se llama durante el bracket.

Lanzar con e2e_run_cpp_windows altsnap_jitter_test.

NO hace falta nada en cada app — toda fn::run_app lo hereda. Si una app necesita renderizar incluso durante external move (caso raro: telemetria en vivo, video stream), tendria que evitar el bypass — actualmente no hay flag para desactivarlo (anadir cfg.pause_on_external_sizemove = true por default si surge necesidad).

8. Convenciones de runtime

Cumplir el checklist completo de cpp/PATTERNS.md. Resumen de lo que NUNCA debe aparecer en una app:

Anti-patron Sustituir por
glfwInit() en main fn::run_app(cfg, render)
ImGui::StyleColorsDark() cfg.theme = ThemeMode::FnDark (default)
ImVec4(0.5,0.5,0.5,1) fn_tokens::colors::*
ImGui::Begin(u8"\xEF...") ImGui::Begin(TI_HOME " ...")
Menubar inline cada frame cfg.panels + cfg.layouts_cb
About hardcoded en un panel cfg.about = {...}
gl* directo sin loader cfg.init_gl_loader = true
Tabla SQLite en la raiz del repo <app_dir>/<app>.db (operations.db es solo para entities/relations/executions)
fopen("foo.ini", ...) con path relativo fopen(fn::local_path("foo.ini"), ...) (ver §7)

8. Tests visuales (recomendado, no obligatorio)

Si la app tiene componentes que se quieren proteger contra regresiones visuales, anadir un demo en cpp/apps/primitives_gallery/demos_<dominio>.cpp que use los mismos componentes/funciones del registry. El sistema de capture-and-compare de primitives_gallery --capture funciona como golden-image gate (ver final de cpp/PATTERNS.md).

9. Decisiones que cada app debe tomar y documentar en su app.md

  • viewports: true (default) si las ventanas pueden arrastrarse fuera del main; false si la app necesita estar siempre embebida.
  • init_gl_loader: true si llama gl* directo (renderers GPU custom como graph_renderer); false si solo usa ImGui/ImPlot.
  • about info: nombre, version (semver), descripcion 1 frase.
  • Persistencia: <app>.db SQLite junto al exe; nunca tocar registry.db ni operations.db salvo lectura.
  • Modo CLI: si la app acepta args, documentarlos en el app.md con ejemplos.

10. Layouts persistentes (default)

fn::run_app provee menu Layouts (Save current as.../Apply/Delete/Reset) sin codigo. Crea <exe_dir>/local_files/layouts.db (tabla imgui_layouts + layout_meta) y persiste el imgui.ini serializado por nombre.

Restore-on-open / save-on-close (1.1.0+): al cerrar la app, el slot del layout activo se reescribe con el imgui.ini actual (los retoques de docking sobreviven). Al abrir, si habia un layout activo persistido en layout_meta.last_active, se carga en el primer frame. Si la app no usa named layouts (nunca clico Save/Apply), el comportamiento sigue siendo el de antes: imgui.ini es la unica fuente.

  • App nueva: nada que tocar — Layouts viene activo.
  • App quiere personalizar on_reset (ej. re-mostrar paneles especificos como shaders_lab): abre su propio LayoutStorage, llama layout_storage_make_callbacks, override on_reset, y pasa cfg.layouts_cb = &cb. Cuando se pasa layouts_cb, el auto-storage se desactiva y la app es responsable de layout_storage_apply_pending al inicio de su render.
  • App headless / capture mode: cfg.auto_layouts = false.
  • Cambiar nombre del archivo: cfg.auto_layouts_db = "<algo>.db" (relativo a local_files/).