feat: two-tier status bar, one glance line with the detail behind an info popover

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-04 15:51:42 +02:00
1 parent f9490061a1
commit 8242812338
4 files changed
+319 -76

No files matched your search

+1
View File
@@ -112,6 +112,7 @@ 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/status_text.cpp
src/shortcuts.cpp src/shortcuts.cpp
src/help_window.cpp src/help_window.cpp
src/ui.cpp src/ui.cpp
+89
View File
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: MIT
#include "status_text.hpp"
#include <cmath>
#include <cstdio>
namespace udpscope {
/// `12 s`, `3m 07s`, `2h 14m` -- one unit coarser as soon as the finer one would need
/// three digits, so the field never grows past six characters and the status line's shape
/// does not depend on how long the wait has been.
std::string format_elapsed(double seconds) {
if (!(seconds >= 0.0)) return "--";
char buf[32];
if (seconds < 60.0) {
std::snprintf(buf, sizeof(buf), "%.0f s", std::floor(seconds));
} else if (seconds < 3600.0) {
const int m = static_cast<int>(seconds) / 60;
std::snprintf(buf, sizeof(buf), "%dm %02ds", m, static_cast<int>(seconds) % 60);
} else {
const int h = static_cast<int>(seconds) / 3600;
std::snprintf(buf, sizeof(buf), "%dh %02dm", h, (static_cast<int>(seconds) / 60) % 60);
}
return buf;
}
/// A sample rate in the unit it reads shortest in. Rates here span six decades (a 1 Hz
/// housekeeping signal next to a 1 MS/s acquisition), so a fixed unit makes one of the
/// two unreadable.
std::string format_rate(double per_s) {
char buf[32];
if (!(per_s > 0.0)) {
return "0 S/s";
} else if (per_s >= 1.0e6) {
std::snprintf(buf, sizeof(buf), "%.3g MS/s", per_s / 1.0e6);
} else if (per_s >= 1.0e3) {
std::snprintf(buf, sizeof(buf), "%.3g kS/s", per_s / 1.0e3);
} else {
std::snprintf(buf, sizeof(buf), "%.3g S/s", per_s);
}
return buf;
}
std::string format_glance(const StatusSnapshot& s) {
std::string out = s.state_word;
out += " · ";
out += s.rate_text.empty() ? "no data" : s.rate_text;
out += " · ";
out += s.window_text;
out += " window";
if (s.trigger_on) {
out += " · ";
if (s.have_event) {
out += "event " + format_elapsed(s.since_event_s) + " ago";
} else {
out += "no event yet";
}
}
return out;
}
std::string format_detail(const StatusSnapshot& s) {
char buf[1024];
std::snprintf(buf, sizeof(buf),
"[%s] %s %s | rate %s | packets %llu frames %llu gaps %llu | %zu signals, "
"%zu channels | plot 1: %d trace(s), %zu samples | buffer %.1f MB, %.2f s "
"span, time x%llu",
s.state_detail.c_str(), s.connected ? "connected" : "waiting",
s.have_config ? "configured" : "-",
s.rate_text.empty() ? "--" : s.rate_text.c_str(),
static_cast<unsigned long long>(s.packets),
static_cast<unsigned long long>(s.frames),
static_cast<unsigned long long>(s.gaps), s.signals, s.channels,
s.plot1_traces, s.plot1_samples, s.buffer_mb, s.span_s,
static_cast<unsigned long long>(s.time_stride));
std::string out = buf;
if (s.trigger_on) {
out += s.have_event ? " | last event " + format_elapsed(s.since_event_s) + " ago"
: " | no event yet";
}
if (s.reconnects > 0) {
char note[48];
std::snprintf(note, sizeof(note), " | %d reconnects", s.reconnects);
out += note;
}
return out;
}
} // namespace udpscope
+65
View File
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace udpscope {
/// Everything either tier of the status bar shows, gathered once per frame by the tick so the
/// two cannot disagree. No GTK here on purpose: this is the one part of the status bar that
/// can be reasoned about as text.
struct StatusSnapshot {
/// One word from the fixed set: "live", "paused", "armed", "capturing", "event held",
/// "disarmed". It is a word from a *fixed* set because the bar sits directly above the plot
/// and a field whose width tracked the stream would relayout it every frame.
const char* state_word = "live";
/// The long prose the old single-line bar carried, kept for the detail string and the dump.
std::string state_detail;
bool connected = false;
bool have_config = false;
bool trigger_on = false;
/// Already collapsed to one figure or a range by the caller, without the "rate " prefix.
std::string rate_text;
double stored_rate_hz = 0.0;
double declared_rate_hz = 0.0;
/// Pre-formatted by ui.cpp's format_window(), which the window control and the Fit notices
/// also use, so it stays there rather than moving here.
std::string window_text;
std::uint64_t packets = 0;
std::uint64_t frames = 0;
std::uint64_t gaps = 0;
std::size_t signals = 0;
std::size_t channels = 0;
int plot1_traces = 0;
std::size_t plot1_samples = 0;
double buffer_mb = 0.0;
double span_s = 0.0;
std::uint64_t time_stride = 1;
bool have_event = false;
double since_event_s = 0.0;
int reconnects = 0;
};
/// The bar: "live · 10.0 kS/s · 2 s window", plus " · event 4.2 s ago" in trigger mode and
/// nothing else ever. The badge glyph is a separate widget and is not part of this string.
std::string format_glance(const StatusSnapshot& s);
/// The popover's prose and the exit dump's line, field for field as the old single-line bar
/// wrote it. CLAUDE.md asserts those fields are read out of the dump, so the field names and
/// their order must not change without updating it.
std::string format_detail(const StatusSnapshot& s);
/// A sample rate as "10.0 kS/s". Moved here from ui.cpp, which still calls it.
std::string format_rate(double hz);
/// An elapsed time, stepping to a coarser unit rather than growing digits, so its width
/// cannot track the stream. Moved here from ui.cpp.
std::string format_elapsed(double seconds);
} // namespace udpscope
+164 -76
View File
@@ -21,6 +21,7 @@
#include "help_window.hpp" #include "help_window.hpp"
#include "layout_store.hpp" #include "layout_store.hpp"
#include "shortcuts.hpp" #include "shortcuts.hpp"
#include "status_text.hpp"
#include "measure.hpp" #include "measure.hpp"
#include "model.hpp" #include "model.hpp"
#include "plot_host.hpp" #include "plot_host.hpp"
@@ -483,6 +484,24 @@ struct App {
std::string dbg_mode_live; std::string dbg_mode_live;
int dbg_toggles_visible = 0; int dbg_toggles_visible = 0;
int dbg_trigger_revealed_default = 0; int dbg_trigger_revealed_default = 0;
/// The detail string the popover and the exit dump read. The dump used to read the label,
/// which no longer carries these fields.
std::string status_detail;
std::size_t status_detail_max = 0;
/// The widest glance line this run produced. The bar sits above the plot, so this is the
/// "layout must not depend on content" reading for the status row.
std::size_t glance_max_chars = 0;
/// The last glance line, for the exit dump's `status bar:` line — the dump runs after the
/// window is destroyed, so it cannot ask the label.
std::string dbg_glance_last;
GtkWidget* status_info_button = nullptr; ///< the ⓘ
GtkWidget* status_popover = nullptr;
/// The popover's eleven labels: four row headings and seven value lines. Built once; the
/// tick sets their text. A rebuild here would be a per-frame relayout of a visible widget.
GtkWidget* popover_label[11] = {};
int popover_rebuilds = 0;
}; };
// --- small helpers ---------------------------------------------------------- // --- small helpers ----------------------------------------------------------
@@ -810,23 +829,6 @@ const char* badge_markup(BadgePhase phase) {
return "<span foreground=\"#5A5A5A\">\u25CF</span>"; return "<span foreground=\"#5A5A5A\">\u25CF</span>";
} }
/// `12 s`, `3m 07s`, `2h 14m` -- one unit coarser as soon as the finer one would need
/// three digits, so the field never grows past six characters and the status line's shape
/// does not depend on how long the wait has been.
std::string format_elapsed(double seconds) {
if (!(seconds >= 0.0)) return "--";
char buf[32];
if (seconds < 60.0) {
std::snprintf(buf, sizeof(buf), "%.0f s", std::floor(seconds));
} else if (seconds < 3600.0) {
const int m = static_cast<int>(seconds) / 60;
std::snprintf(buf, sizeof(buf), "%dm %02ds", m, static_cast<int>(seconds) % 60);
} else {
const int h = static_cast<int>(seconds) / 3600;
std::snprintf(buf, sizeof(buf), "%dh %02dm", h, (static_cast<int>(seconds) / 60) % 60);
}
return buf;
}
/// A measurement: at most two decimals, scaled by an SI prefix so the number reads in /// A measurement: at most two decimals, scaled by an SI prefix so the number reads in
/// `1..999` (0.01052 V is `10.52 mV`), and the unit appended when the stream declares one. /// `1..999` (0.01052 V is `10.52 mV`), and the unit appended when the stream declares one.
@@ -879,22 +881,6 @@ std::string format_measure(double v, const std::string& unit) {
return out; return out;
} }
/// A sample rate in the unit it reads shortest in. Rates here span six decades (a 1 Hz
/// housekeeping signal next to a 1 MS/s acquisition), so a fixed unit makes one of the
/// two unreadable.
std::string format_rate(double per_s) {
char buf[32];
if (!(per_s > 0.0)) {
return "0 S/s";
} else if (per_s >= 1.0e6) {
std::snprintf(buf, sizeof(buf), "%.3g MS/s", per_s / 1.0e6);
} else if (per_s >= 1.0e3) {
std::snprintf(buf, sizeof(buf), "%.3g kS/s", per_s / 1.0e3);
} else {
std::snprintf(buf, sizeof(buf), "%.3g S/s", per_s);
}
return buf;
}
/// A short-lived line in front of the status text: the outcome of an action that the /// A short-lived line in front of the status text: the outcome of an action that the
/// picture does not show by itself (a drop that changed nothing, an export that wrote a /// picture does not show by itself (a drop that changed nothing, an export that wrote a
@@ -4565,6 +4551,43 @@ void report_event_zoom(App& a, const char* what) {
std::fflush(stderr); std::fflush(stderr);
} }
// --- status popover updater -------------------------------------------------
/// Sets the popover's seven value labels. Never creates a widget: `popover_rebuilds` exists to
/// assert that, because the popover is visible when this runs and a rebuild would relayout it.
void update_status_popover(App& app, const udpscope::StatusSnapshot& s) {
if (app.popover_label[4] == nullptr) return;
char buf[256];
std::snprintf(buf, sizeof(buf), "%s, %s", s.connected ? "connected" : "waiting",
s.have_config ? "configured" : "no config");
gtk_label_set_text(GTK_LABEL(app.popover_label[4]), buf);
std::snprintf(buf, sizeof(buf), "packets %llu · frames %llu · gaps %llu",
static_cast<unsigned long long>(s.packets),
static_cast<unsigned long long>(s.frames),
static_cast<unsigned long long>(s.gaps));
gtk_label_set_text(GTK_LABEL(app.popover_label[5]), buf);
std::snprintf(buf, sizeof(buf), "%zu signals · %zu channels", s.signals, s.channels);
gtk_label_set_text(GTK_LABEL(app.popover_label[6]), buf);
std::snprintf(buf, sizeof(buf), "%.1f MB · %.0f s span · time x%llu", s.buffer_mb, s.span_s,
static_cast<unsigned long long>(s.time_stride));
gtk_label_set_text(GTK_LABEL(app.popover_label[7]), buf);
const std::string stored = udpscope::format_rate(s.stored_rate_hz);
const std::string wire = udpscope::format_rate(s.declared_rate_hz);
std::snprintf(buf, sizeof(buf), "stored %s (wire %s)", stored.c_str(), wire.c_str());
gtk_label_set_text(GTK_LABEL(app.popover_label[8]), buf);
std::snprintf(buf, sizeof(buf), "%d traces · %zu samples", s.plot1_traces, s.plot1_samples);
gtk_label_set_text(GTK_LABEL(app.popover_label[9]), buf);
std::snprintf(buf, sizeof(buf), "%d", s.reconnects);
gtk_label_set_text(GTK_LABEL(app.popover_label[10]), buf);
}
// --- the frame tick --------------------------------------------------------- // --- the frame tick ---------------------------------------------------------
gboolean tick(gpointer user) { gboolean tick(gpointer user) {
@@ -5063,10 +5086,8 @@ gboolean tick(gpointer user) {
// line is the receipt for both. // line is the receipt for both.
static gint64 next_channels = 0; static gint64 next_channels = 0;
const gint64 now_c = g_get_monotonic_time(); const gint64 now_c = g_get_monotonic_time();
/// Filled only on a measuring tick, and applied to the status label's tooltip after the // Per-channel rate computed only on the measuring tick; the detail tooltip on the label
/// model lock is dropped: the status line can only carry the aggregate rate, so the // now carries the full status string instead of a per-channel breakdown.
/// per-channel breakdown goes where there is room for it.
std::string rate_tip;
if (app->last_stored.size() != model.channel_count()) { if (app->last_stored.size() != model.channel_count()) {
// Seed new entries with the current count: a fresh entry must not be counted // Seed new entries with the current count: a fresh entry must not be counted
// as a delta from zero. // as a delta from zero.
@@ -5102,8 +5123,6 @@ gboolean tick(gpointer user) {
const double declared = info.dt > 0.0 ? 1.0 / info.dt : 0.0; const double declared = info.dt > 0.0 ? 1.0 / info.dt : 0.0;
app->stored_rate[c] = per_s; app->stored_rate[c] = per_s;
app->declared_rate[c] = declared; app->declared_rate[c] = declared;
rate_tip += (rate_tip.empty() ? "" : "\n") + info.name + ": " + format_rate(per_s) +
" stored (declared " + format_rate(declared) + ")";
const ChannelBuffer& buf = model.live().channel(c); const ChannelBuffer& buf = model.live().channel(c);
const plot::Range y = buf.size() > 0 ? buf.y_bounds() : plot::Range{0.0, 0.0}; const plot::Range y = buf.size() > 0 ? buf.y_bounds() : plot::Range{0.0, 0.0};
std::fprintf(stderr, std::fprintf(stderr,
@@ -5210,7 +5229,7 @@ gboolean tick(gpointer user) {
// The rate the buffer is actually storing at. One figure when every channel agrees and // The rate the buffer is actually storing at. One figure when every channel agrees and
// a range when they do not: a per-channel list would make the field's width depend on // a range when they do not: a per-channel list would make the field's width depend on
// the stream, and the breakdown is on the label's tooltip instead. // the stream, and the breakdown is on the label's tooltip instead.
std::string rate = "rate --"; std::string rate;
if (!app->stored_rate.empty()) { if (!app->stored_rate.empty()) {
double lo = app->stored_rate.front(); double lo = app->stored_rate.front();
double hi = lo; double hi = lo;
@@ -5219,51 +5238,70 @@ gboolean tick(gpointer user) {
hi = std::max(hi, r); hi = std::max(hi, r);
} }
// Within a percent is the same rate measured over a one-second window, not two rates. // Within a percent is the same rate measured over a one-second window, not two rates.
rate = "rate " + (hi - lo <= 0.01 * hi ? format_rate(hi) rate = hi - lo <= 0.01 * hi ? format_rate(hi) : format_rate(lo) + ".." + format_rate(hi);
: format_rate(lo) + ".." + format_rate(hi));
} }
// In trigger mode, how long ago the event on screen was captured. It is what tells a // Derive the one-word state for the glance line from the same conditions the mode prose
// frozen picture apart from a stale one: the view looks identical whether the last // branches on, so the two cannot disagree.
// event was a second or an hour ago. const char* state_word = "live";
std::string since; if (display == DisplayMode::Capturing) {
if (trigger_on) { state_word = "capturing";
since = app->last_latch_us != 0 } else if (display == DisplayMode::Held) {
? " | last event " + state_word = "event held";
format_elapsed(double(g_get_monotonic_time() - app->last_latch_us) / 1.0e6) + } else if (trigger_on && !armed) {
" ago" state_word = "disarmed";
: " | no event yet"; } else if (display == DisplayMode::Waiting) {
state_word = "armed";
} else if (paused) {
state_word = "paused";
} }
// Only when it has happened: a stream that is fine carries no counter, so the line udpscope::StatusSnapshot snap;
// stays as short as it was, and a client being kicked shows the number it was kicked. snap.state_detail = mode;
char reconnects_note[48]; snap.state_word = state_word;
reconnects_note[0] = '\0'; snap.connected = st.connected.load();
if (st.reconnects.load(std::memory_order_relaxed) > 0) { snap.have_config = st.have_config.load();
std::snprintf(reconnects_note, sizeof(reconnects_note), " | %llu reconnects", snap.trigger_on = trigger_on;
static_cast<unsigned long long>(st.reconnects.load(std::memory_order_relaxed))); snap.rate_text = rate;
snap.stored_rate_hz = app->stored_rate.empty() ? 0.0 : app->stored_rate.front();
snap.declared_rate_hz = app->declared_rate.empty() ? 0.0 : app->declared_rate.front();
snap.window_text = format_window(app->rows->time_window());
snap.packets = st.packets.load();
snap.frames = st.frames.load();
snap.gaps = st.gaps.load();
snap.signals = signal_count;
snap.channels = channel_count;
snap.plot1_traces = bound;
snap.plot1_samples = samples;
snap.buffer_mb = double(model.config().buffer_mb);
snap.span_s = span_s;
snap.time_stride = time_stride;
snap.have_event = app->last_latch_us != 0;
snap.since_event_s = snap.have_event
? double(g_get_monotonic_time() - app->last_latch_us) / 1.0e6
: 0.0;
snap.reconnects = static_cast<int>(st.reconnects.load(std::memory_order_relaxed));
const std::string glance = udpscope::format_glance(snap);
app->dbg_glance_last = glance;
app->status_detail = udpscope::format_detail(snap);
if (app->status_detail.size() > app->status_detail_max) {
app->status_detail_max = app->status_detail.size();
} }
if (glance.size() > app->glance_max_chars) app->glance_max_chars = glance.size();
char status[1024];
std::snprintf(status, sizeof(status),
"[%s] %s %s | %s | packets %llu frames %llu gaps %llu | %zu signals, %zu "
"channels | plot 1: %d trace(s), %zu samples | buffer %.1f MB, %.2f s span, "
"time x%llu%s%s",
mode, st.connected.load() ? "connected" : "waiting",
st.have_config.load() ? "configured" : "-", rate.c_str(),
static_cast<unsigned long long>(st.packets.load()),
static_cast<unsigned long long>(st.frames.load()),
static_cast<unsigned long long>(st.gaps.load()), signal_count, channel_count, bound,
samples, double(model.config().buffer_mb), span_s,
static_cast<unsigned long long>(time_stride), since.c_str(), reconnects_note);
if (!app->notice.empty() && g_get_monotonic_time() < app->notice_until_us) { if (!app->notice.empty() && g_get_monotonic_time() < app->notice_until_us) {
const std::string with_notice = app->notice + " | " + status; const std::string with_notice = app->notice + " | " + glance;
gtk_label_set_text(GTK_LABEL(app->status_label), with_notice.c_str()); gtk_label_set_text(GTK_LABEL(app->status_label), with_notice.c_str());
} else { } else {
app->notice.clear(); app->notice.clear();
gtk_label_set_text(GTK_LABEL(app->status_label), status); gtk_label_set_text(GTK_LABEL(app->status_label), glance.c_str());
}
gtk_widget_set_tooltip_text(app->status_label, app->status_detail.c_str());
if (app->status_popover != nullptr &&
gtk_widget_get_visible(app->status_popover) == TRUE) {
update_status_popover(*app, snap);
} }
if (!rate_tip.empty()) gtk_widget_set_tooltip_text(app->status_label, rate_tip.c_str());
const double tick_ms = double(g_get_monotonic_time() - tick_start_us) / 1000.0; const double tick_ms = double(g_get_monotonic_time() - tick_start_us) / 1000.0;
app->dbg_tick_ms_sum += tick_ms; app->dbg_tick_ms_sum += tick_ms;
app->dbg_tick_ms_max = std::max(app->dbg_tick_ms_max, tick_ms); app->dbg_tick_ms_max = std::max(app->dbg_tick_ms_max, tick_ms);
@@ -5760,6 +5798,53 @@ void on_activate(GtkApplication* application, gpointer user) {
gtk_widget_set_hexpand(app->status_label, true); gtk_widget_set_hexpand(app->status_label, true);
gtk_widget_set_halign(app->status_label, GTK_ALIGN_FILL); gtk_widget_set_halign(app->status_label, GTK_ALIGN_FILL);
gtk_box_append(GTK_BOX(status_row), app->status_label); gtk_box_append(GTK_BOX(status_row), app->status_label);
// Guard the construction so the counter means something: if status_popover is already
// non-null (e.g. run_ui called twice), count the extra build and avoid a leak.
if (app->status_popover != nullptr) {
++app->popover_rebuilds;
} else {
app->status_popover = gtk_popover_new();
GtkWidget* detail_grid = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(detail_grid), 2);
gtk_grid_set_column_spacing(GTK_GRID(detail_grid), 16);
gtk_widget_set_margin_start(detail_grid, 10);
gtk_widget_set_margin_end(detail_grid, 10);
gtk_widget_set_margin_top(detail_grid, 8);
gtk_widget_set_margin_bottom(detail_grid, 8);
// Row headings at indices 0..3, value lines at 4..10, in the order format_detail's fields
// appear. Built once: update_status_popover() only ever calls gtk_label_set_text().
static const char* const kHeadings[] = {"Stream", "Buffer", "Plot 1", "Reconnects"};
const int kHeadingRow[] = {0, 3, 5, 6};
for (int i = 0; i < 4; ++i) {
GtkWidget* heading = gtk_label_new(nullptr);
char markup[64];
std::snprintf(markup, sizeof(markup), "<b>%s</b>", kHeadings[i]);
gtk_label_set_markup(GTK_LABEL(heading), markup);
gtk_label_set_xalign(GTK_LABEL(heading), 0.0f);
gtk_label_set_yalign(GTK_LABEL(heading), 0.0f);
gtk_grid_attach(GTK_GRID(detail_grid), heading, 0, kHeadingRow[i], 1, 1);
app->popover_label[i] = heading;
}
for (int i = 0; i < 7; ++i) {
GtkWidget* value = gtk_label_new("");
gtk_label_set_xalign(GTK_LABEL(value), 0.0f);
gtk_grid_attach(GTK_GRID(detail_grid), value, 1, i, 1, 1);
app->popover_label[4 + i] = value;
}
gtk_popover_set_child(GTK_POPOVER(app->status_popover), detail_grid);
app->status_info_button = gtk_menu_button_new();
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(app->status_info_button),
"help-about-symbolic");
gtk_menu_button_set_popover(GTK_MENU_BUTTON(app->status_info_button),
app->status_popover);
gtk_widget_set_tooltip_text(app->status_info_button, "Stream and buffer detail");
gtk_widget_set_has_tooltip(app->status_info_button, TRUE);
gtk_box_append(GTK_BOX(status_row), app->status_info_button);
}
gtk_widget_set_margin_top(status_row, 2); gtk_widget_set_margin_top(status_row, 2);
gtk_widget_set_margin_bottom(status_row, 2); gtk_widget_set_margin_bottom(status_row, 2);
gtk_widget_set_margin_start(status_row, 6); gtk_widget_set_margin_start(status_row, 6);
@@ -5851,7 +5936,7 @@ void on_activate(GtkApplication* application, gpointer user) {
/// exist, then close the window. Runs after the reveal animations have settled. /// exist, then close the window. Runs after the reveal animations have settled.
gboolean exit_probe_step3(gpointer user) { gboolean exit_probe_step3(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
a->dbg_mode_live = gtk_label_get_text(GTK_LABEL(a->status_label)); a->dbg_mode_live = a->status_detail;
GtkWidget* window = a->window; GtkWidget* window = a->window;
a->window = nullptr; // stop the tick before the widgets go a->window = nullptr; // stop the tick before the widgets go
a->dbg_side_w = gtk_widget_get_width(a->side_box); a->dbg_side_w = gtk_widget_get_width(a->side_box);
@@ -5896,7 +5981,7 @@ gboolean exit_probe_step3(gpointer user) {
/// mode the way the "Go live" button does, and stop the tick. /// mode the way the "Go live" button does, and stop the tick.
gboolean exit_probe_step2(gpointer user) { gboolean exit_probe_step2(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
a->dbg_mode_triggered = gtk_label_get_text(GTK_LABEL(a->status_label)); a->dbg_mode_triggered = a->status_detail;
if (a->rows->row(0).plot) { if (a->rows->row(0).plot) {
a->dbg_marker_x = a->rows->row(0).plot->cursor(a->rows->row(0).trigger_marker).x; a->dbg_marker_x = a->rows->row(0).plot->cursor(a->rows->row(0).trigger_marker).x;
a->dbg_marker_max = a->rows->row(0).plot->x_range(0).max; a->dbg_marker_max = a->rows->row(0).plot->x_range(0).max;
@@ -7621,6 +7706,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
app.dbg_marker_x, app.dbg_marker_max, app.dbg_trigger_time, app.dbg_marker_x, app.dbg_marker_max, app.dbg_trigger_time,
app.dbg_trigger_time_valid); app.dbg_trigger_time_valid);
std::printf("[udpscope] mode after go-live: %s\n", app.dbg_mode_live.c_str()); std::printf("[udpscope] mode after go-live: %s\n", app.dbg_mode_live.c_str());
std::printf("[udpscope] status bar: glance=[● %s] max_glance_chars=%zu detail_fields=11 "
"popover_rebuilds=%d\n",
app.dbg_glance_last.c_str(), app.glance_max_chars, app.popover_rebuilds);
// The derived channels beside the channel they came from. A math channel defined by the pane // The derived channels beside the channel they came from. A math channel defined by the pane
// reaches the producer by exactly the path the probe uses, so this is the pipeline's own // reaches the producer by exactly the path the probe uses, so this is the pipeline's own
// assertion: `x2` must read twice its source's extremes, a low pass must read less. // assertion: `x2` must read twice its source's extremes, a low pass must read less.