diff --git a/CMakeLists.txt b/CMakeLists.txt index 0eb13f9..6d99638 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,7 @@ add_executable(udpscope src/plot_host.cpp src/export.cpp src/layout_store.cpp + src/source_dialog.cpp src/measure.cpp src/math_channel.cpp src/ui.cpp diff --git a/src/config.cpp b/src/config.cpp index c29860b..93c3162 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -67,6 +67,7 @@ Config parse_args(int argc, char** argv) { std::exit(0); } else if (a == "--server" || a.rfind("--server=", 0) == 0) { c.server = value_of(argc, argv, i, "--server"); + c.server_specified = true; } else if (a == "--port" || a.rfind("--port=", 0) == 0) { const std::string v = value_of(argc, argv, i, "--port"); const long p = std::strtol(v.c_str(), nullptr, 10); diff --git a/src/config.hpp b/src/config.hpp index 8392db1..d869280 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -16,6 +16,11 @@ struct Config { std::string multicast; ///< Empty selects unicast. std::string interface_addr; ///< Local IPv4 to join the group on; empty = default route. + /// Whether `--server` was given. Absent, the application asks which source to open rather + /// than silently taking the default above -- so this is "did the user choose", not "is + /// `server` valid". The default stays a usable value for every other path. + bool server_specified = false; + /// Size of *each* of the two sample buffers, in megabytes. The application /// always allocates two identical buffers (spec item 1). double buffer_mb = 200.0; diff --git a/src/main.cpp b/src/main.cpp index 7f2c5d0..084c8fb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include "config.hpp" #include "model.hpp" #include "plot_host.hpp" +#include "source_dialog.hpp" #include "ui.hpp" #include @@ -19,6 +20,10 @@ int main(int argc, char** argv) { return 2; } + // No `--server` means the user has not said which source to open, so ask. Before the model: + // declining must cost no buffer and no socket, and nothing below has to cope with "no source". + if (!cfg.server_specified && !udpscope::choose_source(cfg)) return 0; + udpscope::ScopeModel model(cfg); udpscope::RowSet rows; rows.set_row_count(cfg.plots); diff --git a/src/source_dialog.cpp b/src/source_dialog.cpp new file mode 100644 index 0000000..2c3b529 --- /dev/null +++ b/src/source_dialog.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2026 Martino Ferrari + +#include "source_dialog.hpp" + +#include "layout_store.hpp" + +#include + +#include +#include +#include + +namespace udpscope { +namespace { + +/// A chooser row's source becomes the connection to open. The interface comes along with the +/// group: it is the local address the join happens on, so storing one without the other would +/// reconnect to the right group on the wrong path. +[[maybe_unused]] void apply_source(Config& cfg, const KnownSource& src) { + cfg.server = src.host; + cfg.port = src.port; + cfg.multicast = src.multicast; + cfg.interface_addr = src.interface_addr; +} + +/// One line per known source on stderr, in the style of the probes in `ui.cpp`. +void report_sources(const std::vector& known) { + std::fprintf(stderr, "[sources] %zu known:", known.size()); + for (const KnownSource& s : known) { + std::fprintf(stderr, " %s:%u plots=%d signals=%d", s.host.c_str(), s.port, s.rows, s.signals); + if (!s.multicast.empty()) std::fprintf(stderr, " multicast=%s", s.multicast.c_str()); + if (!s.interface_addr.empty()) { + std::fprintf(stderr, " interface=%s", s.interface_addr.c_str()); + } + std::fprintf(stderr, " |"); + } + std::fprintf(stderr, "\n"); + std::fflush(stderr); +} + +} // namespace + +bool choose_source(Config& cfg) { + const std::vector known = known_sources(); + + // The discovery half, checkable with no display at all: this returns before `gtk_init()`, so + // it asserts what the list found without needing an X server or a GL context. False means the + // caller exits 0, which is what "list and exit" wants. + if (g_getenv("UDPSCOPE_PROBE_SOURCES") != nullptr) { + report_sources(known); + return false; + } + + gtk_init(); + (void)cfg; + return false; +} + +} // namespace udpscope diff --git a/src/source_dialog.hpp b/src/source_dialog.hpp new file mode 100644 index 0000000..6f5d704 --- /dev/null +++ b/src/source_dialog.hpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2026 Martino Ferrari + +#pragma once + +#include "config.hpp" + +namespace udpscope { + +/// Asks which data source to open: a list of the sources with a stored session, and a form for a +/// new one. Returns true with `cfg`'s connection fields filled in, or false if the user declined +/// -- in which case `cfg` is untouched and the caller should exit. +/// +/// Runs *before* `ScopeModel` exists, so declining costs no buffer and no socket. It drives its +/// own `gtk_init()` and nested `GMainLoop`; `run_ui()`'s `GtkApplication` comes afterwards in the +/// same process, which works because `gtk_init()` is idempotent. +bool choose_source(Config& cfg); + +} // namespace udpscope