// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® 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 StateMutex; std::atomic Total{0}; std::atomic Done{0}; Crafter::Progress::Verbosity ActiveVerbosity = Crafter::Progress::Verbosity::Default; bool IsTty = false; bool LineDirty = false; // status line has uncleared content bool Finalized = false; std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now(); std::int32_t TerminalWidth() { #if defined(_WIN32) HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO info; if (h != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(h, &info)) { std::int32_t 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 { std::int32_t c = std::stoi(col); if (c > 0) return c; } catch (...) {} } return 80; } // Caller holds StateMutex. void RenderStatus(std::string_view label) { if (ActiveVerbosity != Crafter::Progress::Verbosity::Default || !IsTty) return; std::int32_t done = Done.load(std::memory_order_relaxed); std::int32_t total = Total.load(std::memory_order_relaxed); std::string prefix = std::format("[{}/{}] ", done, total); std::int32_t width = TerminalWidth(); std::int32_t 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); LineDirty = true; } // Caller holds StateMutex. void ClearLineLocked() { if (LineDirty) { std::fputs("\r\033[2K", stdout); std::fflush(stdout); LineDirty = false; } } } namespace Crafter::Progress { void SetVerbosity(Verbosity v) { std::lock_guard lock(StateMutex); ActiveVerbosity = v; IsTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0; StartTime = std::chrono::steady_clock::now(); Total.store(0); Done.store(0); Finalized = false; } Verbosity GetVerbosity() { std::lock_guard lock(StateMutex); return ActiveVerbosity; } Task::Task(std::string label) : label_(std::move(label)) { Total.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(StateMutex); if (ActiveVerbosity == Verbosity::Default && IsTty) { RenderStatus(label_); } } Task::~Task() { std::int32_t done = Done.fetch_add(1, std::memory_order_relaxed) + 1; std::lock_guard lock(StateMutex); if (ActiveVerbosity == Verbosity::Default) { if (IsTty) { RenderStatus(label_); } else { // Non-TTY: one append-only line per completed task (ninja-style). std::int32_t total = Total.load(std::memory_order_relaxed); std::println("[{}/{}] {}", done, total, label_); } } } void EchoCommand(std::string_view command) { std::lock_guard lock(StateMutex); if (ActiveVerbosity == Verbosity::Verbose) { std::println("$ {}", command); } } void Clear() { std::lock_guard lock(StateMutex); ClearLineLocked(); } void Finalize() { std::lock_guard lock(StateMutex); if (Finalized) return; Finalized = true; ClearLineLocked(); if (ActiveVerbosity == Verbosity::Quiet) return; std::int32_t done = Done.load(); if (done == 0) return; // Nothing happened (cached build); stay silent. auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - StartTime); std::println("Built {} step{} in {}ms", done, done == 1 ? "" : "s", elapsed.count()); } } // namespace Crafter::Progress