// Panels v0: // - Browsers: scan + spawn + kill (funcional) // - Tabs / Tab Detail / Network: stubs anunciando v1. #include "imgui.h" #include "core/icons_tabler.h" #include "core/tokens.h" #include "chrome_scanner.h" #include "chrome_launcher.h" #include "local_api.h" #include #include #include #include #include #include #include #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN # include #endif namespace navegator { // ---------- Estado compartido del panel Browsers ---------- namespace { struct BrowsersState { std::mutex mu; std::vector instances; std::chrono::steady_clock::time_point last_scan; std::atomic scanning{false}; std::atomic ever_scanned{false}; std::atomic selected{-1}; char new_profile[128] = "default"; int new_port = 19222; bool new_headless = false; std::string last_error; }; BrowsersState g_browsers; void rescan_async() { if (g_browsers.scanning.exchange(true)) return; std::thread([]{ auto v = scan_chrome_instances(); { std::lock_guard lk(g_browsers.mu); g_browsers.instances = std::move(v); g_browsers.last_scan = std::chrono::steady_clock::now(); } g_browsers.ever_scanned.store(true); g_browsers.scanning.store(false); }).detach(); } std::string default_user_data_dir(const std::string& profile) { // Resolver USERPROFILE si esta disponible. #ifdef _WIN32 char buf[MAX_PATH] = {0}; DWORD n = GetEnvironmentVariableA("USERPROFILE", buf, sizeof(buf)); std::string base = (n > 0 && n < sizeof(buf)) ? buf : "C:\\Users\\Public"; return base + "\\AppData\\Local\\navegator_profiles\\" + profile; #else return std::string("/tmp/navegator_profiles/") + profile; #endif } } // namespace // ---------- Browsers panel ---------- void render_browsers_panel(bool* p_open) { if (!ImGui::Begin(TI_BROWSER " Browsers", p_open)) { ImGui::End(); return; } // Auto-rescan cada 2s. auto now = std::chrono::steady_clock::now(); { std::lock_guard lk(g_browsers.mu); if (now - g_browsers.last_scan > std::chrono::seconds(2) && !g_browsers.scanning.load()) { rescan_async(); } } // API status badge. if (g_api_running.load()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::success); ImGui::Text("API: 127.0.0.1:%d", g_api_port.load()); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled("(reqs: %d)", g_api_request_count.load()); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); } else { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextUnformatted("API: down"); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); } // Toolbar. if (ImGui::Button(TI_REFRESH " Rescan")) rescan_async(); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::TextUnformatted("New profile:"); ImGui::SameLine(); ImGui::SetNextItemWidth(160); ImGui::InputText("##profile", g_browsers.new_profile, sizeof(g_browsers.new_profile)); ImGui::SameLine(); ImGui::TextUnformatted("Port:"); ImGui::SameLine(); ImGui::SetNextItemWidth(80); ImGui::InputInt("##port", &g_browsers.new_port, 0, 0); ImGui::SameLine(); ImGui::Checkbox("Headless", &g_browsers.new_headless); ImGui::SameLine(); if (ImGui::Button(TI_PLAYER_PLAY " Launch")) { LaunchOpts o; o.port = g_browsers.new_port; o.headless = g_browsers.new_headless; std::string profile = g_browsers.new_profile; if (profile.empty()) profile = "default"; o.user_data_dir = default_user_data_dir(profile); auto r = launch_chrome(o); g_browsers.last_error = r.ok ? "" : r.error; // Forzar rescan inmediato (con pequeño delay para que Chrome aparezca en CIM). std::thread([]{ std::this_thread::sleep_for(std::chrono::milliseconds(800)); rescan_async(); }).detach(); } ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (ImGui::Button(TI_X " Kill all navegator")) { kill_chromes_by_userdata("navegator_profiles"); std::thread([]{ std::this_thread::sleep_for(std::chrono::milliseconds(500)); rescan_async(); }).detach(); } if (!g_browsers.last_error.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextWrapped("Error: %s", g_browsers.last_error.c_str()); ImGui::PopStyleColor(); } ImGui::Separator(); // Tabla. std::lock_guard lk(g_browsers.mu); // Anti-flicker: solo mostrar "Scanning..." en el primer scan (cuando aun // no tenemos datos). Una vez tenemos al menos un resultado, mantener el // empty-state estable; el badge "(reqs:N)" + el rescan async siguen // corriendo en background sin tocar la UI. if (!g_browsers.ever_scanned.load() && g_browsers.instances.empty()) { ImGui::TextUnformatted("Scanning..."); } else if (g_browsers.instances.empty()) { ImGui::TextDisabled("No Chrome instances with --remote-debugging-port detected."); ImGui::TextDisabled("Lanza una con el formulario de arriba o con scripts/start.sh."); } else { const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable; if (ImGui::BeginTable("##browsers", 6, flags)) { ImGui::TableSetupColumn("PID", ImGuiTableColumnFlags_WidthFixed, 64); ImGui::TableSetupColumn("Port", ImGuiTableColumnFlags_WidthFixed, 64); ImGui::TableSetupColumn("Profile"); ImGui::TableSetupColumn("Mode", ImGuiTableColumnFlags_WidthFixed, 80); ImGui::TableSetupColumn("user-data-dir"); ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 110); ImGui::TableHeadersRow(); int idx = 0; for (const auto& inst : g_browsers.instances) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text("%u", inst.pid); ImGui::TableNextColumn(); ImGui::Text("%d", inst.port); ImGui::TableNextColumn(); ImGui::TextUnformatted(inst.profile_name.empty() ? "(none)" : inst.profile_name.c_str()); ImGui::TableNextColumn(); if (inst.headless) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::warning); ImGui::TextUnformatted("headless"); ImGui::PopStyleColor(); } else { ImGui::TextUnformatted("visible"); } ImGui::TableNextColumn(); ImGui::TextUnformatted(inst.user_data_dir.c_str()); ImGui::TableNextColumn(); ImGui::PushID(idx); if (ImGui::SmallButton("Kill")) { if (!inst.user_data_dir.empty()) { kill_chromes_by_userdata(inst.user_data_dir); } std::thread([]{ std::this_thread::sleep_for(std::chrono::milliseconds(500)); rescan_async(); }).detach(); } ImGui::SameLine(); if (ImGui::SmallButton("Inspect")) { g_browsers.selected = idx; } ImGui::PopID(); ++idx; } ImGui::EndTable(); } } ImGui::End(); } // ---------- Tabs panel (stub) ---------- void render_tabs_panel(bool* p_open) { if (!ImGui::Begin(TI_LIST " Tabs", p_open)) { ImGui::End(); return; } ImGui::TextDisabled("Coming in v1"); ImGui::TextWrapped("Listara las pestañas de la instancia seleccionada en Browsers via " "CDP /json y permitira navigate/close/focus."); ImGui::End(); } // ---------- Tab Detail panel (stub) ---------- void render_tab_detail_panel(bool* p_open) { if (!ImGui::Begin(TI_FILE_INFO " Tab Detail", p_open)) { ImGui::End(); return; } ImGui::TextDisabled("Coming in v1"); ImGui::TextWrapped("HTML preview, screenshot live y REPL Runtime.evaluate sobre la pestaña " "seleccionada."); ImGui::End(); } // ---------- Network panel (stub) ---------- void render_network_panel(bool* p_open) { if (!ImGui::Begin(TI_ACTIVITY " Network", p_open)) { ImGui::End(); return; } ImGui::TextDisabled("Coming in v1"); ImGui::TextWrapped("Log de peticiones HTTP/WS en vivo via CDP Network.* events. " "Headers, body, timing, filtros."); ImGui::End(); } } // namespace navegator