progressbar: Do not trim messages to terminal width

Print full messages.
This commit is contained in:
Marek Blaha 2025-07-11 15:05:48 +02:00
parent 1081b928e6
commit 5f5b8719eb
4 changed files with 123 additions and 50 deletions

View File

@ -97,6 +97,11 @@ public:
/// remove the last message
void pop_message();
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;
@ -116,6 +121,7 @@ public:
protected:
virtual void to_stream(std::ostream & stream) = 0;
std::size_t get_message_padding(std::size_t terminal_width, std::string_view message, std::size_t message_index);
private:
class LIBDNF_CLI_LOCAL Impl;

View File

@ -217,56 +217,43 @@ 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;
// 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.
stream << std::string(get_message_padding(terminal_width, prefix + message, message_index), ' ');
if (color_used) {
stream << tty::reset;
}
++message_index;
}
}

View File

@ -131,6 +131,7 @@ std::size_t MultiProgressBar::get_total_num_of_bars() const noexcept {
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.
@ -170,7 +171,7 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) {
text_buffer << *bar;
text_buffer << std::endl;
num_of_lines_permanent++;
num_of_lines_permanent += bar->get_messages().size();
num_of_lines_permanent += bar->calculate_messages_terminal_lines(terminal_width);
mbar.p_impl->bars_done.push_back(bar);
// TODO(dmach): use iterator
mbar.p_impl->bars_todo.erase(mbar.p_impl->bars_todo.begin() + static_cast<int>(i));
@ -198,7 +199,7 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) {
text_buffer << *bar;
mbar.p_impl->line_printed = true;
mbar.p_impl->num_of_lines_to_clear++;
mbar.p_impl->num_of_lines_to_clear += bar->get_messages().size();
mbar.p_impl->num_of_lines_to_clear += bar->calculate_messages_terminal_lines(terminal_width);
}
// then print the "total" progress bar
@ -227,8 +228,7 @@ std::ostream & operator<<(std::ostream & stream, MultiProgressBar & mbar) {
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

View File

@ -20,14 +20,25 @@ 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;
};
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;
@ -41,6 +52,10 @@ public:
// 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;
@ -60,6 +75,49 @@ public:
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;
@ -125,12 +183,33 @@ void ProgressBar::set_description(const std::string & 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::get_message_padding(
std::size_t terminal_width, std::string_view message, std::size_t message_index) {
return p_impl->get_message_metrics(terminal_width, message, message_index).padding;
}
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;
}
@ -294,6 +373,7 @@ void ProgressBar::set_total_ticks(int64_t value) {
void ProgressBar::pop_message() {
if (!p_impl->messages.empty()) {
p_impl->messages.pop_back();
p_impl->messages_metrics_cache.pop_back();
}
}