This commit is contained in:
Marek Blaha 2025-07-28 11:27:18 +02:00 committed by GitHub
commit 645ecbe990
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 659 additions and 320 deletions

View File

@ -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)

View File

@ -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

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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);

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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")

View File

@ -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 | ???????";

View File

@ -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();