From d010a03b44c2f35e68993a07cdd4e5f26e0539bc Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 17 May 2026 00:07:04 +0200 Subject: [PATCH] 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) --- CMakeLists.txt | 14 +++ app.md | 45 +++++++ appicon.ico | Bin 0 -> 7463 bytes main.cpp | 310 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 369 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 app.md create mode 100644 appicon.ico create mode 100644 main.cpp 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 0000000000000000000000000000000000000000..e78623670c6b44c04d10c3025ad7fbcf925b89de GIT binary patch literal 7463 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 literal 0 HcmV?d00001 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); +}