diff --git a/README.md b/README.md index 2779c65..5c4d580 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ make * [Progress Bar](#progress-bar) * [Block Progress Bar](#block-progress-bar) * [Multi Progress](#multiprogress) +* [Dynamic Progress](#dynamicprogress) * [Progress Spinner](#progress-spinner) * [Contributing](#contributing) * [License](#license) @@ -350,6 +351,159 @@ int main() { } ``` +# DynamicProgress + +`DynamicProgress` is a container class, similar to `MultiProgress`, for managing multiple progress bars. As the name suggests, with `DynamicProgress`, you can dynamically add new progress bars. Simply call `bars.push_back`. + +Below is an example `DynamicProgress` object that manages six `ProgressBar` objects. Three of these bars are added dynamically. + +

+ +

+ +```cpp +#include +#include +using namespace indicators; + +int main() { + + ProgressBar bar1{option::BarWidth{50}, option::ForegroundColor{Color::red}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"5c90d4a2d1a8: Downloading "}}; + + ProgressBar bar2{option::BarWidth{50}, option::ForegroundColor{Color::yellow}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"22337bfd13a9: Downloading "}}; + + ProgressBar bar3{option::BarWidth{50}, option::ForegroundColor{Color::green}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"10f26c680a34: Downloading "}}; + + ProgressBar bar4{option::BarWidth{50}, option::ForegroundColor{Color::white}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"6364e0d7a283: Downloading "}}; + + ProgressBar bar5{option::BarWidth{50}, option::ForegroundColor{Color::blue}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"ff1356ba118b: Downloading "}}; + + ProgressBar bar6{option::BarWidth{50}, option::ForegroundColor{Color::cyan}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"5a17453338b4: Downloading "}}; + + std::cout << termcolor::bold << termcolor::white << "Pulling image foo:bar/baz\n"; + + // Construct with 3 progress bars. We'll add 3 more at a later point + DynamicProgress bars(bar1, bar2, bar3); + + // Do not hide bars when completed + bars.set_option(option::HideBarWhenComplete{false}); + + std::thread fourth_job, fifth_job, sixth_job; + + auto job4 = [&bars](size_t i) { + while (true) { + bars[i].tick(); + if (bars[i].is_completed()) { + bars[i].set_option(option::PrefixText{"6364e0d7a283: Pull complete "}); + bars[i].mark_as_completed(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + }; + + auto job5 = [&bars](size_t i) { + while (true) { + bars[i].tick(); + if (bars[i].is_completed()) { + bars[i].set_option(option::PrefixText{"ff1356ba118b: Pull complete "}); + bars[i].mark_as_completed(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + }; + + auto job6 = [&bars](size_t i) { + while (true) { + bars[i].tick(); + if (bars[i].is_completed()) { + bars[i].set_option(option::PrefixText{"5a17453338b4: Pull complete "}); + bars[i].mark_as_completed(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(40)); + } + }; + + auto job1 = [&bars, &bar6, &sixth_job, &job6]() { + while (true) { + bars[0].tick(); + if (bars[0].is_completed()) { + bars[0].set_option(option::PrefixText{"5c90d4a2d1a8: Pull complete "}); + // bar1 is completed, adding bar6 + auto i = bars.push_back(bar6); + sixth_job = std::thread(job6, i); + sixth_job.join(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(140)); + } + }; + + auto job2 = [&bars, &bar5, &fifth_job, &job5]() { + while (true) { + bars[1].tick(); + if (bars[1].is_completed()) { + bars[1].set_option(option::PrefixText{"22337bfd13a9: Pull complete "}); + // bar2 is completed, adding bar5 + auto i = bars.push_back(bar5); + fifth_job = std::thread(job5, i); + fifth_job.join(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + } + }; + + auto job3 = [&bars, &bar4, &fourth_job, &job4]() { + while (true) { + bars[2].tick(); + if (bars[2].is_completed()) { + bars[2].set_option(option::PrefixText{"10f26c680a34: Pull complete "}); + // bar3 is completed, adding bar4 + auto i = bars.push_back(bar4); + fourth_job = std::thread(job4, i); + fourth_job.join(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + }; + + std::thread first_job(job1); + std::thread second_job(job2); + std::thread third_job(job3); + + third_job.join(); + second_job.join(); + first_job.join(); + + std::cout << termcolor::bold << termcolor::green << "✔ Downloaded image foo/bar:baz" << std::endl; + std::cout << termcolor::reset; + + return 0; +} +``` + +In the above code, notice the option `bars.set_option(option::HideBarWhenComplete{true});`. Yes, you can hide progress bars as and when they complete by setting this option to `true`. If you do so, the above example will look like this: + +

+ +

+ # Progress Spinner To introduce a progress spinner in your application, include `indicators/progress_spinner.hpp` and create a `ProgressSpinner` object. Here's the general structure of a progress spinner: diff --git a/img/dynamic_progress_bar.gif b/img/dynamic_progress_bar.gif new file mode 100644 index 0000000..2a9381f Binary files /dev/null and b/img/dynamic_progress_bar.gif differ diff --git a/img/dynamic_progress_bar_hide_completed.gif b/img/dynamic_progress_bar_hide_completed.gif new file mode 100644 index 0000000..e1c73cc Binary files /dev/null and b/img/dynamic_progress_bar_hide_completed.gif differ diff --git a/include/indicators/block_progress_bar.hpp b/include/indicators/block_progress_bar.hpp index 3857320..0ce42fa 100644 --- a/include/indicators/block_progress_bar.hpp +++ b/include/indicators/block_progress_bar.hpp @@ -164,6 +164,7 @@ class BlockProgressBar { std::mutex mutex_; template friend class MultiProgress; + template friend class DynamicProgress; std::atomic multi_progress_mode_{false}; void save_start_time() { diff --git a/include/indicators/dynamic_progress.hpp b/include/indicators/dynamic_progress.hpp new file mode 100644 index 0000000..bc8f8a7 --- /dev/null +++ b/include/indicators/dynamic_progress.hpp @@ -0,0 +1,138 @@ +/* +Activity Indicators for Modern C++ +https://github.com/p-ranav/indicators + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2019 Pranav Srinivas Kumar . + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace indicators { + +template class DynamicProgress { + using Settings = std::tuple; + +public: + template explicit DynamicProgress(Indicators &... bars) { + bars_ = {bars...}; + for (auto &bar : bars_) { + bar.get().multi_progress_mode_ = true; + ++total_count_; + ++incomplete_count_; + } + } + + Indicator &operator[](size_t index) { + print_progress(); + std::lock_guard lock{mutex_}; + return bars_[index].get(); + } + + size_t push_back(Indicator &bar) { + std::lock_guard lock{mutex_}; + bar.multi_progress_mode_ = true; + bars_.push_back(bar); + return bars_.size() - 1; + } + + template + void set_option(details::Setting &&setting) { + static_assert(!std::is_same( + std::declval()))>::type>::value, + "Setting has wrong type!"); + std::lock_guard lock(mutex_); + get_value() = std::move(setting).value; + } + + template + void set_option(const details::Setting &setting) { + static_assert(!std::is_same( + std::declval()))>::type>::value, + "Setting has wrong type!"); + std::lock_guard lock(mutex_); + get_value() = setting.value; + } + +private: + Settings settings_; + std::atomic started_{false}; + std::mutex mutex_; + std::vector> bars_; + std::atomic total_count_{0}; + std::atomic incomplete_count_{0}; + + template + auto get_value() -> decltype((details::get_value(std::declval()).value)) { + return details::get_value(settings_).value; + } + + template + auto get_value() const + -> decltype((details::get_value(std::declval()).value)) { + return details::get_value(settings_).value; + } + + void print_progress() { + std::lock_guard lock{mutex_}; + auto &hide_bar_when_complete = get_value(); + if (hide_bar_when_complete) { + // Hide completed bars + if (started_) { + for (size_t i = 0; i < incomplete_count_; ++i) + std::cout << "\033[A\r\033[K" << std::flush; + } + incomplete_count_ = 0; + for (auto &bar : bars_) { + if (!bar.get().is_completed()) { + bar.get().print_progress(true); + std::cout << "\n"; + ++incomplete_count_; + } + } + if (!started_) + started_ = true; + } else { + // Don't hide any bars + if (started_) { + for (size_t i = 0; i < total_count_; ++i) + std::cout << "\x1b[A"; + } + for (auto &bar: bars_) { + bar.get().print_progress(true); + std::cout << "\n"; + } + if (!started_) + started_ = true; + } + total_count_ = bars_.size(); + std::cout << termcolor::reset; + } +}; + +} // namespace indicators diff --git a/include/indicators/progress_bar.hpp b/include/indicators/progress_bar.hpp index 7235f54..200eac5 100644 --- a/include/indicators/progress_bar.hpp +++ b/include/indicators/progress_bar.hpp @@ -176,6 +176,7 @@ class ProgressBar { std::mutex mutex_; template friend class MultiProgress; + template friend class DynamicProgress; std::atomic multi_progress_mode_{false}; void save_start_time() { @@ -189,13 +190,13 @@ class ProgressBar { } void print_progress(bool from_multi_progress = false) { + std::lock_guard lock{mutex_}; if (multi_progress_mode_ && !from_multi_progress) { if (progress_ > 100.0) { get_value() = true; } return; } - std::lock_guard lock{mutex_}; auto now = std::chrono::high_resolution_clock::now(); if (!get_value()) elapsed_ = std::chrono::duration_cast(now - start_time_point_); diff --git a/include/indicators/setting.hpp b/include/indicators/setting.hpp index 8bc25ad..c444589 100644 --- a/include/indicators/setting.hpp +++ b/include/indicators/setting.hpp @@ -85,7 +85,8 @@ enum class ProgressBarOption { saved_start_time, foreground_color, spinner_show, - spinner_states + spinner_states, + hide_bar_when_complete }; template struct Setting { @@ -196,5 +197,7 @@ using ForegroundColor = details::Setting; using SpinnerStates = details::Setting, details::ProgressBarOption::spinner_states>; +using HideBarWhenComplete = + details::BooleanSetting; } // namespace option } // namespace indicators \ No newline at end of file diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 01b3212..9aa8218 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -22,3 +22,6 @@ target_link_libraries(multi_progress_bar PRIVATE indicators::indicators) add_executable(multi_block_progress_bar multi_block_progress_bar.cpp) target_link_libraries(multi_block_progress_bar PRIVATE indicators::indicators) + +add_executable(dynamic_progress dynamic_progress.cpp) +target_link_libraries(dynamic_progress PRIVATE indicators::indicators) diff --git a/samples/dynamic_progress.cpp b/samples/dynamic_progress.cpp new file mode 100644 index 0000000..d16b04c --- /dev/null +++ b/samples/dynamic_progress.cpp @@ -0,0 +1,131 @@ +#include +#include +using namespace indicators; + +int main() { + + ProgressBar bar1{option::BarWidth{50}, option::ForegroundColor{Color::red}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"5c90d4a2d1a8: Downloading "}}; + + ProgressBar bar2{option::BarWidth{50}, option::ForegroundColor{Color::yellow}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"22337bfd13a9: Downloading "}}; + + ProgressBar bar3{option::BarWidth{50}, option::ForegroundColor{Color::green}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"10f26c680a34: Downloading "}}; + + ProgressBar bar4{option::BarWidth{50}, option::ForegroundColor{Color::white}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"6364e0d7a283: Downloading "}}; + + ProgressBar bar5{option::BarWidth{50}, option::ForegroundColor{Color::blue}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"ff1356ba118b: Downloading "}}; + + ProgressBar bar6{option::BarWidth{50}, option::ForegroundColor{Color::cyan}, + option::ShowElapsedTime{true}, option::ShowRemainingTime{true}, + option::PrefixText{"5a17453338b4: Downloading "}}; + + std::cout << termcolor::bold << termcolor::white << "Pulling image foo:bar/baz\n"; + + DynamicProgress bars(bar1, bar2, bar3); + bars.set_option(option::HideBarWhenComplete{false}); + + std::thread fourth_job, fifth_job, sixth_job; + + auto job4 = [&bars](size_t i) { + while (true) { + bars[i].tick(); + if (bars[i].is_completed()) { + bars[i].set_option(option::PrefixText{"6364e0d7a283: Pull complete "}); + bars[i].mark_as_completed(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + }; + + auto job5 = [&bars](size_t i) { + while (true) { + bars[i].tick(); + if (bars[i].is_completed()) { + bars[i].set_option(option::PrefixText{"ff1356ba118b: Pull complete "}); + bars[i].mark_as_completed(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + }; + + auto job6 = [&bars](size_t i) { + while (true) { + bars[i].tick(); + if (bars[i].is_completed()) { + bars[i].set_option(option::PrefixText{"5a17453338b4: Pull complete "}); + bars[i].mark_as_completed(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(40)); + } + }; + + auto job1 = [&bars, &bar6, &sixth_job, &job6]() { + while (true) { + bars[0].tick(); + if (bars[0].is_completed()) { + bars[0].set_option(option::PrefixText{"5c90d4a2d1a8: Pull complete "}); + // bar1 is completed, adding bar6 + auto i = bars.push_back(bar6); + sixth_job = std::thread(job6, i); + sixth_job.join(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(140)); + } + }; + + auto job2 = [&bars, &bar5, &fifth_job, &job5]() { + while (true) { + bars[1].tick(); + if (bars[1].is_completed()) { + bars[1].set_option(option::PrefixText{"22337bfd13a9: Pull complete "}); + // bar2 is completed, adding bar5 + auto i = bars.push_back(bar5); + fifth_job = std::thread(job5, i); + fifth_job.join(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + } + }; + + auto job3 = [&bars, &bar4, &fourth_job, &job4]() { + while (true) { + bars[2].tick(); + if (bars[2].is_completed()) { + bars[2].set_option(option::PrefixText{"10f26c680a34: Pull complete "}); + // bar3 is completed, adding bar4 + auto i = bars.push_back(bar4); + fourth_job = std::thread(job4, i); + fourth_job.join(); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + }; + + std::thread first_job(job1); + std::thread second_job(job2); + std::thread third_job(job3); + + third_job.join(); + second_job.join(); + first_job.join(); + + std::cout << termcolor::bold << termcolor::green << "✔ Downloaded image foo/bar:baz" << std::endl; + std::cout << termcolor::reset; + + return 0; +}