From eabdde34f9ceb01e51a2d9dbcd9d6fa988dfee32 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Sun, 4 Oct 2026 00:08:15 +0200 Subject: [PATCH] list the sources with a stored session known_sources() reads the session files the layout store already owns, keeping only those that name a signal: a development machine accumulates one file per probe run on a throwaway port, and all 140 of them here hold empty slots. --- src/layout_store.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++ src/layout_store.hpp | 22 +++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/layout_store.cpp b/src/layout_store.cpp index 6ef03df..d7eecd1 100644 --- a/src/layout_store.cpp +++ b/src/layout_store.cpp @@ -3,8 +3,11 @@ #include "layout_store.hpp" +#include #include +#include #include +#include #include #include #include @@ -117,6 +120,59 @@ std::string layout_path(const std::string& host, std::uint16_t port) { return dir + "/" + sanitize(host) + "_" + std::to_string(port) + ".toml"; } +std::vector known_sources() { + std::vector out; + const std::string dir = config_dir(); + DIR* d = ::opendir(dir.c_str()); + // No directory yet is an empty list, not an error: a first run has never saved anything. + if (d == nullptr) return out; + while (const dirent* e = ::readdir(d)) { + const std::string name = e->d_name; + if (name.size() <= 5 || name.compare(name.size() - 5, 5, ".toml") != 0) continue; + const std::string stem = name.substr(0, name.size() - 5); + // The *last* '_': `sanitize()` replaces anything unsafe in a host with one, so the host part + // may hold them too and a first-match split would return a host cut in half. + const std::size_t cut = stem.rfind('_'); + if (cut == std::string::npos || cut + 1 == stem.size()) continue; + const long port = std::strtol(stem.c_str() + cut + 1, nullptr, 10); + if (port <= 0 || port > 65535) continue; + + LayoutState st; + const std::string path = dir + "/" + name; + if (!load_layout(path, st)) continue; + + KnownSource s; + s.host = stem.substr(0, cut); + s.port = static_cast(port); + s.multicast = st.multicast; + s.interface_addr = st.interface_addr; + s.rows = static_cast(st.rows.size()); + for (const LayoutState::Row& row : st.rows) { + for (const std::string& sig : row.signals) { + if (!sig.empty()) ++s.signals; + } + } + if (s.signals == 0) continue; // see the header: a session nobody configured + + struct stat sb {}; + s.mtime = (::stat(path.c_str(), &sb) == 0) ? static_cast(sb.st_mtime) : 0; + out.push_back(std::move(s)); + } + ::closedir(d); + // Stable, so two files sharing an mtime keep the order the directory gave them rather than + // swapping between runs. + std::stable_sort(out.begin(), out.end(), + [](const KnownSource& a, const KnownSource& b) { return a.mtime > b.mtime; }); + return out; +} + +bool forget_source(const KnownSource& src) { + // Through `layout_path()` rather than by rebuilding the name here: one expression of the + // scheme. `src.host` came back out of a filename, so it is already sanitized, and `sanitize()` + // keeps alphanumerics, '.', '-' and '_' -- it is idempotent, so the round trip is exact. + return std::remove(layout_path(src.host, src.port).c_str()) == 0; +} + std::string math_node_text(const MathNode& node) { // The op is written as its index through `math_op_at()`, not as the raw enum value: that accessor // is the contract the dialog, the engine and this file share, so the format is tied to it rather diff --git a/src/layout_store.hpp b/src/layout_store.hpp index fa9f5c5..52cd03a 100644 --- a/src/layout_store.hpp +++ b/src/layout_store.hpp @@ -53,6 +53,28 @@ struct LayoutState { std::vector measures; }; +/// A data source udpscope has a stored session for: one row of the startup chooser. +struct KnownSource { + std::string host; ///< the CONFIG server, even for a multicast session + std::uint16_t port = 0; + std::string multicast; ///< empty = unicast + std::string interface_addr; ///< empty = the default route + int rows = 0; ///< plots the session had + int signals = 0; ///< named slots across those plots + std::int64_t mtime = 0; ///< orders the list; never shown +}; + +/// Every session file that names at least one signal, newest first. +/// +/// The filter is what makes the list usable: a development machine accumulates one session file +/// per probe run on a throwaway port -- 140 of them when this was written -- and every one holds +/// `signals = ["", "", "", ""]`. A listed source is one somebody actually configured. +std::vector known_sources(); + +/// Deletes one source's session file; the chooser's `x`. False means the file was already gone or +/// could not be removed, which costs the caller nothing -- there is no state to roll back. +bool forget_source(const KnownSource& src); + /// One `MathNode` as `kind|source|in_a|in_b|gain|offset|fc|q|lo|hi|x|y`, and back. The kind is the /// op's *index* into `math_op_at()`, which is why the enum's order is a stored format and not just /// an implementation detail: reordering it renames every saved graph's operations.