Merge 7998c178de
into ca1eaefc86
This commit is contained in:
commit
645ecbe990
|
@ -1,6 +1,6 @@
|
|||
set(DEFAULT_PROJECT_VERSION_PRIME 5)
|
||||
set(DEFAULT_PROJECT_VERSION_MAJOR 2)
|
||||
set(DEFAULT_PROJECT_VERSION_MINOR 15)
|
||||
set(DEFAULT_PROJECT_VERSION_MAJOR 3)
|
||||
set(DEFAULT_PROJECT_VERSION_MINOR 0)
|
||||
set(DEFAULT_PROJECT_VERSION_MICRO 0)
|
||||
|
||||
if(DEFINED PROJECT_VERSION_PRIME)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%global project_version_prime 5
|
||||
%global project_version_major 2
|
||||
%global project_version_minor 15
|
||||
%global project_version_major 3
|
||||
%global project_version_minor 0
|
||||
%global project_version_micro 0
|
||||
|
||||
%bcond dnf5_obsoletes_dnf %[0%{?fedora} > 40 || 0%{?rhel} > 10]
|
||||
|
@ -434,7 +434,7 @@ Requires: libdnf5%{?_isa} = %{version}-%{release}
|
|||
Library for working with a terminal in a command-line package manager.
|
||||
|
||||
%files -n libdnf5-cli -f libdnf5-cli.lang
|
||||
%{_libdir}/libdnf5-cli.so.2*
|
||||
%{_libdir}/libdnf5-cli.so.3*
|
||||
%license COPYING.md
|
||||
%license lgpl-2.1.txt
|
||||
%endif
|
||||
|
|
|
@ -33,6 +33,8 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
#include "libdnf5-cli/defs.h"
|
||||
|
||||
#include "libdnf5/common/impl_ptr.hpp"
|
||||
|
||||
|
||||
namespace libdnf5::cli::progressbar {
|
||||
|
||||
|
@ -40,6 +42,11 @@ namespace libdnf5::cli::progressbar {
|
|||
class LIBDNF_CLI_API DownloadProgressBar : public ProgressBar {
|
||||
public:
|
||||
explicit DownloadProgressBar(int64_t download_size, const std::string & description);
|
||||
~DownloadProgressBar();
|
||||
DownloadProgressBar(const DownloadProgressBar & src);
|
||||
DownloadProgressBar & operator=(const DownloadProgressBar & src);
|
||||
DownloadProgressBar(DownloadProgressBar && src) noexcept;
|
||||
DownloadProgressBar & operator=(DownloadProgressBar && src) noexcept;
|
||||
|
||||
using ProgressBar::get_messages;
|
||||
using ProgressBar::set_state;
|
||||
|
@ -48,8 +55,8 @@ public:
|
|||
|
||||
// TODO(dmach): add print() method
|
||||
|
||||
bool get_number_widget_visible() const noexcept { return number_widget.get_visible(); }
|
||||
void set_number_widget_visible(bool value) noexcept { number_widget.set_visible(value); }
|
||||
bool get_number_widget_visible() const noexcept;
|
||||
void set_number_widget_visible(bool value) noexcept;
|
||||
|
||||
protected:
|
||||
void to_stream(std::ostream & stream) override;
|
||||
|
@ -58,14 +65,8 @@ private:
|
|||
// TODO(dmach): fix inconsistency - MultiProgresBar::operator<< erases previously printed lines, DownloadProgressBar::operator<< does not
|
||||
LIBDNF_CLI_API friend std::ostream & operator<<(std::ostream & stream, DownloadProgressBar & bar);
|
||||
|
||||
// widgets
|
||||
libdnf5::cli::progressbar::NumberWidget number_widget;
|
||||
libdnf5::cli::progressbar::DescriptionWidget description_widget;
|
||||
libdnf5::cli::progressbar::PercentWidget percent_widget;
|
||||
libdnf5::cli::progressbar::ProgressWidget progress_widget;
|
||||
libdnf5::cli::progressbar::SpeedWidget speed_widget;
|
||||
libdnf5::cli::progressbar::SizeWidget size_widget;
|
||||
libdnf5::cli::progressbar::TimeWidget time_widget;
|
||||
class LIBDNF_CLI_LOCAL Impl;
|
||||
ImplPtr<Impl> p_impl;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
#include "libdnf5-cli/defs.h"
|
||||
|
||||
#include "libdnf5/common/impl_ptr.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
@ -42,21 +44,28 @@ public:
|
|||
explicit MultiProgressBar();
|
||||
~MultiProgressBar();
|
||||
|
||||
MultiProgressBar(const MultiProgressBar & src) = delete;
|
||||
MultiProgressBar & operator=(const MultiProgressBar & src) = delete;
|
||||
|
||||
MultiProgressBar(MultiProgressBar && src) noexcept = delete;
|
||||
MultiProgressBar & operator=(MultiProgressBar && src) noexcept = delete;
|
||||
|
||||
void add_bar(std::unique_ptr<ProgressBar> && bar);
|
||||
|
||||
// In interactive mode MultiProgressBar doesn't print a newline after unfinished progressbars.
|
||||
// Finished progressbars always end with a newline.
|
||||
void print() {
|
||||
std::cerr << *this;
|
||||
std::cerr << std::flush;
|
||||
}
|
||||
void print();
|
||||
|
||||
LIBDNF_CLI_API friend std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar);
|
||||
|
||||
/// Returns the total bar.
|
||||
DownloadProgressBar & get_total_bar() noexcept;
|
||||
|
||||
/// Sets the minimum number of registered progress bars to show the total bar.
|
||||
void set_total_bar_visible_limit(std::size_t value) noexcept { total_bar_visible_limit = value; }
|
||||
void set_total_bar_visible_limit(std::size_t value) noexcept;
|
||||
|
||||
/// Sets the visibility of number widget in the total bar.
|
||||
void set_total_bar_number_widget_visible(bool value) noexcept { total.set_number_widget_visible(value); }
|
||||
void set_total_bar_number_widget_visible(bool value) noexcept;
|
||||
|
||||
/// Allows one to preset the value of the total number of progress bars.
|
||||
/// If the value is lower than the current number of registered progress bars, it is automatically increased.
|
||||
|
@ -67,14 +76,8 @@ public:
|
|||
std::size_t get_total_num_of_bars() const noexcept;
|
||||
|
||||
private:
|
||||
std::size_t total_bar_visible_limit{0};
|
||||
std::vector<std::unique_ptr<ProgressBar>> bars_all;
|
||||
std::vector<ProgressBar *> bars_todo;
|
||||
std::vector<ProgressBar *> bars_done;
|
||||
DownloadProgressBar total;
|
||||
// Whether the last line was printed without a new line ending (such as an in progress bar)
|
||||
bool line_printed{false};
|
||||
std::size_t num_of_lines_to_clear{0};
|
||||
class LIBDNF_CLI_LOCAL Impl;
|
||||
ImplPtr<Impl> p_impl;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
#include "libdnf5-cli/defs.h"
|
||||
|
||||
#include "libdnf5/common/impl_ptr.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
@ -54,64 +56,66 @@ public:
|
|||
|
||||
explicit ProgressBar(int64_t total_ticks);
|
||||
explicit ProgressBar(int64_t total_ticks, const std::string & description);
|
||||
virtual ~ProgressBar() = default;
|
||||
virtual ~ProgressBar();
|
||||
ProgressBar(const ProgressBar & src);
|
||||
ProgressBar & operator=(const ProgressBar & src);
|
||||
ProgressBar(ProgressBar && src) noexcept;
|
||||
ProgressBar & operator=(ProgressBar && src) noexcept;
|
||||
|
||||
void reset();
|
||||
|
||||
// ticks
|
||||
int64_t get_ticks() const noexcept { return ticks; }
|
||||
int64_t get_ticks() const noexcept;
|
||||
void set_ticks(int64_t value);
|
||||
void add_ticks(int64_t value);
|
||||
|
||||
// total ticks
|
||||
int64_t get_total_ticks() const noexcept { return total_ticks; }
|
||||
int64_t get_total_ticks() const noexcept;
|
||||
void set_total_ticks(int64_t value);
|
||||
|
||||
// number
|
||||
int32_t get_number() const noexcept { return number; }
|
||||
void set_number(int32_t value) { number = value; }
|
||||
int32_t get_number() const noexcept;
|
||||
void set_number(int32_t value);
|
||||
|
||||
// total
|
||||
int32_t get_total() const noexcept { return total; }
|
||||
void set_total(int32_t value) { total = value; }
|
||||
int32_t get_total() const noexcept;
|
||||
void set_total(int32_t value);
|
||||
|
||||
// workflow
|
||||
void start();
|
||||
ProgressBarState get_state() const noexcept { return state; }
|
||||
void set_state(ProgressBarState value) {
|
||||
update();
|
||||
state = value;
|
||||
}
|
||||
bool is_finished() const noexcept {
|
||||
return get_state() != ProgressBarState::READY && get_state() != ProgressBarState::STARTED;
|
||||
}
|
||||
bool is_failed() const noexcept {
|
||||
return get_state() == ProgressBarState::ERROR || get_state() == ProgressBarState::WARNING;
|
||||
}
|
||||
ProgressBarState get_state() const noexcept;
|
||||
void set_state(ProgressBarState value);
|
||||
bool is_finished() const noexcept;
|
||||
bool is_failed() const noexcept;
|
||||
|
||||
// description
|
||||
std::string get_description() const noexcept { return description; }
|
||||
void set_description(const std::string & value) { description = value; }
|
||||
std::string get_description() const noexcept;
|
||||
void set_description(const std::string & value);
|
||||
|
||||
// messages
|
||||
void add_message(MessageType type, const std::string & message) { messages.emplace_back(type, message); }
|
||||
void add_message(MessageType type, const std::string & message);
|
||||
/// remove the last message
|
||||
void pop_message();
|
||||
const std::vector<Message> & get_messages() const noexcept { return messages; }
|
||||
const std::vector<Message> & get_messages() const noexcept;
|
||||
const std::string & get_message_prefix() const noexcept;
|
||||
|
||||
/// Calculate number of lines occupied by messages when printed on terminal of the given width.
|
||||
/// Takes new lines and wide utf characters into account.
|
||||
std::size_t calculate_messages_terminal_lines(std::size_t terminal_width);
|
||||
|
||||
// auto-finish feature; turn off if you want to handle state manually
|
||||
bool get_auto_finish() const noexcept { return auto_finish; }
|
||||
void set_auto_finish(bool value) { auto_finish = value; }
|
||||
bool get_auto_finish() const noexcept;
|
||||
void set_auto_finish(bool value);
|
||||
|
||||
// stats
|
||||
void update();
|
||||
int32_t get_percent_done() const noexcept { return percent_done; }
|
||||
int64_t get_current_speed() const noexcept { return current_speed; }
|
||||
int64_t get_average_speed() const noexcept { return average_speed; }
|
||||
int64_t get_elapsed_seconds() const noexcept { return elapsed_seconds; }
|
||||
int64_t get_remaining_seconds() const noexcept { return remaining_seconds; }
|
||||
int32_t get_percent_done() const noexcept;
|
||||
int64_t get_current_speed() const noexcept;
|
||||
int64_t get_average_speed() const noexcept;
|
||||
int64_t get_elapsed_seconds() const noexcept;
|
||||
int64_t get_remaining_seconds() const noexcept;
|
||||
|
||||
std::chrono::time_point<std::chrono::system_clock> get_begin() { return begin; }
|
||||
std::chrono::time_point<std::chrono::system_clock> get_begin();
|
||||
|
||||
LIBDNF_CLI_API friend std::ostream & operator<<(std::ostream & os, ProgressBar & bar);
|
||||
|
||||
|
@ -119,36 +123,8 @@ protected:
|
|||
virtual void to_stream(std::ostream & stream) = 0;
|
||||
|
||||
private:
|
||||
// ticks
|
||||
int64_t ticks = -1;
|
||||
int64_t total_ticks = -1;
|
||||
|
||||
// numbers
|
||||
int32_t number = 0;
|
||||
int32_t total = 0;
|
||||
|
||||
// description
|
||||
std::string description;
|
||||
|
||||
// messages
|
||||
std::vector<Message> messages;
|
||||
|
||||
ProgressBarState state = ProgressBarState::READY;
|
||||
|
||||
int32_t percent_done = -1;
|
||||
int64_t elapsed_seconds = 0;
|
||||
int64_t remaining_seconds = 0;
|
||||
int64_t average_speed = 0;
|
||||
|
||||
bool auto_finish = true;
|
||||
|
||||
std::chrono::time_point<std::chrono::system_clock> begin = std::chrono::system_clock::from_time_t(0);
|
||||
std::chrono::time_point<std::chrono::system_clock> end = std::chrono::system_clock::from_time_t(0);
|
||||
|
||||
// current (average) speed over the last second
|
||||
std::chrono::time_point<std::chrono::system_clock> current_speed_window_start = std::chrono::system_clock::now();
|
||||
int64_t current_speed = 0;
|
||||
int64_t current_speed_window_ticks = 0;
|
||||
class LIBDNF_CLI_LOCAL Impl;
|
||||
ImplPtr<Impl> p_impl;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ LIBDNF_CLI_API std::ostream & cyan(std::ostream & stream);
|
|||
LIBDNF_CLI_API std::ostream & white(std::ostream & stream);
|
||||
|
||||
LIBDNF_CLI_API std::ostream & clear_line(std::ostream & stream);
|
||||
LIBDNF_CLI_API std::ostream & clear_to_end(std::ostream & stream);
|
||||
LIBDNF_CLI_API std::ostream & cursor_up(std::ostream & stream);
|
||||
LIBDNF_CLI_API std::ostream & cursor_down(std::ostream & stream);
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ include_directories(.)
|
|||
|
||||
# build libdnf5-cli.so
|
||||
add_library(libdnf5-cli SHARED ${LIBDNF5_CLI_SOURCES})
|
||||
set(DNF_CLI_SO_VERSION 2)
|
||||
set(DNF_CLI_SO_VERSION 3)
|
||||
set_target_properties(libdnf5-cli PROPERTIES OUTPUT_NAME "dnf5-cli")
|
||||
set_target_properties(libdnf5-cli PROPERTIES SOVERSION ${DNF_CLI_SO_VERSION})
|
||||
set_target_properties(libdnf5-cli PROPERTIES C_VISIBILITY_PRESET hidden CXX_VISIBILITY_PRESET hidden)
|
||||
|
|
|
@ -41,17 +41,44 @@ namespace libdnf5::cli::progressbar {
|
|||
// [ 1/999] Total 10% [=== ] 543.2 kB/s | 543.2 MB | -12m34s
|
||||
|
||||
|
||||
DownloadProgressBar::DownloadProgressBar(int64_t download_size, const std::string & description)
|
||||
: ProgressBar(download_size, description) {
|
||||
number_widget.set_bar(this);
|
||||
description_widget.set_bar(this);
|
||||
percent_widget.set_bar(this);
|
||||
progress_widget.set_bar(this);
|
||||
speed_widget.set_bar(this);
|
||||
size_widget.set_bar(this);
|
||||
time_widget.set_bar(this);
|
||||
class DownloadProgressBar::Impl {
|
||||
public:
|
||||
Impl(DownloadProgressBar & download_progress_bar);
|
||||
|
||||
// widgets
|
||||
libdnf5::cli::progressbar::NumberWidget number_widget;
|
||||
libdnf5::cli::progressbar::DescriptionWidget description_widget;
|
||||
libdnf5::cli::progressbar::PercentWidget percent_widget;
|
||||
libdnf5::cli::progressbar::ProgressWidget progress_widget;
|
||||
libdnf5::cli::progressbar::SpeedWidget speed_widget;
|
||||
libdnf5::cli::progressbar::SizeWidget size_widget;
|
||||
libdnf5::cli::progressbar::TimeWidget time_widget;
|
||||
|
||||
private:
|
||||
DownloadProgressBar * download_progress_bar;
|
||||
};
|
||||
|
||||
DownloadProgressBar::Impl::Impl(DownloadProgressBar & download_progress_bar)
|
||||
: download_progress_bar(&download_progress_bar) {
|
||||
number_widget.set_bar(this->download_progress_bar);
|
||||
description_widget.set_bar(this->download_progress_bar);
|
||||
percent_widget.set_bar(this->download_progress_bar);
|
||||
progress_widget.set_bar(this->download_progress_bar);
|
||||
speed_widget.set_bar(this->download_progress_bar);
|
||||
size_widget.set_bar(this->download_progress_bar);
|
||||
time_widget.set_bar(this->download_progress_bar);
|
||||
}
|
||||
|
||||
DownloadProgressBar::~DownloadProgressBar() = default;
|
||||
DownloadProgressBar::DownloadProgressBar(const DownloadProgressBar & src) = default;
|
||||
DownloadProgressBar & DownloadProgressBar::operator=(const DownloadProgressBar & src) = default;
|
||||
DownloadProgressBar::DownloadProgressBar(DownloadProgressBar && src) noexcept = default;
|
||||
DownloadProgressBar & DownloadProgressBar::operator=(DownloadProgressBar && src) noexcept = default;
|
||||
|
||||
DownloadProgressBar::DownloadProgressBar(int64_t download_size, const std::string & description)
|
||||
: ProgressBar(download_size, description),
|
||||
p_impl(new Impl(*this)) {}
|
||||
|
||||
|
||||
static std::size_t get_bar_width(const std::vector<Widget *> & widgets) {
|
||||
std::size_t result = 0;
|
||||
|
@ -68,6 +95,14 @@ std::ostream & operator<<(std::ostream & stream, DownloadProgressBar & bar) {
|
|||
}
|
||||
|
||||
|
||||
bool DownloadProgressBar::get_number_widget_visible() const noexcept {
|
||||
return p_impl->number_widget.get_visible();
|
||||
}
|
||||
|
||||
void DownloadProgressBar::set_number_widget_visible(bool value) noexcept {
|
||||
p_impl->number_widget.set_visible(value);
|
||||
}
|
||||
|
||||
void DownloadProgressBar::to_stream(std::ostream & stream) {
|
||||
update();
|
||||
|
||||
|
@ -78,27 +113,27 @@ void DownloadProgressBar::to_stream(std::ostream & stream) {
|
|||
}
|
||||
|
||||
// set the default delimiters
|
||||
number_widget.set_delimiter_before("");
|
||||
description_widget.set_delimiter_before(" ");
|
||||
percent_widget.set_delimiter_before(" ");
|
||||
progress_widget.set_delimiter_before(" ");
|
||||
speed_widget.set_delimiter_before(" | ");
|
||||
size_widget.set_delimiter_before(" | ");
|
||||
time_widget.set_delimiter_before(" | ");
|
||||
p_impl->number_widget.set_delimiter_before("");
|
||||
p_impl->description_widget.set_delimiter_before(" ");
|
||||
p_impl->percent_widget.set_delimiter_before(" ");
|
||||
p_impl->progress_widget.set_delimiter_before(" ");
|
||||
p_impl->speed_widget.set_delimiter_before(" | ");
|
||||
p_impl->size_widget.set_delimiter_before(" | ");
|
||||
p_impl->time_widget.set_delimiter_before(" | ");
|
||||
|
||||
// set the default description widget width
|
||||
// we'll increase the size if terminal width allows
|
||||
description_widget.set_width(21);
|
||||
p_impl->description_widget.set_width(21);
|
||||
|
||||
// create vector of all widgets
|
||||
std::vector<Widget *> widgets = {
|
||||
&number_widget,
|
||||
&description_widget,
|
||||
&percent_widget,
|
||||
&progress_widget,
|
||||
&speed_widget,
|
||||
&size_widget,
|
||||
&time_widget,
|
||||
&p_impl->number_widget,
|
||||
&p_impl->description_widget,
|
||||
&p_impl->percent_widget,
|
||||
&p_impl->progress_widget,
|
||||
&p_impl->speed_widget,
|
||||
&p_impl->size_widget,
|
||||
&p_impl->time_widget,
|
||||
};
|
||||
|
||||
// remove widgets that are not visible
|
||||
|
@ -115,37 +150,39 @@ void DownloadProgressBar::to_stream(std::ostream & stream) {
|
|||
// if bar doesn't fit terminal width, hide progress widget
|
||||
std::size_t bar_width = get_bar_width(widgets);
|
||||
if (bar_width > terminal_width) {
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), &progress_widget), widgets.end());
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), &p_impl->progress_widget), widgets.end());
|
||||
bar_width = get_bar_width(widgets);
|
||||
}
|
||||
|
||||
// if bar doesn't fit terminal width, hide speed widget
|
||||
if (bar_width > terminal_width) {
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), &speed_widget), widgets.end());
|
||||
speed_widget.set_delimiter_before(" ");
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), &p_impl->speed_widget), widgets.end());
|
||||
p_impl->speed_widget.set_delimiter_before(" ");
|
||||
bar_width = get_bar_width(widgets);
|
||||
}
|
||||
|
||||
// if bar doesn't fit terminal width, hide time widget
|
||||
if (bar_width > terminal_width) {
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), &time_widget), widgets.end());
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), &p_impl->time_widget), widgets.end());
|
||||
bar_width = get_bar_width(widgets);
|
||||
}
|
||||
|
||||
// if bar is finished, hide the progress widget
|
||||
if (get_state() != ProgressBarState::STARTED) {
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), &progress_widget), widgets.end());
|
||||
widgets.erase(std::remove(widgets.begin(), widgets.end(), &p_impl->progress_widget), widgets.end());
|
||||
bar_width = get_bar_width(widgets);
|
||||
}
|
||||
|
||||
// if bar doesn't fit terminal width, reduce description width
|
||||
if (bar_width > terminal_width) {
|
||||
description_widget.set_total_width(description_widget.get_total_width() + terminal_width - bar_width);
|
||||
p_impl->description_widget.set_total_width(
|
||||
p_impl->description_widget.get_total_width() + terminal_width - bar_width);
|
||||
bar_width = get_bar_width(widgets);
|
||||
}
|
||||
|
||||
if (bar_width < terminal_width) {
|
||||
description_widget.set_total_width(description_widget.get_total_width() + terminal_width - bar_width);
|
||||
p_impl->description_widget.set_total_width(
|
||||
p_impl->description_widget.get_total_width() + terminal_width - bar_width);
|
||||
bar_width = get_bar_width(widgets);
|
||||
}
|
||||
|
||||
|
@ -180,56 +217,39 @@ void DownloadProgressBar::to_stream(std::ostream & stream) {
|
|||
stream << tty::reset;
|
||||
}
|
||||
|
||||
for (auto & msg : get_messages()) {
|
||||
auto message_type = msg.first;
|
||||
auto message = msg.second;
|
||||
std::size_t message_index = 0;
|
||||
for (const auto & [message_type, message] : get_messages()) {
|
||||
const auto & prefix = get_message_prefix();
|
||||
|
||||
const auto & prefix = ">>> ";
|
||||
const auto prefix_width = libdnf5::cli::utils::utf8::width(prefix);
|
||||
stream << std::endl << prefix;
|
||||
|
||||
stream << std::endl;
|
||||
// print only part of the prefix that fits the terminal width
|
||||
stream << libdnf5::cli::utils::utf8::substr_width(prefix, 0, terminal_width);
|
||||
|
||||
if (prefix_width < terminal_width) {
|
||||
// only proceed if there is at least some space for the message
|
||||
color_used = false;
|
||||
if (tty::is_coloring_enabled()) {
|
||||
// color the message in interactive terminal
|
||||
switch (message_type) {
|
||||
case MessageType::INFO:
|
||||
break;
|
||||
case MessageType::SUCCESS:
|
||||
stream << tty::green;
|
||||
color_used = true;
|
||||
break;
|
||||
case MessageType::WARNING:
|
||||
stream << tty::yellow;
|
||||
color_used = true;
|
||||
break;
|
||||
case MessageType::ERROR:
|
||||
stream << tty::red;
|
||||
color_used = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add padding to fully fill the terminal_width, this is because MultiProgressBar
|
||||
// overrides its own messages, it doesn't clear the lines.
|
||||
// If the message is short some leftover characters could be still present after it.
|
||||
const auto message_width = libdnf5::cli::utils::utf8::width(message);
|
||||
const auto space_available = terminal_width - prefix_width;
|
||||
if (message_width < space_available) {
|
||||
message.append(space_available - message_width, ' ');
|
||||
}
|
||||
|
||||
// print only part of the message that fits the terminal width
|
||||
stream << libdnf5::cli::utils::utf8::substr_width(message, 0, space_available);
|
||||
|
||||
if (color_used) {
|
||||
stream << tty::reset;
|
||||
color_used = false;
|
||||
if (tty::is_coloring_enabled()) {
|
||||
// color the message in interactive terminal
|
||||
switch (message_type) {
|
||||
case MessageType::INFO:
|
||||
break;
|
||||
case MessageType::SUCCESS:
|
||||
stream << tty::green;
|
||||
color_used = true;
|
||||
break;
|
||||
case MessageType::WARNING:
|
||||
stream << tty::yellow;
|
||||
color_used = true;
|
||||
break;
|
||||
case MessageType::ERROR:
|
||||
stream << tty::red;
|
||||
color_used = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stream << message;
|
||||
|
||||
if (color_used) {
|
||||
stream << tty::reset;
|
||||
}
|
||||
++message_index;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,73 +32,106 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
namespace libdnf5::cli::progressbar {
|
||||
|
||||
class MultiProgressBar::Impl {
|
||||
public:
|
||||
Impl();
|
||||
|
||||
MultiProgressBar::MultiProgressBar() : total(0, _("Total")) {
|
||||
std::size_t total_bar_visible_limit{0};
|
||||
std::vector<std::unique_ptr<ProgressBar>> bars_all;
|
||||
std::vector<ProgressBar *> bars_todo;
|
||||
std::vector<ProgressBar *> bars_done;
|
||||
DownloadProgressBar total;
|
||||
// Whether the last line was printed without a new line ending (such as an in progress bar)
|
||||
bool line_printed{false};
|
||||
std::size_t num_of_lines_to_clear{0};
|
||||
};
|
||||
|
||||
MultiProgressBar::Impl::Impl() : total(0, _("Total")) {
|
||||
total.set_auto_finish(false);
|
||||
total.start();
|
||||
}
|
||||
|
||||
|
||||
MultiProgressBar::MultiProgressBar() : p_impl(new Impl()) {
|
||||
if (tty::is_interactive()) {
|
||||
std::cerr << tty::cursor_hide;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MultiProgressBar::~MultiProgressBar() {
|
||||
if (tty::is_interactive()) {
|
||||
std::cerr << tty::cursor_show;
|
||||
}
|
||||
}
|
||||
|
||||
void MultiProgressBar::print() {
|
||||
std::cerr << *this;
|
||||
std::cerr << std::flush;
|
||||
}
|
||||
|
||||
void MultiProgressBar::set_total_bar_visible_limit(std::size_t value) noexcept {
|
||||
p_impl->total_bar_visible_limit = value;
|
||||
}
|
||||
|
||||
DownloadProgressBar & MultiProgressBar::get_total_bar() noexcept {
|
||||
return p_impl->total;
|
||||
}
|
||||
|
||||
void MultiProgressBar::set_total_bar_number_widget_visible(bool value) noexcept {
|
||||
p_impl->total.set_number_widget_visible(value);
|
||||
}
|
||||
|
||||
void MultiProgressBar::add_bar(std::unique_ptr<ProgressBar> && bar) {
|
||||
bars_todo.push_back(bar.get());
|
||||
p_impl->bars_todo.push_back(bar.get());
|
||||
|
||||
// if the number is not set, automatically find and set the next available
|
||||
if (bar->get_number() == 0) {
|
||||
int number = 0;
|
||||
for (auto i : bars_todo) {
|
||||
for (auto i : p_impl->bars_todo) {
|
||||
number = std::max(number, i->get_number());
|
||||
}
|
||||
bar->set_number(number + 1);
|
||||
}
|
||||
|
||||
bars_all.push_back(std::move(bar));
|
||||
p_impl->bars_all.push_back(std::move(bar));
|
||||
|
||||
// update total (in [num/total]) in total progress bar
|
||||
auto registered_bars_count = static_cast<int32_t>(bars_all.size());
|
||||
if (total.get_total() < registered_bars_count) {
|
||||
total.set_total(registered_bars_count);
|
||||
auto registered_bars_count = static_cast<int32_t>(p_impl->bars_all.size());
|
||||
if (p_impl->total.get_total() < registered_bars_count) {
|
||||
p_impl->total.set_total(registered_bars_count);
|
||||
}
|
||||
|
||||
// update total (in [num/total]) in all bars to do
|
||||
for (auto & i : bars_todo) {
|
||||
i->set_total(total.get_total());
|
||||
for (auto & i : p_impl->bars_todo) {
|
||||
i->set_total(p_impl->total.get_total());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MultiProgressBar::set_total_num_of_bars(std::size_t value) noexcept {
|
||||
if (value < bars_all.size()) {
|
||||
value = bars_all.size();
|
||||
if (value < p_impl->bars_all.size()) {
|
||||
value = p_impl->bars_all.size();
|
||||
}
|
||||
auto num_of_bars = static_cast<int>(value);
|
||||
if (num_of_bars != total.get_total()) {
|
||||
total.set_total(num_of_bars);
|
||||
if (num_of_bars != p_impl->total.get_total()) {
|
||||
p_impl->total.set_total(num_of_bars);
|
||||
|
||||
// update total (in [num/total]) in all bars to do
|
||||
for (auto & i : bars_todo) {
|
||||
i->set_total(total.get_total());
|
||||
for (auto & i : p_impl->bars_todo) {
|
||||
i->set_total(p_impl->total.get_total());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::size_t MultiProgressBar::get_total_num_of_bars() const noexcept {
|
||||
return static_cast<std::size_t>(total.get_total());
|
||||
return static_cast<std::size_t>(p_impl->total.get_total());
|
||||
}
|
||||
|
||||
|
||||
std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) {
|
||||
const bool is_interactive{tty::is_interactive()};
|
||||
auto terminal_width = static_cast<std::size_t>(tty::get_width());
|
||||
|
||||
// We'll buffer the output text to a single string and print it all at once.
|
||||
// This is to avoid multiple writes to the terminal, which can cause flickering.
|
||||
|
@ -106,30 +139,28 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) {
|
|||
text_buffer.str("");
|
||||
text_buffer.clear();
|
||||
|
||||
std::size_t last_num_of_lines_to_clear = mbar.num_of_lines_to_clear;
|
||||
std::size_t num_of_lines_permanent = 0;
|
||||
|
||||
if (is_interactive && mbar.num_of_lines_to_clear > 0) {
|
||||
if (mbar.num_of_lines_to_clear > 1) {
|
||||
if (is_interactive && mbar.p_impl->num_of_lines_to_clear > 0) {
|
||||
if (mbar.p_impl->num_of_lines_to_clear > 1) {
|
||||
// Move the cursor up by the number of lines we want to write over
|
||||
text_buffer << "\033[" << (mbar.num_of_lines_to_clear - 1) << "A";
|
||||
text_buffer << "\033[" << (mbar.p_impl->num_of_lines_to_clear - 1) << "A";
|
||||
}
|
||||
text_buffer << "\r";
|
||||
text_buffer << tty::clear_to_end;
|
||||
}
|
||||
|
||||
// Number of lines that need to be cleared, including the last line even if it is empty
|
||||
mbar.num_of_lines_to_clear = 0;
|
||||
mbar.line_printed = false;
|
||||
mbar.p_impl->num_of_lines_to_clear = 0;
|
||||
mbar.p_impl->line_printed = false;
|
||||
|
||||
// store numbers of bars in progress
|
||||
std::vector<int32_t> numbers;
|
||||
for (auto * bar : mbar.bars_todo) {
|
||||
for (auto * bar : mbar.p_impl->bars_todo) {
|
||||
numbers.insert(numbers.begin(), bar->get_number());
|
||||
}
|
||||
|
||||
// print completed bars first and remove them from the list
|
||||
for (std::size_t i = 0; i < mbar.bars_todo.size(); i++) {
|
||||
auto * bar = mbar.bars_todo[i];
|
||||
for (std::size_t i = 0; i < mbar.p_impl->bars_todo.size(); i++) {
|
||||
auto * bar = mbar.p_impl->bars_todo[i];
|
||||
if (!bar->is_finished()) {
|
||||
continue;
|
||||
}
|
||||
|
@ -137,16 +168,14 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) {
|
|||
numbers.pop_back();
|
||||
text_buffer << *bar;
|
||||
text_buffer << std::endl;
|
||||
num_of_lines_permanent++;
|
||||
num_of_lines_permanent += bar->get_messages().size();
|
||||
mbar.bars_done.push_back(bar);
|
||||
mbar.p_impl->bars_done.push_back(bar);
|
||||
// TODO(dmach): use iterator
|
||||
mbar.bars_todo.erase(mbar.bars_todo.begin() + static_cast<int>(i));
|
||||
mbar.p_impl->bars_todo.erase(mbar.p_impl->bars_todo.begin() + static_cast<int>(i));
|
||||
i--;
|
||||
}
|
||||
|
||||
// then print incomplete
|
||||
for (auto & bar : mbar.bars_todo) {
|
||||
for (auto & bar : mbar.p_impl->bars_todo) {
|
||||
bar->set_number(numbers.back());
|
||||
numbers.pop_back();
|
||||
|
||||
|
@ -160,13 +189,13 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) {
|
|||
bar->update();
|
||||
continue;
|
||||
}
|
||||
if (mbar.line_printed) {
|
||||
if (mbar.p_impl->line_printed) {
|
||||
text_buffer << std::endl;
|
||||
}
|
||||
text_buffer << *bar;
|
||||
mbar.line_printed = true;
|
||||
mbar.num_of_lines_to_clear++;
|
||||
mbar.num_of_lines_to_clear += bar->get_messages().size();
|
||||
mbar.p_impl->line_printed = true;
|
||||
mbar.p_impl->num_of_lines_to_clear++;
|
||||
mbar.p_impl->num_of_lines_to_clear += bar->calculate_messages_terminal_lines(terminal_width);
|
||||
}
|
||||
|
||||
// then print the "total" progress bar
|
||||
|
@ -174,7 +203,7 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) {
|
|||
int64_t ticks = 0;
|
||||
int64_t total_ticks = 0;
|
||||
|
||||
for (auto & bar : mbar.bars_done) {
|
||||
for (auto & bar : mbar.p_impl->bars_done) {
|
||||
total_numbers = std::max(total_numbers, bar->get_total());
|
||||
// completed bars can be unfinished
|
||||
// add only processed ticks to both values
|
||||
|
@ -182,63 +211,44 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) {
|
|||
ticks += bar->get_ticks();
|
||||
}
|
||||
|
||||
for (auto & bar : mbar.bars_todo) {
|
||||
for (auto & bar : mbar.p_impl->bars_todo) {
|
||||
total_numbers = std::max(total_numbers, bar->get_total());
|
||||
total_ticks += bar->get_total_ticks();
|
||||
ticks += bar->get_ticks();
|
||||
}
|
||||
|
||||
|
||||
if ((mbar.bars_all.size() >= mbar.total_bar_visible_limit) && (is_interactive || mbar.bars_todo.empty())) {
|
||||
if (mbar.line_printed) {
|
||||
if ((mbar.p_impl->bars_all.size() >= mbar.p_impl->total_bar_visible_limit) &&
|
||||
(is_interactive || mbar.p_impl->bars_todo.empty())) {
|
||||
if (mbar.p_impl->line_printed) {
|
||||
text_buffer << std::endl;
|
||||
}
|
||||
// print divider
|
||||
int terminal_width = tty::get_width();
|
||||
text_buffer << std::string(static_cast<std::size_t>(terminal_width), '-');
|
||||
text_buffer << std::string(terminal_width, '-');
|
||||
text_buffer << std::endl;
|
||||
|
||||
// print Total progress bar
|
||||
mbar.total.set_number(static_cast<int>(mbar.bars_done.size()));
|
||||
auto & mbar_total = mbar.get_total_bar();
|
||||
mbar_total.set_number(static_cast<int>(mbar.p_impl->bars_done.size()));
|
||||
|
||||
mbar.total.set_total_ticks(total_ticks);
|
||||
mbar.total.set_ticks(ticks);
|
||||
mbar_total.set_total_ticks(total_ticks);
|
||||
mbar_total.set_ticks(ticks);
|
||||
|
||||
if (mbar.bars_todo.empty()) {
|
||||
if (mbar.p_impl->bars_todo.empty()) {
|
||||
// all bars have finished, set the "Total" bar as finished too according to their states
|
||||
mbar.total.set_state(ProgressBarState::SUCCESS);
|
||||
mbar_total.set_state(ProgressBarState::SUCCESS);
|
||||
}
|
||||
|
||||
text_buffer << mbar.total;
|
||||
text_buffer << mbar_total;
|
||||
// +1 for the divider line, +1 for the total bar line
|
||||
mbar.num_of_lines_to_clear += 2;
|
||||
if (mbar.total.is_finished()) {
|
||||
mbar.p_impl->num_of_lines_to_clear += 2;
|
||||
if (mbar_total.is_finished()) {
|
||||
text_buffer << std::endl;
|
||||
} else {
|
||||
mbar.line_printed = true;
|
||||
mbar.p_impl->line_printed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have written less lines than last time we need to clear the rest otherwise
|
||||
// there would be garbage under the updated progressbar. This is because normally
|
||||
// we don't actually clear the lines we just write over the old output to ensure smooth
|
||||
// output updating.
|
||||
// TODO(amatej): It would be sufficient to do this only once after all progressbars have
|
||||
// finished but unfortunaly MultiProgressBar doesn't have a pImpl so we cannot
|
||||
// store the highest line count it had. We could fix this when breaking ABI.
|
||||
size_t all_written = num_of_lines_permanent + mbar.num_of_lines_to_clear;
|
||||
if (last_num_of_lines_to_clear > all_written) {
|
||||
auto delta = last_num_of_lines_to_clear - all_written;
|
||||
if (!mbar.line_printed) {
|
||||
text_buffer << tty::clear_line;
|
||||
}
|
||||
for (std::size_t i = 0; i < delta; i++) {
|
||||
text_buffer << tty::cursor_down << tty::clear_line;
|
||||
}
|
||||
// Move cursor back up after clearing lines leftover from previous print
|
||||
text_buffer << "\033[" << delta << "A";
|
||||
}
|
||||
|
||||
stream << text_buffer.str(); // Single syscall to output all commands
|
||||
|
||||
return stream;
|
||||
|
|
|
@ -20,36 +20,241 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
#include "libdnf5-cli/progressbar/progress_bar.hpp"
|
||||
|
||||
#include <optional>
|
||||
|
||||
|
||||
namespace libdnf5::cli::progressbar {
|
||||
|
||||
class ProgressBar::Impl {
|
||||
public:
|
||||
struct MessageMetrics {
|
||||
std::size_t terminal_width;
|
||||
std::size_t lines;
|
||||
std::size_t padding;
|
||||
};
|
||||
|
||||
ProgressBar::ProgressBar(int64_t total_ticks) : total_ticks{total_ticks} {}
|
||||
explicit Impl(int64_t total_ticks) : total_ticks{total_ticks} {}
|
||||
Impl(int64_t total_ticks, const std::string & description) : total_ticks{total_ticks}, description{description} {}
|
||||
|
||||
const MessageMetrics & get_message_metrics(
|
||||
std::size_t terminal_width, std::string_view message, std::size_t message_index) noexcept;
|
||||
|
||||
// ticks
|
||||
int64_t ticks = -1;
|
||||
int64_t total_ticks = -1;
|
||||
|
||||
// numbers
|
||||
int32_t number = 0;
|
||||
int32_t total = 0;
|
||||
|
||||
// description
|
||||
std::string description;
|
||||
|
||||
// messages
|
||||
std::vector<ProgressBar::Message> messages;
|
||||
std::string message_prefix{">>> "};
|
||||
// cache for number of terminal lines occupied by the message and padding
|
||||
// needed to fill the last line completely.
|
||||
std::vector<std::optional<MessageMetrics>> messages_metrics_cache;
|
||||
|
||||
ProgressBarState state = ProgressBarState::READY;
|
||||
|
||||
int32_t percent_done = -1;
|
||||
int64_t elapsed_seconds = 0;
|
||||
int64_t remaining_seconds = 0;
|
||||
int64_t average_speed = 0;
|
||||
|
||||
bool auto_finish = true;
|
||||
|
||||
std::chrono::time_point<std::chrono::system_clock> begin = std::chrono::system_clock::from_time_t(0);
|
||||
std::chrono::time_point<std::chrono::system_clock> end = std::chrono::system_clock::from_time_t(0);
|
||||
|
||||
// current (average) speed over the last second
|
||||
std::chrono::time_point<std::chrono::system_clock> current_speed_window_start = std::chrono::system_clock::now();
|
||||
int64_t current_speed = 0;
|
||||
int64_t current_speed_window_ticks = 0;
|
||||
};
|
||||
|
||||
const ProgressBar::Impl::MessageMetrics & ProgressBar::Impl::get_message_metrics(
|
||||
std::size_t terminal_width, std::string_view message, std::size_t message_index) noexcept {
|
||||
auto & cached_mm = messages_metrics_cache.at(message_index);
|
||||
if (cached_mm && (*cached_mm).terminal_width == terminal_width) {
|
||||
// the value is cached and valid for current terminal_with
|
||||
return *cached_mm;
|
||||
}
|
||||
|
||||
std::size_t msg_lines = 1;
|
||||
std::size_t current_column = 0;
|
||||
std::mbstate_t mbstate = std::mbstate_t();
|
||||
|
||||
while (!message.empty()) {
|
||||
if (message.front() == '\n') {
|
||||
++msg_lines;
|
||||
current_column = 0;
|
||||
message.remove_prefix(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// calculate the display width of the character
|
||||
wchar_t wc;
|
||||
auto bytes_consumed = std::mbrtowc(&wc, message.data(), message.size(), &mbstate);
|
||||
if (bytes_consumed <= 0) {
|
||||
break;
|
||||
}
|
||||
auto char_width = static_cast<std::size_t>(wcwidth(wc));
|
||||
|
||||
// If the character doesn't fit, wrap to the next line
|
||||
if (current_column + char_width > terminal_width) {
|
||||
++msg_lines;
|
||||
current_column = char_width;
|
||||
} else {
|
||||
current_column += char_width;
|
||||
}
|
||||
|
||||
message.remove_prefix(static_cast<std::size_t>(bytes_consumed));
|
||||
}
|
||||
messages_metrics_cache[message_index].emplace(
|
||||
MessageMetrics{terminal_width, msg_lines, terminal_width - current_column});
|
||||
return messages_metrics_cache.at(message_index).value();
|
||||
}
|
||||
|
||||
|
||||
ProgressBar::~ProgressBar() = default;
|
||||
|
||||
ProgressBar::ProgressBar(int64_t total_ticks) : p_impl(new Impl(total_ticks)) {}
|
||||
|
||||
ProgressBar::ProgressBar(int64_t total_ticks, const std::string & description)
|
||||
: total_ticks{total_ticks},
|
||||
description{description} {}
|
||||
: p_impl(new Impl(total_ticks, description)) {}
|
||||
|
||||
ProgressBar::ProgressBar(const ProgressBar & src) = default;
|
||||
ProgressBar & ProgressBar::operator=(const ProgressBar & src) = default;
|
||||
ProgressBar::ProgressBar(ProgressBar && src) noexcept = default;
|
||||
ProgressBar & ProgressBar::operator=(ProgressBar && src) noexcept = default;
|
||||
|
||||
|
||||
int64_t ProgressBar::get_ticks() const noexcept {
|
||||
return p_impl->ticks;
|
||||
}
|
||||
|
||||
int64_t ProgressBar::get_total_ticks() const noexcept {
|
||||
return p_impl->total_ticks;
|
||||
}
|
||||
|
||||
int32_t ProgressBar::get_number() const noexcept {
|
||||
return p_impl->number;
|
||||
}
|
||||
|
||||
void ProgressBar::set_number(int32_t value) {
|
||||
p_impl->number = value;
|
||||
}
|
||||
|
||||
int32_t ProgressBar::get_total() const noexcept {
|
||||
return p_impl->total;
|
||||
}
|
||||
|
||||
void ProgressBar::set_total(int32_t value) {
|
||||
p_impl->total = value;
|
||||
}
|
||||
|
||||
ProgressBarState ProgressBar::get_state() const noexcept {
|
||||
return p_impl->state;
|
||||
}
|
||||
|
||||
void ProgressBar::set_state(ProgressBarState value) {
|
||||
update();
|
||||
p_impl->state = value;
|
||||
}
|
||||
|
||||
bool ProgressBar::is_finished() const noexcept {
|
||||
return get_state() != ProgressBarState::READY && get_state() != ProgressBarState::STARTED;
|
||||
}
|
||||
|
||||
bool ProgressBar::is_failed() const noexcept {
|
||||
return get_state() == ProgressBarState::ERROR || get_state() == ProgressBarState::WARNING;
|
||||
}
|
||||
|
||||
std::string ProgressBar::get_description() const noexcept {
|
||||
return p_impl->description;
|
||||
}
|
||||
|
||||
void ProgressBar::set_description(const std::string & value) {
|
||||
p_impl->description = value;
|
||||
}
|
||||
|
||||
void ProgressBar::add_message(MessageType type, const std::string & message) {
|
||||
p_impl->messages.emplace_back(type, message);
|
||||
p_impl->messages_metrics_cache.emplace_back(std::nullopt);
|
||||
}
|
||||
|
||||
const std::vector<ProgressBar::Message> & ProgressBar::get_messages() const noexcept {
|
||||
return p_impl->messages;
|
||||
}
|
||||
|
||||
const std::string & ProgressBar::get_message_prefix() const noexcept {
|
||||
return p_impl->message_prefix;
|
||||
}
|
||||
|
||||
std::size_t ProgressBar::calculate_messages_terminal_lines(std::size_t terminal_width) {
|
||||
std::size_t num_lines = 0;
|
||||
std::size_t message_index = 0;
|
||||
for (const auto & [msg_type, msg] : get_messages()) {
|
||||
std::string full_msg = p_impl->message_prefix + msg;
|
||||
num_lines += p_impl->get_message_metrics(terminal_width, full_msg, message_index).lines;
|
||||
++message_index;
|
||||
}
|
||||
return num_lines;
|
||||
}
|
||||
|
||||
bool ProgressBar::get_auto_finish() const noexcept {
|
||||
return p_impl->auto_finish;
|
||||
}
|
||||
|
||||
void ProgressBar::set_auto_finish(bool value) {
|
||||
p_impl->auto_finish = value;
|
||||
}
|
||||
|
||||
int32_t ProgressBar::get_percent_done() const noexcept {
|
||||
return p_impl->percent_done;
|
||||
}
|
||||
|
||||
int64_t ProgressBar::get_current_speed() const noexcept {
|
||||
return p_impl->current_speed;
|
||||
}
|
||||
|
||||
int64_t ProgressBar::get_average_speed() const noexcept {
|
||||
return p_impl->average_speed;
|
||||
}
|
||||
|
||||
int64_t ProgressBar::get_elapsed_seconds() const noexcept {
|
||||
return p_impl->elapsed_seconds;
|
||||
}
|
||||
|
||||
int64_t ProgressBar::get_remaining_seconds() const noexcept {
|
||||
return p_impl->remaining_seconds;
|
||||
}
|
||||
|
||||
std::chrono::time_point<std::chrono::system_clock> ProgressBar::get_begin() {
|
||||
return p_impl->begin;
|
||||
}
|
||||
|
||||
void ProgressBar::reset() {
|
||||
ticks = -1;
|
||||
total_ticks = -1;
|
||||
number = 0;
|
||||
total = 0;
|
||||
description = "";
|
||||
messages.clear();
|
||||
state = ProgressBarState::READY;
|
||||
percent_done = -1;
|
||||
elapsed_seconds = 0;
|
||||
remaining_seconds = 0;
|
||||
average_speed = 0;
|
||||
auto_finish = true;
|
||||
begin = std::chrono::system_clock::from_time_t(0);
|
||||
end = std::chrono::system_clock::from_time_t(0);
|
||||
current_speed_window_start = std::chrono::system_clock::now();
|
||||
current_speed = 0;
|
||||
current_speed_window_ticks = 0;
|
||||
p_impl->ticks = -1;
|
||||
p_impl->total_ticks = -1;
|
||||
p_impl->number = 0;
|
||||
p_impl->total = 0;
|
||||
p_impl->description = "";
|
||||
p_impl->messages.clear();
|
||||
p_impl->state = ProgressBarState::READY;
|
||||
p_impl->percent_done = -1;
|
||||
p_impl->elapsed_seconds = 0;
|
||||
p_impl->remaining_seconds = 0;
|
||||
p_impl->average_speed = 0;
|
||||
p_impl->auto_finish = true;
|
||||
p_impl->begin = std::chrono::system_clock::from_time_t(0);
|
||||
p_impl->end = std::chrono::system_clock::from_time_t(0);
|
||||
p_impl->current_speed_window_start = std::chrono::system_clock::now();
|
||||
p_impl->current_speed = 0;
|
||||
p_impl->current_speed_window_ticks = 0;
|
||||
}
|
||||
|
||||
|
||||
|
@ -58,27 +263,27 @@ void ProgressBar::set_ticks(int64_t value) {
|
|||
return;
|
||||
}
|
||||
auto new_ticks = value;
|
||||
if (total_ticks >= 0) {
|
||||
new_ticks = std::min(new_ticks, total_ticks);
|
||||
if (p_impl->total_ticks >= 0) {
|
||||
new_ticks = std::min(new_ticks, p_impl->total_ticks);
|
||||
}
|
||||
if (new_ticks >= ticks) {
|
||||
current_speed_window_ticks += new_ticks - ticks;
|
||||
if (new_ticks >= p_impl->ticks) {
|
||||
p_impl->current_speed_window_ticks += new_ticks - p_impl->ticks;
|
||||
} else {
|
||||
current_speed_window_ticks = 0;
|
||||
p_impl->current_speed_window_ticks = 0;
|
||||
}
|
||||
ticks = new_ticks;
|
||||
p_impl->ticks = new_ticks;
|
||||
}
|
||||
|
||||
|
||||
void ProgressBar::add_ticks(int64_t value) {
|
||||
set_ticks(ticks + value);
|
||||
set_ticks(p_impl->ticks + value);
|
||||
}
|
||||
|
||||
|
||||
void ProgressBar::start() {
|
||||
if (begin == std::chrono::system_clock::from_time_t(0)) {
|
||||
begin = std::chrono::system_clock::now();
|
||||
state = ProgressBarState::STARTED;
|
||||
if (p_impl->begin == std::chrono::system_clock::from_time_t(0)) {
|
||||
p_impl->begin = std::chrono::system_clock::now();
|
||||
p_impl->state = ProgressBarState::STARTED;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,65 +293,66 @@ void ProgressBar::update() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (total_ticks < 0) {
|
||||
if (p_impl->total_ticks < 0) {
|
||||
// unknown total ticks
|
||||
percent_done = -1;
|
||||
} else if (total_ticks == 0) {
|
||||
p_impl->percent_done = -1;
|
||||
} else if (p_impl->total_ticks == 0) {
|
||||
// can't divide by zero, consider progressbar 100% complete
|
||||
percent_done = 100;
|
||||
} else if (ticks >= total_ticks) {
|
||||
percent_done = 100;
|
||||
p_impl->percent_done = 100;
|
||||
} else if (p_impl->ticks >= p_impl->total_ticks) {
|
||||
p_impl->percent_done = 100;
|
||||
} else {
|
||||
percent_done = static_cast<int32_t>(static_cast<float>(ticks) / static_cast<float>(total_ticks) * 100);
|
||||
p_impl->percent_done =
|
||||
static_cast<int32_t>(static_cast<float>(p_impl->ticks) / static_cast<float>(p_impl->total_ticks) * 100);
|
||||
}
|
||||
|
||||
auto now = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// compute the current speed (ticks per second)
|
||||
auto delta = now - current_speed_window_start;
|
||||
auto delta = now - p_impl->current_speed_window_start;
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(delta).count();
|
||||
|
||||
if (ms > 950) {
|
||||
current_speed = current_speed_window_ticks * 1000 / ms;
|
||||
p_impl->current_speed = p_impl->current_speed_window_ticks * 1000 / ms;
|
||||
// reset the window
|
||||
current_speed_window_ticks = 0;
|
||||
current_speed_window_start = now;
|
||||
} else if (current_speed == 0 && ms != 0) {
|
||||
current_speed = current_speed_window_ticks * 1000 / ms;
|
||||
p_impl->current_speed_window_ticks = 0;
|
||||
p_impl->current_speed_window_start = now;
|
||||
} else if (p_impl->current_speed == 0 && ms != 0) {
|
||||
p_impl->current_speed = p_impl->current_speed_window_ticks * 1000 / ms;
|
||||
}
|
||||
|
||||
// compute average speed
|
||||
delta = now - begin;
|
||||
delta = now - p_impl->begin;
|
||||
ms = std::chrono::duration_cast<std::chrono::milliseconds>(delta).count();
|
||||
if (ms == 0) {
|
||||
average_speed = 0;
|
||||
p_impl->average_speed = 0;
|
||||
} else {
|
||||
average_speed = ticks * 1000 / ms;
|
||||
p_impl->average_speed = p_impl->ticks * 1000 / ms;
|
||||
}
|
||||
|
||||
// compute elapsed seconds
|
||||
// round the result to display 00m00s less frequently
|
||||
elapsed_seconds = std::chrono::round<std::chrono::seconds>(delta).count();
|
||||
p_impl->elapsed_seconds = std::chrono::round<std::chrono::seconds>(delta).count();
|
||||
|
||||
// compute remaining time
|
||||
if (total_ticks >= 0) {
|
||||
int64_t remaining_ticks = total_ticks - ticks;
|
||||
if (current_speed != 0) {
|
||||
remaining_seconds = remaining_ticks / current_speed;
|
||||
if (p_impl->total_ticks >= 0) {
|
||||
int64_t remaining_ticks = p_impl->total_ticks - p_impl->ticks;
|
||||
if (p_impl->current_speed != 0) {
|
||||
p_impl->remaining_seconds = remaining_ticks / p_impl->current_speed;
|
||||
}
|
||||
} else {
|
||||
// unknown total ticks
|
||||
remaining_seconds = -1;
|
||||
p_impl->remaining_seconds = -1;
|
||||
}
|
||||
|
||||
if (total_ticks >= 0 && ticks >= total_ticks) {
|
||||
if (auto_finish && state == ProgressBarState::STARTED) {
|
||||
if (p_impl->total_ticks >= 0 && p_impl->ticks >= p_impl->total_ticks) {
|
||||
if (p_impl->auto_finish && p_impl->state == ProgressBarState::STARTED) {
|
||||
// avoid calling set_state() because it triggers update() and ends up in an endless recursion
|
||||
state = ProgressBarState::SUCCESS;
|
||||
p_impl->state = ProgressBarState::SUCCESS;
|
||||
}
|
||||
end = now;
|
||||
percent_done = 100;
|
||||
remaining_seconds = 0;
|
||||
p_impl->end = now;
|
||||
p_impl->percent_done = 100;
|
||||
p_impl->remaining_seconds = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,13 +361,14 @@ void ProgressBar::set_total_ticks(int64_t value) {
|
|||
if (is_finished()) {
|
||||
return;
|
||||
}
|
||||
total_ticks = value;
|
||||
p_impl->total_ticks = value;
|
||||
}
|
||||
|
||||
|
||||
void ProgressBar::pop_message() {
|
||||
if (!messages.empty()) {
|
||||
messages.pop_back();
|
||||
if (!p_impl->messages.empty()) {
|
||||
p_impl->messages.pop_back();
|
||||
p_impl->messages_metrics_cache.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -126,6 +126,9 @@ TTY_COMMAND(white, "\033[37m")
|
|||
// tty::clear_line
|
||||
TTY_COMMAND(clear_line, "\033[2K")
|
||||
|
||||
// tty::clear_to_end
|
||||
TTY_COMMAND(clear_to_end, "\033[0J")
|
||||
|
||||
// tty::cursor_up
|
||||
TTY_COMMAND(cursor_up, "\x1b[A")
|
||||
|
||||
|
|
|
@ -27,12 +27,14 @@ along with libdnf. If not, see <https://www.gnu.org/licenses/>.
|
|||
#include <libdnf5-cli/progressbar/download_progress_bar.hpp>
|
||||
#include <libdnf5-cli/progressbar/multi_progress_bar.hpp>
|
||||
|
||||
#include <regex>
|
||||
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(ProgressbarInteractiveTest);
|
||||
|
||||
namespace {
|
||||
|
||||
// We look for control sequnces (such as move cursor up N times and carriage return)
|
||||
// We look for control sequences (such as move cursor up N times and carriage return)
|
||||
// and perform them. It basically simulates a terminal emulator.
|
||||
//
|
||||
// It can look like: "\x1b[9A\r" = move cursor 9 times up followed by carriage return
|
||||
|
@ -71,7 +73,7 @@ std::string perform_control_sequences(std::string target) {
|
|||
state = CONTROL_SEQUENCE_AMOUNT;
|
||||
} else if ((state == CONTROL_SEQUENCE_INTRO || state == CONTROL_SEQUENCE_AMOUNT) && current_value == 'A') {
|
||||
if (amount > current_row) {
|
||||
CPPUNIT_FAIL(fmt::format("Cursor up control sequnce outside of output"));
|
||||
CPPUNIT_FAIL(fmt::format("Cursor up control sequence outside of output"));
|
||||
}
|
||||
current_row -= amount;
|
||||
state = EMPTY;
|
||||
|
@ -84,6 +86,14 @@ std::string perform_control_sequences(std::string target) {
|
|||
output[current_row].clear();
|
||||
}
|
||||
state = EMPTY;
|
||||
} else if (state == CONTROL_SEQUENCE_AMOUNT && amount == 0 && current_value == 'J') {
|
||||
// erase the entire output starting with current line
|
||||
if (current_row < output.size()) {
|
||||
for (std::size_t i = current_row; i < output.size(); ++i) {
|
||||
output[i].clear();
|
||||
}
|
||||
}
|
||||
state = EMPTY;
|
||||
} else if (state == CONTROL_SEQUENCE_AMOUNT && current_value == 'm') {
|
||||
// This is a color control sequence, just skip it
|
||||
state = EMPTY;
|
||||
|
@ -109,6 +119,31 @@ std::string perform_control_sequences(std::string target) {
|
|||
return libdnf5::utils::string::join(output, "\n");
|
||||
}
|
||||
|
||||
int count_cursor_up_lines(const std::string & text) {
|
||||
int lines_up = 0;
|
||||
|
||||
// Regular expression to find "\x1b[A" or "\x1b[NA"
|
||||
std::regex cursor_up_regex("\\x1b\\[(\\d*)A");
|
||||
|
||||
auto end_of_sequence = std::sregex_iterator();
|
||||
for (std::sregex_iterator cursor_up = std::sregex_iterator(text.begin(), text.end(), cursor_up_regex);
|
||||
cursor_up != end_of_sequence;
|
||||
++cursor_up) {
|
||||
std::smatch match = *cursor_up;
|
||||
std::string number = match[1].str(); // The captured number string
|
||||
|
||||
if (number.empty()) {
|
||||
// Case: "\x1b[A" (equivalent to N=1)
|
||||
++lines_up;
|
||||
} else {
|
||||
// Case: "\x1b[NA"
|
||||
lines_up += std::stoi(number);
|
||||
}
|
||||
}
|
||||
|
||||
return lines_up;
|
||||
}
|
||||
|
||||
} //namespace
|
||||
|
||||
void ProgressbarInteractiveTest::setUp() {
|
||||
|
@ -188,9 +223,9 @@ void ProgressbarInteractiveTest::test_download_progress_bar_with_messages() {
|
|||
oss << *download_progress_bar;
|
||||
Pattern expected =
|
||||
"\\[0/0\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> test message1 \n"
|
||||
">>> test message2 \n"
|
||||
">>> test もで 諤奯ゞ ";
|
||||
">>> test message1\n"
|
||||
">>> test message2\n"
|
||||
">>> test もで 諤奯ゞ";
|
||||
ASSERT_MATCHES(expected, oss.str());
|
||||
|
||||
download_progress_bar->pop_message();
|
||||
|
@ -203,6 +238,87 @@ void ProgressbarInteractiveTest::test_download_progress_bar_with_messages() {
|
|||
ASSERT_MATCHES(expected, oss.str());
|
||||
}
|
||||
|
||||
void ProgressbarInteractiveTest::test_download_progress_bar_with_long_messages() {
|
||||
// The messages that do not fit within the terminal line are not trimmed.
|
||||
// After the message is removed, the correct number of "cursor up" control
|
||||
// sequences is issued.
|
||||
|
||||
auto download_progress_bar = std::make_unique<libdnf5::cli::progressbar::DownloadProgressBar>(10, "test");
|
||||
|
||||
libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar;
|
||||
multi_progress_bar.set_total_bar_visible_limit(libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT);
|
||||
auto download_progress_bar_raw = download_progress_bar.get();
|
||||
multi_progress_bar.add_bar(std::move(download_progress_bar));
|
||||
|
||||
download_progress_bar_raw->set_ticks(4);
|
||||
download_progress_bar_raw->set_state(libdnf5::cli::progressbar::ProgressBarState::STARTED);
|
||||
|
||||
{
|
||||
// A string exactly 70 characters long (there will be a ">>> " prefix
|
||||
// prepended) fits on one terminal line
|
||||
download_progress_bar_raw->add_message(
|
||||
libdnf5::cli::progressbar::MessageType::INFO,
|
||||
".....1.........2.........3.........4.........5.........6.........7");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << multi_progress_bar;
|
||||
Pattern expected =
|
||||
"\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> .....1.........2.........3.........4.........5.........6.........7";
|
||||
|
||||
ASSERT_MATCHES(expected, perform_control_sequences(oss.str()));
|
||||
|
||||
download_progress_bar_raw->pop_message();
|
||||
oss << multi_progress_bar;
|
||||
expected = "\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n";
|
||||
CPPUNIT_ASSERT_EQUAL(1, count_cursor_up_lines(oss.str()));
|
||||
}
|
||||
|
||||
{
|
||||
// A message longer than 70 characters will be split to multiple lines
|
||||
download_progress_bar_raw->add_message(
|
||||
libdnf5::cli::progressbar::MessageType::INFO,
|
||||
".....1.........2.........3.........4.........5.........6.........71");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << multi_progress_bar;
|
||||
// whole message is printed
|
||||
Pattern expected =
|
||||
"\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> .....1.........2.........3.........4.........5.........6.........71";
|
||||
|
||||
ASSERT_MATCHES(expected, perform_control_sequences(oss.str()));
|
||||
|
||||
download_progress_bar_raw->pop_message();
|
||||
oss << multi_progress_bar;
|
||||
expected = "\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n";
|
||||
// two "cursor up" lines expected
|
||||
CPPUNIT_ASSERT_EQUAL(2, count_cursor_up_lines(oss.str()));
|
||||
}
|
||||
|
||||
{
|
||||
// two cols wide characters are correctly counted
|
||||
download_progress_bar_raw->add_message(
|
||||
libdnf5::cli::progressbar::MessageType::INFO,
|
||||
".....1.........2..もで...3.........4.........5.........6.........71");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << multi_progress_bar;
|
||||
// whole message is printed
|
||||
Pattern expected =
|
||||
"\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> .....1.........2..もで...3.........4.........5.........6.........71";
|
||||
|
||||
ASSERT_MATCHES(expected, perform_control_sequences(oss.str()));
|
||||
|
||||
download_progress_bar_raw->pop_message();
|
||||
oss << multi_progress_bar;
|
||||
expected = "\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n";
|
||||
// two "cursor up" lines expected
|
||||
CPPUNIT_ASSERT_EQUAL(2, count_cursor_up_lines(oss.str()));
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressbarInteractiveTest::test_multi_progress_bar_with_total_finished() {
|
||||
// In interactive mode finished multi progressbar ends with a new line.
|
||||
|
||||
|
@ -248,7 +364,7 @@ void ProgressbarInteractiveTest::test_multi_progress_bar_with_messages_with_tota
|
|||
oss << multi_progress_bar;
|
||||
Pattern expected =
|
||||
"\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> test message1 \n"
|
||||
">>> test message1\n"
|
||||
"----------------------------------------------------------------------\n"
|
||||
"\\[0/1\\] Total 40% | ????? ??B\\/s | 4.0 B | ???????";
|
||||
|
||||
|
@ -329,9 +445,9 @@ void ProgressbarInteractiveTest::test_multi_progress_bars_with_messages_with_tot
|
|||
Pattern expected =
|
||||
"\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n"
|
||||
"\\[2/2\\] test2 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> test message1 \n"
|
||||
">>> test message2 \n"
|
||||
">>> test もで 諤奯ゞ \n"
|
||||
">>> test message1\n"
|
||||
">>> test message2\n"
|
||||
">>> test もで 諤奯ゞ\n"
|
||||
"----------------------------------------------------------------------\n"
|
||||
"\\[1/2\\] Total 70% | ????? ??B\\/s | 14.0 B | ???????";
|
||||
|
||||
|
@ -377,7 +493,7 @@ void ProgressbarInteractiveTest::test_multi_progress_bar_with_messages() {
|
|||
oss << multi_progress_bar;
|
||||
Pattern expected =
|
||||
"\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> test message1 ";
|
||||
">>> test message1";
|
||||
|
||||
ASSERT_MATCHES(expected, perform_control_sequences(oss.str()));
|
||||
|
||||
|
@ -432,9 +548,9 @@ void ProgressbarInteractiveTest::test_multi_progress_bars_with_messages() {
|
|||
Pattern expected =
|
||||
"\\[1/2\\] test1 100% | ????? ??B\\/s | 10.0 B | ???????\n"
|
||||
"\\[2/2\\] test2 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> test message1 \n"
|
||||
">>> test message2 \n"
|
||||
">>> test こんにちは世界! ";
|
||||
">>> test message1\n"
|
||||
">>> test message2\n"
|
||||
">>> test こんにちは世界!";
|
||||
|
||||
ASSERT_MATCHES(expected, perform_control_sequences(oss.str()));
|
||||
|
||||
|
@ -508,7 +624,7 @@ void ProgressbarInteractiveTest::test_multi_progress_bar_with_short_messages() {
|
|||
oss << multi_progress_bar;
|
||||
Pattern expected =
|
||||
"\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> test loooooooooooooooooooooooooooooooooooooooooong message ";
|
||||
">>> test loooooooooooooooooooooooooooooooooooooooooong message";
|
||||
|
||||
ASSERT_MATCHES(expected, perform_control_sequences(oss.str()));
|
||||
|
||||
|
@ -518,7 +634,7 @@ void ProgressbarInteractiveTest::test_multi_progress_bar_with_short_messages() {
|
|||
oss << multi_progress_bar;
|
||||
expected =
|
||||
"\\[1/1\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
">>> test short message ";
|
||||
">>> test short message";
|
||||
|
||||
ASSERT_MATCHES(expected, perform_control_sequences(oss.str()));
|
||||
}
|
||||
|
@ -547,7 +663,7 @@ void ProgressbarInteractiveTest::test_multi_progress_bars_with_already_downloade
|
|||
oss << multi_progress_bar;
|
||||
Pattern expected =
|
||||
"\\[1/3\\] test 100% | ????? ??B\\/s | 0.0 B | ???????\n"
|
||||
">>> Already Downloaded \n"
|
||||
">>> Already Downloaded\n"
|
||||
"----------------------------------------------------------------------\n"
|
||||
"\\[1/3\\] Total ???% | ????? ??B\\/s | -2.0 B | ???????";
|
||||
|
||||
|
@ -563,9 +679,9 @@ void ProgressbarInteractiveTest::test_multi_progress_bars_with_already_downloade
|
|||
oss << multi_progress_bar;
|
||||
expected =
|
||||
"\\[1/3\\] test 100% | ????? ??B\\/s | 0.0 B | ???????\n"
|
||||
">>> Already Downloaded \n"
|
||||
">>> Already Downloaded\n"
|
||||
"\\[2/3\\] test 100% | ????? ??B\\/s | 0.0 B | ???????\n"
|
||||
">>> Already Downloaded \n"
|
||||
">>> Already Downloaded\n"
|
||||
"----------------------------------------------------------------------\n"
|
||||
"\\[2/3\\] Total ???% | ????? ??B\\/s | -1.0 B | ???????";
|
||||
ASSERT_MATCHES(expected, perform_control_sequences(oss.str()));
|
||||
|
@ -577,9 +693,9 @@ void ProgressbarInteractiveTest::test_multi_progress_bars_with_already_downloade
|
|||
oss << multi_progress_bar;
|
||||
expected =
|
||||
"\\[1/3\\] test 100% | ????? ??B\\/s | 0.0 B | ???????\n"
|
||||
">>> Already Downloaded \n"
|
||||
">>> Already Downloaded\n"
|
||||
"\\[2/3\\] test 100% | ????? ??B\\/s | 0.0 B | ???????\n"
|
||||
">>> Already Downloaded \n"
|
||||
">>> Already Downloaded\n"
|
||||
"\\[3/3\\] test 40% | ????? ??B\\/s | 4.0 B | ???????\n"
|
||||
"----------------------------------------------------------------------\n"
|
||||
"\\[2/3\\] Total 40% | ????? ??B\\/s | 4.0 B | ???????";
|
||||
|
|
|
@ -30,6 +30,7 @@ class ProgressbarInteractiveTest : public CppUnit::TestCase {
|
|||
CPPUNIT_TEST(test_perform_control_sequences);
|
||||
CPPUNIT_TEST(test_download_progress_bar);
|
||||
CPPUNIT_TEST(test_download_progress_bar_with_messages);
|
||||
CPPUNIT_TEST(test_download_progress_bar_with_long_messages);
|
||||
CPPUNIT_TEST(test_multi_progress_bar_with_total_finished);
|
||||
CPPUNIT_TEST(test_multi_progress_bar_with_messages_with_total);
|
||||
CPPUNIT_TEST(test_multi_progress_bars_with_messages_with_total);
|
||||
|
@ -47,6 +48,7 @@ public:
|
|||
void test_perform_control_sequences();
|
||||
void test_download_progress_bar();
|
||||
void test_download_progress_bar_with_messages();
|
||||
void test_download_progress_bar_with_long_messages();
|
||||
void test_multi_progress_bar_with_total_finished();
|
||||
void test_multi_progress_bar_with_messages_with_total();
|
||||
void test_multi_progress_bars_with_messages_with_total();
|
||||
|
|
Loading…
Reference in New Issue