GTK4 and libadwaita. It receives one UDP stream through the vendored udpsclient, buffers it in a large ring, and draws it with the vendored plotter: a trigger with a pre/post event window, a pause that freezes the window, cursors, measurements, math channels in a node editor, an event cache, named configurations, preferences, and an export of the visible window to .npy/.csv. Waveform files can be opened and analysed in place. The interface is two threads -- a producer that owns the socket and the GTK main thread that owns every widget and the GL context -- sharing one model under one mutex, with samples crossing through lock-free rings. deps/plotter is a submodule pinned to the commit this tree builds against; deps/udpsclient is vendored in place. docs/claude/ documents the rules that cost a defect to learn, and SPECS.md is the requirement document.
88 lines
3.0 KiB
C++
88 lines
3.0 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#include "settings.hpp"
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
|
|
#include <glib.h>
|
|
|
|
#include "layout_store.hpp" // `mkdirs()`, so a first run's config directory is created with parents
|
|
|
|
namespace udpscope {
|
|
namespace {
|
|
|
|
/// One `key = value` per line, `#` for a comment: the same shape the layout files use, and parsed just
|
|
/// as leniently -- an unknown key, an empty line or a file written by an older version is not an error.
|
|
bool parse_bool(const std::string& v, bool fallback) {
|
|
if (v == "true" || v == "1" || v == "yes") return true;
|
|
if (v == "false" || v == "0" || v == "no") return false;
|
|
return fallback;
|
|
}
|
|
|
|
std::string unquote(const std::string& v) {
|
|
if (v.size() >= 2 && v.front() == '"' && v.back() == '"') return v.substr(1, v.size() - 2);
|
|
return v;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Settings default_settings() { return Settings{}; }
|
|
|
|
std::string settings_path() {
|
|
const std::string dir = std::string(g_get_user_config_dir()) + "/udpscope";
|
|
mkdirs(dir); // every component: `<config>` itself may not exist on a fresh install
|
|
return dir + "/settings.toml";
|
|
}
|
|
|
|
Settings load_settings() {
|
|
Settings s; // the defaults, so the file only has to mention what a user changed
|
|
std::ifstream in(settings_path());
|
|
if (!in) return s;
|
|
std::string line;
|
|
while (std::getline(in, line)) {
|
|
const std::size_t hash = line.find('#');
|
|
if (hash != std::string::npos) line = line.substr(0, hash);
|
|
const std::size_t eq = line.find('=');
|
|
if (eq == std::string::npos) continue;
|
|
std::string key = line.substr(0, eq);
|
|
std::string value = line.substr(eq + 1);
|
|
const auto trim = [](std::string& t) {
|
|
const std::size_t a = t.find_first_not_of(" \t\r\n");
|
|
const std::size_t b = t.find_last_not_of(" \t\r\n");
|
|
t = a == std::string::npos ? std::string() : t.substr(a, b - a + 1);
|
|
};
|
|
trim(key);
|
|
trim(value);
|
|
value = unquote(value);
|
|
if (value.empty()) continue;
|
|
if (key == "event_cache_dir") {
|
|
s.event_cache_dir = value;
|
|
} else if (key == "event_cache_max") {
|
|
s.event_cache_max = std::max(0, std::atoi(value.c_str()));
|
|
} else if (key == "relative_time") {
|
|
s.relative_time = parse_bool(value, s.relative_time);
|
|
} else if (key == "theme") {
|
|
if (value == "auto" || value == "light" || value == "dark") s.theme = value;
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
bool save_settings(const Settings& settings) {
|
|
std::ofstream out(settings_path(), std::ios::trunc);
|
|
if (!out) return false;
|
|
out << "# udpscope preferences -- written by the preferences dialog, read at start-up.\n";
|
|
out << "# A key that is absent means its default, so this file only has to mention what changed.\n";
|
|
out << "event_cache_dir = \"" << settings.event_cache_dir << "\"\n";
|
|
out << "event_cache_max = " << settings.event_cache_max << "\n";
|
|
out << "relative_time = " << (settings.relative_time ? "true" : "false") << "\n";
|
|
out << "theme = \"" << settings.theme << "\"\n";
|
|
out.flush();
|
|
return out.good();
|
|
}
|
|
|
|
} // namespace udpscope
|