feat: About dialog with version, commit, build time and settings path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-10-04 11:52:43 +02:00
1 parent 82f91d00c0
commit a0fa845b58
8 files changed
+170 -9

No files matched your search

+22 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24) cmake_minimum_required(VERSION 3.24)
project(udpscope LANGUAGES C CXX) project(udpscope VERSION 0.1.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -51,9 +51,28 @@ add_custom_command(
COMMENT "Compiling udpscope icon resources" COMMENT "Compiling udpscope icon resources"
VERBATIM) VERBATIM)
# --- build info: version, commit and timestamp compiled into the binary -------
# Written at configure time so add_executable can name the file, and again on every
# build so the commit and the timestamp belong to the binary that is running rather
# than to whenever the tree was last configured.
set(UDPSCOPE_BUILD_INFO ${CMAKE_CURRENT_BINARY_DIR}/build_info.cpp)
set(UDPSCOPE_BUILD_INFO_ARGS
-DSRC=${CMAKE_CURRENT_SOURCE_DIR}
-DOUT=${UDPSCOPE_BUILD_INFO}
-DVERSION=${PROJECT_VERSION}
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/write_build_info.cmake)
execute_process(COMMAND ${CMAKE_COMMAND} ${UDPSCOPE_BUILD_INFO_ARGS})
add_custom_target(udpscope_build_info
BYPRODUCTS ${UDPSCOPE_BUILD_INFO}
COMMAND ${CMAKE_COMMAND} ${UDPSCOPE_BUILD_INFO_ARGS}
COMMENT "Writing udpscope build info"
VERBATIM)
set_source_files_properties(${UDPSCOPE_BUILD_INFO} PROPERTIES GENERATED TRUE)
# --- the application ---------------------------------------------------------- # --- the application ----------------------------------------------------------
add_executable(udpscope add_executable(udpscope
${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c ${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
${UDPSCOPE_BUILD_INFO}
src/main.cpp src/main.cpp
src/config.cpp src/config.cpp
src/channel.cpp src/channel.cpp
@@ -66,11 +85,13 @@ add_executable(udpscope
src/source_dialog.cpp src/source_dialog.cpp
src/measure.cpp src/measure.cpp
src/math_channel.cpp src/math_channel.cpp
src/help_window.cpp
src/ui.cpp src/ui.cpp
) )
target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads) target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads)
add_dependencies(udpscope udpscope_build_info)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(udpscope PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(udpscope PRIVATE -Wall -Wextra -Wpedantic)
endif() endif()
+38
View File
@@ -0,0 +1,38 @@
# SPDX-License-Identifier: MIT
#
# Writes ${OUT} with the three strings src/build_info.hpp declares. Run both at
# configure time (so the file exists when add_executable names it) and on every
# build (so the commit and the timestamp are the ones that produced the binary).
#
# Expects: SRC (the source tree), OUT (the .cpp to write), VERSION.
set(COMMIT "unknown")
find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${SRC}/.git")
execute_process(
COMMAND ${GIT_EXECUTABLE} describe --always --dirty
WORKING_DIRECTORY "${SRC}"
OUTPUT_VARIABLE GIT_OUT
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
RESULT_VARIABLE GIT_RC)
if(GIT_RC EQUAL 0 AND NOT GIT_OUT STREQUAL "")
set(COMMIT "${GIT_OUT}")
endif()
endif()
string(TIMESTAMP STAMP "%Y-%m-%d %H:%M UTC" UTC)
file(WRITE "${OUT}"
"// SPDX-License-Identifier: MIT
// Generated by cmake/write_build_info.cmake. Do not edit, do not commit.
#include \"build_info.hpp\"
namespace udpscope::build_info {
const char* const kVersion = \"${VERSION}\";
const char* const kCommit = \"${COMMIT}\";
const char* const kTimestamp = \"${STAMP}\";
} // namespace udpscope::build_info
")
+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
#pragma once
namespace udpscope::build_info {
/// The CMake project version, e.g. "0.1.0".
extern const char* const kVersion;
/// `git describe --always --dirty` at build time, or "unknown" if git was not available.
extern const char* const kCommit;
/// When the binary was configured/built, as "YYYY-MM-DD HH:MM UTC".
extern const char* const kTimestamp;
} // namespace udpscope::build_info
+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
#include "help_window.hpp"
#include <cstdio>
#include "build_info.hpp"
#include "layout_store.hpp"
namespace udpscope {
namespace {
/// Nulled by the `close-request` handler so a second open cannot present a dead widget.
/// One pointer per window, file-scope because there is one application.
GtkWidget* g_about = nullptr;
gboolean on_window_close(GtkWindow* window, gpointer slot) {
(void)window;
*static_cast<GtkWidget**>(slot) = nullptr;
return FALSE; // let the default handler destroy it
}
} // namespace
void show_about(GtkWindow* parent, const std::string& session_path) {
if (g_about != nullptr) {
gtk_window_present(GTK_WINDOW(g_about));
return;
}
// GtkAboutDialog is not deprecated in GTK 4 (unlike GtkShortcutsWindow), so the
// stock dialog is used here and the overview is hand-rolled.
GtkWidget* dialog = gtk_about_dialog_new();
g_about = dialog;
GtkAboutDialog* about = GTK_ABOUT_DIALOG(dialog);
gtk_about_dialog_set_program_name(about, "udpscope");
gtk_about_dialog_set_version(about, build_info::kVersion);
gtk_about_dialog_set_comments(about, "An oscilloscope for UDPStreamer sources.");
gtk_about_dialog_set_license_type(about, GTK_LICENSE_MIT_X11);
char details[1024];
std::snprintf(details, sizeof(details),
"Commit: %s\nBuilt: %s\nSettings: %s\nSession file: %s",
build_info::kCommit, build_info::kTimestamp, config_dir().c_str(),
session_path.c_str());
gtk_about_dialog_set_system_information(about, details);
gtk_window_set_transient_for(GTK_WINDOW(dialog), parent);
gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
g_signal_connect(dialog, "close-request", G_CALLBACK(on_window_close), &g_about);
gtk_window_present(GTK_WINDOW(dialog));
}
} // namespace udpscope
+14
View File
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
/// The About dialog: version, commit, build timestamp and where the session files live.
/// Modal on `parent`. `session_path` is this run's own session file.
void show_about(GtkWindow* parent, const std::string& session_path);
} // namespace udpscope
+8 -8
View File
@@ -31,14 +31,6 @@ std::string sanitize(const std::string& host) {
return out.empty() ? "_" : out; return out.empty() ? "_" : out;
} }
std::string config_dir() {
const char* xdg = std::getenv("XDG_CONFIG_HOME");
if (xdg != nullptr && xdg[0] != '\0') return std::string(xdg) + "/udpscope";
const char* home = std::getenv("HOME");
if (home == nullptr || home[0] == '\0') return "/tmp/udpscope-config";
return std::string(home) + "/.config/udpscope";
}
void mkdirs(const std::string& path) { void mkdirs(const std::string& path) {
// **Every** component, the last one included. The condition used to be "the path has a trailing // **Every** component, the last one included. The condition used to be "the path has a trailing
// slash", so the final directory was never created: `~/.config/udpscope` did not exist, the // slash", so the final directory was never created: `~/.config/udpscope` did not exist, the
@@ -114,6 +106,14 @@ bool parse_string_array(const std::string& in, std::vector<std::string>& out) {
} // namespace } // namespace
std::string config_dir() {
const char* xdg = std::getenv("XDG_CONFIG_HOME");
if (xdg != nullptr && xdg[0] != '\0') return std::string(xdg) + "/udpscope";
const char* home = std::getenv("HOME");
if (home == nullptr || home[0] == '\0') return "/tmp/udpscope-config";
return std::string(home) + "/.config/udpscope";
}
std::string layout_path(const std::string& host, std::uint16_t port) { std::string layout_path(const std::string& host, std::uint16_t port) {
const std::string dir = config_dir(); const std::string dir = config_dir();
mkdirs(dir); mkdirs(dir);
+4
View File
@@ -81,6 +81,10 @@ bool forget_source(const KnownSource& src);
std::string math_node_text(const MathNode& node); std::string math_node_text(const MathNode& node);
bool math_node_from_text(const std::string& text, MathNode& out); 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();
/// `~/.config/udpscope/<host>_<port>.toml`, with the directory created. /// `~/.config/udpscope/<host>_<port>.toml`, with the directory created.
/// Characters unsafe in a filename are replaced in the host part. /// Characters unsafe in a filename are replaced in the host part.
std::string layout_path(const std::string& host, std::uint16_t port); std::string layout_path(const std::string& host, std::uint16_t port);
+18
View File
@@ -17,6 +17,7 @@
#include "acquisition.hpp" #include "acquisition.hpp"
#include "config.hpp" #include "config.hpp"
#include "export.hpp" #include "export.hpp"
#include "help_window.hpp"
#include "layout_store.hpp" #include "layout_store.hpp"
#include "measure.hpp" #include "measure.hpp"
#include "model.hpp" #include "model.hpp"
@@ -4083,6 +4084,16 @@ void on_export_clicked(GtkButton*, gpointer user) {
g_object_unref(dialog); g_object_unref(dialog);
} }
void on_about_clicked(GtkButton*, gpointer user) {
App* app = static_cast<App*>(user);
if (app->menu_button != nullptr) {
GtkPopover* popover = gtk_menu_button_get_popover(GTK_MENU_BUTTON(app->menu_button));
if (popover != nullptr) gtk_popover_popdown(popover);
}
show_about(GTK_WINDOW(app->window),
layout_path(app->model->config().server, app->model->config().port));
}
// --- right-click menu ------------------------------------------------------- // --- right-click menu -------------------------------------------------------
void on_y_auto_clicked(GtkButton* button, gpointer user) { void on_y_auto_clicked(GtkButton* button, gpointer user) {
@@ -5542,6 +5553,13 @@ GtkWidget* build_toolbar(App& app) {
g_signal_connect(app.signal_toggle, "toggled", G_CALLBACK(on_signals_toggled), &app); g_signal_connect(app.signal_toggle, "toggled", G_CALLBACK(on_signals_toggled), &app);
gtk_box_append(GTK_BOX(menu_box), app.signal_toggle); gtk_box_append(GTK_BOX(menu_box), app.signal_toggle);
gtk_box_append(GTK_BOX(menu_box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
GtkWidget* about_item = gtk_button_new_with_label("About udpscope");
gtk_widget_set_tooltip_text(about_item, "version, build timestamp and settings paths");
g_signal_connect(about_item, "clicked", G_CALLBACK(on_about_clicked), &app);
gtk_box_append(GTK_BOX(menu_box), about_item);
GtkWidget* popover = gtk_popover_new(); GtkWidget* popover = gtk_popover_new();
gtk_popover_set_child(GTK_POPOVER(popover), menu_box); gtk_popover_set_child(GTK_POPOVER(popover), menu_box);
app.menu_button = gtk_menu_button_new(); app.menu_button = gtk_menu_button_new();