Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.mdvalido (framework, dir_path, repo_url) parafn index. - Registra
add_subdirectoryencpp/CMakeLists.txt(raiz o bloque_DIRpara 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.cppSIEMPRE — punto de entrada conint main()+fn::run_app(...)+ funcionrender().- Si la app supera ~400 lineas en
main.cpp, partir endata.{h,cpp}(carga/persistencia) +views.{h,cpp}(UI por panel). - Modulos especificos del dominio en archivos propios (
compiler.cppenshaders_lab,data_http.cppenregistry_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 confn_frameworky copia de TTFs. - Listar explicitamente cada
.cppdel 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 enfn_frameworky dan multiple-definition si se duplican. - En
WIN32, marcarWIN32_EXECUTABLE TRUEpara 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_functionsse rellena a mano con los IDs de las funciones del registry usadas enCMakeLists.txt. Auditar con:sqlite3 registry.db "SELECT id FROM apps WHERE id='<id>';"+ revisar diffs.framework: "imgui"siempre que usefn::run_app. Otros valores solo si la app NO usa el shell (raro).tags: incluirservicesi es daemon de larga duracion (verfunction_tags.md).repo_urlapunta 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.gitignoredefn_registry(exceptoapp.md). - El propio directorio tiene
.git/apuntando al sub-repo. - TBD obligatorio mientras se desarrolla la app: ver
apps_tbd.md. Trabajar enissue/<NNNN>-<slug>oquick/<slug>, mergear amastercon--no-ff. - Sync entre PCs y push/pull se gestionan con
/full-git-pushy/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:
- GLFW pos/size callbacks —
vp->Pos/Sizese sincronizan al instante conglfwSetWindowPos/Size(no espera al siguiente NewFrame). - Per-frame viewport sync al inicio del main loop — cubre viewports secundarios (paneles drag-out) que la backend crea dinamicamente.
- Win32 WndProc subclass (
#ifdef _WIN32) — observaWM_ENTERSIZEMOVE/WM_EXITSIZEMOVEque AltSnap fakea alrededor de cada drag. Mientras el bracket esta abierto el main loop SKIPEArender_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): drivesglfwSetWindowPoscada frame, assertavp->Possigue OS dentro de 1px.p2.altsnap(Windows): worker thread fakeaWM_ENTERSIZEMOVE+ burst deSetWindowPos(SWP_ASYNCWINDOWPOS)+WM_EXITSIZEMOVE, asserta querender()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;falsesi la app necesita estar siempre embebida.init_gl_loader:truesi llamagl*directo (renderers GPU custom comograph_renderer);falsesi solo usa ImGui/ImPlot.aboutinfo: nombre, version (semver), descripcion 1 frase.- Persistencia:
<app>.dbSQLite junto al exe; nunca tocarregistry.dbnioperations.dbsalvo lectura. - Modo CLI: si la app acepta args, documentarlos en el
app.mdcon 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 comoshaders_lab): abre su propioLayoutStorage, llamalayout_storage_make_callbacks, overrideon_reset, y pasacfg.layouts_cb = &cb. Cuando se pasalayouts_cb, el auto-storage se desactiva y la app es responsable delayout_storage_apply_pendingal inicio de surender. - App headless / capture mode:
cfg.auto_layouts = false. - Cambiar nombre del archivo:
cfg.auto_layouts_db = "<algo>.db"(relativo alocal_files/).