Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ void App::init(SDL_Window* window) {
TRACE_FUNCTION_CAT("app");
window_ = window;
toolbar_.set_window(window);
stats_.set_window(window);
load_settings();
if (platform::supports_vsync()) {
SDL_GL_SetSwapInterval(vsync_ ? 1 : 0);
Expand Down
1 change: 1 addition & 0 deletions src/platform/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ void run_main_loop(void (*step)(), bool* running);
std::string settings_path();
bool supports_vsync();
void open_file_dialog(SDL_Window*);
void save_file_dialog(SDL_Window*, const std::string& default_name, const std::string& content);
void handle_file_drop(const char* path);
bool has_pending_file();
PendingFile take_pending_file();
Expand Down
3 changes: 2 additions & 1 deletion src/platform/platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ void run_main_loop(void (*step)(), bool* running);
std::string settings_path(); // empty = no persistence
bool supports_vsync();

// File dialog
// File dialogs
void open_file_dialog(SDL_Window* window);
void save_file_dialog(SDL_Window* window, const std::string& default_name, const std::string& content);

// File drop handling (called from SDL event loop)
void handle_file_drop(const char* path);
Expand Down
24 changes: 24 additions & 0 deletions src/platform/platform_desktop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ static void file_dialog_callback(void* /*userdata*/, const char* const* filelist
}
}

static std::string g_save_content;

static void save_dialog_callback(void* /*userdata*/, const char* const* filelist, int /*filter*/) {
if (filelist && filelist[0]) {
SDL_IOStream* io = SDL_IOFromFile(filelist[0], "w");
if (io) {
SDL_WriteIO(io, g_save_content.data(), g_save_content.size());
SDL_CloseIO(io);
}
g_save_content.clear();
}
}

void platform::set_gl_attributes() {
TRACE_FUNCTION_CAT("platform");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
Expand Down Expand Up @@ -67,6 +80,17 @@ void platform::open_file_dialog(SDL_Window* window) {
SDL_ShowOpenFileDialog(file_dialog_callback, nullptr, window, filters, 2, nullptr, false);
}

void platform::save_file_dialog(SDL_Window* window, const std::string& default_name, const std::string& content) {
if (!window) return;
static const SDL_DialogFileFilter filters[] = {
{"CSV Files", "csv"},
{"TSV Files", "tsv"},
{"All Files", "*"},
};
g_save_content = content;
SDL_ShowSaveFileDialog(save_dialog_callback, nullptr, window, filters, 3, default_name.c_str());
}

void platform::handle_file_drop(const char* path) {
TRACE_FUNCTION_CAT("platform");
g_pending.path = path;
Expand Down
22 changes: 22 additions & 0 deletions src/platform/platform_wasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ bool platform::supports_vsync() {
return false;
}

void platform::save_file_dialog(SDL_Window* /*window*/, const std::string& default_name, const std::string& content) {
// Trigger browser download via JS blob
EM_ASM(
{
var name = UTF8ToString($0);
var data = UTF8ToString($1);
var blob = new Blob([data], {
type:
'text/plain'
});
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
default_name.c_str(), content.c_str());
}

void platform::open_file_dialog(SDL_Window* /*window*/) {
trigger_file_input();
}
Expand Down
8 changes: 7 additions & 1 deletion src/ui/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,22 @@ void on_model_changed();
void render(const TraceModel&, ViewState&);
```

## stats_panel.h / stats_panel.cpp — SQL editor + result table + visual query builder; tabs serialized to JSON
## stats_panel.h / stats_panel.cpp — SQL editor + result table + visual query builder; tabs serialized to JSON; CSV/TSV export
```
void render(const TraceModel&, QueryDb&, ViewState&);
void set_window(SDL_Window*);
nlohmann::json save_tabs() const;
void load_tabs(const nlohmann::json&);
// QueryBuilderState
void reset();
std::string build_sql(const char* const* columns, int num_columns) const;
```

## export_utils.h — export query results to CSV or TSV format
```
std::string export_result(const QueryDb::QueryResult&, char delimiter);
```

## flame_graph_panel.h / flame_graph_panel.cpp — per-thread icicle charts with flat node pool; filterable sidebar, zoom, search highlighting, context menu
```
void render(const TraceModel&, ViewState&);
Expand Down
41 changes: 41 additions & 0 deletions src/ui/export_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once
#include "model/query_db.h"
#include <sstream>
#include <string>

// Export query results as delimited text (CSV with ',' or TSV with '\t').
// CSV quoting follows RFC 4180: fields containing the delimiter, quotes, or
// newlines are wrapped in double-quotes with internal quotes escaped.
inline std::string export_result(const QueryDb::QueryResult& result, char delimiter) {
auto needs_quoting = [&](const std::string& s) {
if (delimiter != ',') return false;
return s.find_first_of(",\"\n") != std::string::npos;
};
auto write_field = [&](std::ostringstream& out, const std::string& s) {
if (needs_quoting(s)) {
out << '"';
for (char ch : s) {
if (ch == '"') out << '"';
out << ch;
}
out << '"';
} else {
out << s;
}
};

std::ostringstream out;
for (size_t c = 0; c < result.columns.size(); c++) {
if (c > 0) out << delimiter;
write_field(out, result.columns[c]);
}
out << '\n';
for (const auto& row : result.rows) {
for (size_t c = 0; c < row.size(); c++) {
if (c > 0) out << delimiter;
write_field(out, row[c]);
}
out << '\n';
}
return out.str();
}
12 changes: 12 additions & 0 deletions src/ui/stats_panel.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include "stats_panel.h"
#include "export_utils.h"
#include "format_time.h"
#include "sort_utils.h"
#include "tracing.h"
#include "platform/platform.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
#include <algorithm>
Expand Down Expand Up @@ -400,6 +402,16 @@ void StatsPanel::render_tab(QueryTab& tab, const TraceModel& model, QueryDb& db,
if (!tab.result.ok || tab.result.columns.empty()) return;

ImGui::Text("%zu rows", tab.result.rows.size());
ImGui::SameLine();
if (ImGui::SmallButton("Export CSV")) {
std::string content = export_result(tab.result, ',');
platform::save_file_dialog(window_, tab.title + ".csv", content);
}
ImGui::SameLine();
if (ImGui::SmallButton("Export TSV")) {
std::string content = export_result(tab.result, '\t');
platform::save_file_dialog(window_, tab.title + ".tsv", content);
}

// Find the "name" column index for click-to-browse
int name_col = -1;
Expand Down
5 changes: 5 additions & 0 deletions src/ui/stats_panel.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include <string>
#include <nlohmann/json_fwd.hpp>

struct SDL_Window;

struct QueryTab {
std::string title = "Query";
std::string query;
Expand Down Expand Up @@ -72,6 +74,7 @@ struct QueryBuilderState {
class StatsPanel {
public:
void render(const TraceModel& model, QueryDb& db, ViewState& view);
void set_window(SDL_Window* window) { window_ = window; }

nlohmann::json save_tabs() const;
void load_tabs(const nlohmann::json& j);
Expand All @@ -89,6 +92,8 @@ class StatsPanel {
int active_tab_ = 0;
float sql_height_ = 0.0f; // draggable SQL editor height (0 = use default)

SDL_Window* window_ = nullptr;

bool show_schema_ = false;
bool show_builder_ = false;
QueryBuilderState builder_;
Expand Down
58 changes: 58 additions & 0 deletions tests/test_export_utils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include <gtest/gtest.h>
#include "ui/export_utils.h"

static QueryDb::QueryResult make_result(std::vector<std::string> cols, std::vector<std::vector<std::string>> rows) {
QueryDb::QueryResult r;
r.columns = std::move(cols);
r.rows = std::move(rows);
r.ok = true;
return r;
}

TEST(ExportUtils, CsvBasic) {
auto r = make_result({"name", "count"}, {{"foo", "3"}, {"bar", "7"}});
std::string csv = export_result(r, ',');
EXPECT_EQ(csv, "name,count\nfoo,3\nbar,7\n");
}

TEST(ExportUtils, TsvBasic) {
auto r = make_result({"name", "count"}, {{"foo", "3"}, {"bar", "7"}});
std::string tsv = export_result(r, '\t');
EXPECT_EQ(tsv, "name\tcount\nfoo\t3\nbar\t7\n");
}

TEST(ExportUtils, CsvQuotesFieldsWithCommas) {
auto r = make_result({"val"}, {{"hello, world"}, {"plain"}});
std::string csv = export_result(r, ',');
EXPECT_EQ(csv, "val\n\"hello, world\"\nplain\n");
}

TEST(ExportUtils, CsvQuotesFieldsWithQuotes) {
auto r = make_result({"val"}, {{"say \"hi\""}});
std::string csv = export_result(r, ',');
EXPECT_EQ(csv, "val\n\"say \"\"hi\"\"\"\n");
}

TEST(ExportUtils, CsvQuotesFieldsWithNewlines) {
auto r = make_result({"val"}, {{"line1\nline2"}});
std::string csv = export_result(r, ',');
EXPECT_EQ(csv, "val\n\"line1\nline2\"\n");
}

TEST(ExportUtils, TsvNoQuoting) {
// TSV should not quote fields even if they contain commas or quotes
auto r = make_result({"val"}, {{"hello, world"}, {"say \"hi\""}});
std::string tsv = export_result(r, '\t');
EXPECT_EQ(tsv, "val\nhello, world\nsay \"hi\"\n");
}

TEST(ExportUtils, EmptyResult) {
auto r = make_result({"a", "b"}, {});
EXPECT_EQ(export_result(r, ','), "a,b\n");
EXPECT_EQ(export_result(r, '\t'), "a\tb\n");
}

TEST(ExportUtils, SingleColumn) {
auto r = make_result({"x"}, {{"1"}, {"2"}});
EXPECT_EQ(export_result(r, ','), "x\n1\n2\n");
}
Loading