statusbarlog 1.0
This api is used for creating statusbars and for better logging alongside these statusbars.
Loading...
Searching...
No Matches
statusbarlog.cpp
Go to the documentation of this file.
1// -- statusbarlog/src/statusbarlog.cpp
2
3// clang-format off
4
5#include "statusbarlog/statusbarlog.h"
6
7#ifdef _WIN32
8#include <windows.h>
9#else
10#include <sys/ioctl.h>
11#include <unistd.h>
12#endif
13
14#include <algorithm>
15#include <array>
16#include <cassert>
17#include <cmath>
18#include <cstdarg>
19#include <cstddef>
20#include <cstdint>
21#include <cstdio>
22#include <iostream>
23#include <mutex>
24#include <ostream>
25#include <sstream>
26#include <iomanip>
27#include <string>
28#include <vector>
29
30
31// clang-format on
32
33const std::string kFilename = "statusbarlog.cpp";
34
35namespace statusbar_log {
36
37// Hidden implementation detail
38namespace {
39
54// clang-format off
55typedef struct {
56 std::vector<double> percentages;
57 std::vector<unsigned int> positions;
58 std::vector<unsigned int> bar_sizes;
59 std::vector<std::string> prefixes;
60 std::vector<std::string> postfixes;
61 std::vector<std::size_t> spin_idxs;
62 unsigned int id;
64} Statusbar;
65// clang-format on
66
67std::vector<Statusbar> _statusbar_registry = {};
68std::vector<StatusbarHandle> _statusbar_free_handles = {};
69unsigned int _handle_id_count = 0;
70
71static std::mutex _registry_mutex;
72static std::mutex _id_count_mutex;
73static std::mutex _console_mutex;
74
79void _ConditionalFlush() {
80 if (!kStatusbarLogNoAutoFlush){
81 std::cout << std::flush;
82 }
83}
84
93void _MoveCursorUp(int move) {
94 if (move > 0) {
95 std::cout << "\033[" << move << "A";
96 } else if (move < 0) {
97 std::cout << std::string(-move, '\n');
98 }
99 _ConditionalFlush();
100}
101
111int _GetTerminalWidth(int& width) {
112 width = 80; // Default value
113
114#ifdef _WIN32
115 HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
116 if (hConsole != INVALID_HANDLE_VALUE) {
117 CONSOLE_SCREEN_BUFFER_INFO csbi;
118 if (GetConsoleScreenBufferInfo(hConsole, &csbi)) {
119 width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
120 } else {
121 return -1;
122 }
123 }
124#else
125 winsize w;
126 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) {
127 width = w.ws_col;
128 } else {
129 return -2;
130 }
131#endif
132 return kStatusbarLogSuccess;
133}
134
142std::string _SanitizeStringWithNewline(const std::string& input) {
143 std::string output;
144 output.reserve(input.size());
145 for (char c : input) {
146 // Allow for '\n' and '\t' charachters
147 if (c == '\n' || c == '\t' || (c >= 32 && c <= 126)) {
148 output += c;
149 } else if (static_cast<unsigned char>(c) < 32 || c == 127) {
150 output += "�";
151 } else {
152 output += c;
153 }
154 }
155 return output;
156}
157
165std::string _SanitizeString(const std::string& input) {
166 std::string output;
167 output.reserve(input.size());
168 for (char c : input) {
169 // Allow for '\n' and '\t' charachters
170 if (c == '\t' || (c >= 32 && c <= 126)) {
171 output += c;
172 } else if (static_cast<unsigned char>(c) < 32 || c == 127) {
173 output += "�";
174 } else {
175 output += c;
176 }
177 }
178 return output;
179}
180
202int _IsValidHandle(const StatusbarHandle& statusbar_handle) {
203 const std::size_t idx = statusbar_handle.idx;
204
205 if (!statusbar_handle.valid) {
206 return -1;
207 }
208
209 if ((statusbar_handle.idx >= _statusbar_registry.size()) ||
210 (statusbar_handle.idx == SIZE_MAX)) {
211 return -2;
212 }
213 if (statusbar_handle.id != _statusbar_registry[idx].id) {
214 return -3;
215 }
216 if (statusbar_handle.id == 0) {
217 return -4;
218 }
219 return kStatusbarLogSuccess;
220}
221
246int _IsValidHandleVerbose(const StatusbarHandle& statusbar_handle) {
247 const int is_valid_handle = _IsValidHandle(statusbar_handle);
248 if (is_valid_handle == -1) {
249 LogWrn(kFilename,
250 "Invalid handle: Valid flag set to false (idx: %zu, ID: %u)",
251 statusbar_handle.idx, statusbar_handle.id);
252 return -1;
253 }
254
255 if (is_valid_handle == -2) {
256 LogWrn(kFilename,
257 "Invalid Handle: Handle index %zu out of bounds (max %zu)",
258 statusbar_handle.idx, _statusbar_registry.size());
259 return -2;
260 }
261
262 Statusbar& target = _statusbar_registry[statusbar_handle.idx];
263
264 if (is_valid_handle == -3) {
265 LogWrn(kFilename, "Invalid Handle: ID mismatch: handle %u vs registry %u",
266 statusbar_handle.id, target.id);
267 return -3;
268 }
269
270 if (is_valid_handle != kStatusbarLogSuccess) {
271 LogWrn(kFilename, "Invalid Handle: ID is 0 (i.e. invalid)");
272 return -4;
273 }
274
275 if (is_valid_handle != kStatusbarLogSuccess) {
276 LogWrn(kFilename, "Invalid Handle: Errorcode not handled!");
277 return -5;
278 }
279
280 return kStatusbarLogSuccess;
281}
282
320int _DrawStatusbarComponent(const double percent, const unsigned int bar_width,
321 const std::string& prefix,
322 const std::string& postfix,
323 std::size_t& spinner_idx, const int move) {
324 if (percent > 100.0 || percent < 0.0) {
325 LogErr(kFilename, "Failed to update statusbar: Invalid percentage.");
326 return -5;
327 }
328
329 int err = kStatusbarLogSuccess;
330 static const std::array<char, 4> spinner = {'|', '/', '-', '\\'};
331 spinner_idx %= spinner.size();
332 char spin_char = spinner[spinner_idx];
333
334 const unsigned int fill =
335 std::floor((percent * static_cast<double>(bar_width)) / 100.0);
336 const unsigned int empty = bar_width - fill;
337
338 std::ostringstream oss;
339 oss << prefix;
340 oss << "[";
341 oss << std::string(fill, '#');
342 if (empty > 0) {
343 oss << spin_char;
344 oss << std::string(empty - 1, ' ');
345 } else {
346 oss << std::string(empty, ' ');
347 }
348 oss << "] ";
349 oss << std::fixed << std::setprecision(2) << std::setw(6) << percent;
350 oss << postfix;
351 std::string status_str = oss.str();
352
353 int term_width;
354 err = _GetTerminalWidth(term_width);
355
356 if (status_str.length() > static_cast<size_t>(term_width)) {
357 status_str = status_str.substr(0, term_width - 1);
358 switch (err) {
359 case kStatusbarLogSuccess:
360 err = -3;
361 break;
362 case -1:
363 err = -4;
364 break;
365 case -2:
366 err = -5;
367 break;
368 }
369 }
370
371 _MoveCursorUp(move);
373 std::cout << status_str;
374 _ConditionalFlush();
375 _MoveCursorUp(-move);
376
377 return err;
378}
379
380} // namespace
381
382void FlushOutput() { std::cout << std::flush; }
383
385 std::cout << "\033[s"; // ANSI escape code to save cursor position
386 _ConditionalFlush();
387}
388
390 std::cout << "\033[u"; // ANSI escape code to restore cursor position
391 _ConditionalFlush();
392}
393
395 std::cout << "\033[0K"; // ANSI escape code to clear to end of line
396 _ConditionalFlush();
397}
398
400 std::cout << "\033[1K"; // ANSI escape code to clear to end of line
401 _ConditionalFlush();
402}
403
404void ClearLine() {
405 std::cout << "\033[2K";
406 _ConditionalFlush();
407}
408
410 std::cout << "\r" // Return to line start
411 << "\033[2K"; // Clear entire line
412 _ConditionalFlush();
413}
414
415int Log(const LogLevel log_level, const std::string& filename, const char* fmt,
416 ...) {
417 if (log_level > kLogLevel) return kStatusbarLogSuccess;
418 std::unique_lock<std::mutex> console_lock(_console_mutex, std::defer_lock);
419 std::unique_lock<std::mutex> registry_lock(_registry_mutex, std::defer_lock);
420 std::lock(console_lock, registry_lock);
421 // std::lock_guard<std::mutex> ID_count_lock(_id_count_mutex);
422
423 const bool statusbars_active = !_statusbar_registry.empty();
424
425 const char* prefix = "";
426 // clang-format off
427 switch(log_level){
428 case kLogLevelErr: prefix = "ERROR"; break;
429 case kLogLevelWrn: prefix = "WARNING"; break;
430 case kLogLevelInf: prefix = "INFO"; break;
431 case kLogLevelDbg: prefix = "DEBUG"; break;
432 default: break;
433 }
434 // clang-format on
435
436 int move = 0;
437 if (statusbars_active) {
438 for (std::size_t i = 0; i < _statusbar_registry.size(); ++i) {
439 for (std::size_t j = 0; j < _statusbar_registry[i].positions.size();
440 ++j) {
441 int current_pos = _statusbar_registry[i].positions[j];
442 if (current_pos > move) {
443 move = current_pos;
444 }
445 }
446 }
447 }
448
449 va_list args;
450 va_list args_copy;
451 _MoveCursorUp(move);
452 if (statusbars_active) printf("\r\033[2K\r");
453 va_start(args, fmt);
454 va_copy(args_copy, args);
455 unsigned int size = std::vsnprintf(nullptr, 0, fmt, args_copy);
456 size = std::min(size, kMaxLogLength);
457 std::vector<char> buffer(size + 1);
458 std::vsnprintf(buffer.data(), buffer.size(), fmt, args);
459 std::string message = _SanitizeStringWithNewline(buffer.data());
460 va_end(args);
461
462 std::string sanitized_filename = _SanitizeStringWithNewline(filename);
463 if (sanitized_filename.length() > kMaxFilenameLength) {
464 sanitized_filename.resize(kMaxFilenameLength - 3);
465 sanitized_filename += "...";
466 }
467
468 printf("%s [%s]: %s\n", prefix, sanitized_filename.c_str(), message.c_str());
469 _ConditionalFlush();
470 _MoveCursorUp(-move);
471
472 if (statusbars_active) {
473 for (std::size_t i = 0; i < _statusbar_registry.size(); ++i) {
474 for (std::size_t j = 0; j < _statusbar_registry[i].positions.size();
475 ++j) {
476 _DrawStatusbarComponent(_statusbar_registry[i].percentages[j],
477 _statusbar_registry[i].bar_sizes[j],
478 _statusbar_registry[i].prefixes[j],
479 _statusbar_registry[i].postfixes[j],
480 _statusbar_registry[i].spin_idxs[j],
481 _statusbar_registry[i].positions[j]);
482 }
483 }
484 }
485 return kStatusbarLogSuccess;
486}
487
488int CreateStatusbarHandle(StatusbarHandle& statusbar_handle,
489 const std::vector<unsigned int> _positions,
490 const std::vector<unsigned int> _bar_sizes,
491 const std::vector<std::string> _prefixes,
492 const std::vector<std::string> _postfixes) {
493 statusbar_handle.valid = false;
494 statusbar_handle.id = 0;
495 if (_positions.size() != _bar_sizes.size() ||
496 _bar_sizes.size() != _prefixes.size() ||
497 _prefixes.size() != _postfixes.size()) {
498 LogErr(kFilename,
499 "Failed to create statusbar_handle The vecotors '_positions', "
500 "'_bar_sizes', '_prefixes' and "
501 "'_postfixes' must have the same size! Got: '_positions': %d, "
502 "'_bar_sizes': %d, '_prefixes': %d, '_postfixes': %d.",
503 _positions.size(), _bar_sizes.size(), _prefixes.size(),
504 _postfixes.size());
505 return -1;
506 }
507
508 if (_statusbar_registry.size() - _statusbar_free_handles.size() >=
509 kMaxHandles) {
510 LogErr(
511 kFilename,
512 "Failed to create statusbar handle. Maximum status bars (%zu) reached",
513 statusbar_log::kMaxHandles);
514 return -2;
515 }
516
517 std::unique_lock<std::mutex> console_lock(_console_mutex, std::defer_lock);
518 std::unique_lock<std::mutex> registry_lock(_registry_mutex, std::defer_lock);
519 std::unique_lock<std::mutex> ID_count_lock(_id_count_mutex, std::defer_lock);
520 std::lock(console_lock, registry_lock, ID_count_lock);
521
522 _handle_id_count++;
523 if (_handle_id_count == 0) {
524 console_lock.unlock();
525 registry_lock.unlock();
526 LogWrn(kFilename,
527 "Max number of possible statusbar handle.ids reached, looping back "
528 "to 1");
529 std::lock(console_lock, registry_lock);
530 _handle_id_count++;
531 }
532
533 const std::size_t num_bars = _positions.size();
534 const std::vector<double> percentages(num_bars, 0.0);
535 const std::vector<std::size_t> spin_idxs(num_bars, 0);
536
537 std::vector<std::string> sanitized_prefixes;
538 sanitized_prefixes.reserve(_prefixes.size());
539 std::vector<std::string> sanitized_postfixes;
540 sanitized_prefixes.reserve(_postfixes.size());
541 std::vector<unsigned int> sanitized_bar_sizes;
542 sanitized_bar_sizes.reserve(_bar_sizes.size());
543 for (std::size_t i = 0; i < _prefixes.size(); ++i) {
544 std::string _prefix = _prefixes[i];
545 if (_prefix.length() > kMaxPrefixLength) {
546 _prefix.resize(kMaxPrefixLength - 3);
547 _prefix += "...";
548 }
549 sanitized_prefixes.push_back(_SanitizeString(_prefix));
550
551 std::string _postfix = _postfixes[i];
552 if (_postfix.length() > kMaxPostfixLength) {
553 _postfix.resize(kMaxPostfixLength - 3);
554 _postfix += "...";
555 }
556 sanitized_postfixes.push_back(_SanitizeString(_postfix));
557
558 sanitized_bar_sizes.push_back(
559 std::min<unsigned int>(_bar_sizes[i], kMaxBarWidth));
560 }
561
562 if (!_statusbar_free_handles.empty()) {
563 StatusbarHandle free_handle = _statusbar_free_handles.back();
564 _statusbar_free_handles.pop_back();
565 statusbar_handle.idx = free_handle.idx;
566 _statusbar_registry[statusbar_handle.idx] = {
567 percentages, _positions,
568 sanitized_bar_sizes, sanitized_prefixes,
569 sanitized_postfixes, spin_idxs,
570 _handle_id_count, false};
571 } else {
572 statusbar_handle.idx = _statusbar_registry.size();
573 _statusbar_registry.emplace_back(Statusbar{
574 percentages, _positions, sanitized_bar_sizes, sanitized_prefixes,
575 sanitized_postfixes, spin_idxs, _handle_id_count, false});
576 }
577 statusbar_handle.id = _handle_id_count;
578 statusbar_handle.valid = true;
579 for (std::size_t idx = 0; idx < num_bars; idx++) {
580 _DrawStatusbarComponent(
581 0.0, _statusbar_registry[statusbar_handle.idx].bar_sizes[idx],
582 _statusbar_registry[statusbar_handle.idx].prefixes[idx],
583 _statusbar_registry[statusbar_handle.idx].postfixes[idx],
584 _statusbar_registry[statusbar_handle.idx].spin_idxs[idx],
585 _statusbar_registry[statusbar_handle.idx].positions[idx]);
586 }
587 return kStatusbarLogSuccess;
588}
589
590int DestroyStatusbarHandle(StatusbarHandle& statusbar_handle) {
591 std::unique_lock<std::mutex> console_lock(_console_mutex, std::defer_lock);
592 std::unique_lock<std::mutex> registry_lock(_registry_mutex, std::defer_lock);
593 std::lock(console_lock, registry_lock);
594
595 const int err = _IsValidHandle(statusbar_handle);
596 if (err != kStatusbarLogSuccess) {
597 console_lock.unlock();
598 registry_lock.unlock();
599 _IsValidHandleVerbose(statusbar_handle);
600 LogErr(kFilename, "Failed to destory statusbar_handle!");
601 return err;
602 }
603 Statusbar& target = _statusbar_registry[statusbar_handle.idx];
604
605 for (std::size_t i = 0; i < target.positions.size(); i++) {
606 _MoveCursorUp(target.positions[i]);
608 _MoveCursorUp(-target.positions[i]);
609 }
610 std::cout.flush();
611
612 target.percentages.clear();
613 target.positions.clear();
614 target.bar_sizes.clear();
615
616 for (std::string& str : target.prefixes) {
617 std::fill(str.begin(), str.end(), '\0');
618 }
619 for (std::string& str : target.postfixes) {
620 std::fill(str.begin(), str.end(), '\0');
621 }
622 target.prefixes.clear();
623 target.postfixes.clear();
624
625 target.id = 0;
626 target.spin_idxs.clear();
627
628 _statusbar_free_handles.push_back(statusbar_handle);
629
630 statusbar_handle.valid = false;
631 statusbar_handle.id = 0;
632
633 return kStatusbarLogSuccess;
634}
635
636int UpdateStatusbar(StatusbarHandle& statusbar_handle, const std::size_t idx,
637 const double percent) {
638 std::unique_lock<std::mutex> console_lock(_console_mutex, std::defer_lock);
639 std::unique_lock<std::mutex> registry_lock(_registry_mutex, std::defer_lock);
640 std::lock(console_lock, registry_lock);
641
642 // std::lock_guard<std::mutex> ID_count_lock(_id_count_mutex);
643
644 const int err = _IsValidHandle(statusbar_handle);
645 if (err != kStatusbarLogSuccess) {
646 console_lock.unlock();
647 registry_lock.unlock();
648 _IsValidHandleVerbose(statusbar_handle);
649 LogErr(kFilename, "Failed to update statusbar: Invalid handle.");
650 return err;
651 }
652
653 if (percent > 100.0 || percent < 0.0) {
654 console_lock.unlock();
655 registry_lock.unlock();
656 LogErr(kFilename, "Failed to update statusbar: Invalid percentage.");
657 return -5;
658 }
659
660 Statusbar& statusbar = _statusbar_registry[statusbar_handle.idx];
661
662 if (idx >= statusbar.percentages.size()) {
663 console_lock.unlock();
664 registry_lock.unlock();
665 LogErr(kFilename, "Failed to update statusbar: Invalid bar index.");
666 return -6;
667 }
668
669 statusbar.percentages[idx] = percent;
670 statusbar.spin_idxs[idx] = statusbar.spin_idxs[idx] + 1;
671 int bar_error_code = _DrawStatusbarComponent(
672 percent, statusbar.bar_sizes[idx], statusbar.prefixes[idx],
673 statusbar.postfixes[idx], statusbar.spin_idxs[idx],
674 statusbar.positions[idx]);
675
676 if (bar_error_code != kStatusbarLogSuccess && !statusbar.error_reported) {
677 statusbar.error_reported = true;
678 const char* why;
679 switch (bar_error_code) {
680 case -1:
681 why = "Terminal width detection failed (Windows)";
682 break;
683 case -2:
684 why = "Terminal width detection failed (Linux)";
685 break;
686 case -3:
687 why = "Truncating was needed";
688 break;
689 case -4:
690 why =
691 "Terminal width detection failed (Windows) and truncation was "
692 "needed";
693 break;
694 case -5:
695 why =
696 "Terminal width detection failed (Linux) and truncation was "
697 "needed";
698 break;
699 }
700 LogErr(kFilename, "%s on statusbar with ID %u at bar idx %zu!", why,
701 statusbar.id, idx);
702 }
703
704 return kStatusbarLogSuccess;
705}
706
707}; // namespace statusbar_log
int UpdateStatusbar(StatusbarHandle &statusbar_handle, const std::size_t idx, const double percent)
void ClearToEndOfLine()
void ClearCurrentLine()
void ClearFromStartOfLine()
void SaveCursorPosition()
int CreateStatusbarHandle(StatusbarHandle &statusbar_handle, const std::vector< unsigned int > _positions, const std::vector< unsigned int > _bar_sizes, const std::vector< std::string > _prefixes, const std::vector< std::string > _postfixes)
int Log(const LogLevel log_level, const std::string &filename, const char *fmt,...)
void RestoreCursorPosition()
int DestroyStatusbarHandle(StatusbarHandle &statusbar_handle)
std::vector< unsigned int > positions
Vertical positions (1=topmost).
std::vector< unsigned int > bar_sizes
Total width (characters) of each bar.
const std::string kFilename
std::vector< std::size_t > spin_idxs
Spinner animation indices.
std::vector< double > percentages
Progress percentages (0-100) for each bar.
std::vector< std::string > prefixes
Text displayed before each bar.
unsigned int id
unique id corresponding to the handle
bool error_reported
Indicator whether error already has been reported.
std::vector< std::string > postfixes
Text displayed after each bar.