commit d010a03b44c2f35e68993a07cdd4e5f26e0539bc Author: Egutierrez Date: Sun May 17 00:07:04 2026 +0200 docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard) Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..da5bcf9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +add_imgui_app(app_hub_launcher + main.cpp +) +target_include_directories(app_hub_launcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +# fn_table_viz: provides data_table::render(), viz_render, TQL engine, Lua, LLM. +# Guard keeps the app compilable in builds where vendor/lua is absent. +if(TARGET fn_table_viz) + target_link_libraries(app_hub_launcher PRIVATE fn_table_viz) +endif() + +if(WIN32) + set_target_properties(app_hub_launcher PROPERTIES WIN32_EXECUTABLE TRUE) +endif() diff --git a/app.md b/app.md new file mode 100644 index 0000000..cc6b5c2 --- /dev/null +++ b/app.md @@ -0,0 +1,45 @@ +--- +name: app_hub_launcher +lang: cpp +domain: tools +description: "Hub launcher: lista y arranca apps C++ desplegadas en Windows Desktop" +tags: [launcher, hub, suite, imgui] +uses_functions: + # Uncomment when using data_table::render() — provided via fn_table_viz: + # - data_table_cpp_viz + # - viz_render_cpp_viz + # - compute_stage_cpp_core + # - compute_pipeline_cpp_core + # - compute_column_stats_cpp_core + # - auto_detect_type_cpp_core + # - tql_emit_cpp_core + # - tql_apply_cpp_core + # - lua_engine_cpp_core + # - join_tables_cpp_core + # - tql_to_sql_cpp_core + # - llm_anthropic_cpp_core +uses_types: [] +framework: "imgui" +entry_point: "main.cpp" +dir_path: "apps/app_hub_launcher" +repo_url: "https://gitea.organic-machine.com/dataforge/app_hub_launcher" +icon: + phosphor: "squares-four" + accent: "#8b5cf6" +--- + +# app_hub_launcher + +Hub launcher: lista y arranca apps C++ desplegadas en Windows Desktop + +## Build + +```bash +cd cpp && cmake --build build --target app_hub_launcher -j +``` + +## Run + +```bash +./cpp/build/app_hub_launcher +``` diff --git a/appicon.ico b/appicon.ico new file mode 100644 index 0000000..e786236 Binary files /dev/null and b/appicon.ico differ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..615c1b8 --- /dev/null +++ b/main.cpp @@ -0,0 +1,310 @@ +#include +#include +#include "app_base.h" +#include "core/panel_menu.h" +#include "core/icons_tabler.h" +#include "core/logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#endif + +namespace fs = std::filesystem; + +struct AppMeta { + std::string display_name; + std::string description; + ImU32 accent; +}; + +struct AppEntry { + std::string name; + fs::path exe_path; + AppMeta meta; +}; + +static std::vector g_apps; +static bool g_show_main = true; +static char g_filter[128] = {0}; +static std::string g_last_status; + +static ImU32 hex_to_imu32(std::string const& hex_in) { + std::string h = hex_in; + if (!h.empty() && h[0] == '#') h.erase(0, 1); + if (h.size() != 6) return IM_COL32(100, 116, 139, 255); + unsigned int v = 0; + std::stringstream ss; + ss << std::hex << h; + ss >> v; + int r = (v >> 16) & 0xFF; + int g = (v >> 8) & 0xFF; + int b = v & 0xFF; + return IM_COL32(r, g, b, 255); +} + +static std::string camel_case_from_snake(std::string const& s) { + std::string out; + out.reserve(s.size() + 2); + bool cap = true; + for (char c : s) { + if (c == '_' || c == '-') { + out.push_back(' '); + cap = true; + } else if (cap) { + out.push_back((char)std::toupper((unsigned char)c)); + cap = false; + } else { + out.push_back((char)std::tolower((unsigned char)c)); + } + } + return out; +} + +static std::unordered_map load_manifest() { + std::unordered_map map; + fs::path path = fs::path(fn::local_path("hub_manifest.tsv")); + std::ifstream f(path); + if (!f.is_open()) { + fn_log::log_warn("hub: manifest not found at %s", path.string().c_str()); + return map; + } + std::string line; + bool header = true; + while (std::getline(f, line)) { + if (header) { header = false; continue; } + if (line.empty()) continue; + std::vector cols; + std::stringstream ss(line); + std::string item; + while (std::getline(ss, item, '\t')) cols.push_back(item); + if (cols.size() < 4) continue; + AppMeta m; + m.display_name = cols[1]; + m.description = cols[2]; + m.accent = hex_to_imu32(cols[3]); + map[cols[0]] = m; + } + fn_log::log_info("hub: loaded %zu manifest rows from %s", + map.size(), path.string().c_str()); + return map; +} + +static fs::path apps_root_dir() { + return fs::path(fn::exe_dir()).parent_path(); +} + +static void scan_apps() { + g_apps.clear(); + auto manifest = load_manifest(); + fs::path root = apps_root_dir(); + std::error_code ec; + if (!fs::exists(root, ec) || !fs::is_directory(root, ec)) { + fn_log::log_warn("hub: apps root not found: %s", root.string().c_str()); + return; + } + std::string self_name = fs::path(fn::exe_dir()).filename().string(); + for (auto const& entry : fs::directory_iterator(root, ec)) { + if (!entry.is_directory(ec)) continue; + std::string name = entry.path().filename().string(); + if (name == self_name) continue; + fs::path exe = entry.path() / (name + ".exe"); + if (!(fs::exists(exe, ec) && fs::is_regular_file(exe, ec))) continue; + AppEntry e; + e.name = name; + e.exe_path = exe; + auto it = manifest.find(name); + if (it != manifest.end()) { + e.meta = it->second; + } else { + e.meta.display_name = camel_case_from_snake(name); + e.meta.description = ""; + e.meta.accent = IM_COL32(100, 116, 139, 255); + } + g_apps.push_back(std::move(e)); + } + std::sort(g_apps.begin(), g_apps.end(), + [](AppEntry const& a, AppEntry const& b) { + return a.meta.display_name < b.meta.display_name; + }); + fn_log::log_info("hub: scanned %zu apps at %s", + g_apps.size(), root.string().c_str()); +} + +static bool launch_app(AppEntry const& app) { +#ifdef _WIN32 + std::wstring wexe(app.exe_path.wstring()); + std::wstring wdir(app.exe_path.parent_path().wstring()); + HINSTANCE r = ShellExecuteW(NULL, L"open", wexe.c_str(), NULL, + wdir.c_str(), SW_SHOWNORMAL); + bool ok = ((INT_PTR)r > 32); + if (ok) { + fn_log::log_info("hub: launched %s", app.name.c_str()); + g_last_status = std::string("Launched ") + app.meta.display_name; + } else { + fn_log::log_warn("hub: ShellExecuteW failed for %s (code=%lld)", + app.name.c_str(), (long long)(INT_PTR)r); + g_last_status = std::string("FAILED to launch ") + app.meta.display_name; + } + return ok; +#else + (void)app; + g_last_status = "Launch only supported on Windows"; + fn_log::log_warn("hub: launch requested on non-Windows build"); + return false; +#endif +} + +static bool matches_filter(AppEntry const& app) { + if (g_filter[0] == '\0') return true; + auto lower = [](std::string s) { + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { return (char)std::tolower(c); }); + return s; + }; + std::string f = lower(g_filter); + return lower(app.name).find(f) != std::string::npos || + lower(app.meta.display_name).find(f) != std::string::npos || + lower(app.meta.description).find(f) != std::string::npos; +} + +static ImU32 with_alpha(ImU32 c, float a) { + ImVec4 v = ImGui::ColorConvertU32ToFloat4(c); + v.w = a; + return ImGui::ColorConvertFloat4ToU32(v); +} + +static ImU32 lighten(ImU32 c, float amount) { + ImVec4 v = ImGui::ColorConvertU32ToFloat4(c); + v.x = v.x + (1.0f - v.x) * amount; + v.y = v.y + (1.0f - v.y) * amount; + v.z = v.z + (1.0f - v.z) * amount; + return ImGui::ColorConvertFloat4ToU32(v); +} + +static void draw_card(int idx, AppEntry const& app, float card_w, float card_h) { + ImGui::PushID(idx); + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("card", ImVec2(card_w, card_h)); + bool hovered = ImGui::IsItemHovered(); + bool clicked = ImGui::IsItemClicked(); + bool held = ImGui::IsItemActive(); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 p0 = pos; + ImVec2 p1 = ImVec2(pos.x + card_w, pos.y + card_h); + + ImU32 bg = with_alpha(app.meta.accent, hovered ? 0.28f : 0.16f); + if (held) bg = with_alpha(app.meta.accent, 0.40f); + ImU32 border = with_alpha(app.meta.accent, hovered ? 0.95f : 0.55f); + float rounding = 8.0f; + + dl->AddRectFilled(p0, p1, bg, rounding); + dl->AddRect(p0, p1, border, rounding, 0, 1.5f); + + // Accent stripe on left + float stripe_w = 6.0f; + dl->AddRectFilled(p0, ImVec2(p0.x + stripe_w, p1.y), app.meta.accent, rounding, + ImDrawFlags_RoundCornersLeft); + + float pad_x = stripe_w + 12.0f; + float pad_y = 10.0f; + ImVec2 text_pos = ImVec2(p0.x + pad_x, p0.y + pad_y); + + // Title + ImU32 title_col = IM_COL32(245, 245, 250, 255); + dl->AddText(text_pos, title_col, app.meta.display_name.c_str()); + + // Description (wrapped) below + if (!app.meta.description.empty()) { + ImVec2 desc_pos = ImVec2(text_pos.x, + text_pos.y + ImGui::GetTextLineHeight() + 4.0f); + ImU32 desc_col = IM_COL32(195, 200, 210, 230); + float wrap_w = card_w - pad_x - 10.0f; + dl->AddText(NULL, 0.0f, desc_pos, desc_col, + app.meta.description.c_str(), NULL, wrap_w); + } + + if (hovered) { + ImGui::SetTooltip("%s\n%s", app.name.c_str(), + app.exe_path.string().c_str()); + } + if (clicked) launch_app(app); + + ImGui::PopID(); +} + +static void draw_main() { + if (!ImGui::Begin(TI_APPS " Apps", &g_show_main)) { + ImGui::End(); + return; + } + + ImGui::TextUnformatted("Hub Launcher"); + ImGui::SameLine(); + ImGui::TextDisabled("(%zu apps)", g_apps.size()); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 90.0f); + if (ImGui::Button(TI_REFRESH " Refresh")) { + scan_apps(); + } + ImGui::Separator(); + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##filter", "Filter apps...", + g_filter, sizeof(g_filter)); + ImGui::Spacing(); + + float card_w = 280.0f; + float card_h = 96.0f; + float spacing = 12.0f; + float avail = ImGui::GetContentRegionAvail().x; + int cols = std::max(1, (int)((avail + spacing) / (card_w + spacing))); + + int shown = 0; + int idx = 0; + for (auto const& app : g_apps) { + if (!matches_filter(app)) continue; + if (shown % cols != 0) ImGui::SameLine(0.0f, spacing); + draw_card(idx++, app, card_w, card_h); + ++shown; + if (shown % cols == 0) ImGui::Dummy(ImVec2(0.0f, spacing - 4.0f)); + } + if (shown == 0) { + ImGui::TextDisabled("No apps match filter."); + } + if (!g_last_status.empty()) { + ImGui::Separator(); + ImGui::TextUnformatted(g_last_status.c_str()); + } + ImGui::End(); +} + +static void render() { + if (g_show_main) draw_main(); +} + +int main(int /*argc*/, char** /*argv*/) { + static fn_ui::PanelToggle panels[] = { + { "Apps", nullptr, &g_show_main }, + }; + scan_apps(); + fn::AppConfig cfg; + cfg.title = "App Hub Launcher"; + cfg.about = { "app_hub_launcher", "0.2.0", + "Lista y arranca apps C++ desplegadas en Windows Desktop" }; + cfg.log = { "app_hub_launcher.log", 1 }; + cfg.panels = panels; + cfg.panel_count = sizeof(panels) / sizeof(panels[0]); + return fn::run_app(cfg, render); +}