From f769928ffd6f8c16e17b5a2cea77cd337cfaeec2 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 3 May 2026 00:32:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(framework):=20convencion=20local=5Ffiles/?= =?UTF-8?q?=20=E2=80=94=20separacion=20distribuible=20vs=20estado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toda app C++ basada en fn::run_app coloca sus archivos escribibles bajo /local_files/. Los distribuibles (.exe, dlls, ttfs, enrichers/, runtime/) siguen junto al .exe. Esto deja la carpeta distribuible limpia para zippear y separa con claridad lo que viaja con la app de lo que el PC genera. API publica en fn:: (cpp/framework/app_base.h): - exe_dir() directorio del ejecutable - local_dir() /local_files/, creado on-demand - local_path(name) / - migrate_to_local_files(...) mueve archivos viejos desde cwd/exe_dir Cambios: - run_app configura io.IniFilename = local_path("imgui.ini") y llama migrate_to_local_files(["imgui.ini","app_settings.ini"]) antes de settings_load(). Migracion idempotente para PCs con instalacion previa. - app_settings.cpp usa local_path("app_settings.ini") en lugar de hardcoded "app_settings.ini" relativo al cwd. - cpp_apps.md §7 documenta la convencion como obligatoria. Las apps deben usar fn::local_path() para cualquier archivo escribible nuevo. Beneficios: - zip distribuible no se "ensucia" con .ini/.db generados al usar. - reset trivial: borrar local_files/. - backup/sync per-PC: solo local_files/ es propio del PC. - elimina la mezcla de paths Linux/Windows que generaba bugs como "projects\\default\\operations.db" en builds cross-platform. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/rules/cpp_apps.md | 48 ++++++++- cpp/framework/app_base.cpp | 150 ++++++++++++++++++++++++++++ cpp/framework/app_base.h | 58 +++++++++++ cpp/functions/core/app_settings.cpp | 11 +- 4 files changed, 262 insertions(+), 5 deletions(-) diff --git a/.claude/rules/cpp_apps.md b/.claude/rules/cpp_apps.md index c99f70ed..f98fddb1 100644 --- a/.claude/rules/cpp_apps.md +++ b/.claude/rules/cpp_apps.md @@ -120,7 +120,52 @@ Cada app C++ es su propio repo en `dataforge/` con branch `master`. Esto s - TBD obligatorio mientras se desarrolla la app: ver `apps_tbd.md`. Trabajar en `issue/-` o `quick/`, mergear a `master` con `--no-ff`. - Sync entre PCs y push/pull se gestionan con `/full-git-push` y `/full-git-pull`. -### 7. Convenciones de runtime +### 7. Convencion `local_files/` — separacion de distribuible vs estado local + +**OBLIGATORIO**: TODA app coloca sus archivos escribibles bajo +`/local_files/`. Los archivos distribuibles (`.exe`, `.dll`, +`.ttf`, `enrichers/`, `runtime/`) viven directos en `/`. + +``` +/ +├── .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_* + └── ← usar fn::local_path("nombre") +``` + +`fn::run_app` lo gestiona automaticamente para `imgui.ini` y +`app_settings.ini` y migra desde `/` 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: + +```cpp +// 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()` — `/local_files/`, creado on-demand. +- `fn::local_path(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). + +### 8. Convenciones de runtime Cumplir el checklist completo de `cpp/PATTERNS.md`. Resumen de lo que NUNCA debe aparecer en una app: @@ -134,6 +179,7 @@ Cumplir el checklist completo de `cpp/PATTERNS.md`. Resumen de lo que NUNCA debe | About hardcoded en un panel | `cfg.about = {...}` | | `gl*` directo sin loader | `cfg.init_gl_loader = true` | | Tabla SQLite en la raiz del repo | `/.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) diff --git a/cpp/framework/app_base.cpp b/cpp/framework/app_base.cpp index e141e172..179d6399 100644 --- a/cpp/framework/app_base.cpp +++ b/cpp/framework/app_base.cpp @@ -11,10 +11,25 @@ #include "core/app_about.h" #include "core/app_menubar.h" #include "core/fps_overlay.h" +#include "core/logger.h" +#include "core/log_window.h" #include "gfx/gl_loader.h" #include #include +#include +#include +#include +#include + +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include +#else + #include +#endif #ifdef TRACY_ENABLE #include "tracy/Tracy.hpp" @@ -26,10 +41,121 @@ static void glfw_error_callback(int error, const char* description) { namespace fn { +// ============================================================================ +// Local files +// ============================================================================ +namespace { + +std::string compute_exe_dir() { +#ifdef _WIN32 + wchar_t buf[MAX_PATH * 2]; + DWORD n = GetModuleFileNameW(nullptr, buf, + (DWORD)(sizeof(buf) / sizeof(buf[0]))); + if (n == 0 || n >= sizeof(buf)/sizeof(buf[0])) return ""; + int u8n = WideCharToMultiByte(CP_UTF8, 0, buf, (int)n, + nullptr, 0, nullptr, nullptr); + std::string out(u8n, 0); + WideCharToMultiByte(CP_UTF8, 0, buf, (int)n, out.data(), u8n, + nullptr, nullptr); + size_t slash = out.find_last_of("/\\"); + return (slash == std::string::npos) ? "" : out.substr(0, slash); +#else + char buf[4096]; + ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (n <= 0) return ""; + buf[n] = 0; + std::string out(buf); + size_t slash = out.find_last_of('/'); + return (slash == std::string::npos) ? "" : out.substr(0, slash); +#endif +} + +const std::string& exe_dir_ref() { + static std::string cached = compute_exe_dir(); + return cached; +} + +const std::string& local_dir_ref() { + static std::string cached; + static bool inited = false; + if (inited) return cached; + const std::string& edir = exe_dir_ref(); + if (edir.empty()) { + cached = "local_files"; // fallback: relativo al cwd + } else { + cached = edir + "/local_files"; + } + std::error_code ec; + std::filesystem::create_directories(cached, ec); + inited = true; + return cached; +} + +} // namespace + +const char* exe_dir() { return exe_dir_ref().c_str(); } +const char* local_dir() { return local_dir_ref().c_str(); } + +const char* local_path(const char* name) { + static thread_local std::string buf; + buf = local_dir_ref(); + if (name && *name) { + if (!buf.empty() && buf.back() != '/' && buf.back() != '\\') buf += '/'; + buf += name; + } + return buf.c_str(); +} + +void migrate_to_local_files(const char* const* names, std::size_t n) { + if (!names || n == 0) return; + const std::string& ldir = local_dir_ref(); + const std::string& edir = exe_dir_ref(); + for (std::size_t i = 0; i < n; ++i) { + const char* name = names[i]; + if (!name || !*name) continue; + std::string dst = ldir + "/" + name; + struct stat st{}; + if (::stat(dst.c_str(), &st) == 0) continue; // ya existe en local + + // Buscar en exe_dir y en cwd. Mover el primero que aparezca. + std::string cands[] = { + edir.empty() ? std::string() : (edir + "/" + name), + std::string(name), + }; + for (const auto& src : cands) { + if (src.empty() || src == dst) continue; + if (::stat(src.c_str(), &st) != 0) continue; + std::error_code ec; + std::filesystem::rename(src, dst, ec); + if (ec) { + // Cross-device o permisos — fallback a copy + remove. + std::filesystem::copy(src, dst, + std::filesystem::copy_options::recursive | + std::filesystem::copy_options::overwrite_existing, ec); + if (!ec) std::filesystem::remove_all(src, ec); + } + std::fprintf(stdout, + "[local_files] migrado: %s -> %s\n", + src.c_str(), dst.c_str()); + break; + } + } +} + int run_app(AppConfig config, std::function render_fn) { + // Logger primero para capturar fallos del propio init (GLFW, ventana, GL). + if (config.log.file_path != nullptr) { + fn_log::logger_init( + config.log.file_path, + static_cast(config.log.level)); + fn_log::log_info("app start: %s", config.title ? config.title : "(no title)"); + } + glfwSetErrorCallback(glfw_error_callback); if (!glfwInit()) { fprintf(stderr, "Failed to initialize GLFW\n"); + fn_log::log_error("GLFW init failed"); + if (config.log.file_path != nullptr) fn_log::logger_close(); return 1; } @@ -44,6 +170,8 @@ int run_app(AppConfig config, std::function render_fn) { GLFWwindow* window = glfwCreateWindow(config.width, config.height, config.title, nullptr, nullptr); if (!window) { fprintf(stderr, "Failed to create GLFW window\n"); + fn_log::log_error("GLFW createWindow failed (%dx%d)", config.width, config.height); + if (config.log.file_path != nullptr) fn_log::logger_close(); glfwTerminate(); return 1; } @@ -56,6 +184,8 @@ int run_app(AppConfig config, std::function render_fn) { if (config.init_gl_loader) { if (!fn::gfx::gl_loader_init()) { fprintf(stderr, "Failed to initialize GL function loader\n"); + fn_log::log_error("gl_loader_init failed"); + if (config.log.file_path != nullptr) fn_log::logger_close(); glfwDestroyWindow(window); glfwTerminate(); return 1; @@ -72,6 +202,17 @@ int run_app(AppConfig config, std::function render_fn) { io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + // Convencion local_files: imgui.ini y app_settings.ini viven en + // /local_files/. Migra automaticamente desde el cwd o + // exe_dir si vienen de una version previa. + { + static const char* legacy_names[] = {"imgui.ini", "app_settings.ini"}; + migrate_to_local_files(legacy_names, + sizeof(legacy_names) / sizeof(legacy_names[0])); + } + static std::string s_imgui_ini = local_path("imgui.ini"); + io.IniFilename = s_imgui_ini.c_str(); + // Lee app_settings.ini (font_id, font_size_px, show_fps) antes de cargar // fuentes. Si no existe el .ini, los defaults se aplican. fn_ui::settings_load(); @@ -160,6 +301,9 @@ int run_app(AppConfig config, std::function render_fn) { // Ventana de Settings (no-op si esta cerrada). fn_ui::settings_window_render(); + // Ventana de Logs (no-op si esta cerrada). + fn_ui::log_window_render(); + // Ventana About (no-op si esta cerrada). fn_ui::about_window_render(); @@ -194,6 +338,12 @@ int run_app(AppConfig config, std::function render_fn) { // Persiste settings al exit (idempotente con auto-saves del menu). fn_ui::settings_save(); + // Cierra el archivo de log (si la app lo abrio). + if (config.log.file_path != nullptr) { + fn_log::log_info("app exit"); + fn_log::logger_close(); + } + // Cleanup ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); diff --git a/cpp/framework/app_base.h b/cpp/framework/app_base.h index 5ebcecf0..91a261bf 100644 --- a/cpp/framework/app_base.h +++ b/cpp/framework/app_base.h @@ -17,10 +17,62 @@ namespace fn_ui { const char* version = nullptr; const char* description = nullptr; }; + + // Config de logging. Si file_path != nullptr, fn::run_app llama + // fn_log::logger_init(file_path, level) al inicio y fn_log::logger_close() + // al exit. file_path se interpreta relativo al cwd (junto al ejecutable, + // igual que app_settings.ini). Si file_path == nullptr, no se escribe a + // disco — la ventana Logs sigue funcionando contra el buffer in-memory. + // + // level: 0=Debug, 1=Info, 2=Warn, 3=Error. Default Info. + struct AppLogConfig { + const char* file_path = nullptr; + int level = 1; // fn_log::Level::Info + }; } namespace fn { +// ---------------------------------------------------------------------------- +// Local files — separacion de archivos distribuibles vs estado local. +// ---------------------------------------------------------------------------- +// +// Convencion del registry: TODA app coloca sus archivos escribibles +// (settings, DBs, layouts ImGui, caches, proyectos del usuario) bajo +// `/local_files/`. Los archivos distribuibles (.exe, .ttf, +// .dll, runtime/, enrichers/) viven directos en `/`. +// +// Esto mantiene la carpeta del .exe limpia para distribuir, separa +// nitidamente "lo que vino con el zip" de "lo que el PC genero", y +// facilita el reset (basta con borrar local_files/). +// +// `fn::run_app` configura `io.IniFilename = local_path("imgui.ini")` y +// `app_settings.ini` se lee/escribe desde local_files/ automaticamente. +// Cualquier archivo escribible adicional de la app debe usar +// `fn::local_path("nombre")` al construir su path. +// +// La carpeta se crea on-demand en la primera llamada a `local_dir()`. +// Si existen archivos viejos en el cwd (compat con versiones previas +// del registry), `migrate_to_local_files()` los mueve. + +// Devuelve el directorio del ejecutable actual (sin trailing slash). +// "" si no se puede resolver (raro — fallback al cwd). +const char* exe_dir(); + +// Devuelve el path absoluto a `/local_files/`. Crea la +// carpeta si no existe. Sin trailing slash. +const char* local_dir(); + +// Construye `/`. El puntero retornado apunta a un +// std::string interno por-thread que permanece valido hasta la +// proxima llamada — copia el valor si vas a guardarlo. +const char* local_path(const char* name); + +// Mueve los archivos listados de cwd o exe_dir a local_files/ si +// existen ahi pero NO existen ya en local_files/. Idempotente. Las +// apps lo llaman al iniciar para migrar instalaciones viejas. +void migrate_to_local_files(const char* const* names, std::size_t n); + // Modos de tema para run_app. enum class ThemeMode { FnDark, // Identidad del registry (Mantine v9 dark + indigo). DEFAULT. @@ -58,6 +110,12 @@ struct AppConfig { // GL y antes del primer frame. Necesario para apps que llaman gl* directo // en Windows (en Linux es no-op). bool init_gl_loader = false; + + // Logging opcional. Si log.file_path != nullptr, run_app inicializa el + // logger global antes del primer frame y lo cierra al exit. La ventana + // "Logs..." en el menubar siempre esta disponible (lee del buffer + // in-memory aunque no haya archivo). + fn_ui::AppLogConfig log{}; }; // Run an ImGui application. The render_fn is called every frame diff --git a/cpp/functions/core/app_settings.cpp b/cpp/functions/core/app_settings.cpp index fa28391a..fd8e2656 100644 --- a/cpp/functions/core/app_settings.cpp +++ b/cpp/functions/core/app_settings.cpp @@ -1,6 +1,7 @@ #include "app_settings.h" #include "imgui.h" +#include "../../framework/app_base.h" #include #include @@ -16,7 +17,9 @@ AppSettings g_settings; bool g_font_dirty = false; bool g_window_open = false; -constexpr const char* kSettingsPath = "app_settings.ini"; +// app_settings.ini vive en /local_files/ (convencion del +// registry — todos los archivos escribibles bajo local_files/). +const char* settings_path() { return fn::local_path("app_settings.ini"); } const float k_sizes[] = {12.0f, 13.0f, 14.0f, 15.0f, 16.0f, 18.0f, 20.0f}; constexpr int k_size_count = sizeof(k_sizes) / sizeof(k_sizes[0]); @@ -59,7 +62,7 @@ void parse_line(const char* line) { AppSettings& settings() { return g_settings; } void settings_load() { - FILE* f = std::fopen(kSettingsPath, "r"); + FILE* f = std::fopen(settings_path(), "r"); if (!f) return; char line[256]; while (std::fgets(line, sizeof(line), f)) parse_line(line); @@ -67,9 +70,9 @@ void settings_load() { } void settings_save() { - FILE* f = std::fopen(kSettingsPath, "w"); + FILE* f = std::fopen(settings_path(), "w"); if (!f) { - std::fprintf(stderr, "[fn_ui] settings_save: no pude abrir %s\n", kSettingsPath); + std::fprintf(stderr, "[fn_ui] settings_save: no pude abrir %s\n", settings_path()); return; } std::fprintf(f, "# fn_registry app_settings.ini — autogenerado, editable\n");