265 lines
9.0 KiB
C++
265 lines
9.0 KiB
C++
// 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 <atomic>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# include <windows.h>
|
|
#endif
|
|
|
|
namespace navegator {
|
|
|
|
// ---------- Estado compartido del panel Browsers ----------
|
|
namespace {
|
|
|
|
struct BrowsersState {
|
|
std::mutex mu;
|
|
std::vector<ChromeInstance> instances;
|
|
std::chrono::steady_clock::time_point last_scan;
|
|
std::atomic<bool> scanning{false};
|
|
std::atomic<bool> ever_scanned{false};
|
|
std::atomic<int> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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
|