/* Crafter® Build Copyright (C) 2026 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; #include #include #if defined(_WIN32) #include #include #ifndef STDOUT_FILENO #define STDOUT_FILENO _fileno(stdout) #endif #define CRAFTER_PROGRESS_ISATTY _isatty #else #include #include #define CRAFTER_PROGRESS_ISATTY isatty #endif export module Crafter.Build:Progress_impl; import std; import :Progress; namespace { std::mutex g_mutex; std::atomic g_total{0}; std::atomic g_done{0}; Crafter::Progress::Verbosity g_verbosity = Crafter::Progress::Verbosity::Default; bool g_isTty = false; bool g_lineDirty = false; // status line has uncleared content bool g_finalized = false; std::chrono::steady_clock::time_point g_startTime = std::chrono::steady_clock::now(); int TerminalWidth() { #if defined(_WIN32) HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO info; if (h != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(h, &info)) { int w = info.srWindow.Right - info.srWindow.Left + 1; if (w > 0) return w; } #else winsize ws{}; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) { return ws.ws_col; } #endif if (const char* col = std::getenv("COLUMNS")) { try { int c = std::stoi(col); if (c > 0) return c; } catch (...) {} } return 80; } // Caller holds g_mutex. void RenderStatus(std::string_view label) { if (g_verbosity != Crafter::Progress::Verbosity::Default || !g_isTty) return; int done = g_done.load(std::memory_order_relaxed); int total = g_total.load(std::memory_order_relaxed); std::string prefix = std::format("[{}/{}] ", done, total); int width = TerminalWidth(); int avail = width - static_cast(prefix.size()) - 1; std::string trimmed{label}; if (avail > 0 && static_cast(trimmed.size()) > avail) { trimmed.resize(static_cast(avail)); } // \r returns to col 0, \033[2K erases the whole line. No newline. std::fputs("\r\033[2K", stdout); std::fputs(prefix.c_str(), stdout); std::fputs(trimmed.c_str(), stdout); std::fflush(stdout); g_lineDirty = true; } // Caller holds g_mutex. void ClearLineLocked() { if (g_lineDirty) { std::fputs("\r\033[2K", stdout); std::fflush(stdout); g_lineDirty = false; } } } namespace Crafter::Progress { void SetVerbosity(Verbosity v) { std::lock_guard lock(g_mutex); g_verbosity = v; g_isTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0; g_startTime = std::chrono::steady_clock::now(); g_total.store(0); g_done.store(0); g_finalized = false; } Verbosity GetVerbosity() { std::lock_guard lock(g_mutex); return g_verbosity; } Task::Task(std::string label) : label_(std::move(label)) { g_total.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(g_mutex); if (g_verbosity == Verbosity::Default && g_isTty) { RenderStatus(label_); } } Task::~Task() { int done = g_done.fetch_add(1, std::memory_order_relaxed) + 1; std::lock_guard lock(g_mutex); if (g_verbosity == Verbosity::Default) { if (g_isTty) { RenderStatus(label_); } else { // Non-TTY: one append-only line per completed task (ninja-style). int total = g_total.load(std::memory_order_relaxed); std::println("[{}/{}] {}", done, total, label_); } } } void EchoCommand(std::string_view command) { std::lock_guard lock(g_mutex); if (g_verbosity == Verbosity::Verbose) { std::println("$ {}", command); } } void Clear() { std::lock_guard lock(g_mutex); ClearLineLocked(); } void Finalize() { std::lock_guard lock(g_mutex); if (g_finalized) return; g_finalized = true; ClearLineLocked(); if (g_verbosity == Verbosity::Quiet) return; int done = g_done.load(); if (done == 0) return; // Nothing happened (cached build); stay silent. auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - g_startTime); std::println("Built {} step{} in {}ms", done, done == 1 ? "" : "s", elapsed.count()); } } // namespace Crafter::Progress