From c49e5b15e0f86080d7d33e4e31aecfb701f8f96f Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Mon, 13 Apr 2026 14:05:03 +0200 Subject: 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 --- src/zen/progressbar.cpp | 224 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 201 insertions(+), 23 deletions(-) (limited to 'src/zen/progressbar.cpp') 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 #include +#if !ZEN_PLATFORM_WINDOWS +# include +#endif + ZEN_THIRD_PARTY_INCLUDES_START #include 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 g_ActiveScrollRegionOwner{nullptr}; +static std::atomic 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()) { @@ -146,6 +238,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) { @@ -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; -- cgit v1.2.3 From 3d59b5d7036c35fe484d052ff32dbdc9d0a75cf7 Mon Sep 17 00:00:00 2001 From: Dan Engelbrecht Date: Mon, 13 Apr 2026 19:17:09 +0200 Subject: fix utf characters in source code (#953) --- src/zen/progressbar.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/zen/progressbar.cpp') diff --git a/src/zen/progressbar.cpp b/src/zen/progressbar.cpp index d39b756ae..7a0668b80 100644 --- a/src/zen/progressbar.cpp +++ b/src/zen/progressbar.cpp @@ -295,7 +295,7 @@ ProgressBar::RenderStatusLine(std::string_view Line) uint32_t CurrentRows = TuiConsoleRows(0); if (CurrentRows >= 3 && CurrentRows != m_ScrollRegionRows) { - // Terminal was resized — reinstall scroll region + // Terminal was resized - reinstall scroll region TuiSetScrollRegion(1, CurrentRows - 1); m_ScrollRegionRows = CurrentRows; } @@ -304,12 +304,12 @@ ProgressBar::RenderStatusLine(std::string_view Line) // is atomic and log output from other threads cannot interleave. ExtendableStringBuilder<512> Buf; Buf << "\x1b" - "7" // ESC 7 — save cursor + "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 + "8"; // ESC 8 - restore cursor OutputToConsoleRaw(Buf); } -- cgit v1.2.3 From 8750ec743da8979aca9473bf5bed457867280b19 Mon Sep 17 00:00:00 2001 From: Dan Engelbrecht Date: Fri, 17 Apr 2026 10:16:22 +0200 Subject: operationlogoutput refactor (#967) - Improvement: Replaced `OperationLogOutput` with `ProgressBase` in `zenutil`; logging and progress reporting are now separate concerns. Operation classes receive a `LoggerRef` for logging and a `ProgressBase&` for progress bars --- src/zen/progressbar.cpp | 54 +++++++++---------------------------------------- 1 file changed, 10 insertions(+), 44 deletions(-) (limited to 'src/zen/progressbar.cpp') diff --git a/src/zen/progressbar.cpp b/src/zen/progressbar.cpp index 7a0668b80..34174d35d 100644 --- a/src/zen/progressbar.cpp +++ b/src/zen/progressbar.cpp @@ -5,10 +5,11 @@ #include "progressbar.h" +#include #include #include -#include #include +#include #if !ZEN_PLATFORM_WINDOWS # include @@ -531,64 +532,29 @@ ProgressBar::HasActiveTask() const return !m_State.Task.empty(); } -class ConsoleOpLogProgressBar : public OperationLogOutput::ProgressBar -{ -public: - ConsoleOpLogProgressBar(zen::ProgressBar::Mode InMode, std::string_view InSubTask) : m_Inner(InMode, InSubTask) {} - - virtual void UpdateState(const State& NewState, bool DoLinebreak) - { - zen::ProgressBar::State State = {.Task = NewState.Task, - .Details = NewState.Details, - .TotalCount = NewState.TotalCount, - .RemainingCount = NewState.RemainingCount, - .Status = ConvertStatus(NewState.Status)}; - m_Inner.UpdateState(State, DoLinebreak); - } - virtual void Finish() { m_Inner.Finish(); } - -private: - zen::ProgressBar::State::EStatus ConvertStatus(State::EStatus Status) - { - switch (Status) - { - case State::EStatus::Running: - return zen::ProgressBar::State::EStatus::Running; - case State::EStatus::Aborted: - return zen::ProgressBar::State::EStatus::Aborted; - case State::EStatus::Paused: - return zen::ProgressBar::State::EStatus::Paused; - default: - return (zen::ProgressBar::State::EStatus)Status; - } - } - zen::ProgressBar m_Inner; -}; - -class ConsoleOpLogOutput : public OperationLogOutput +class ConsoleOpLogOutput : public ProgressBase { public: ConsoleOpLogOutput(zen::ProgressBar::Mode InMode) : m_Mode(InMode) {} - virtual void EmitLogMessage(const logging::LogPoint& Point, fmt::format_args Args) override - { - logging::EmitConsoleLogMessage(Point, Args); - } virtual void SetLogOperationName(std::string_view Name) override { zen::ProgressBar::SetLogOperationName(m_Mode, Name); } virtual void SetLogOperationProgress(uint32_t StepIndex, uint32_t StepCount) override { zen::ProgressBar::SetLogOperationProgress(m_Mode, StepIndex, StepCount); } - virtual uint32_t GetProgressUpdateDelayMS() override { return GetUpdateDelayMS(m_Mode); } + virtual uint32_t GetProgressUpdateDelayMS() const override { return GetUpdateDelayMS(m_Mode); } - virtual ProgressBar* CreateProgressBar(std::string_view InSubTask) override { return new ConsoleOpLogProgressBar(m_Mode, InSubTask); } + virtual std::unique_ptr CreateProgressBar(std::string_view InSubTask) override + { + return std::make_unique(m_Mode, InSubTask); + } private: zen::ProgressBar::Mode m_Mode; }; -OperationLogOutput* -CreateConsoleLogOutput(ProgressBar::Mode InMode) +ProgressBase* +CreateConsoleProgress(ProgressBar::Mode InMode) { return new ConsoleOpLogOutput(InMode); } -- cgit v1.2.3 From f07e04aa501b26b96e345f2e8ac42d231a015e55 Mon Sep 17 00:00:00 2001 From: Dan Engelbrecht Date: Fri, 17 Apr 2026 15:00:01 +0200 Subject: replace pretty progress with prettyscroll implementation (#970) --- src/zen/progressbar.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'src/zen/progressbar.cpp') diff --git a/src/zen/progressbar.cpp b/src/zen/progressbar.cpp index 34174d35d..780b08707 100644 --- a/src/zen/progressbar.cpp +++ b/src/zen/progressbar.cpp @@ -151,7 +151,6 @@ 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; @@ -205,7 +204,7 @@ ProgressBar::PopLogOperation(Mode InMode) } ProgressBar::ProgressBar(Mode InMode, std::string_view InSubTask) -: m_Mode((!TuiIsStdoutTty() && (InMode == Mode::Pretty || InMode == Mode::PrettyScroll)) ? Mode::Plain : InMode) +: m_Mode((!TuiIsStdoutTty() && InMode == Mode::Pretty) ? Mode::Plain : InMode) , m_LastUpdateMS((uint64_t)-1) , m_PausedMS(0) , m_SubTask(InSubTask) @@ -216,7 +215,7 @@ ProgressBar::ProgressBar(Mode InMode, std::string_view InSubTask) PushLogOperation(InMode, m_SubTask); } - if (m_Mode == Mode::PrettyScroll) + if (m_Mode == Mode::Pretty) { SetupScrollRegion(); } @@ -242,6 +241,12 @@ ProgressBar::~ProgressBar() void ProgressBar::SetupScrollRegion() { + // Only one scroll region owner at a time; nested bars fall back to the inline \r path. + if (g_ActiveScrollRegionOwner.load(std::memory_order_acquire) != nullptr) + { + return; + } + uint32_t Rows = TuiConsoleRows(0); if (Rows < 3) { @@ -375,7 +380,7 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak) OutputToConsoleRaw(Output); m_State = NewState; } - else if (m_Mode == Mode::Pretty || m_Mode == Mode::PrettyScroll) + else if (m_Mode == Mode::Pretty) { size_t ProgressBarSize = 20; @@ -432,7 +437,7 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak) } else { - // Fallback: inline \r-based overwrite (original behavior) + // Fallback: inline \r-based overwrite (terminal too small for scroll region) std::string_view Output = OutputBuilder.ToView(); std::string::size_type EraseLength = m_LastOutputLength > (Output.length() + 1) ? (m_LastOutputLength - Output.length() - 1) : 0; -- cgit v1.2.3 From c7c59cdc5a70bfd6e5f66f3b032ea3f8f6b4d12a Mon Sep 17 00:00:00 2001 From: Dan Engelbrecht Date: Mon, 20 Apr 2026 07:27:35 +0200 Subject: builds cmd refactor (#975) - Bugfix: `builds download` partial-block fetch decisions now account for build storage host latency - Bugfix: Transfer rate displays in `builds` commands now smooth correctly - Split `buildstorageoperations.cpp` (8.5k lines) into per-operation TUs: buildinspect, buildprimecache, buildstorageresolve, buildupdatefolder, builduploadfolder, buildvalidatebuildpart; stats moved to buildstoragestats.h. - FilteredRate extracted to zenutil. - BuildsCommand shared state consolidated into a BuildsConfiguration struct; subcommands inherit from BuildsSubCmdBase holding a `const BuildsConfiguration&` instead of a `BuildsCommand&`. - `ProgressBar` renamed to `ConsoleProgressBar`; mode enum (`ConsoleProgressMode`) lifted to namespace scope; `PushLogOperation`/`PopLogOperation`/`ForceLinebreak` promoted to virtuals on `ProgressBase`. - Free-function wrappers (`UploadFolder`, `DownloadFolder`, `ValidateBuildPart`) added around the existing operation classes so callers stop reimplementing setup + stats logging. --- src/zen/progressbar.cpp | 567 ------------------------------------------------ 1 file changed, 567 deletions(-) delete mode 100644 src/zen/progressbar.cpp (limited to 'src/zen/progressbar.cpp') diff --git a/src/zen/progressbar.cpp b/src/zen/progressbar.cpp deleted file mode 100644 index 780b08707..000000000 --- a/src/zen/progressbar.cpp +++ /dev/null @@ -1,567 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -// Zen command line client utility -// - -#include "progressbar.h" - -#include -#include -#include -#include -#include - -#if !ZEN_PLATFORM_WINDOWS -# include -#endif - -ZEN_THIRD_PARTY_INCLUDES_START -#include -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 g_ActiveScrollRegionOwner{nullptr}; -static std::atomic 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() -{ - static HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); - return hStdOut; -} -#endif - -static void -OutputToConsoleRaw(const char* String, size_t Length) -{ -#if ZEN_PLATFORM_WINDOWS - HANDLE hStdOut = GetConsoleHandle(); - if (TuiIsStdoutTty()) - { - WriteConsoleA(hStdOut, String, (DWORD)Length, 0, 0); - } - else - { - ::WriteFile(hStdOut, (LPCVOID)String, (DWORD)Length, 0, 0); - } -#else - fwrite(String, 1, Length, stdout); -#endif -} - -static void -OutputToConsoleRaw(const std::string& String) -{ - OutputToConsoleRaw(String.c_str(), String.length()); -} - -static void -OutputToConsoleRaw(const StringBuilderBase& SB) -{ - OutputToConsoleRaw(SB.c_str(), SB.Size()); -} - -uint32_t -GetUpdateDelayMS(ProgressBar::Mode InMode) -{ - switch (InMode) - { - case ProgressBar::Mode::Plain: - return 5000; - case ProgressBar::Mode::Pretty: - return 200; - case ProgressBar::Mode::Log: - return 2000; - default: - ZEN_ASSERT(false); - return 0; - } -} - -void -ProgressBar::SetLogOperationName(Mode InMode, std::string_view Name) -{ - ZEN_ASSERT(Name.find('\"') == std::string_view::npos); - if (InMode == Mode::Log) - { - std::string String = fmt::format("@progress \"{}\"\n", Name); - OutputToConsoleRaw(String); - } -} - -void -ProgressBar::SetLogOperationProgress(Mode InMode, uint32_t StepIndex, uint32_t StepCount) -{ - if (InMode == Mode::Log) - { - const size_t PercentDone = StepCount > 0u ? gsl::narrow((100 * StepIndex) / StepCount) : 0u; - - std::string String = fmt::format("@progress {}%\n", PercentDone); - OutputToConsoleRaw(String); - } -} - -void -ProgressBar::PushLogOperation(Mode InMode, std::string_view Name) -{ - if (InMode == Mode::Log) - { - std::string String = fmt::format("@progress push \"{}\"\n", Name); - OutputToConsoleRaw(String); - } -} - -void -ProgressBar::PopLogOperation(Mode InMode) -{ - if (InMode == Mode::Log) - { - const std::string String("@progress pop\n"); - OutputToConsoleRaw(String); - } -} - -ProgressBar::ProgressBar(Mode InMode, std::string_view InSubTask) -: m_Mode((!TuiIsStdoutTty() && InMode == Mode::Pretty) ? Mode::Plain : InMode) -, m_LastUpdateMS((uint64_t)-1) -, m_PausedMS(0) -, m_SubTask(InSubTask) -{ - ZEN_ASSERT(InSubTask.find('\"') == std::string_view::npos); - if (!m_SubTask.empty()) - { - PushLogOperation(InMode, m_SubTask); - } - - if (m_Mode == Mode::Pretty) - { - SetupScrollRegion(); - } -} - -ProgressBar::~ProgressBar() -{ - try - { - TeardownScrollRegion(); - ForceLinebreak(); - if (!m_SubTask.empty()) - { - PopLogOperation(m_Mode); - } - } - catch (const std::exception& Ex) - { - ZEN_ERROR("ProgressBar::~ProgressBar() failed with {}", Ex.what()); - } -} - -void -ProgressBar::SetupScrollRegion() -{ - // Only one scroll region owner at a time; nested bars fall back to the inline \r path. - if (g_ActiveScrollRegionOwner.load(std::memory_order_acquire) != nullptr) - { - return; - } - - 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); - ZEN_ASSERT(NewState.Task.find('\"') == std::string::npos); - if (DoLinebreak == false && m_State == NewState) - { - return; - } - - uint64_t ElapsedTimeMS = NewState.OptionalElapsedTime == (uint64_t)-1 ? m_SW.GetElapsedTimeMs() : NewState.OptionalElapsedTime; - if (m_LastUpdateMS != (uint64_t)-1) - { - if (!DoLinebreak && (NewState.Status == m_State.Status) && (NewState.Task == m_State.Task) && - ((m_LastUpdateMS + 200) > ElapsedTimeMS)) - { - return; - } - if (m_State.Status == State::EStatus::Paused) - { - uint64_t ElapsedSinceLast = ElapsedTimeMS - m_LastUpdateMS; - m_PausedMS += ElapsedSinceLast; - } - } - - m_LastUpdateMS = ElapsedTimeMS; - - std::string Task = NewState.Task; - switch (NewState.Status) - { - case State::EStatus::Aborted: - Task = "Aborting"; - break; - case State::EStatus::Paused: - Task = "Paused"; - break; - default: - break; - } - if (NewState.Task.length() > Task.length()) - { - Task += std::string(NewState.Task.length() - Task.length(), ' '); - } - - const size_t PercentDone = - NewState.TotalCount > 0u ? gsl::narrow((100 * (NewState.TotalCount - NewState.RemainingCount)) / NewState.TotalCount) : 0u; - - uint64_t Completed = NewState.TotalCount - NewState.RemainingCount; - uint64_t ETAElapsedMS = ElapsedTimeMS - m_PausedMS; - uint64_t ETAMS = ((m_State.TotalCount == NewState.TotalCount) && (NewState.Status == State::EStatus::Running)) && (PercentDone > 5) - ? (ETAElapsedMS * NewState.RemainingCount) / Completed - : 0; - const std::string ETAString = (ETAMS > 0) ? fmt::format(" ETA {}", NiceTimeSpanMs(ETAMS)) : ""; - - if (m_Mode == Mode::Plain) - { - const std::string Details = (!NewState.Details.empty()) ? fmt::format(": {}", NewState.Details) : ""; - const std::string Output = fmt::format("{} {}% {}{}{}\n", Task, PercentDone, NiceTimeSpanMs(ElapsedTimeMS), ETAString, Details); - OutputToConsoleRaw(Output); - m_State = NewState; - } - else if (m_Mode == Mode::Pretty) - { - size_t ProgressBarSize = 20; - - size_t ProgressBarCount = (ProgressBarSize * PercentDone) / 100; - - uint32_t ConsoleColumns = TuiConsoleColumns(1024); - - const std::string PercentString = fmt::format("{:#3}%", PercentDone); - - const std::string ProgressBarString = - fmt::format(": |{}{}|", std::string(ProgressBarCount, '#'), std::string(ProgressBarSize - ProgressBarCount, ' ')); - - const std::string ElapsedString = fmt::format(": {}", NiceTimeSpanMs(ElapsedTimeMS)); - - const std::string DetailsString = (!NewState.Details.empty()) ? fmt::format(". {}", NewState.Details) : ""; - - ExtendableStringBuilder<256> OutputBuilder; - - OutputBuilder << Task << " " << PercentString; - if (OutputBuilder.Size() + 1 < ConsoleColumns) - { - size_t RemainingSpace = ConsoleColumns - (OutputBuilder.Size() + 1); - bool ElapsedFits = RemainingSpace >= ElapsedString.length(); - RemainingSpace -= ElapsedString.length(); - bool ETAFits = ElapsedFits && RemainingSpace >= ETAString.length(); - RemainingSpace -= ETAString.length(); - bool DetailsFits = ETAFits && RemainingSpace >= DetailsString.length(); - RemainingSpace -= DetailsString.length(); - bool ProgressBarFits = DetailsFits && RemainingSpace >= ProgressBarString.length(); - RemainingSpace -= ProgressBarString.length(); - - if (ProgressBarFits) - { - OutputBuilder << ProgressBarString; - } - if (ElapsedFits) - { - OutputBuilder << ElapsedString; - } - if (ETAFits) - { - OutputBuilder << ETAString; - } - if (DetailsFits) - { - OutputBuilder << DetailsString; - } - } - - if (m_ScrollRegionActive) - { - // Render on the pinned bottom status line - RenderStatusLine(OutputBuilder.ToView()); - } - else - { - // Fallback: inline \r-based overwrite (terminal too small for scroll region) - 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; - - if (Output.length() + 1 + EraseLength >= ConsoleColumns) - { - if (m_LastOutputLength > 0) - { - LineToPrint << "\n"; - } - LineToPrint << Output; - DoLinebreak = true; - } - else - { - LineToPrint << "\r" << Output << std::string(EraseLength, ' '); - } - - if (DoLinebreak) - { - LineToPrint << "\n"; - } - - OutputToConsoleRaw(LineToPrint); - m_LastOutputLength = DoLinebreak ? 0 : (Output.length() + 1); // +1 for \r prefix - } - - m_State = NewState; - } - else if (m_Mode == Mode::Log) - { - if (m_State.Task != NewState.Task || - m_State.Details != NewState.Details) // TODO: Should we output just because details change? Will this spam the log collector? - { - std::string Details = (!NewState.Details.empty()) ? fmt::format(": {}", NewState.Details) : ""; - for (std::string::value_type& Char : Details) - { - if (Char == '"') - { - Char = '\''; - } - } - const std::string Message = - fmt::format("@progress \"{} {}{}{}\"\n", NewState.Task, NiceTimeSpanMs(ElapsedTimeMS), ETAString, Details); - OutputToConsoleRaw(Message); - } - - const size_t OldPercentDone = - m_State.TotalCount > 0u ? gsl::narrow((100 * (m_State.TotalCount - m_State.RemainingCount)) / m_State.TotalCount) : 0u; - - if (OldPercentDone != PercentDone) - { - const std::string Progress = fmt::format("@progress {}%\n", PercentDone); - OutputToConsoleRaw(Progress); - } - m_State = NewState; - } -} - -void -ProgressBar::ForceLinebreak() -{ - if (m_LastOutputLength > 0) - { - State NewState = m_State; - UpdateState(NewState, /*DoLinebreak*/ true); - } -} - -void -ProgressBar::Finish() -{ - TeardownScrollRegion(); - - if (m_LastOutputLength > 0 || m_State.RemainingCount > 0) - { - State NewState = m_State; - NewState.RemainingCount = 0; - NewState.Details = ""; - UpdateState(NewState, /*DoLinebreak*/ true); - } - m_State = State{}; - m_LastOutputLength = 0; - m_SW.Reset(); -} - -bool -ProgressBar::IsSameTask(std::string_view Task) const -{ - return Task == m_State.Task; -} - -bool -ProgressBar::HasActiveTask() const -{ - return !m_State.Task.empty(); -} - -class ConsoleOpLogOutput : public ProgressBase -{ -public: - ConsoleOpLogOutput(zen::ProgressBar::Mode InMode) : m_Mode(InMode) {} - - virtual void SetLogOperationName(std::string_view Name) override { zen::ProgressBar::SetLogOperationName(m_Mode, Name); } - virtual void SetLogOperationProgress(uint32_t StepIndex, uint32_t StepCount) override - { - zen::ProgressBar::SetLogOperationProgress(m_Mode, StepIndex, StepCount); - } - virtual uint32_t GetProgressUpdateDelayMS() const override { return GetUpdateDelayMS(m_Mode); } - - virtual std::unique_ptr CreateProgressBar(std::string_view InSubTask) override - { - return std::make_unique(m_Mode, InSubTask); - } - -private: - zen::ProgressBar::Mode m_Mode; -}; - -ProgressBase* -CreateConsoleProgress(ProgressBar::Mode InMode) -{ - return new ConsoleOpLogOutput(InMode); -} - -} // namespace zen -- cgit v1.2.3