12 #include <fmt/format.h>
21 #include <string_view>
28 static std::recursive_mutex s_stdout_mutex;
29 return s_stdout_mutex;
83 auto
add(std::
size_t count) noexcept ->
void;
89 return m_processed.load(std::memory_order_acquire);
95 [[nodiscard]]
auto get_total() const noexcept -> std::
size_t {
102 [[nodiscard]]
auto get_progress() const noexcept ->
double;
107 auto
start() noexcept ->
void;
112 auto
finish() noexcept ->
void;
130 [[nodiscard]] static auto
format_duration(std::chrono::seconds duration) noexcept -> std::
string;
135 static constexpr std::string_view CLEAR_LINE =
"\r\033[K";
136 static constexpr std::string_view RESET =
"\033[0m";
137 static constexpr std::string_view BOLD =
"\033[1m";
138 static constexpr std::string_view DIM =
"\033[2m";
141 static constexpr std::string_view GREEN =
"\033[32m";
142 static constexpr std::string_view CYAN =
"\033[36m";
143 static constexpr std::string_view YELLOW =
"\033[33m";
144 static constexpr std::string_view MAGENTA =
"\033[35m";
145 static constexpr std::string_view WHITE =
"\033[37m";
146 static constexpr std::string_view BLUE =
"\033[34m";
147 static constexpr std::string_view RED =
"\033[31m";
150 static constexpr std::string_view BRIGHT_GREEN =
"\033[92m";
151 static constexpr std::string_view BRIGHT_CYAN =
"\033[96m";
152 static constexpr std::string_view BRIGHT_YELLOW =
"\033[93m";
153 static constexpr std::string_view BRIGHT_MAGENTA =
"\033[95m";
154 static constexpr std::string_view BRIGHT_WHITE =
"\033[97m";
157 static constexpr std::string_view BG_CYAN =
"\033[46m";
158 static constexpr std::string_view BG_BLUE =
"\033[44m";
159 static constexpr std::string_view BG_MAGENTA =
"\033[45m";
163 static constexpr std::array<std::string_view, 9> BLOCK_CHARS = {
164 " ",
"▏",
"▎",
"▍",
"▌",
"▋",
"▊",
"▉",
"█"};
167 static constexpr std::array<std::string_view, 10> SPINNER_CHARS = {
168 "⠋",
"⠙",
"⠹",
"⠸",
"⠼",
"⠴",
"⠦",
"⠧",
"⠇",
"⠏"};
171 auto update_display() noexcept ->
void;
174 auto render_bar(
double progress) const noexcept ->
void;
176 auto update_loop() noexcept ->
void;
178 auto stop_update_thread() noexcept ->
void;
183 [[nodiscard]] static auto format_number(std::
size_t num) noexcept -> std::
string;
186 [[nodiscard]] auto get_elapsed() const noexcept -> std::chrono::seconds;
189 [[nodiscard]] auto calculate_eta() const noexcept -> std::chrono::seconds;
192 [[nodiscard]] auto calculate_rate() const noexcept ->
double;
195 [[nodiscard]] auto get_progress_color() const noexcept -> std::string_view;
197 const std::
size_t m_total;
198 std::atomic<std::
size_t> m_processed{0};
199 std::chrono::steady_clock::time_point m_start_time;
201 mutable std::mutex m_display_mutex;
202 bool m_finished{
false};
203 mutable std::size_t m_spinner_index{0};
206 std::atomic<bool> m_should_stop{
false};
207 std::thread m_update_thread;
211 : m_total{total}, m_start_time{std::chrono::steady_clock::now()}, m_config{config} {
212 if (total == 0)
throw std::invalid_argument(
"ProgressTracker: total must be greater than 0");
216 stop_update_thread();
221 : m_total{other.m_total}, m_processed{other.m_processed.load(std::memory_order_acquire)}, m_start_time{other.m_start_time}, m_config{other.m_config}, m_finished{other.m_finished} {
222 other.stop_update_thread();
223 other.m_finished =
true;
227 if (
this != &other) {
228 stop_update_thread();
229 if (!m_finished) clear_line();
230 other.stop_update_thread();
232 const_cast<std::size_t&
>(m_total) = other.m_total;
233 m_processed.store(other.m_processed.load(std::memory_order_acquire), std::memory_order_release);
234 m_start_time = other.m_start_time;
235 m_config = other.m_config;
236 m_finished = other.m_finished;
238 other.m_finished =
true;
244 m_processed.fetch_add(1, std::memory_order_acq_rel);
248 m_processed.fetch_add(count, std::memory_order_acq_rel);
252 const auto current = m_processed.load(std::memory_order_acquire);
253 return m_total > 0 ?
static_cast<double>(current) /
static_cast<double>(m_total) : 0.0;
256 inline auto ProgressTracker::get_elapsed() const noexcept -> std::chrono::seconds {
257 const auto elapsed = std::chrono::steady_clock::now() - m_start_time;
258 return std::chrono::duration_cast<std::chrono::seconds>(elapsed);
261 inline auto ProgressTracker::calculate_rate() const noexcept ->
double {
262 const auto elapsed = get_elapsed().count();
263 const auto current = m_processed.load(std::memory_order_acquire);
264 return elapsed > 0 ?
static_cast<double>(current) /
static_cast<double>(elapsed) : 0.0;
267 inline auto ProgressTracker::calculate_eta() const noexcept -> std::chrono::seconds {
268 const auto current = m_processed.load(std::memory_order_acquire);
269 const auto rate = calculate_rate();
271 if (rate > 0.0 && current < m_total) {
272 const auto remaining = m_total - current;
273 return std::chrono::seconds{
static_cast<long>(remaining / rate)};
275 return std::chrono::seconds{0};
279 const auto m_totalseconds = duration.count();
281 if (m_totalseconds < 60)
return fmt::format(
"{}s", m_totalseconds);
282 if (m_totalseconds < 3600) {
283 const auto minutes = m_totalseconds / 60;
284 const auto seconds = m_totalseconds % 60;
285 return fmt::format(
"{}m {:02d}s", minutes, seconds);
288 const auto hours = m_totalseconds / 3600;
289 const auto minutes = (m_totalseconds % 3600) / 60;
290 return fmt::format(
"{}h {:02d}m", hours, minutes);
293 inline auto ProgressTracker::format_number(std::size_t num) noexcept -> std::string {
294 if (num < 1000)
return fmt::format(
"{}", num);
295 if (num < 1000000)
return fmt::format(
"{:.1f}k", num / 1000.0);
296 return fmt::format(
"{:.1f}M", num / 1000000.0);
302 fmt::print(
"{}", AnsiCodes::CLEAR_LINE);
308 inline auto ProgressTracker::get_progress_color() const noexcept -> std::string_view {
310 return AnsiCodes::BRIGHT_GREEN;
313 inline auto ProgressTracker::render_bar(
double progress)
const noexcept ->
void {
316 const auto& colors = m_config.use_colors;
317 progress = std::clamp(progress, 0.0, 1.0);
319 const auto filled_width = m_config.bar_width * progress;
320 const auto full_blocks =
static_cast<std::size_t
>(filled_width);
321 const auto remainder = filled_width -
static_cast<double>(full_blocks);
322 const auto partial_block_idx =
static_cast<std::size_t
>(remainder * 8.0);
325 if (colors) fmt::print(
"{}", AnsiCodes::DIM);
327 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
330 for (std::size_t i = 0; i < m_config.bar_width; ++i) {
331 if (i < full_blocks) {
333 fmt::print(
"{}", get_progress_color());
335 fmt::print(
"{}", BLOCK_CHARS[8]);
336 }
else if (i == full_blocks) {
338 fmt::print(
"{}", get_progress_color());
340 fmt::print(
"{}", BLOCK_CHARS[partial_block_idx]);
342 if (colors) fmt::print(
"{}", AnsiCodes::DIM);
347 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
350 if (colors) fmt::print(
"{}", AnsiCodes::DIM);
352 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
359 inline auto ProgressTracker::update_display() noexcept ->
void {
360 std::lock_guard lock{m_display_mutex};
362 if (m_finished)
return;
365 const auto current = m_processed.load(std::memory_order_acquire);
367 const auto elapsed = get_elapsed();
376 if (progress < 1.0) {
377 if (colors) fmt::print(
"{}", AnsiCodes::BRIGHT_CYAN);
378 fmt::print(
"{}", SPINNER_CHARS[m_spinner_index % SPINNER_CHARS.size()]);
380 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
383 if (colors) fmt::print(
"{}", AnsiCodes::BRIGHT_GREEN);
385 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
390 if (!m_config.
label.empty()) {
391 if (colors) fmt::print(
"{}", AnsiCodes::BOLD);
392 fmt::print(
"{}", m_config.
label);
393 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
397 render_bar(progress);
400 if (progress >= 1.0) {
401 if (colors) fmt::print(
"{}{}", AnsiCodes::BRIGHT_GREEN, AnsiCodes::BOLD);
403 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
405 if (colors) fmt::print(
"{}", get_progress_color());
406 fmt::print(
"{:>3.0f}%", progress * 100.0);
407 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
412 if (colors) fmt::print(
"{}", AnsiCodes::DIM);
414 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
417 if (colors) fmt::print(
"{}", AnsiCodes::BRIGHT_WHITE);
418 fmt::print(
"{}", format_number(current));
419 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
421 if (colors) fmt::print(
"{}", AnsiCodes::DIM);
423 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
425 if (colors) fmt::print(
"{}", AnsiCodes::BRIGHT_CYAN);
426 fmt::print(
"{}", format_number(m_total));
427 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
431 if (colors) fmt::print(
"{}", AnsiCodes::DIM);
433 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
435 if (colors) fmt::print(
"{}", AnsiCodes::MAGENTA);
437 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
440 if (m_config.
show_eta && current < m_total) {
441 const auto eta = calculate_eta();
442 if (eta.count() > 0) {
444 if (colors) fmt::print(
"{}", AnsiCodes::DIM);
446 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
448 if (colors) fmt::print(
"{}", AnsiCodes::YELLOW);
450 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
456 const auto rate = calculate_rate();
459 if (colors) fmt::print(
"{}", AnsiCodes::DIM);
461 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
463 if (colors) fmt::print(
"{}", AnsiCodes::BRIGHT_MAGENTA);
464 fmt::print(
"{:.1f}/s", rate);
465 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
475 inline auto ProgressTracker::update_loop() noexcept ->
void {
476 while (!m_should_stop.load(std::memory_order_acquire)) {
478 if (m_processed.load(std::memory_order_acquire) >= m_total)
break;
486 if (!m_update_thread.joinable() && !m_finished) {
487 m_should_stop.store(
false, std::memory_order_release);
489 m_update_thread = std::thread(&ProgressTracker::update_loop,
this);
493 inline auto ProgressTracker::stop_update_thread() noexcept ->
void {
494 if (m_update_thread.joinable()) {
495 m_should_stop.store(
true, std::memory_order_release);
496 m_update_thread.join();
501 stop_update_thread();
503 std::lock_guard lock{m_display_mutex};
505 if (m_finished)
return;
509 const auto elapsed = get_elapsed();
518 if (colors) fmt::print(
"{}", AnsiCodes::BRIGHT_GREEN);
519 fmt::print(
" {} Complete! │ ", m_config.
label);
520 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
523 fmt::print(
" Items: ");
524 if (colors) fmt::print(
"{}", AnsiCodes::BRIGHT_CYAN);
525 fmt::print(
"{}", format_number(m_total));
526 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
530 fmt::print(
"⏱ Time: ");
531 if (colors) fmt::print(
"{}", AnsiCodes::MAGENTA);
533 if (colors) fmt::print(
"{}", AnsiCodes::RESET);
Thread-safe progress tracker with visual progress bar.
auto get_total() const noexcept -> std::size_t
Gets total item count (thread-safe).
static auto clear_line() noexcept -> void
Clears the current progress line.
auto increment() noexcept -> void
Increments the progress counter (thread-safe, no immediate display update).
auto get_processed() const noexcept -> std::size_t
Gets current progress count (thread-safe).
ProgressTracker & operator=(const ProgressTracker &)=delete
~ProgressTracker() noexcept
auto add(std::size_t count) noexcept -> void
Adds multiple items to progress (thread-safe, no immediate display update).
static auto format_duration(std::chrono::seconds duration) noexcept -> std::string
Formats a duration given in seconds into a human-readable string.
auto start() noexcept -> void
Starts the background update thread and shows initial display.
auto get_progress() const noexcept -> double
Gets current progress ratio [0.0, 1.0] (thread-safe).
auto finish() noexcept -> void
Stops the update thread and displays final completion message.
ProgressTracker(std::size_t total, Config config={})
Constructs a progress tracker.
std::recursive_mutex & get_stdout_mutex()
Returns the global recursive mutex used to synchronize stdout access. A recursive mutex is used to al...
Configuration settings for progress bar display.
bool use_gradient
Use gradient coloring for progress bar (default: true)
Config()
Default constructor initializes all fields to reasonable defaults.
std::size_t bar_width
Width of the progress bar in characters (default: 50)
std::string label
Label text displayed before the progress bar (default: "Processing")
bool show_rate
Display processing rate in items per second (default: true)
std::chrono::milliseconds update_interval
Time between display updates (default: 100ms)
bool show_eta
Display estimated time to completion (default: true)
bool use_colors
Use ANSI color codes in output (default: true)