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
+121
View File
@@ -1,5 +1,6 @@
#include "views.h"
#include "entity_ops.h"
#include "project_manager.h"
#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
@@ -91,6 +92,74 @@ void views_apply_visibility(AppState& app) {
void views_toolbar(AppState& app) {
using namespace fn_ui;
toolbar_begin();
// Project switcher — etiqueta = proyecto activo. Click abre popup con
// New / Open / Recent / Reveal. Si no hay proyecto activo (modo
// legacy con --input directo), muestra "(no project)".
{
char btn_label[128];
const char* p = app.active_project.empty()
? "(no project)" : app.active_project.c_str();
std::snprintf(btn_label, sizeof(btn_label), TI_FOLDER " Project: %s", p);
if (button(btn_label, ButtonVariant::Secondary)) {
// Refresca caches al abrir
project_list(&app.project_list_cache);
ProjectSettings ps;
project_settings_load(&ps);
app.project_recent_cache = ps.recent;
ImGui::OpenPopup("##project_menu");
}
if (ImGui::BeginPopup("##project_menu")) {
if (ImGui::MenuItem(TI_PLUS " New project...")) {
app.show_new_project_modal = true;
app.new_project_buf[0] = 0;
app.new_project_error.clear();
}
ImGui::Separator();
// Recent submenu
if (!app.project_recent_cache.empty()) {
if (ImGui::BeginMenu("Recent")) {
for (const auto& slug : app.project_recent_cache) {
bool is_active = (slug == app.active_project);
if (ImGui::MenuItem(slug.c_str(), nullptr, is_active)
&& !is_active) {
app.want_switch_project = true;
app.switch_project_target = slug;
}
}
ImGui::EndMenu();
}
}
// Open submenu — todos los detectados
if (ImGui::BeginMenu("Open")) {
if (app.project_list_cache.empty()) {
ImGui::TextDisabled("(no projects yet)");
} else {
for (const auto& slug : app.project_list_cache) {
bool is_active = (slug == app.active_project);
if (ImGui::MenuItem(slug.c_str(), nullptr, is_active)
&& !is_active) {
app.want_switch_project = true;
app.switch_project_target = slug;
}
}
}
ImGui::EndMenu();
}
ImGui::Separator();
bool can_reveal = !app.active_project.empty();
if (ImGui::MenuItem(TI_FOLDER_OPEN " Reveal in explorer",
nullptr, false, can_reveal)) {
project_reveal_in_explorer(app.active_project.c_str());
}
ImGui::EndPopup();
}
}
ImGui::SameLine();
toolbar_separator();
if (button(TI_FOLDER " Open file...", ButtonVariant::Secondary)) {
app.show_open_modal = true;
}
@@ -490,6 +559,58 @@ bool views_filters_modal(AppState& app) {
return changed;
}
// ----------------------------------------------------------------------------
// Modal: New project
// ----------------------------------------------------------------------------
bool views_new_project_modal(AppState& app) {
if (!app.show_new_project_modal) return false;
bool created = false;
if (fn_ui::modal_dialog_begin("New project", &app.show_new_project_modal,
ImVec2(480, 0))) {
ImGui::TextWrapped(
"Crea una subcarpeta en projects/ con su propio operations.db,"
" types.yaml y graph_explorer.db.");
ImGui::Spacing();
fn_ui::text_input("Slug", app.new_project_buf, sizeof(app.new_project_buf),
"caso_aurgi");
ImGui::TextDisabled("a-z, 0-9, '_' y '-' (max 64)");
if (!app.new_project_error.empty()) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.4f, 1.0f));
ImGui::TextWrapped("%s", app.new_project_error.c_str());
ImGui::PopStyleColor();
}
ImGui::Spacing();
if (fn_ui::button("Create", fn_ui::ButtonVariant::Primary)) {
std::string err;
if (!project_validate_slug(app.new_project_buf, &err)) {
app.new_project_error = err;
} else if (project_exists(app.new_project_buf)) {
app.new_project_error = "ya existe un proyecto con ese slug";
} else if (!project_create(app.new_project_buf, &err)) {
app.new_project_error = err;
} else {
// Switch al recien creado
app.want_switch_project = true;
app.switch_project_target = app.new_project_buf;
app.show_new_project_modal = false;
app.new_project_error.clear();
created = true;
}
}
ImGui::SameLine();
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
app.show_new_project_modal = false;
app.new_project_error.clear();
}
}
fn_ui::modal_dialog_end();
return created;
}
bool views_open_modal(AppState& app) {
if (!app.show_open_modal) return false;
bool opened = false;