diff --git a/README.md b/README.md index 717ee33..c933315 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ int main() { bar.fill_bar_remainder_with(" "); bar.end_bar_with("]"); bar.set_postfix_text("Getting started"); - bar.set_foreground_color(indicators::Color::GREEN); + bar.set_foreground_color(indicators::Color::GREEN); // Update bar state @@ -49,8 +49,8 @@ int main() { Here's the general structure of a progress bar: ``` - ? - ^^^^^^^^^^^^^^^^^^ Bar Width ^^^^^^^^^^^^^^^^^^ ^^^^^ Show/Hide ^^^^^ +{prefix} {start} {fill} {lead} {remaining} {end} {percentage} [{elapsed}<{remaining}?] {postfix} + ^^^^^^^^^^^^^ Bar Width ^^^^^^^^^^^^^^^ ``` The amount of progress in ProgressBar is maintained as a float in range `[0, 100]`. When progress reaches 100, the progression is complete. @@ -206,6 +206,57 @@ int main() { } ``` +## Showing Time Elapsed/Remaining + +All progress bars and spinners in `indicators` support showing time elapsed and time remaining. Inspired by python's [tqdm](https://github.com/tqdm/tqdm) module, the format of this meter is as follows: + +```bash +[{elapsed}<{remaining}] +``` + +Below is an example for configuring this meter: + +

+ +

+ +```cpp +#include +#include +#include + +int main() { + indicators::ProgressBar bar; + + // Configure the bar + bar.set_bar_width(50); + bar.start_bar_with(" ["); + bar.fill_bar_progress_with("█"); + bar.lead_bar_progress_with("█"); + bar.fill_bar_remainder_with("-"); + bar.end_bar_with("]"); + bar.set_prefix_text("Training Gaze Network 👀"); + bar.set_foreground_color(indicators::Color::YELLOW); + + // Show time elapsed and remaining + bar.show_elapsed_time(); + bar.show_remaining_time(); + + // Update bar state + while (true) { + bar.tick(); + if (bar.is_completed()) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } + + // Show cursor + std::cout << "\e[?25h"; + + return 0; +} +``` + # Block Progress Bar Are you in need of a smooth block progress bar using [unicode block elements](https://en.wikipedia.org/wiki/Block_Elements)? Use `BlockProgressBar` instead of `ProgressBar`. Thanks to [this blog post](https://mike42.me/blog/2018-06-make-better-cli-progress-bars-with-unicode-block-characters) for making `BlockProgressBar` an easy addition to the library. @@ -274,7 +325,7 @@ int main() { Here's the general structure of a progress spinner: ``` - ? +{prefix} {spinner} {percentage} [{elapsed}<{remaining}] {postfix} ``` ProgressSpinner has a vector of strings: `spinner_states`. At each update, the spinner will pick the next string from this sequence to print to the console. The spinner state can be updated similarly to ProgressBars: Using either `tick()` or `set_progress(value)`. @@ -299,7 +350,7 @@ int main() { spinner.hide_spinner(); spinner.hide_percentage(); spinner.set_postfix_text("Authenticated!"); - spinner.mark_as_completed(); + spinner.mark_as_completed(); break; } else spinner.tick(); diff --git a/clang-format.bash b/clang-format.bash index 897445b..49b6ac6 100755 --- a/clang-format.bash +++ b/clang-format.bash @@ -1,2 +1,2 @@ #!/usr/bin/env bash -find ./include ./demo/ -type f \( -iname \*.cpp -o -iname \*.hpp \) | xargs clang-format -style="{ColumnLimit : 100}" -i +find ./include ./demo/ ./samples/ -type f \( -iname \*.cpp -o -iname \*.hpp \) | xargs clang-format -style="{ColumnLimit : 100}" -i diff --git a/img/time_meter.gif b/img/time_meter.gif new file mode 100644 index 0000000..c32b79e Binary files /dev/null and b/img/time_meter.gif differ diff --git a/include/indicators/block_progress_bar.hpp b/include/indicators/block_progress_bar.hpp index 62a8e51..44f01b1 100644 --- a/include/indicators/block_progress_bar.hpp +++ b/include/indicators/block_progress_bar.hpp @@ -27,13 +27,15 @@ SOFTWARE. #pragma once #include #include +#include +#include #include +#include #include #include #include #include #include -#include namespace indicators { @@ -75,11 +77,20 @@ public: void hide_percentage() { _show_percentage = false; } + void show_elapsed_time() { _show_elapsed_time = true; } + + void hide_elapsed_time() { _show_elapsed_time = false; } + + void show_remaining_time() { _show_remaining_time = true; } + + void hide_remaining_time() { _show_remaining_time = false; } + void set_progress(float value) { { std::unique_lock lock{_mutex}; _progress = value; } + _save_start_time(); _print_progress(); } @@ -88,12 +99,11 @@ public: std::unique_lock lock{_mutex}; _progress += 1; } + _save_start_time(); _print_progress(); } - size_t current() { - return std::min(static_cast(_progress), size_t(100)); - } + size_t current() { return std::min(static_cast(_progress), size_t(100)); } bool is_completed() const { return _completed; } @@ -115,11 +125,47 @@ private: std::atomic _max_postfix_text_length{0}; std::atomic _completed{false}; std::atomic _show_percentage{true}; + std::atomic _show_elapsed_time{false}; + std::atomic _show_remaining_time{false}; + std::atomic _saved_start_time{false}; + std::chrono::time_point _start_time_point; std::mutex _mutex; Color _foreground_color; + std::ostream &_print_duration(std::ostream &os, std::chrono::nanoseconds ns) { + using namespace std; + using namespace std::chrono; + typedef duration> days; + char fill = os.fill(); + os.fill('0'); + auto d = duration_cast(ns); + ns -= d; + auto h = duration_cast(ns); + ns -= h; + auto m = duration_cast(ns); + ns -= m; + auto s = duration_cast(ns); + if (d.count() > 0) + os << setw(2) << d.count() << "d:"; + if (h.count() > 0) + os << setw(2) << h.count() << "h:"; + os << setw(2) << m.count() << "m:" << setw(2) << s.count() << 's'; + os.fill(fill); + return os; + }; + + void _save_start_time() { + if ((_show_elapsed_time || _show_remaining_time) && !_saved_start_time) { + _start_time_point = std::chrono::high_resolution_clock::now(); + _saved_start_time = true; + } + } + void _print_progress() { std::unique_lock lock{_mutex}; + auto now = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(now - _start_time_point); + std::cout << termcolor::bold; switch (_foreground_color) { case Color::GREY: @@ -163,11 +209,29 @@ private: std::cout << _lead; for (size_t i = 0; i < (_bar_width - whole_width - 1); ++i) std::cout << " "; - + std::cout << _end; if (_show_percentage) { std::cout << " " << std::min(static_cast(_progress), size_t(100)) << "%"; } + + if (_show_elapsed_time) { + std::cout << " ["; + _print_duration(std::cout, elapsed); + } + + if (_show_remaining_time) { + if (_show_elapsed_time) + std::cout << "<"; + else + std::cout << " ["; + auto eta = std::chrono::nanoseconds( + _progress > 0 ? static_cast(elapsed.count() * 100 / _progress) : 0); + auto remaining = eta > elapsed ? (eta - elapsed) : (elapsed - eta); + _print_duration(std::cout, remaining); + std::cout << "]"; + } + if (_max_postfix_text_length == 0) _max_postfix_text_length = 10; std::cout << " " << _postfix_text << std::string(_max_postfix_text_length, ' ') << "\r"; diff --git a/include/indicators/progress_bar.hpp b/include/indicators/progress_bar.hpp index 4fe3f92..15ee67a 100644 --- a/include/indicators/progress_bar.hpp +++ b/include/indicators/progress_bar.hpp @@ -27,7 +27,10 @@ SOFTWARE. #pragma once #include #include +#include +#include #include +#include #include #include #include @@ -88,11 +91,20 @@ public: void hide_percentage() { _show_percentage = false; } + void show_elapsed_time() { _show_elapsed_time = true; } + + void hide_elapsed_time() { _show_elapsed_time = false; } + + void show_remaining_time() { _show_remaining_time = true; } + + void hide_remaining_time() { _show_remaining_time = false; } + void set_progress(float value) { { std::unique_lock lock{_mutex}; _progress = value; } + _save_start_time(); _print_progress(); } @@ -101,12 +113,11 @@ public: std::unique_lock lock{_mutex}; _progress += 1; } + _save_start_time(); _print_progress(); } - size_t current() { - return std::min(static_cast(_progress), size_t(100)); - } + size_t current() { return std::min(static_cast(_progress), size_t(100)); } bool is_completed() const { return _completed; } @@ -128,11 +139,47 @@ private: std::atomic _max_postfix_text_length{0}; std::atomic _completed{false}; std::atomic _show_percentage{true}; + std::atomic _show_elapsed_time{false}; + std::atomic _show_remaining_time{false}; + std::atomic _saved_start_time{false}; + std::chrono::time_point _start_time_point; std::mutex _mutex; Color _foreground_color; + std::ostream &_print_duration(std::ostream &os, std::chrono::nanoseconds ns) { + using namespace std; + using namespace std::chrono; + typedef duration> days; + char fill = os.fill(); + os.fill('0'); + auto d = duration_cast(ns); + ns -= d; + auto h = duration_cast(ns); + ns -= h; + auto m = duration_cast(ns); + ns -= m; + auto s = duration_cast(ns); + if (d.count() > 0) + os << setw(2) << d.count() << "d:"; + if (h.count() > 0) + os << setw(2) << h.count() << "h:"; + os << setw(2) << m.count() << "m:" << setw(2) << s.count() << 's'; + os.fill(fill); + return os; + }; + + void _save_start_time() { + if ((_show_elapsed_time || _show_remaining_time) && !_saved_start_time) { + _start_time_point = std::chrono::high_resolution_clock::now(); + _saved_start_time = true; + } + } + void _print_progress() { std::unique_lock lock{_mutex}; + auto now = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(now - _start_time_point); + std::cout << termcolor::bold; switch (_foreground_color) { case Color::GREY: @@ -175,6 +222,24 @@ private: if (_show_percentage) { std::cout << " " << std::min(static_cast(_progress), size_t(100)) << "%"; } + + if (_show_elapsed_time) { + std::cout << " ["; + _print_duration(std::cout, elapsed); + } + + if (_show_remaining_time) { + if (_show_elapsed_time) + std::cout << "<"; + else + std::cout << " ["; + auto eta = std::chrono::nanoseconds( + _progress > 0 ? static_cast(elapsed.count() * 100 / _progress) : 0); + auto remaining = eta > elapsed ? (eta - elapsed) : (elapsed - eta); + _print_duration(std::cout, remaining); + std::cout << "]"; + } + if (_max_postfix_text_length == 0) _max_postfix_text_length = 10; std::cout << " " << _postfix_text << std::string(_max_postfix_text_length, ' ') << "\r"; diff --git a/include/indicators/progress_spinner.hpp b/include/indicators/progress_spinner.hpp index 5d62344..1481d9b 100644 --- a/include/indicators/progress_spinner.hpp +++ b/include/indicators/progress_spinner.hpp @@ -27,7 +27,10 @@ SOFTWARE. #pragma once #include #include +#include +#include #include +#include #include #include #include @@ -59,6 +62,14 @@ public: void hide_percentage() { _show_percentage = false; } + void show_elapsed_time() { _show_elapsed_time = true; } + + void hide_elapsed_time() { _show_elapsed_time = false; } + + void show_remaining_time() { _show_remaining_time = true; } + + void hide_remaining_time() { _show_remaining_time = false; } + void show_spinner() { _show_spinner = true; } void hide_spinner() { _show_spinner = false; } @@ -68,6 +79,7 @@ public: std::unique_lock lock{_mutex}; _progress = value; } + _save_start_time(); _print_progress(); } @@ -76,12 +88,11 @@ public: std::unique_lock lock{_mutex}; _progress += 1; } + _save_start_time(); _print_progress(); } - size_t current() { - return std::min(static_cast(_progress), size_t(100)); - } + size_t current() { return std::min(static_cast(_progress), size_t(100)); } bool is_completed() const { return _completed; } @@ -104,12 +115,48 @@ private: std::atomic _max_postfix_text_length{0}; std::atomic _completed{false}; std::atomic _show_percentage{true}; + std::atomic _show_elapsed_time{false}; + std::atomic _show_remaining_time{false}; + std::atomic _saved_start_time{false}; + std::chrono::time_point _start_time_point; std::atomic _show_spinner{true}; std::mutex _mutex; Color _foreground_color; + std::ostream &_print_duration(std::ostream &os, std::chrono::nanoseconds ns) { + using namespace std; + using namespace std::chrono; + typedef duration> days; + char fill = os.fill(); + os.fill('0'); + auto d = duration_cast(ns); + ns -= d; + auto h = duration_cast(ns); + ns -= h; + auto m = duration_cast(ns); + ns -= m; + auto s = duration_cast(ns); + if (d.count() > 0) + os << setw(2) << d.count() << "d:"; + if (h.count() > 0) + os << setw(2) << h.count() << "h:"; + os << setw(2) << m.count() << "m:" << setw(2) << s.count() << 's'; + os.fill(fill); + return os; + }; + + void _save_start_time() { + if ((_show_elapsed_time || _show_remaining_time) && !_saved_start_time) { + _start_time_point = std::chrono::high_resolution_clock::now(); + _saved_start_time = true; + } + } + void _print_progress() { std::unique_lock lock{_mutex}; + auto now = std::chrono::high_resolution_clock::now(); + auto elapsed = std::chrono::duration_cast(now - _start_time_point); + std::cout << termcolor::bold; switch (_foreground_color) { case Color::GREY: @@ -143,6 +190,24 @@ private: if (_show_percentage) { std::cout << " " << std::min(static_cast(_progress), size_t(100)) << "%"; } + + if (_show_elapsed_time) { + std::cout << " ["; + _print_duration(std::cout, elapsed); + } + + if (_show_remaining_time) { + if (_show_elapsed_time) + std::cout << "<"; + else + std::cout << " ["; + auto eta = std::chrono::nanoseconds( + _progress > 0 ? static_cast(elapsed.count() * 100 / _progress) : 0); + auto remaining = eta > elapsed ? (eta - elapsed) : (elapsed - eta); + _print_duration(std::cout, remaining); + std::cout << "]"; + } + if (_max_postfix_text_length == 0) _max_postfix_text_length = 10; std::cout << " " << _postfix_text << std::string(_max_postfix_text_length, ' ') << "\r"; diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 346c227..983290e 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -13,3 +13,6 @@ target_link_libraries(progress_bar_tick PRIVATE indica::indica) add_executable(progress_spinner progress_spinner.cpp) target_link_libraries(progress_spinner PRIVATE indica::indica) + +add_executable(time_meter time_meter.cpp) +target_link_libraries(time_meter PRIVATE indica::indica) diff --git a/samples/block_progress_bar.cpp b/samples/block_progress_bar.cpp index e1ce407..d0cbcfd 100644 --- a/samples/block_progress_bar.cpp +++ b/samples/block_progress_bar.cpp @@ -1,20 +1,20 @@ +#include #include #include -#include int main() { // Hide cursor std::cout << "\e[?25l"; - + indicators::BlockProgressBar bar; - + // Configure the bar bar.set_bar_width(80); bar.start_bar_with("["); bar.end_bar_with("]"); - bar.set_foreground_color(indicators::Color::WHITE); - + bar.set_foreground_color(indicators::Color::WHITE); + // Update bar state auto progress = 0.0f; while (true) { @@ -26,7 +26,7 @@ int main() { } // Show cursor - std::cout << "\e[?25h"; + std::cout << "\e[?25h"; return 0; } diff --git a/samples/multi_threaded_bar.cpp b/samples/multi_threaded_bar.cpp index ec07cf4..457c479 100644 --- a/samples/multi_threaded_bar.cpp +++ b/samples/multi_threaded_bar.cpp @@ -17,25 +17,22 @@ int main() { // [■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■-------------] 70% // // - + std::atomic index{0}; - std::vector status_text = - { - "Rocket.exe is not responding", - "Finding a replacement engineer", - "Buying more snacks", - "Assimilating the modding community", - "Crossing fingers", - "Porting KSP to a Nokia 3310" - }; - + std::vector status_text = {"Rocket.exe is not responding", + "Finding a replacement engineer", + "Buying more snacks", + "Assimilating the modding community", + "Crossing fingers", + "Porting KSP to a Nokia 3310"}; + // Let's say you want to append some status text to the right of the progress bar // You can use bar.set_postfix_text(...) to append text to the right // // [■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■-------------] 70% Finding a replacement engineer // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // - // + // auto job = [&bar, &index, &status_text]() { while (true) { @@ -58,6 +55,6 @@ int main() { second_job.join(); third_job.join(); last_job.join(); - + return 0; } diff --git a/samples/progress_bar_set_progress.cpp b/samples/progress_bar_set_progress.cpp index 34b5d9f..ca9bb26 100644 --- a/samples/progress_bar_set_progress.cpp +++ b/samples/progress_bar_set_progress.cpp @@ -1,10 +1,10 @@ +#include #include #include -#include int main() { indicators::ProgressBar bar; - + // Configure the bar bar.set_bar_width(50); bar.start_bar_with("["); @@ -13,26 +13,26 @@ int main() { bar.fill_bar_remainder_with(" "); bar.end_bar_with("]"); bar.set_postfix_text("Getting started"); - bar.set_foreground_color(indicators::Color::GREEN); - + bar.set_foreground_color(indicators::Color::GREEN); + // Update bar state - + bar.set_progress(10); // 10% done - + // do some work std::this_thread::sleep_for(std::chrono::milliseconds(100)); - + bar.set_progress(30); // 30% done - - // do some more work + + // do some more work std::this_thread::sleep_for(std::chrono::milliseconds(600)); - + bar.set_progress(65); // 65% done - + // do final bit of work std::this_thread::sleep_for(std::chrono::milliseconds(300)); - + bar.set_progress(100); // all done - + return 0; } diff --git a/samples/progress_bar_tick.cpp b/samples/progress_bar_tick.cpp index 3f0b389..5d8e594 100644 --- a/samples/progress_bar_tick.cpp +++ b/samples/progress_bar_tick.cpp @@ -1,10 +1,10 @@ +#include #include #include -#include int main() { indicators::ProgressBar bar; - + // Configure the bar bar.set_bar_width(50); bar.start_bar_with("["); @@ -13,8 +13,8 @@ int main() { bar.fill_bar_remainder_with(" "); bar.end_bar_with("]"); bar.set_postfix_text("Getting started"); - bar.set_foreground_color(indicators::Color::GREEN); - + bar.set_foreground_color(indicators::Color::GREEN); + // Update bar state while (true) { bar.tick(); diff --git a/samples/progress_spinner.cpp b/samples/progress_spinner.cpp index abb918d..37de709 100644 --- a/samples/progress_spinner.cpp +++ b/samples/progress_spinner.cpp @@ -6,12 +6,12 @@ int main() { std::cout << "\e[?25l"; indicators::ProgressSpinner spinner; - + // Configure the spinner spinner.set_postfix_text("Checking credentials"); spinner.set_foreground_color(indicators::Color::YELLOW); spinner.set_spinner_states({"⠈", "⠐", "⠠", "⢀", "⡀", "⠄", "⠂", "⠁"}); - + // Update spinner state auto job = [&spinner]() { while (true) { @@ -32,7 +32,7 @@ int main() { thread.join(); // Show cursor - std::cout << "\e[?25h"; + std::cout << "\e[?25h"; return 0; } diff --git a/samples/time_meter.cpp b/samples/time_meter.cpp new file mode 100644 index 0000000..23e41cb --- /dev/null +++ b/samples/time_meter.cpp @@ -0,0 +1,34 @@ +#include +#include +#include + +int main() { + indicators::ProgressBar bar; + + // Configure the bar + bar.set_bar_width(50); + bar.start_bar_with(" ["); + bar.fill_bar_progress_with("█"); + bar.lead_bar_progress_with("█"); + bar.fill_bar_remainder_with("-"); + bar.end_bar_with("]"); + bar.set_prefix_text("Training Gaze Network 👀"); + bar.set_foreground_color(indicators::Color::YELLOW); + + // Show time elapsed and remaining + bar.show_elapsed_time(); + bar.show_remaining_time(); + + // Update bar state + while (true) { + bar.tick(); + if (bar.is_completed()) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } + + // Show cursor + std::cout << "\e[?25h"; + + return 0; +}