#include #include "app_base.h" #include "core/panel_menu.h" #include "core/icons_tabler.h" #include "core/logger.h" #include "core/app_card.h" #include "gfx/gl_texture_load.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 std::unordered_map g_icons; 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 void load_icons() { fs::path icons_dir = fs::path(fn::local_dir()) / "icons"; std::error_code ec; if (!fs::is_directory(icons_dir, ec)) { fn_log::log_warn("hub: icons dir missing: %s", icons_dir.string().c_str()); return; } int loaded = 0; for (auto const& e : g_apps) { auto it = g_icons.find(e.name); if (it != g_icons.end() && it->second.ok()) continue; fs::path png = icons_dir / (e.name + ".png"); if (!fs::exists(png, ec)) continue; fn::GlTexture tex = fn::gl_texture_load(png.string().c_str(), /*flip_y=*/false, /*srgb=*/false); if (tex.ok()) { g_icons[e.name] = tex; ++loaded; } else { fn_log::log_warn("hub: failed to load %s: %s", png.string().c_str(), fn::gl_texture_last_error()); } } if (loaded > 0) { fn_log::log_info("hub: loaded %d icon textures (cache=%zu)", loaded, g_icons.size()); } } 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 void draw_card(AppEntry const& app, float card_w, float card_h) { fn_ui::AppCardData data; data.id = app.name.c_str(); data.title = app.meta.display_name.c_str(); data.description = app.meta.description.c_str(); data.accent = app.meta.accent; auto it = g_icons.find(app.name); data.icon = (it != g_icons.end() && it->second.ok()) ? (ImTextureID)(intptr_t)it->second.id : (ImTextureID)0; bool clicked = fn_ui::app_card(data, ImVec2(card_w, card_h)); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s\n%s", app.name.c_str(), app.exe_path.string().c_str()); } if (clicked) launch_app(app); } 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(); load_icons(); } ImGui::Separator(); ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextWithHint("##filter", "Filter apps...", g_filter, sizeof(g_filter)); ImGui::Spacing(); float card_w = 320.0f; float card_h = 110.0f; float spacing = 12.0f; float avail = ImGui::GetContentRegionAvail().x; int cols = std::max(1, (int)((avail + spacing) / (card_w + spacing))); int shown = 0; for (auto const& app : g_apps) { if (!matches_filter(app)) continue; if (shown % cols != 0) ImGui::SameLine(0.0f, spacing); draw_card(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() { static bool icons_loaded_once = false; if (!icons_loaded_once) { load_icons(); icons_loaded_once = true; } 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.3.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]); cfg.init_gl_loader = true; // needed for gl_texture_load return fn::run_app(cfg, render); }