diff --git a/include/fgc/ui/TuiUi.h b/include/fgc/ui/TuiUi.h index daeb191..aa64519 100644 --- a/include/fgc/ui/TuiUi.h +++ b/include/fgc/ui/TuiUi.h @@ -3,6 +3,7 @@ #include "fgc/ui/IUserInterface.h" #include +#include #include #include #include @@ -46,7 +47,15 @@ private: std::atomic running_{false}; // When set, the 100ms auto-redraw is suspended so the screen holds still and // the operator can select/copy text (terminals drop a selection on repaint). + // Display freeze (the `p` key), for selecting text out of the pane. While + // set, refreshLoop() stops posting redraws, so the frame on screen ages. + // paused_at_ is when it was frozen, so the badge can say how stale it is. std::atomic paused_{false}; + // Wall-clock time of the freeze, "HH:MM:SS", shown in the badge. Captured + // once: the badge must stay truthful WITHOUT a repaint, because repainting + // would pull the live snapshot and unfreeze what the pause is holding still. + std::string paused_at_hms_; + static std::string nowHms(); // True while the gimbal overlay is open, so the refresh loop can periodically // re-pull a firmware dump (keeps the tracking counters ticking without 'd'). std::atomic gimbal_overlay_open_{false}; diff --git a/src/ui/TuiUi.cpp b/src/ui/TuiUi.cpp index 24347e8..caa09f1 100644 --- a/src/ui/TuiUi.cpp +++ b/src/ui/TuiUi.cpp @@ -1,5 +1,7 @@ #include "fgc/ui/TuiUi.h" +#include + #include "fgc/ChannelTopics.h" #include "fgc/DumpParser.h" @@ -1247,13 +1249,30 @@ void TuiUi::pushLog(LogLevel level, const std::string& line) { } } +// Wall-clock time the frame was frozen at, "HH:MM:SS". Captured once, because +// the badge must not need a repaint to stay truthful - repainting would unfreeze +// the very data the pause is holding still. +std::string TuiUi::nowHms() { + const auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm tm{}; + localtime_r(&t, &tm); + char buf[16]; + std::strftime(buf, sizeof(buf), "%H:%M:%S", &tm); + return buf; +} + void TuiUi::refreshLoop() { using namespace std::chrono_literals; auto last_dump = std::chrono::steady_clock::now(); while (running_) { std::this_thread::sleep_for(100ms); if (!running_) break; - if (paused_) continue; // frozen for text selection; don't repaint + // Frozen for text selection: do NOT repaint. Re-rendering would pull the + // live snapshot and the numbers would keep moving under the cursor, + // which is precisely what the freeze exists to prevent. The badge + // carries the wall-clock time of the freeze instead, so the frame says + // how old it is without having to be redrawn to say it. + if (paused_) continue; // While the gimbal overlay is open, re-pull a firmware dump every ~3s so // the encoder-tracking counters stay current without pressing 'd'. if (gimbal_overlay_open_.load() && sink_ && @@ -1332,7 +1351,15 @@ void TuiUi::uiLoop() { text(" " + s.header.tower + " ") | bold, text("build " + s.header.build) | dim, filler(), - (paused_ ? text(" PAUSED ") | color(Color::Black) | bgcolor(Color::Yellow) | bold + // A frozen frame has to say that it is frozen, and for how long. + // Without the age it is indistinguishable from a hung program: the + // numbers simply stop, which during a multi-minute home reads as a + // crash. Costing 15 minutes of "is it stuck?" is how this got added. + // Says WHEN it froze, not just that it is frozen. Without the time a + // held frame is indistinguishable from a hung program - the numbers + // simply stop - which during a multi-minute home reads as a crash. + (paused_ ? text(" PAUSED @" + paused_at_hms_ + " ") + | color(Color::Black) | bgcolor(Color::Yellow) | bold : text("")), text(" " + mode + " ") | (s.header.live ? color(Color::Green) : color(Color::Yellow)) | bold, text(" up " + std::to_string(s.header.uptime_ms / 1000) + "s ") | dim, @@ -1649,6 +1676,7 @@ void TuiUi::uiLoop() { } if (c == "p") { // freeze/unfreeze the live refresh so text can be selected paused_ = !paused_; + if (paused_) paused_at_hms_ = nowHms(); return true; // this keypress redraws once so the indicator updates } if (overlay == Overlay::Help) { // vim-style section nav while help is open