#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); }