// Panels v1+v2: // - Browsers : scan + spawn + kill + seleccion (click selecciona instancia) // - Tabs : lista pestañas via CDP HTTP /json + Focus/Close/New/Select // - Tab Detail : Runtime.evaluate REPL minimo (placeholder upgradeable) // - Network : panel DevTools-like — tabla + filtros + detalle por tab // // Estado cross-panel via g_session() (session_state.h). // Issue 0081-J: tablas ##browsers, ##tabs, ##wsframes, ##requests migradas a // data_table::render() con CellRenderers declarativos (Badge, Duration). #include "imgui.h" #include "implot.h" #include "core/icons_tabler.h" #include "core/tokens.h" #include "core/data_table_types.h" #include "data_table/data_table.h" #include "chrome_scanner.h" #include "chrome_launcher.h" #include "local_api.h" #include "cdp_http.h" #include "session_state.h" #include "picker_state.h" #include "py_subprocess.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN # include #endif namespace navegator { // =========================================================================== // Browsers panel // =========================================================================== 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}; char new_profile[128] = "default"; int new_port = 19222; bool new_headless = false; std::string last_error; // data_table state (persists between frames). data_table::State dt_state; }; 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) { #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 } } // anon void render_browsers_panel(bool* p_open) { if (!ImGui::Begin(TI_BROWSER " Browsers", p_open)) { ImGui::End(); return; } 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(); } } 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()); } else { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextUnformatted("API: down"); ImGui::PopStyleColor(); } ImGui::Separator(); 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; 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(); int sel_port = 0; { std::lock_guard lk(g_session().mu); sel_port = g_session().selected_port; } std::lock_guard lk(g_browsers.mu); 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 { // --- Build TableInput (6 data cols, no Actions) --- // Cols: Sel, PID, Port, Profile, Mode, user-data-dir static std::vector cell_backing; const int NCOLS = 6; const int nrows = (int)g_browsers.instances.size(); cell_backing.clear(); cell_backing.reserve((size_t)(nrows * NCOLS)); for (const auto& inst : g_browsers.instances) { cell_backing.push_back(sel_port == inst.port ? TI_CHECK : ""); cell_backing.push_back(std::to_string(inst.pid)); cell_backing.push_back(std::to_string(inst.port)); cell_backing.push_back(inst.profile_name.empty() ? "(none)" : inst.profile_name); cell_backing.push_back(inst.headless ? "headless" : "visible"); cell_backing.push_back(inst.user_data_dir); } static std::vector cell_ptrs; cell_ptrs.clear(); cell_ptrs.reserve(cell_backing.size()); for (const auto& s : cell_backing) cell_ptrs.push_back(s.c_str()); data_table::TableInput tbl; tbl.name = "browsers"; tbl.headers = {"", "PID", "Port", "Profile", "Mode", "user-data-dir"}; tbl.types = { data_table::ColumnType::String, data_table::ColumnType::Int, data_table::ColumnType::Int, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String }; tbl.cells = cell_ptrs.data(); tbl.rows = nrows; tbl.cols = NCOLS; // Declarative renderers. tbl.column_specs.resize((size_t)NCOLS); // Col 4: Mode -> Badge { data_table::ColumnSpec& cs = tbl.column_specs[4]; cs.id = "mode"; cs.renderer = data_table::CellRenderer::Badge; cs.badges = { data_table::BadgeRule{"headless", "#f59e0b", "headless"}, data_table::BadgeRule{"visible", "#22c55e", "visible"}, }; } std::vector dt_events; data_table::render("##dt_browsers", {tbl}, g_browsers.dt_state, &dt_events, /*show_chrome=*/false); for (auto& ev : dt_events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && ev.row < static_cast(g_browsers.instances.size())) { g_session().select_browser(g_browsers.instances[ev.row].port); } } // --- Actions: inline button list per row (no BeginTable — Button renderer not Fase-1) --- ImGui::Separator(); ImGui::TextDisabled("Actions:"); int idx = 0; for (const auto& inst : g_browsers.instances) { bool is_sel = (sel_port == inst.port); ImGui::PushID(idx); ImGui::Text(":%d", inst.port); ImGui::SameLine(); if (ImGui::SmallButton(is_sel ? "Selected" : "Select")) { g_session().select_browser(inst.port); } ImGui::SameLine(); if (ImGui::SmallButton("Kill")) { if (!inst.user_data_dir.empty()) { kill_chromes_by_userdata(inst.user_data_dir); } if (sel_port == inst.port) g_session().clear_selection(); std::thread([]{ std::this_thread::sleep_for(std::chrono::milliseconds(500)); rescan_async(); }).detach(); } ImGui::PopID(); ++idx; } } ImGui::End(); } // =========================================================================== // Tabs panel // =========================================================================== namespace { struct TabsUiState { std::atomic refreshing{false}; char new_url_input[1024] = "https://example.com"; char filter[128] = ""; // data_table state (persists between frames). data_table::State dt_state; }; TabsUiState g_tabs_ui; void refresh_tabs_async(int port) { if (port <= 0) return; if (g_tabs_ui.refreshing.exchange(true)) return; std::thread([port]{ std::vector v; std::string err; bool ok = cdp_list_tabs(port, v, &err); { std::lock_guard lk(g_session().mu); if (g_session().selected_port == port) { g_session().tabs = std::move(v); g_session().tabs_error = ok ? "" : err; g_session().last_tabs_refresh = std::chrono::steady_clock::now(); } } g_tabs_ui.refreshing.store(false); }).detach(); } } // anon void render_tabs_panel(bool* p_open) { if (!ImGui::Begin(TI_LIST " Tabs", p_open)) { ImGui::End(); return; } int port = 0; std::string sel_tab_id; { std::lock_guard lk(g_session().mu); port = g_session().selected_port; sel_tab_id = g_session().selected_tab_id; } if (port <= 0) { ImGui::TextDisabled("Select a browser in the Browsers panel."); ImGui::End(); return; } auto now = std::chrono::steady_clock::now(); { std::lock_guard lk(g_session().mu); if (now - g_session().last_tabs_refresh > std::chrono::seconds(2) && !g_tabs_ui.refreshing.load()) { refresh_tabs_async(port); } } ImGui::Text("Browser :%d", port); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (ImGui::Button(TI_REFRESH " Refresh")) refresh_tabs_async(port); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::SetNextItemWidth(280); ImGui::InputTextWithHint("##new_url", "https://...", g_tabs_ui.new_url_input, sizeof(g_tabs_ui.new_url_input)); ImGui::SameLine(); if (ImGui::Button(TI_PLUS " New tab")) { std::string url = g_tabs_ui.new_url_input; std::thread([port, url]{ CdpTab t; std::string err; cdp_new_tab(port, url, t, &err); refresh_tabs_async(port); }).detach(); } ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::SetNextItemWidth(200); ImGui::InputTextWithHint("##filter", "filter title/url", g_tabs_ui.filter, sizeof(g_tabs_ui.filter)); { std::lock_guard lk(g_session().mu); if (!g_session().tabs_error.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextWrapped("Error: %s", g_session().tabs_error.c_str()); ImGui::PopStyleColor(); } } ImGui::Separator(); std::vector tabs_copy; { std::lock_guard lk(g_session().mu); tabs_copy = g_session().tabs; } if (tabs_copy.empty()) { ImGui::TextDisabled("No tabs (or CDP HTTP not reachable on :%d).", port); ImGui::End(); return; } // Apply filter to get visible tabs. std::string filter_str = g_tabs_ui.filter; std::transform(filter_str.begin(), filter_str.end(), filter_str.begin(), ::tolower); std::vector visible_tabs; visible_tabs.reserve(tabs_copy.size()); for (const auto& t : tabs_copy) { if (!filter_str.empty()) { std::string lt = t.title; std::transform(lt.begin(), lt.end(), lt.begin(), ::tolower); std::string lu = t.url; std::transform(lu.begin(), lu.end(), lu.begin(), ::tolower); if (lt.find(filter_str) == std::string::npos && lu.find(filter_str) == std::string::npos) continue; } visible_tabs.push_back(&t); } // --- Build TableInput (5 data cols, no Actions) --- // Cols: Sel, Type, Title, URL, Attached static std::vector tab_cell_backing; const int NCOLS = 5; const int nrows = (int)visible_tabs.size(); tab_cell_backing.clear(); tab_cell_backing.reserve((size_t)(nrows * NCOLS)); for (const CdpTab* tp : visible_tabs) { tab_cell_backing.push_back(sel_tab_id == tp->id ? TI_CHECK : ""); tab_cell_backing.push_back(tp->type); tab_cell_backing.push_back(tp->title.empty() ? "(no title)" : tp->title); tab_cell_backing.push_back(tp->url); tab_cell_backing.push_back(tp->attached ? "yes" : "no"); } static std::vector tab_cell_ptrs; tab_cell_ptrs.clear(); tab_cell_ptrs.reserve(tab_cell_backing.size()); for (const auto& s : tab_cell_backing) tab_cell_ptrs.push_back(s.c_str()); data_table::TableInput tbl; tbl.name = "tabs"; tbl.headers = {"", "Type", "Title", "URL", "Attached"}; tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String }; tbl.cells = tab_cell_ptrs.data(); tbl.rows = nrows; tbl.cols = NCOLS; // Declarative renderers. tbl.column_specs.resize((size_t)NCOLS); // Col 1: Type -> Badge { data_table::ColumnSpec& cs = tbl.column_specs[1]; cs.id = "type"; cs.renderer = data_table::CellRenderer::Badge; cs.badges = { data_table::BadgeRule{"page", "#3b82f6", "page"}, data_table::BadgeRule{"iframe", "#8b5cf6", "iframe"}, data_table::BadgeRule{"service_worker", "#f59e0b", "service_worker"}, data_table::BadgeRule{"worker", "#f59e0b", "worker"}, }; } // Col 4: Attached -> Badge { data_table::ColumnSpec& cs = tbl.column_specs[4]; cs.id = "attached"; cs.renderer = data_table::CellRenderer::Badge; cs.badges = { data_table::BadgeRule{"yes", "#22c55e", "yes"}, data_table::BadgeRule{"no", "#6b7280", "no"}, }; } std::vector dt_events; data_table::render("##dt_tabs", {tbl}, g_tabs_ui.dt_state, &dt_events, /*show_chrome=*/false); for (auto& ev : dt_events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && ev.row < static_cast(visible_tabs.size())) { const CdpTab* tp = visible_tabs[ev.row]; if (tp && !tp->ws_url.empty()) { g_session().select_tab(tp->id, tp->ws_url); } } } // --- Actions: inline button list per visible row (no BeginTable — Button renderer not Fase-1) --- ImGui::Separator(); ImGui::TextDisabled("Actions:"); { int idx = 0; for (const CdpTab* tp : visible_tabs) { bool is_sel = (sel_tab_id == tp->id); ImGui::PushID(idx); std::string short_title = tp->title.empty() ? "(no title)" : tp->title; if (short_title.size() > 32) short_title = short_title.substr(0, 29) + "..."; ImGui::TextUnformatted(short_title.c_str()); ImGui::SameLine(); if (ImGui::SmallButton(is_sel ? "Sel*" : "Select")) { if (!tp->ws_url.empty()) g_session().select_tab(tp->id, tp->ws_url); } ImGui::SameLine(); if (ImGui::SmallButton("Focus")) { std::string id = tp->id; std::thread([port, id]{ cdp_activate_tab(port, id, nullptr); refresh_tabs_async(port); }).detach(); } ImGui::SameLine(); if (ImGui::SmallButton("Close")) { std::string id = tp->id; bool is_s = is_sel; std::thread([port, id]{ cdp_close_tab(port, id, nullptr); refresh_tabs_async(port); }).detach(); if (is_s) g_session().select_tab("", ""); } ImGui::PopID(); ++idx; } } ImGui::End(); } // =========================================================================== // Tab Detail panel (placeholder funcional) // =========================================================================== namespace { struct TabDetailUiState { char repl_input[4096] = "1+1"; std::string repl_output; std::mutex mu; }; TabDetailUiState g_tab_detail_ui; void tab_detail_eval_async(const std::string& expr) { NetworkSession* net = nullptr; { std::lock_guard lk(g_session().mu); net = g_session().net.get(); if (!net) return; } // No tenemos acceso directo al CdpWs desde NetworkSession publicamente. // Para v1.5 — Tab Detail dedicado abrira su propio CdpWs. De momento, // el panel solo muestra info estatica + tip. (void)expr; } } // anon void render_tab_detail_panel(bool* p_open) { if (!ImGui::Begin(TI_FILE_INFO " Tab Detail", p_open)) { ImGui::End(); return; } std::string sel_id, sel_ws; int port = 0; { std::lock_guard lk(g_session().mu); port = g_session().selected_port; sel_id = g_session().selected_tab_id; sel_ws = g_session().selected_tab_ws_url; } if (sel_id.empty()) { ImGui::TextDisabled("Select a tab in the Tabs panel."); ImGui::End(); return; } ImGui::Text("Browser :%d", port); ImGui::Text("Tab id %s", sel_id.c_str()); ImGui::TextWrapped("WS %s", sel_ws.c_str()); ImGui::Separator(); // --- Pick element --- bool active = picker_is_active(); if (active) ImGui::PushStyleColor(ImGuiCol_Button, fn_tokens::colors::primary); if (ImGui::Button(active ? (TI_FLASK " Picking... (click to stop)") : (TI_FLASK " Pick element"))) { if (active) { picker_stop(); } else { std::string err = picker_start(port, sel_id, sel_ws); if (!err.empty()) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextWrapped("Pick error: %s", err.c_str()); ImGui::PopStyleColor(); } } } if (active) ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled("(injects functions/browser/cdp_pick_element_js.js via CDP)"); PickedElement last = picker_last(); if (last.valid) { ImGui::Separator(); ImGui::TextDisabled("Last picked:"); if (ImGui::BeginChild("##picked_card", ImVec2(0, 110), true)) { ImGui::Text("tag: %s", last.tag.c_str()); ImGui::TextWrapped("selector: %s", last.selector.c_str()); ImGui::TextWrapped("xpath: %s", last.xpath.c_str()); std::string short_text = last.text; if (short_text.size() > 200) short_text = short_text.substr(0, 200) + "..."; ImGui::TextWrapped("text: %s", short_text.c_str()); } ImGui::EndChild(); if (ImGui::SmallButton("Copy selector")) { ImGui::SetClipboardText(last.selector.c_str()); } ImGui::SameLine(); if (ImGui::SmallButton("Save to recipe (new)")) { // Placeholder: futura integracion para crear recipe nueva con un // unico field a partir del selector. Por ahora se copia. ImGui::SetClipboardText(last.selector.c_str()); } ImGui::SameLine(); if (ImGui::SmallButton("Clear")) picker_clear_last(); } else { ImGui::TextDisabled("(no picked element yet — click 'Pick element' and click on the page)"); } ImGui::Separator(); ImGui::TextWrapped( "Tab Detail (HTML preview + screenshot + Runtime.evaluate REPL) llega " "en v1.5 (issue 0003). El WebSocket esta vivo via Network panel — el " "REPL re-utilizara la misma conexion en una proxima iteracion."); ImGui::End(); } // =========================================================================== // Network panel (DevTools-like) // =========================================================================== namespace { // Filtros chips (tipo recurso). Bitmask sobre ResourceType. struct NetUiState { char filter_text[256] = ""; bool invert_filter = false; bool hide_data_urls = true; bool only_blocked = false; // chips mask. true = mostrar este tipo. start: All on. bool type_doc = true; bool type_css = true; bool type_js = true; bool type_xhr = true; // XHR + Fetch bool type_img = true; bool type_media = true; bool type_font = true; bool type_ws = true; bool type_other = true; bool all_types = true; bool paused = false; int selected_index = -1; // index en snapshot filtrado std::string selected_id; // requestId estable int detail_tab = 0; // 0 headers, 1 payload, 2 response, 3 cookies, 4 timing, 5 ws // Histograma overview (request starts/s). bool show_histogram = true; int histogram_bins = 30; bool reload_ignore_cache = false; // data_table state for ##requests (persists between frames). data_table::State dt_state; }; NetUiState g_net_ui; bool type_passes(const NetUiState& s, ResourceType t) { if (s.all_types) return true; switch (t) { case ResourceType::Document: return s.type_doc; case ResourceType::Stylesheet: return s.type_css; case ResourceType::Script: return s.type_js; case ResourceType::Image: return s.type_img; case ResourceType::Media: return s.type_media; case ResourceType::Font: return s.type_font; case ResourceType::XHR: case ResourceType::Fetch: return s.type_xhr; case ResourceType::WebSocket: case ResourceType::EventSource: return s.type_ws; default: return s.type_other; } } ImVec4 status_color(int status) { if (status == 0) return fn_tokens::colors::text_muted; if (status >= 500) return fn_tokens::colors::error; if (status >= 400) return fn_tokens::colors::warning; if (status >= 300) return fn_tokens::colors::info; return fn_tokens::colors::success; } std::string short_name_from_url(const std::string& url) { if (url.empty()) return ""; size_t scheme_end = url.find("://"); size_t path_start = (scheme_end == std::string::npos) ? 0 : scheme_end + 3; size_t qmark = url.find('?', path_start); std::string path = url.substr(path_start, qmark == std::string::npos ? std::string::npos : qmark - path_start); size_t slash = path.find('/'); if (slash == std::string::npos) return path; std::string after = path.substr(slash); size_t last = after.find_last_of('/'); if (last == std::string::npos || last == after.size() - 1) { // ends with "/" -> use host return path.substr(0, slash); } return after.substr(last + 1); } std::string fmt_size(int64_t b) { char buf[64]; if (b < 1024) std::snprintf(buf, sizeof(buf), "%lld B", (long long)b); else if (b < 1024 * 1024) std::snprintf(buf, sizeof(buf), "%.1f kB", b / 1024.0); else std::snprintf(buf, sizeof(buf), "%.2f MB", b / (1024.0 * 1024.0)); return buf; } std::string fmt_dur_ms(double s) { char buf[32]; if (s <= 0) return "—"; if (s < 1.0) std::snprintf(buf, sizeof(buf), "%.0f ms", s * 1000.0); else std::snprintf(buf, sizeof(buf), "%.2f s", s); return buf; } void copy_to_clipboard(const std::string& s) { ImGui::SetClipboardText(s.c_str()); } std::string build_curl(const NetworkRequest& r) { std::ostringstream os; os << "curl -X " << (r.method.empty() ? "GET" : r.method) << " '" << r.url << "'"; for (const auto& h : r.request_headers) { if (h.name.size() >= 1 && h.name[0] == ':') continue; // pseudo h2 os << " -H '" << h.name << ": " << h.value << "'"; } if (r.has_post_data && !r.post_data.empty()) { os << " --data-raw '" << r.post_data << "'"; } return os.str(); } std::string build_fetch(const NetworkRequest& r) { std::ostringstream os; os << "fetch('" << r.url << "', { method: '" << (r.method.empty() ? "GET" : r.method) << "', headers: {"; bool first = true; for (const auto& h : r.request_headers) { if (h.name.size() >= 1 && h.name[0] == ':') continue; if (!first) os << ", "; first = false; os << "'" << h.name << "': '" << h.value << "'"; } os << "}"; if (r.has_post_data && !r.post_data.empty()) { os << ", body: '" << r.post_data << "'"; } os << "})"; return os.str(); } void draw_filter_chips() { auto chip = [](const char* label, bool* state, ImVec4 color) { if (*state) { ImGui::PushStyleColor(ImGuiCol_Button, color); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, color); } if (ImGui::SmallButton(label)) *state = !*state; if (*state) ImGui::PopStyleColor(2); ImGui::SameLine(); }; if (ImGui::SmallButton(g_net_ui.all_types ? "All*" : "All")) { g_net_ui.all_types = !g_net_ui.all_types; } ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); chip("Doc", &g_net_ui.type_doc, fn_tokens::colors::primary); chip("CSS", &g_net_ui.type_css, fn_tokens::colors::primary); chip("JS", &g_net_ui.type_js, fn_tokens::colors::primary); chip("XHR", &g_net_ui.type_xhr, fn_tokens::colors::primary); chip("Img", &g_net_ui.type_img, fn_tokens::colors::primary); chip("Media", &g_net_ui.type_media, fn_tokens::colors::primary); chip("Font", &g_net_ui.type_font, fn_tokens::colors::primary); chip("WS", &g_net_ui.type_ws, fn_tokens::colors::primary); chip("Other", &g_net_ui.type_other, fn_tokens::colors::primary); ImGui::NewLine(); } void draw_request_detail(const NetworkRequest& r, NetworkSession* net) { if (ImGui::BeginTabBar("##req_detail_tabs")) { if (ImGui::BeginTabItem("Headers")) { ImGui::TextDisabled("General"); ImGui::Text("URL: %s", r.url.c_str()); ImGui::Text("Method: %s", r.method.c_str()); ImGui::Text("Status: %d %s", r.status, r.status_text.c_str()); ImGui::Text("Remote: %s:%d", r.remote_ip.c_str(), r.remote_port); ImGui::Text("Protocol: %s", r.protocol.c_str()); if (r.failed) { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::Text("Error: %s%s", r.error_text.c_str(), r.canceled ? " (canceled)" : ""); ImGui::PopStyleColor(); } ImGui::Separator(); ImGui::TextDisabled("Request headers (%d)", (int)r.request_headers.size()); for (const auto& h : r.request_headers) { ImGui::Text("%s:", h.name.c_str()); ImGui::SameLine(); ImGui::TextWrapped("%s", h.value.c_str()); } ImGui::Separator(); ImGui::TextDisabled("Response headers (%d)", (int)r.response_headers.size()); for (const auto& h : r.response_headers) { ImGui::Text("%s:", h.name.c_str()); ImGui::SameLine(); ImGui::TextWrapped("%s", h.value.c_str()); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Payload")) { if (!r.has_post_data) { ImGui::TextDisabled("(no request body)"); } else { if (ImGui::SmallButton("Copy")) copy_to_clipboard(r.post_data); ImGui::Separator(); ImGui::InputTextMultiline("##postdata", (char*)r.post_data.c_str(), r.post_data.size() + 1, ImVec2(-1, -1), ImGuiInputTextFlags_ReadOnly); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Response")) { // Detect JSON response (content-type: application/json). bool is_json = false; for (const auto& h : r.response_headers) { std::string n = h.name; std::transform(n.begin(), n.end(), n.begin(), ::tolower); if (n == "content-type" && h.value.find("application/json") != std::string::npos) { is_json = true; break; } } if (r.body_fetched && !r.body_text.empty()) { if (ImGui::SmallButton("Copy")) copy_to_clipboard(r.body_text); if (is_json) { ImGui::SameLine(); if (ImGui::SmallButton(TI_LIST_DETAILS " Parse")) { // Llama infer_json_rows_schema via subprocess. static std::string g_parsed; // sticky entre frames g_parsed.clear(); const char* code = R"PY( import sys, os, json, traceback root = os.environ.get('FN_REGISTRY_ROOT', '') if not root: print(json.dumps({"error":"FN_REGISTRY_ROOT not set"})); sys.exit(2) for sub in ('core',): sys.path.insert(0, os.path.join(root, 'python', 'functions', sub)) try: from infer_json_rows_schema import infer_json_rows_schema body = sys.stdin.read() obj = json.loads(body) res = infer_json_rows_schema(obj) print(json.dumps(res if isinstance(res, dict) else {"result": res})) except Exception as e: print(json.dumps({"error": str(e), "trace": traceback.format_exc()})); sys.exit(1) )PY"; std::vector argv; argv.push_back(py_resolve_interpreter()); argv.push_back("-c"); argv.push_back(code); // Lanza un thread y deja log en g_net_ui.* via clipboard (simple). std::string body = r.body_text; std::thread([argv, body]() { (void)argv; (void)body; // py_run no soporta stdin todavia; usamos un archivo temporal. // Para mantener el patch minimo: escribimos body a archivo temp, // y pasamos su path como argv extra; el script lo lee. char tmp[256]; std::snprintf(tmp, sizeof(tmp), "%s%snav_body_%lld.json", #ifdef _WIN32 getenv("TEMP") ? getenv("TEMP") : ".", "\\", #else "/tmp", "/", #endif (long long)std::time(nullptr)); { std::ofstream f(tmp, std::ios::binary); if (f) f.write(body.data(), body.size()); } const char* code2 = R"PY( import sys, os, json, traceback root = os.environ.get('FN_REGISTRY_ROOT', '') if not root: print(json.dumps({"error":"FN_REGISTRY_ROOT not set"})); sys.exit(2) for sub in ('core',): sys.path.insert(0, os.path.join(root, 'python', 'functions', sub)) try: from infer_json_rows_schema import infer_json_rows_schema with open(sys.argv[1], 'rb') as f: body = f.read().decode('utf-8','replace') obj = json.loads(body) res = infer_json_rows_schema(obj) print(json.dumps(res if isinstance(res, dict) else {"result": res})) except Exception as e: print(json.dumps({"error": str(e), "trace": traceback.format_exc()})); sys.exit(1) )PY"; std::vector a2 = { py_resolve_interpreter(), "-c", code2, tmp }; PyResult pr = py_run(a2, 30000); ImGui::SetClipboardText(pr.stdout_data.c_str()); std::remove(tmp); }).detach(); } ImGui::SameLine(); ImGui::TextDisabled("(result -> clipboard)"); } ImGui::Separator(); ImGui::InputTextMultiline("##body", (char*)r.body_text.c_str(), r.body_text.size() + 1, ImVec2(-1, -1), ImGuiInputTextFlags_ReadOnly); } else { if (ImGui::Button("Fetch response body")) { if (net) net->request_body(r.id); } ImGui::TextDisabled("(body lazy-loaded via Network.getResponseBody)"); ImGui::TextDisabled("Limitacion conocida: matching id->requestId pendiente — body llega via WS pero no se pinta hasta v1.5."); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Cookies")) { // Buscar Cookie / Set-Cookie en headers. ImGui::TextDisabled("Sent (Cookie):"); for (const auto& h : r.request_headers) { if (h.name == "Cookie" || h.name == "cookie") { ImGui::TextWrapped("%s", h.value.c_str()); } } ImGui::Separator(); ImGui::TextDisabled("Set (Set-Cookie):"); for (const auto& h : r.response_headers) { if (h.name == "Set-Cookie" || h.name == "set-cookie") { ImGui::TextWrapped("%s", h.value.c_str()); } } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Timing")) { double dur = (r.t_finished > 0 ? r.t_finished : r.t_response) - r.t_started; ImGui::Text("Started: %.3f s", r.t_started); ImGui::Text("Response: %.3f s", r.t_response); ImGui::Text("Finished: %.3f s", r.t_finished); ImGui::Text("Total: %s", fmt_dur_ms(dur).c_str()); ImGui::Separator(); ImGui::Text("CDP timestamps"); ImGui::Text(" requestWillBeSent: %.6f", r.ts_request_will_be_sent); ImGui::Text(" responseReceived: %.6f", r.ts_response_received); ImGui::Text(" loadingFinished: %.6f", r.ts_loading_finished); ImGui::Text(" loadingFailed: %.6f", r.ts_loading_failed); ImGui::EndTabItem(); } if (r.type == ResourceType::WebSocket && ImGui::BeginTabItem("Messages")) { ImGui::Text("Frames: %d", (int)r.ws_frames.size()); ImGui::Separator(); // --- data_table::render for ##wsframes --- // Cols: Dir, Op, Time, Payload static std::vector ws_cell_backing; static data_table::State g_dt_wsframes; const int WS_COLS = 4; const int ws_rows = (int)r.ws_frames.size(); ws_cell_backing.clear(); ws_cell_backing.reserve((size_t)(ws_rows * WS_COLS)); for (const auto& wf : r.ws_frames) { ws_cell_backing.push_back(wf.outgoing ? "send" : "recv"); // Op: opcode as string label switch (wf.opcode) { case 1: ws_cell_backing.push_back("text"); break; case 2: ws_cell_backing.push_back("binary"); break; case 8: ws_cell_backing.push_back("close"); break; case 9: ws_cell_backing.push_back("ping"); break; case 10: ws_cell_backing.push_back("pong"); break; default: { char buf[16]; std::snprintf(buf, sizeof(buf), "%d", wf.opcode); ws_cell_backing.push_back(buf); } } { char buf[32]; std::snprintf(buf, sizeof(buf), "%.3f", wf.time); ws_cell_backing.push_back(buf); } ws_cell_backing.push_back(wf.payload); } static std::vector ws_cell_ptrs; ws_cell_ptrs.clear(); ws_cell_ptrs.reserve(ws_cell_backing.size()); for (const auto& s : ws_cell_backing) ws_cell_ptrs.push_back(s.c_str()); data_table::TableInput ws_tbl; ws_tbl.name = "wsframes"; ws_tbl.headers = {"Dir", "Op", "Time", "Payload"}; ws_tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Float, data_table::ColumnType::String }; ws_tbl.cells = ws_rows > 0 ? ws_cell_ptrs.data() : nullptr; ws_tbl.rows = ws_rows; ws_tbl.cols = WS_COLS; // Declarative renderers. ws_tbl.column_specs.resize((size_t)WS_COLS); // Col 0: Dir -> Badge { data_table::ColumnSpec& cs = ws_tbl.column_specs[0]; cs.id = "dir"; cs.renderer = data_table::CellRenderer::Badge; cs.badges = { data_table::BadgeRule{"send", "#3b82f6", "send"}, data_table::BadgeRule{"recv", "#22c55e", "recv"}, }; } // Col 1: Op -> Badge { data_table::ColumnSpec& cs = ws_tbl.column_specs[1]; cs.id = "op"; cs.renderer = data_table::CellRenderer::Badge; cs.badges = { data_table::BadgeRule{"text", "#3b82f6", "text"}, data_table::BadgeRule{"binary", "#8b5cf6", "binary"}, data_table::BadgeRule{"close", "#ef4444", "close"}, data_table::BadgeRule{"ping", "#6b7280", "ping"}, data_table::BadgeRule{"pong", "#6b7280", "pong"}, }; } data_table::render("##dt_wsframes", {ws_tbl}, g_dt_wsframes, false); ImGui::EndTabItem(); } ImGui::EndTabBar(); } } void render_network_toolbar(NetworkSession* net) { // Reload page (Page.reload via CDP). Si no hay sesion, deshabilita. if (!net) ImGui::BeginDisabled(); if (ImGui::Button(TI_REFRESH " Reload")) { if (net) net->reload_page(g_net_ui.reload_ignore_cache); } if (!net) ImGui::EndDisabled(); ImGui::SameLine(); ImGui::Checkbox("Bypass cache", &g_net_ui.reload_ignore_cache); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (ImGui::Button(TI_TRASH " Clear")) { if (net) net->clear_log(); g_net_ui.selected_id.clear(); g_net_ui.selected_index = -1; } ImGui::SameLine(); if (ImGui::Button(g_net_ui.paused ? (TI_PLAYER_PLAY " Resume") : (TI_PLAYER_PAUSE " Pause"))) { g_net_ui.paused = !g_net_ui.paused; } ImGui::SameLine(); ImGui::Checkbox(TI_CHART_HISTOGRAM " Histogram", &g_net_ui.show_histogram); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); bool preserve = net ? net->preserve_log() : true; if (ImGui::Checkbox("Preserve log", &preserve)) { if (net) net->set_preserve_log(preserve); } ImGui::SameLine(); bool cache_disabled = net ? net->cache_disabled() : false; if (ImGui::Checkbox("Disable cache", &cache_disabled)) { if (net) net->set_cache_disabled(cache_disabled); } ImGui::SameLine(); ImGui::Checkbox("Hide data:", &g_net_ui.hide_data_urls); ImGui::SameLine(); ImGui::Checkbox("Only failed", &g_net_ui.only_blocked); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::SetNextItemWidth(220); ImGui::InputTextWithHint("##netfilter", "filter (regex-like substring)", g_net_ui.filter_text, sizeof(g_net_ui.filter_text)); ImGui::SameLine(); ImGui::Checkbox("Invert", &g_net_ui.invert_filter); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (ImGui::Button(TI_DOWNLOAD " Export HAR")) { if (net) { std::string har = net->export_har_json(); // Escribir junto al exe. char path[1024]; std::snprintf(path, sizeof(path), "navegator_har_%lld.har", (long long)std::time(nullptr)); FILE* f = std::fopen(path, "w"); if (f) { std::fwrite(har.data(), 1, har.size(), f); std::fclose(f); } } } } } // anon void render_network_panel(bool* p_open) { if (!ImGui::Begin(TI_ACTIVITY " Network", p_open, ImGuiWindowFlags_MenuBar)) { ImGui::End(); return; } NetworkSession* net = nullptr; int port = 0; std::string sel_tab_id; std::string net_err; { std::lock_guard lk(g_session().mu); net = g_session().net.get(); port = g_session().selected_port; sel_tab_id = g_session().selected_tab_id; net_err = g_session().net_error; } if (!net) { if (sel_tab_id.empty()) { ImGui::TextDisabled("Select a tab in the Tabs panel to capture network."); } else { ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::error); ImGui::TextWrapped("Network session not open: %s", net_err.c_str()); ImGui::PopStyleColor(); } ImGui::End(); return; } // Drenar eventos cada frame (a menos que pause). if (!g_net_ui.paused) net->pump(); render_network_toolbar(net); draw_filter_chips(); ImGui::Separator(); // Snapshot + filtrado. auto reqs = net->snapshot(); // ---------- Histograma overview ---------- if (g_net_ui.show_histogram) { std::vector starts; starts.reserve(reqs.size()); double t_max = 1.0; for (const auto& r : reqs) { if (r->t_started >= 0.0) { starts.push_back(r->t_started); if (r->t_started > t_max) t_max = r->t_started; } } // Tamaño bin dinamico — bins fijo 30, rango 0..t_max. if (ImPlot::BeginPlot("##req_histogram", ImVec2(-1, 100), ImPlotFlags_NoTitle | ImPlotFlags_NoMouseText | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMenus | ImPlotFlags_NoFrame)) { ImPlot::SetupAxes("t (s)", "reqs/bin", ImPlotAxisFlags_NoMenus, ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_NoMenus); ImPlot::SetupAxisLimits(ImAxis_X1, 0.0, t_max + 0.001, ImPlotCond_Always); if (!starts.empty()) { ImPlot::PlotHistogram("Requests/bin", starts.data(), (int)starts.size(), g_net_ui.histogram_bins, 1.0, ImPlotRange(0.0, t_max + 0.001)); } // Marcadores DOMContentLoaded / Load. auto stats = net->stats(); if (stats.dom_content_loaded > 0) { double x = stats.dom_content_loaded; ImPlot::TagX(x, fn_tokens::colors::info, "DCL"); } if (stats.load_event > 0) { double x = stats.load_event; ImPlot::TagX(x, fn_tokens::colors::success, "L"); } ImPlot::EndPlot(); } ImGui::Separator(); } // ---------- /histograma ---------- std::string filt = g_net_ui.filter_text; std::string filt_lower = filt; std::transform(filt_lower.begin(), filt_lower.end(), filt_lower.begin(), ::tolower); std::vector> filtered; filtered.reserve(reqs.size()); for (auto& r : reqs) { if (g_net_ui.hide_data_urls && r->url.compare(0, 5, "data:") == 0) continue; if (g_net_ui.only_blocked && !r->failed) continue; if (!type_passes(g_net_ui, r->type)) continue; if (!filt_lower.empty()) { std::string lu = r->url; std::transform(lu.begin(), lu.end(), lu.begin(), ::tolower); bool match = (lu.find(filt_lower) != std::string::npos); if (g_net_ui.invert_filter) match = !match; if (!match) continue; } filtered.push_back(r); } // Layout: split top (table) / bottom (detail) cuando hay seleccion. bool has_sel = !g_net_ui.selected_id.empty(); float avail_h = ImGui::GetContentRegionAvail().y; float status_bar_h = ImGui::GetTextLineHeightWithSpacing() + 4.0f; float top_h = has_sel ? std::max(80.0f, (avail_h - status_bar_h) * 0.55f) : (avail_h - status_bar_h); ImGui::BeginChild("##nettable", ImVec2(0, top_h), true); { // --- data_table::render for ##requests --- // Cols: Name, Status, Method, Type, Initiator, Size, Time // (Waterfall col omitted: requires custom ImDrawList rendering — not a data_table renderer) static std::vector req_cell_backing; const int REQ_COLS = 7; const int req_rows = (int)filtered.size(); req_cell_backing.clear(); req_cell_backing.reserve((size_t)(req_rows * REQ_COLS)); for (const auto& r : filtered) { // Name std::string name = short_name_from_url(r->url); if (name.empty()) name = "(empty)"; req_cell_backing.push_back(std::move(name)); // Status: bucket string for Badge + numeric for display if (r->status > 0) { req_cell_backing.push_back(std::to_string(r->status)); } else if (r->failed) { req_cell_backing.push_back("failed"); } else { req_cell_backing.push_back("..."); } // Method req_cell_backing.push_back(r->method.empty() ? "GET" : r->method); // Type req_cell_backing.push_back(resource_type_label(r->type)); // Initiator if (!r->initiator_url.empty()) { req_cell_backing.push_back(short_name_from_url(r->initiator_url)); } else { req_cell_backing.push_back(r->initiator_type); } // Size if (r->from_cache) { req_cell_backing.push_back("(cache)"); } else { req_cell_backing.push_back(fmt_size(r->encoded_data_length)); } // Time (duration_ms as float string for Duration renderer) { double dur = (r->t_finished > 0 ? r->t_finished : r->t_response) - r->t_started; if (dur < 0) dur = 0; char buf[32]; std::snprintf(buf, sizeof(buf), "%.3f", dur * 1000.0); req_cell_backing.push_back(buf); } } static std::vector req_cell_ptrs; req_cell_ptrs.clear(); req_cell_ptrs.reserve(req_cell_backing.size()); for (const auto& s : req_cell_backing) req_cell_ptrs.push_back(s.c_str()); data_table::TableInput req_tbl; req_tbl.name = "requests"; req_tbl.headers = {"Name", "Status", "Method", "Type", "Initiator", "Size", "Time (ms)"}; req_tbl.types = { data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::String, data_table::ColumnType::Float, }; req_tbl.cells = req_rows > 0 ? req_cell_ptrs.data() : nullptr; req_tbl.rows = req_rows; req_tbl.cols = REQ_COLS; // Declarative renderers. req_tbl.column_specs.resize((size_t)REQ_COLS); // Col 1: Status -> Badge by bucket { data_table::ColumnSpec& cs = req_tbl.column_specs[1]; cs.id = "status"; cs.renderer = data_table::CellRenderer::Badge; cs.badges = { // 2xx — success green data_table::BadgeRule{"200", "#22c55e", "200"}, data_table::BadgeRule{"201", "#22c55e", "201"}, data_table::BadgeRule{"204", "#22c55e", "204"}, data_table::BadgeRule{"206", "#22c55e", "206"}, // 3xx — info blue data_table::BadgeRule{"301", "#3b82f6", "301"}, data_table::BadgeRule{"302", "#3b82f6", "302"}, data_table::BadgeRule{"304", "#3b82f6", "304"}, // 4xx — warning yellow data_table::BadgeRule{"400", "#f59e0b", "400"}, data_table::BadgeRule{"401", "#f59e0b", "401"}, data_table::BadgeRule{"403", "#f59e0b", "403"}, data_table::BadgeRule{"404", "#f59e0b", "404"}, data_table::BadgeRule{"429", "#f59e0b", "429"}, // 5xx — error red data_table::BadgeRule{"500", "#ef4444", "500"}, data_table::BadgeRule{"502", "#ef4444", "502"}, data_table::BadgeRule{"503", "#ef4444", "503"}, data_table::BadgeRule{"504", "#ef4444", "504"}, // special data_table::BadgeRule{"failed", "#ef4444", "failed"}, data_table::BadgeRule{"...", "#6b7280", "..."}, }; } // Col 2: Method -> Badge { data_table::ColumnSpec& cs = req_tbl.column_specs[2]; cs.id = "method"; cs.renderer = data_table::CellRenderer::Badge; cs.badges = { data_table::BadgeRule{"GET", "#22c55e", "GET"}, data_table::BadgeRule{"POST", "#3b82f6", "POST"}, data_table::BadgeRule{"PUT", "#f59e0b", "PUT"}, data_table::BadgeRule{"DELETE", "#ef4444", "DELETE"}, data_table::BadgeRule{"PATCH", "#8b5cf6", "PATCH"}, data_table::BadgeRule{"HEAD", "#6b7280", "HEAD"}, data_table::BadgeRule{"OPTIONS","#6b7280", "OPTIONS"}, }; } // Col 6: Time -> Duration (value already in ms) { data_table::ColumnSpec& cs = req_tbl.column_specs[6]; cs.id = "time_ms"; cs.renderer = data_table::CellRenderer::Duration; cs.duration_warn_ms = 1000.0f; cs.duration_error_ms = 5000.0f; } std::vector dt_events; data_table::render("##dt_requests", {req_tbl}, g_net_ui.dt_state, &dt_events, /*show_chrome=*/true); for (auto& ev : dt_events) { if (ev.kind == data_table::TableEventKind::RowDoubleClick && ev.row >= 0 && ev.row < static_cast(filtered.size())) { g_net_ui.selected_id = filtered[ev.row]->id; g_net_ui.selected_index = ev.row; } } // Context menu + selection: track clicked row by matching Name col // (handled inside data_table via row-click; URL copy available via right-click // on cell in data_table chrome). } ImGui::EndChild(); // Detail pane (selected request). if (has_sel) { ImGui::BeginChild("##netdetail", ImVec2(0, 0), true); std::shared_ptr sel; for (auto& r : filtered) if (r->id == g_net_ui.selected_id) { sel = r; break; } if (!sel) { for (auto& r : reqs) if (r->id == g_net_ui.selected_id) { sel = r; break; } } if (sel) { draw_request_detail(*sel, net); } else { ImGui::TextDisabled("(request gone — log was cleared)"); } ImGui::EndChild(); } // Status bar auto stats = net->stats(); ImGui::Separator(); ImGui::Text("%d requests", stats.total_requests); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::Text("%s transferred", fmt_size(stats.transferred).c_str()); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::Text("%s resources", fmt_size(stats.resources).c_str()); ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); if (stats.finish_time > 0) ImGui::Text("Finish: %.2f s", stats.finish_time); else ImGui::TextDisabled("Finish: —"); if (stats.dom_content_loaded > 0) { ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); ImGui::Text("DCL: %.2f", stats.dom_content_loaded); } if (stats.load_event > 0) { ImGui::SameLine(); ImGui::Text("L: %.2f", stats.load_event); } ImGui::SameLine(); ImGui::TextDisabled("|"); ImGui::SameLine(); { uint64_t fin = net->ws_frames_in(); uint64_t bin = net->ws_bytes_in(); uint64_t bout = net->ws_bytes_out(); bool alive = (fin > 0); ImGui::PushStyleColor(ImGuiCol_Text, alive ? fn_tokens::colors::success : fn_tokens::colors::warning); ImGui::Text("CDP: %s", alive ? "alive" : "no events"); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::TextDisabled("(%llu frames, in %s / out %s)", (unsigned long long)fin, fmt_size((int64_t)bin).c_str(), fmt_size((int64_t)bout).c_str()); } ImGui::End(); } } // namespace navegator