feat(projects): integrate project switcher in app shell + Inspector menu

main.cpp:
- Forward decl + switch_to_project: cierra layout_store, libera grafo,
  aplica nuevos paths, vuelve a cargar.
- apply_project_paths: deriva operations.db/types.yaml/graph_explorer.db
  del slug y los expone a g_app.active_project.
- main: arg --project <slug>; modo legacy si --input/positional dado;
  modo proyecto si no — migra layout legacy, decide target via
  arg/last_active/'default', crea si no existe, abre BDs y carga.
- render(): handler want_switch_project + monta views_new_project_modal.

views.h: AppState gana active_project, want_switch_project,
switch_project_target, show_new_project_modal, new_project_buf,
new_project_error, project_list_cache, project_recent_cache.

views.cpp:
- Toolbar: boton 'Project: <slug>' con popup (New/Recent/Open/Reveal).
  Refresca caches al abrir el menu.
- views_new_project_modal: input slug + validacion + creacion + switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 23:46:47 +02:00
parent c365b1bc43
commit d6f7318c24
3 changed files with 254 additions and 15 deletions
+119 -15
View File
@@ -24,6 +24,7 @@
#include "types_registry.h"
#include "layout_store.h"
#include "entity_ops.h"
#include "project_manager.h"
#include <cstdio>
#include <cstdlib>
@@ -48,6 +49,12 @@ static std::string g_layout_initial; // --layout flag
static uint64_t g_graph_hash = 0;
static bool g_loaded = false;
// Project state — paths derivados del proyecto activo. En modo legacy
// (--input/positional explicito), `g_active_project` queda vacio y los paths
// vienen del CLI directamente.
static std::string g_active_project;
static std::string g_layout_db_path; // ruta de graph_explorer.db
// Force layout GPU context (lazy init).
static ForceLayoutGPU* g_gpu_ctx = nullptr;
static bool g_gpu_dirty = true;
@@ -101,6 +108,60 @@ static void apply_static_layout(int mode) {
}
}
// Forward decl — definido mas abajo, lo necesita switch_to_project.
static bool load_input();
// ----------------------------------------------------------------------------
// Project lifecycle
// ----------------------------------------------------------------------------
// Aplica los paths del proyecto `slug` a las globales (g_input_path,
// g_types_path, g_layout_db_path) y actualiza g_active_project. No abre BDs
// ni carga el grafo — eso lo hace el caller.
static void apply_project_paths(const std::string& slug) {
ge::ProjectPaths p = ge::project_paths(slug.c_str());
g_active_project = slug;
g_input_path = p.operations_db;
g_types_path = p.types_yaml;
g_layout_db_path = p.layout_db;
g_app.active_project = slug;
}
// Cambia al proyecto `slug`: cierra layout_store, libera grafo, abre BDs
// nuevas, carga grafo, persiste como last_active. Devuelve true en exito.
static bool switch_to_project(const std::string& slug) {
if (slug.empty()) return false;
if (!ge::project_exists(slug.c_str())) {
std::fprintf(stderr, "[graph_explorer] project '%s' no existe\n",
slug.c_str());
return false;
}
// Cierra estado del proyecto actual
ge::layout_store_close();
if (g_loaded) {
graph::graph_free(&g_graph);
g_loaded = false;
}
if (g_atlas) {
graph_icons_destroy(g_atlas);
g_atlas = nullptr;
}
g_atlas_bound = false;
g_viewport.selection.clear();
g_viewport.hovered_node = -1;
g_viewport.selected_node = -1;
// Aplica paths nuevos y abre BDs
apply_project_paths(slug);
if (!ge::layout_store_open(g_layout_db_path.c_str())) {
std::fprintf(stderr, "[graph_explorer] layout_store_open failed: %s\n",
g_layout_db_path.c_str());
}
bool ok = load_input();
if (ok) ge::project_settings_touch(slug.c_str());
return ok;
}
static bool load_input() {
g_input.kind = ge::INPUT_OPERATIONS;
g_input.uri = g_input_path.c_str();
@@ -424,6 +485,18 @@ static void render() {
// Modals
ge::views_open_modal(g_app);
ge::views_filters_modal(g_app);
ge::views_new_project_modal(g_app);
// Project switch (desde menu Project, o tras crear proyecto nuevo)
if (g_app.want_switch_project && !g_app.switch_project_target.empty()) {
std::string target = g_app.switch_project_target;
g_app.want_switch_project = false;
g_app.switch_project_target.clear();
if (!switch_to_project(target)) {
std::fprintf(stderr,
"[graph_explorer] switch_to_project('%s') failed\n", target.c_str());
}
}
// Si el usuario aplico nuevo layout en la toolbar
if (g_app.apply_layout_tick > 0) {
@@ -685,10 +758,14 @@ static void usage() {
"Usage: graph_explorer [<operations.db>]\n"
" graph_explorer --input operations <path>\n"
" graph_explorer --types <types.yaml>\n"
" graph_explorer --layout force|grid|circular|radial|hierarchical|fixed\n");
" graph_explorer --layout force|grid|circular|radial|hierarchical|fixed\n"
" graph_explorer --project <slug>\n");
}
int main(int argc, char** argv) {
bool legacy_mode = false; // --input / positional dado: NO usar proyecto
std::string project_arg; // --project <slug> (puede estar vacio)
for (int i = 1; i < argc; ++i) {
const char* a = argv[i];
if (std::strcmp(a, "--input") == 0 && i + 2 < argc) {
@@ -696,6 +773,7 @@ int main(int argc, char** argv) {
const char* path = argv[++i];
if (std::strcmp(kind, "operations") == 0) {
g_input_path = path;
legacy_mode = true;
} else {
std::fprintf(stderr, "[graph_explorer] unsupported input kind: %s\n", kind);
return 1;
@@ -704,6 +782,8 @@ int main(int argc, char** argv) {
g_types_path = argv[++i];
} else if (std::strcmp(a, "--layout") == 0 && i + 1 < argc) {
g_layout_initial = argv[++i];
} else if (std::strcmp(a, "--project") == 0 && i + 1 < argc) {
project_arg = argv[++i];
} else if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) {
usage();
return 0;
@@ -712,25 +792,49 @@ int main(int argc, char** argv) {
usage();
return 1;
} else {
// Positional: tratado como operations.db
if (g_input_path.empty()) g_input_path = a;
// Positional: tratado como operations.db (legacy)
if (g_input_path.empty()) {
g_input_path = a;
legacy_mode = true;
}
}
}
// SQLite store junto al exe.
ge::layout_store_open("graph_explorer.db");
// Si no llego --input/positional, intentar operations.db en el cwd
// (mismo criterio que graph_explorer.db: relativo al directorio de ejecucion).
if (g_input_path.empty()) {
if (FILE* f = std::fopen("operations.db", "rb")) {
std::fclose(f);
g_input_path = "operations.db";
std::fprintf(stdout, "[graph_explorer] using default ./operations.db\n");
if (legacy_mode) {
// Modo legacy: paths sueltos junto al exe (compat con flujo anterior)
ge::layout_store_open("graph_explorer.db");
g_layout_db_path = "graph_explorer.db";
if (!g_input_path.empty()) {
load_input();
}
}
} else {
// Modo proyecto: migra layout legacy si aplica, decide proyecto activo,
// crea default si no existe ninguno.
ge::projects_migrate_legacy_layout();
if (!g_input_path.empty()) {
std::string target = project_arg;
if (target.empty()) {
ge::ProjectSettings ps;
ge::project_settings_load(&ps);
target = ps.last_active;
}
if (target.empty()) target = ge::k_default_project;
if (!ge::project_exists(target.c_str())) {
std::string err;
if (!ge::project_create(target.c_str(), &err)) {
std::fprintf(stderr,
"[graph_explorer] no se pudo crear el proyecto '%s': %s\n",
target.c_str(), err.c_str());
return 1;
}
std::fprintf(stdout,
"[graph_explorer] proyecto creado: projects/%s/\n", target.c_str());
}
apply_project_paths(target);
ge::layout_store_open(g_layout_db_path.c_str());
ge::project_settings_touch(target.c_str());
load_input();
}