// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include "export.hpp" #include #include #include #include namespace udpscope { /// What a cached event is, without loading its samples: the sidecar's fields, and what the pane /// shows in one row. struct EventRecord { std::string stem; ///< the file's name without the extension std::string file; ///< `.npy`, the event's table in the export format std::string thumbnail; ///< `.bmp`, the preview drawn from the samples; else empty double wall_time = 0.0; ///< CLOCK_REALTIME at the latch, for the row's label double duration_s = 0.0; ///< pre + post double pre_s = 0.0; double post_s = 0.0; double edge = 0.0; ///< the stream time of the event's x = 0 double rate_hz = 0.0; ///< the stored sample rate at the latch std::size_t signals = 0; ///< columns in the file std::size_t samples = 0; ///< rows std::uint64_t seq = 0; ///< the run's own event counter, so two events in one second differ }; /// The cache of captured events: `/events//`, one `.npy` per event written in /// the **export format** -- so `--open` and the chooser's Waveforms page read it back with the /// loader that already exists -- beside a `.txt` sidecar holding what the list shows without /// loading the samples, and whatever thumbnail the caller rendered. /// /// No GTK: the caller renders the thumbnail and hands over a path, exactly as it hands over the /// table. The store owns naming, the sidecar, pruning and listing, which is what makes it testable /// without a window. /// The directory every source's event folder is created under when no preference says otherwise: /// `/udpscope/events`. Exposed because the preferences dialog shows it -- "the standard /// location" is not an answer a user can act on, and a second copy of the path computed in the dialog /// is a second thing to get wrong. std::string default_events_dir(); class EventStore { public: /// `source` is the `-` pair, the same key the packet capture uses. /// `base_dir` is the directory every source's own folder is created under; empty means /// `/udpscope/events`. It comes from the preferences (item 9), because a user with a small /// home partition and a second disk should not have to choose between events and free space. explicit EventStore(std::string source = {}, std::string base_dir = {}); /// How many events to keep, newest first. The oldest beyond it are deleted on every save; item 9's /// preferences are where this becomes the user's number. static constexpr std::size_t kDefaultKeep = 32; const std::string& dir() const noexcept { return dir_; } bool empty_dir() const noexcept { return dir_.empty(); } /// Writes `table` as one event, with `meta` in the sidecar and a preview drawn beside it. Returns /// the samples' path, or an empty string on failure -- a cache that cannot be written must not stop /// the scope. /// /// The preview is drawn from the *data* -- min/max per pixel column, one strip per signal -- and not /// captured from the window: the plot is a `GtkGLArea`, and a GL area's ink is not in the widget's /// paint tree, so a snapshot of it is empty (`gtk_snapshot_free_to_node()` returns NULL). Drawing it /// here also keeps this class GTK-free, which is what makes it testable without a window. std::string save(const ExportTable& table, const EventRecord& meta, std::size_t keep = kDefaultKeep); /// The cached events, newest first: the sidecars, never the sample files, so a listing is cheap /// however large the captures are. std::vector list() const; /// Draws `record`'s preview again from its samples and returns the thumbnail's path, or an empty /// string when the samples cannot be read. /// /// This is the repair for a preview that is missing or will not decode -- a write that failed, a /// truncated file, a cache written by an older build. The `.npy` is the event, the `.bmp` is only a /// picture of it, so a lost picture is recoverable and must not be shown as a blank row. It reads /// the samples, so unlike `list()` it is **not** cheap: call it for the one event that needs it. std::string redraw_preview(const EventRecord& record) const; /// Deletes one event's samples, sidecar and thumbnail. bool forget(const std::string& stem); /// Deletes the oldest events beyond `keep`. void prune(std::size_t keep) const; private: std::string dir_; }; /// The sidecar's text, and back. One `key = value` per line, the project's own TOML subset, so a /// human can read the cache with `cat` -- which is the point of a forensic directory. std::string event_meta_text(const EventRecord& record); bool event_meta_parse(const std::string& text, EventRecord* out); } // namespace udpscope