diff options
| author | Stefan Boberg <[email protected]> | 2026-04-13 14:05:03 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-04-13 14:05:03 +0200 |
| commit | c49e5b15e0f86080d7d33e4e31aecfb701f8f96f (patch) | |
| tree | ff41dcec20502c0cc4cf853222273f50d025cda3 /src/zen/progressbar.cpp | |
| parent | Add MemoryCidStore and ChunkStore interface (#940) (diff) | |
| download | archived-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.cpp | 224 |
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; |