aboutsummaryrefslogtreecommitdiff
path: root/src/zen/progressbar.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-04-13 14:05:03 +0200
committerGitHub Enterprise <[email protected]>2026-04-13 14:05:03 +0200
commitc49e5b15e0f86080d7d33e4e31aecfb701f8f96f (patch)
treeff41dcec20502c0cc4cf853222273f50d025cda3 /src/zen/progressbar.cpp
parentAdd MemoryCidStore and ChunkStore interface (#940) (diff)
downloadarchived-zen-c49e5b15e0f86080d7d33e4e31aecfb701f8f96f.tar.xz
archived-zen-c49e5b15e0f86080d7d33e4e31aecfb701f8f96f.zip
Some minor polish from tourist branch (#949)
- Replace per-type fmt::formatter specializations (StringBuilderBase, NiceBase) with a single generic formatter using a HasStringViewConversion concept - Add ThousandsNum for comma-separated integer formatting (e.g. "1,234,567") - Thread naming now accepts a sort hint for trace ordering - Fix main thread trace registration to use actual thread ID and sort first - Add ExpandEnvironmentVariables() for expanding %VAR% references in strings, with tests - Add ParseHexBytes() overload with expected byte count validation - Add Flag_BelowNormalPriority to CreateProcOptions (BELOW_NORMAL_PRIORITY_CLASS on Windows, setpriority on POSIX) - Add PrettyScroll progress bar mode that pins the status line to the bottom of the terminal using scroll regions, with signal handler cleanup for Ctrl+C/SIGTERM
Diffstat (limited to 'src/zen/progressbar.cpp')
-rw-r--r--src/zen/progressbar.cpp224
1 files changed, 201 insertions, 23 deletions
diff --git a/src/zen/progressbar.cpp b/src/zen/progressbar.cpp
index 6581cd116..d39b756ae 100644
--- a/src/zen/progressbar.cpp
+++ b/src/zen/progressbar.cpp
@@ -10,6 +10,10 @@
#include <zenremotestore/operationlogoutput.h>
#include <zenutil/consoletui.h>
+#if !ZEN_PLATFORM_WINDOWS
+# include <csignal>
+#endif
+
ZEN_THIRD_PARTY_INCLUDES_START
#include <gsl/gsl-lite.hpp>
ZEN_THIRD_PARTY_INCLUDES_END
@@ -18,6 +22,87 @@ ZEN_THIRD_PARTY_INCLUDES_END
namespace zen {
+// Global tracking for scroll region cleanup on abnormal termination (Ctrl+C etc.)
+// Only one ProgressBar can own a scroll region at a time.
+static std::atomic<ProgressBar*> g_ActiveScrollRegionOwner{nullptr};
+static std::atomic<uint32_t> g_ActiveScrollRegionRows{0};
+
+static void
+ResetScrollRegionRaw()
+{
+ // Signal-safe: emit raw escape sequences to restore terminal state.
+ // These are async-signal-safe on POSIX (write()) and safe in console
+ // ctrl handlers on Windows (WriteConsole is allowed).
+ uint32_t Rows = g_ActiveScrollRegionRows.load(std::memory_order_acquire);
+ if (Rows >= 3)
+ {
+ // Move to status line, erase it, reset scroll region, move cursor to end of content
+ TuiMoveCursor(Rows, 1);
+ TuiEraseLine();
+ TuiResetScrollRegion();
+ TuiMoveCursor(Rows - 1, 1);
+ }
+ else
+ {
+ TuiResetScrollRegion();
+ }
+ TuiShowCursor(true);
+ TuiFlush();
+}
+
+#if ZEN_PLATFORM_WINDOWS
+static BOOL WINAPI
+ScrollRegionCtrlHandler(DWORD CtrlType)
+{
+ if (CtrlType == CTRL_C_EVENT || CtrlType == CTRL_BREAK_EVENT)
+ {
+ ResetScrollRegionRaw();
+ }
+ // Return FALSE so the default handler (process termination) still runs
+ return FALSE;
+}
+#else
+static struct sigaction s_PrevSigIntAction;
+static struct sigaction s_PrevSigTermAction;
+
+static void
+ScrollRegionSignalHandler(int Signal)
+{
+ ResetScrollRegionRaw();
+
+ // Re-raise with the previous handler
+ struct sigaction* PrevAction = (Signal == SIGINT) ? &s_PrevSigIntAction : &s_PrevSigTermAction;
+ sigaction(Signal, PrevAction, nullptr);
+ raise(Signal);
+}
+#endif
+
+static void
+InstallScrollRegionCleanupHandler()
+{
+#if ZEN_PLATFORM_WINDOWS
+ SetConsoleCtrlHandler(ScrollRegionCtrlHandler, TRUE);
+#else
+ struct sigaction Action = {};
+ Action.sa_handler = ScrollRegionSignalHandler;
+ Action.sa_flags = SA_RESETHAND; // one-shot
+ sigemptyset(&Action.sa_mask);
+ sigaction(SIGINT, &Action, &s_PrevSigIntAction);
+ sigaction(SIGTERM, &Action, &s_PrevSigTermAction);
+#endif
+}
+
+static void
+RemoveScrollRegionCleanupHandler()
+{
+#if ZEN_PLATFORM_WINDOWS
+ SetConsoleCtrlHandler(ScrollRegionCtrlHandler, FALSE);
+#else
+ sigaction(SIGINT, &s_PrevSigIntAction, nullptr);
+ sigaction(SIGTERM, &s_PrevSigTermAction, nullptr);
+#endif
+}
+
#if ZEN_PLATFORM_WINDOWS
static HANDLE
GetConsoleHandle()
@@ -65,6 +150,7 @@ GetUpdateDelayMS(ProgressBar::Mode InMode)
case ProgressBar::Mode::Plain:
return 5000;
case ProgressBar::Mode::Pretty:
+ case ProgressBar::Mode::PrettyScroll:
return 200;
case ProgressBar::Mode::Log:
return 2000;
@@ -118,7 +204,7 @@ ProgressBar::PopLogOperation(Mode InMode)
}
ProgressBar::ProgressBar(Mode InMode, std::string_view InSubTask)
-: m_Mode((!TuiIsStdoutTty() && InMode == Mode::Pretty) ? Mode::Plain : InMode)
+: m_Mode((!TuiIsStdoutTty() && (InMode == Mode::Pretty || InMode == Mode::PrettyScroll)) ? Mode::Plain : InMode)
, m_LastUpdateMS((uint64_t)-1)
, m_PausedMS(0)
, m_SubTask(InSubTask)
@@ -128,12 +214,18 @@ ProgressBar::ProgressBar(Mode InMode, std::string_view InSubTask)
{
PushLogOperation(InMode, m_SubTask);
}
+
+ if (m_Mode == Mode::PrettyScroll)
+ {
+ SetupScrollRegion();
+ }
}
ProgressBar::~ProgressBar()
{
try
{
+ TeardownScrollRegion();
ForceLinebreak();
if (!m_SubTask.empty())
{
@@ -147,6 +239,81 @@ ProgressBar::~ProgressBar()
}
void
+ProgressBar::SetupScrollRegion()
+{
+ uint32_t Rows = TuiConsoleRows(0);
+ if (Rows < 3)
+ {
+ return;
+ }
+
+ TuiEnableOutput();
+
+ // Ensure cursor is not on the last row before we install the region.
+ // Print a newline to push content up if needed, then set the region.
+ OutputToConsoleRaw("\n");
+ TuiSetScrollRegion(1, Rows - 1);
+
+ // Move cursor into the scroll region so normal output stays there
+ TuiMoveCursor(Rows - 1, 1);
+
+ m_ScrollRegionActive = true;
+ m_ScrollRegionRows = Rows;
+
+ g_ActiveScrollRegionRows.store(Rows, std::memory_order_release);
+ g_ActiveScrollRegionOwner.store(this, std::memory_order_release);
+ InstallScrollRegionCleanupHandler();
+}
+
+void
+ProgressBar::TeardownScrollRegion()
+{
+ if (!m_ScrollRegionActive)
+ {
+ return;
+ }
+ m_ScrollRegionActive = false;
+
+ RemoveScrollRegionCleanupHandler();
+ g_ActiveScrollRegionOwner.store(nullptr, std::memory_order_release);
+ g_ActiveScrollRegionRows.store(0, std::memory_order_release);
+
+ // Emit all teardown escape sequences as a single atomic write
+ ExtendableStringBuilder<128> Buf;
+ Buf << fmt::format("\x1b[{};1H", m_ScrollRegionRows) // move to status line
+ << "\x1b[2K" // erase it
+ << "\x1b[r" // reset scroll region
+ << fmt::format("\x1b[{};1H", m_ScrollRegionRows - 1); // move to end of content
+ OutputToConsoleRaw(Buf);
+ TuiFlush();
+}
+
+void
+ProgressBar::RenderStatusLine(std::string_view Line)
+{
+ // Handle terminal resizes by re-querying row count
+ uint32_t CurrentRows = TuiConsoleRows(0);
+ if (CurrentRows >= 3 && CurrentRows != m_ScrollRegionRows)
+ {
+ // Terminal was resized — reinstall scroll region
+ TuiSetScrollRegion(1, CurrentRows - 1);
+ m_ScrollRegionRows = CurrentRows;
+ }
+
+ // Build the entire escape sequence as a single string so the console write
+ // is atomic and log output from other threads cannot interleave.
+ ExtendableStringBuilder<512> Buf;
+ Buf << "\x1b"
+ "7" // ESC 7 — save cursor
+ << fmt::format("\x1b[{};1H", m_ScrollRegionRows) // move to bottom row
+ << "\x1b[2K" // erase entire line
+ << Line // progress bar content
+ << "\x1b"
+ "8"; // ESC 8 — restore cursor
+ OutputToConsoleRaw(Buf);
+}
+
+void
ProgressBar::UpdateState(const State& NewState, bool DoLinebreak)
{
ZEN_ASSERT(NewState.TotalCount >= NewState.RemainingCount);
@@ -207,7 +374,7 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak)
OutputToConsoleRaw(Output);
m_State = NewState;
}
- else if (m_Mode == Mode::Pretty)
+ else if (m_Mode == Mode::Pretty || m_Mode == Mode::PrettyScroll)
{
size_t ProgressBarSize = 20;
@@ -226,7 +393,7 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak)
ExtendableStringBuilder<256> OutputBuilder;
- OutputBuilder << "\r" << Task << " " << PercentString;
+ OutputBuilder << Task << " " << PercentString;
if (OutputBuilder.Size() + 1 < ConsoleColumns)
{
size_t RemainingSpace = ConsoleColumns - (OutputBuilder.Size() + 1);
@@ -257,34 +424,43 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak)
}
}
- std::string_view Output = OutputBuilder.ToView();
- std::string::size_type EraseLength = m_LastOutputLength > Output.length() ? (m_LastOutputLength - Output.length()) : 0;
+ if (m_ScrollRegionActive)
+ {
+ // Render on the pinned bottom status line
+ RenderStatusLine(OutputBuilder.ToView());
+ }
+ else
+ {
+ // Fallback: inline \r-based overwrite (original behavior)
+ std::string_view Output = OutputBuilder.ToView();
+ std::string::size_type EraseLength =
+ m_LastOutputLength > (Output.length() + 1) ? (m_LastOutputLength - Output.length() - 1) : 0;
+ ExtendableStringBuilder<256> LineToPrint;
- ExtendableStringBuilder<256> LineToPrint;
+ if (Output.length() + 1 + EraseLength >= ConsoleColumns)
+ {
+ if (m_LastOutputLength > 0)
+ {
+ LineToPrint << "\n";
+ }
+ LineToPrint << Output;
+ DoLinebreak = true;
+ }
+ else
+ {
+ LineToPrint << "\r" << Output << std::string(EraseLength, ' ');
+ }
- if (Output.length() + EraseLength >= ConsoleColumns)
- {
- if (m_LastOutputLength > 0)
+ if (DoLinebreak)
{
LineToPrint << "\n";
}
- LineToPrint << Output.substr(1);
- DoLinebreak = true;
- }
- else
- {
- LineToPrint << Output << std::string(EraseLength, ' ');
- }
- if (DoLinebreak)
- {
- LineToPrint << "\n";
+ OutputToConsoleRaw(LineToPrint);
+ m_LastOutputLength = DoLinebreak ? 0 : (Output.length() + 1); // +1 for \r prefix
}
- OutputToConsoleRaw(LineToPrint);
-
- m_LastOutputLength = DoLinebreak ? 0 : Output.length();
- m_State = NewState;
+ m_State = NewState;
}
else if (m_Mode == Mode::Log)
{
@@ -329,6 +505,8 @@ ProgressBar::ForceLinebreak()
void
ProgressBar::Finish()
{
+ TeardownScrollRegion();
+
if (m_LastOutputLength > 0 || m_State.RemainingCount > 0)
{
State NewState = m_State;