// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include #include #include #include "math_channel.hpp" #include "measure.hpp" namespace udpscope { /// The user-arranged part of a session, persisted per data source in /// `~/.config/udpscope/_.toml`: the display window, the plot layout /// (rows and which signal each of a row's slots holds) and the per-signal /// calibration. Signal names are the stable identity (channel indexes are /// renumbered by every enable/disable, see the model's own invariant). struct LayoutState { struct Row { /// One entry per slot; an empty string is an empty slot. std::vector signals; /// The row's share of the figure's height, 1 for equal rows. Part of the layout because a sizing a /// user dragged is an arrangement, and an arrangement that is not saved is one that is lost. double weight = 1.0; }; double window_s = 0.0; ///< 0 keeps the default window std::vector rows; struct Calibration { std::string name; double gain = 1.0; double offset = 0.0; }; std::vector calibration; /// The trace colours the user chose, `0xRRGGBB`. Only the *overrides*: a signal with no entry /// takes the positional palette entry, which is what keeps a stream's new signals coloured /// without a session having to name them. Saved by name like every identity here. struct Color { std::string name; std::uint32_t rgb = 0; }; std::vector colors; /// Signals the session had enabled. Enabling is what starts acquisition, so this is what makes a /// reopened scope look like the one that was closed -- and it is saved by *name*, like every other /// identity here, because a CONFIG renumbers the channels. std::vector enabled; /// How this source was reached, so the startup chooser can reconnect to it exactly. The file is /// named `_.toml` and that name can say neither of these. Empty `multicast` is /// unicast; empty `interface_addr` is the default route. std::string multicast; std::string interface_addr; /// The derived channels, graphs and all: the editor's work is part of the session, not of the /// process. A graph is stored as one string per node so the file stays in this store's TOML /// subset (tables, string arrays, scalars) rather than growing a nested-table parser for it; see /// `math_node_text()` for the field order. std::vector math; /// Whether the measurements pane shows its statistics line. The only piece of the /// statistics that is persisted: the accumulated figures themselves describe a session's /// stream and mean nothing restored against another one. bool measure_stats = false; /// The measurements pane, by signal *name* and with the type and options a row was built with. std::vector measures; /// Free text the user attached to this configuration: what it is for, what to look at first. /// A configuration is a *named* session, and the name alone rarely says what makes it different /// from its neighbour. std::string comment; /// The startup settings the configuration carries, applied when the chooser opens it -- which is /// why they live here and not only on the command line: two sources on one machine want different /// buffers and different clocks, and retyping the flags to look at the second one is how a /// setting gets forgotten. A zero `buffer_mb` and an empty `capture_packets` mean "not stored": /// the command line's value stands, so an old file cannot silently shrink a buffer. double buffer_mb = 0.0; bool relative_time = false; std::uint32_t capture_packets = 0; }; /// 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 /// Which *configuration* of that source this row is. A source may have several, each a named /// session of its own, so the pair (host, port, config) is what identifies a row. std::string config; std::string comment; ///< the configuration's note, shown beside its name /// The startup settings this configuration carries, so the chooser can apply them. double buffer_mb = 0.0; bool relative_time = false; std::uint32_t capture_packets = 0; }; /// Every configuration that names at least one signal *or* carries a setting, 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 — and a /// buffer size, a note or a clock counts as configuring it, because that is how the chooser's form /// sets a host up before it has ever streamed. 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. std::string math_node_text(const MathNode& node); bool math_node_from_text(const std::string& text, MathNode& out); /// The directory session files live in: $XDG_CONFIG_HOME/udpscope, else /// ~/.config/udpscope, else /tmp/udpscope-config. Not created by this call. std::string config_dir(); /// `$XDG_CACHE_HOME/udpscope`, or `~/.cache/udpscope`: where bulky, disposable state /// lives (the packet captures; item 9's event cache). Created on demand by callers. std::string cache_dir(); /// Creates `path` and every missing component, like `mkdir -p`. Public because the preferences are /// written under the config directory too. void mkdirs(const std::string& path); /// The *legacy* single-file path, `~/.config/udpscope/_.toml`, with the directory /// created. Still the import source for a file written before configurations existed; nothing /// writes here any more. std::string layout_path(const std::string& host, std::uint16_t port); /// The per-source directory a source's configurations live in: /// `~/.config/udpscope/_/`. Not created by this call. std::string config_dir_for(const std::string& host, std::uint16_t port); /// One configuration's file, `/.toml`, with the directory /// created. The name is sanitized the way the host is, so a name the user typed with a slash in it /// cannot escape the directory. std::string configuration_path(const std::string& host, std::uint16_t port, const std::string& name); /// The source's configurations, newest first. A legacy single-file session appears here as the /// configuration named `default`, and importing it is the first thing this does. std::vector configuration_names(const std::string& host, std::uint16_t port); /// The configuration the source was last left in: `/active`, or the newest that exists when /// nothing wrote one. Never empty for a source that has any configuration at all. std::string active_configuration(const std::string& host, std::uint16_t port); /// Remembers which configuration this source is on. Called by every save. bool set_active_configuration(const std::string& host, std::uint16_t port, const std::string& name); /// Deletes one configuration's file, and `active` if it named it. bool forget_configuration(const std::string& host, std::uint16_t port, const std::string& name); /// Parses the file's TOML subset. Returns false when the file is missing or /// malformed; `out` is left usable either way (a malformed file loads as much as /// parsed, which beats refusing a hand-edited file over one stray character). bool load_layout(const std::string& path, LayoutState& out); /// Writes `state` as TOML. Atomic in effect: writes a sibling temp file and /// renames it over the target. bool save_layout(const std::string& path, const LayoutState& state); } // namespace udpscope