importctl: draw a pretty progress bar while downloading

Everybody loves pretty terminal progress bar.
This commit is contained in:
Lennart Poettering
2024-02-26 15:46:50 +01:00
parent 8ce438bb71
commit 71cb203a6e
9 changed files with 228 additions and 17 deletions

View File

@@ -41,6 +41,8 @@ const char *special_glyph_full(SpecialGlyph code, bool force_utf) {
[SPECIAL_GLYPH_TREE_SPACE] = " ",
[SPECIAL_GLYPH_TREE_TOP] = ",-",
[SPECIAL_GLYPH_VERTICAL_DOTTED] = ":",
[SPECIAL_GLYPH_HORIZONTAL_DOTTED] = "-",
[SPECIAL_GLYPH_HORIZONTAL_FAT] = "=",
[SPECIAL_GLYPH_TRIANGULAR_BULLET] = ">",
[SPECIAL_GLYPH_BLACK_CIRCLE] = "*",
[SPECIAL_GLYPH_WHITE_CIRCLE] = "*",
@@ -91,6 +93,8 @@ const char *special_glyph_full(SpecialGlyph code, bool force_utf) {
/* Single glyphs in both cases */
[SPECIAL_GLYPH_VERTICAL_DOTTED] = u8"",
[SPECIAL_GLYPH_HORIZONTAL_DOTTED] = u8"",
[SPECIAL_GLYPH_HORIZONTAL_FAT] = u8"",
[SPECIAL_GLYPH_TRIANGULAR_BULLET] = u8"",
[SPECIAL_GLYPH_BLACK_CIRCLE] = u8"",
[SPECIAL_GLYPH_WHITE_CIRCLE] = u8"",

View File

@@ -13,6 +13,8 @@ typedef enum SpecialGlyph {
SPECIAL_GLYPH_TREE_SPACE,
SPECIAL_GLYPH_TREE_TOP,
SPECIAL_GLYPH_VERTICAL_DOTTED,
SPECIAL_GLYPH_HORIZONTAL_DOTTED,
SPECIAL_GLYPH_HORIZONTAL_FAT,
SPECIAL_GLYPH_TRIANGULAR_BULLET,
SPECIAL_GLYPH_BLACK_CIRCLE,
SPECIAL_GLYPH_WHITE_CIRCLE,

View File

@@ -45,6 +45,8 @@ static const char* arg_format = NULL;
static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
static ImageClass arg_image_class = _IMAGE_CLASS_INVALID;
#define PROGRESS_PREFIX "Total: "
static int settle_image_class(void) {
if (arg_image_class < 0) {
@@ -68,13 +70,21 @@ static int settle_image_class(void) {
return 0;
}
typedef struct Context {
const char *object_path;
double progress;
} Context;
static int match_log_message(sd_bus_message *m, void *userdata, sd_bus_error *error) {
const char **our_path = userdata, *line;
Context *c = ASSERT_PTR(userdata);
const char *line;
unsigned priority;
int r;
assert(m);
assert(our_path);
if (!streq_ptr(c->object_path, sd_bus_message_get_path(m)))
return 0;
r = sd_bus_message_read(m, "us", &priority, &line);
if (r < 0) {
@@ -82,23 +92,51 @@ static int match_log_message(sd_bus_message *m, void *userdata, sd_bus_error *er
return 0;
}
if (!streq_ptr(*our_path, sd_bus_message_get_path(m)))
return 0;
if (arg_quiet && LOG_PRI(priority) >= LOG_INFO)
return 0;
if (!arg_quiet)
clear_progress_bar(PROGRESS_PREFIX);
log_full(priority, "%s", line);
if (!arg_quiet)
draw_progress_bar(PROGRESS_PREFIX, c->progress * 100);
return 0;
}
static int match_progress_update(sd_bus_message *m, void *userdata, sd_bus_error *error) {
Context *c = ASSERT_PTR(userdata);
int r;
assert(m);
if (!streq_ptr(c->object_path, sd_bus_message_get_path(m)))
return 0;
r = sd_bus_message_read(m, "d", &c->progress);
if (r < 0) {
bus_log_parse_error(r);
return 0;
}
if (!arg_quiet)
draw_progress_bar(PROGRESS_PREFIX, c->progress * 100);
return 0;
}
static int match_transfer_removed(sd_bus_message *m, void *userdata, sd_bus_error *error) {
const char **our_path = userdata, *path, *result;
Context *c = ASSERT_PTR(userdata);
const char *path, *result;
uint32_t id;
int r;
assert(m);
assert(our_path);
if (!arg_quiet)
clear_progress_bar(PROGRESS_PREFIX);
r = sd_bus_message_read(m, "uos", &id, &path, &result);
if (r < 0) {
@@ -106,7 +144,7 @@ static int match_transfer_removed(sd_bus_message *m, void *userdata, sd_bus_erro
return 0;
}
if (!streq_ptr(*our_path, path))
if (!streq_ptr(c->object_path, path))
return 0;
sd_event_exit(sd_bus_get_event(sd_bus_message_get_bus(m)), !streq_ptr(result, "done"));
@@ -117,6 +155,9 @@ static int transfer_signal_handler(sd_event_source *s, const struct signalfd_sig
assert(s);
assert(si);
if (!arg_quiet)
clear_progress_bar(PROGRESS_PREFIX);
if (!arg_quiet)
log_info("Continuing download in the background. Use \"%s cancel-transfer %" PRIu32 "\" to abort transfer.",
program_invocation_short_name,
@@ -127,11 +168,11 @@ static int transfer_signal_handler(sd_event_source *s, const struct signalfd_sig
}
static int transfer_image_common(sd_bus *bus, sd_bus_message *m) {
_cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot_job_removed = NULL, *slot_log_message = NULL;
_cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot_job_removed = NULL, *slot_log_message = NULL, *slot_progress_update = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_(sd_event_unrefp) sd_event* event = NULL;
const char *path = NULL;
Context c = {};
uint32_t id;
int r;
@@ -153,7 +194,9 @@ static int transfer_image_common(sd_bus *bus, sd_bus_message *m) {
&slot_job_removed,
bus_import_mgr,
"TransferRemoved",
match_transfer_removed, NULL, &path);
match_transfer_removed,
/* add_callback= */ NULL,
&c);
if (r < 0)
return log_error_errno(r, "Failed to request match: %m");
@@ -161,10 +204,25 @@ static int transfer_image_common(sd_bus *bus, sd_bus_message *m) {
bus,
&slot_log_message,
"org.freedesktop.import1",
NULL,
/* object_path= */ NULL,
"org.freedesktop.import1.Transfer",
"LogMessage",
match_log_message, NULL, &path);
match_log_message,
/* add_callback= */ NULL,
&c);
if (r < 0)
return log_error_errno(r, "Failed to request match: %m");
r = sd_bus_match_signal_async(
bus,
&slot_progress_update,
"org.freedesktop.import1",
/* object_path= */ NULL,
"org.freedesktop.import1.Transfer",
"ProgressUpdate",
match_progress_update,
/* add_callback= */ NULL,
&c);
if (r < 0)
return log_error_errno(r, "Failed to request match: %m");
@@ -172,12 +230,15 @@ static int transfer_image_common(sd_bus *bus, sd_bus_message *m) {
if (r < 0)
return log_error_errno(r, "Failed to transfer image: %s", bus_error_message(&error, r));
r = sd_bus_message_read(reply, "uo", &id, &path);
r = sd_bus_message_read(reply, "uo", &id, &c.object_path);
if (r < 0)
return bus_log_parse_error(r);
if (!arg_quiet)
if (!arg_quiet) {
clear_progress_bar(PROGRESS_PREFIX);
log_info("Enqueued transfer job %u. Press C-c to continue download in background.", id);
draw_progress_bar(PROGRESS_PREFIX, c.progress);
}
(void) sd_event_add_signal(event, NULL, SIGINT|SD_EVENT_SIGNAL_PROCMASK, transfer_signal_handler, UINT32_TO_PTR(id));
(void) sd_event_add_signal(event, NULL, SIGTERM|SD_EVENT_SIGNAL_PROCMASK, transfer_signal_handler, UINT32_TO_PTR(id));

View File

@@ -83,6 +83,7 @@ struct Transfer {
unsigned n_canceled;
unsigned progress_percent;
unsigned progress_percent_sent;
int stdin_fd;
int stdout_fd;
@@ -166,7 +167,8 @@ static int transfer_new(Manager *m, Transfer **ret) {
.stdin_fd = -EBADF,
.stdout_fd = -EBADF,
.verify = _IMPORT_VERIFY_INVALID,
.progress_percent= UINT_MAX,
.progress_percent = UINT_MAX,
.progress_percent_sent = UINT_MAX,
};
id = m->current_transfer_id + 1;
@@ -217,7 +219,28 @@ static void transfer_send_log_line(Transfer *t, const char *line) {
line);
if (r < 0)
log_warning_errno(r, "Cannot emit log message signal, ignoring: %m");
}
}
static void transfer_send_progress_update(Transfer *t) {
int r;
assert(t);
if (t->progress_percent_sent == t->progress_percent)
return;
r = sd_bus_emit_signal(
t->manager->bus,
t->object_path,
"org.freedesktop.import1.Transfer",
"ProgressUpdate",
"d",
transfer_percent_as_double(t));
if (r < 0)
log_warning_errno(r, "Cannot emit progress update signal, ignoring: %m");
t->progress_percent_sent = t->progress_percent;
}
static void transfer_send_logs(Transfer *t, bool flush) {
assert(t);
@@ -635,6 +658,8 @@ static int manager_on_notify(sd_event_source *s, int fd, uint32_t revents, void
t->progress_percent = (unsigned) r;
log_debug("Got percentage from client: %u%%", t->progress_percent);
transfer_send_progress_update(t);
return 0;
}
@@ -1369,6 +1394,10 @@ static const sd_bus_vtable transfer_vtable[] = {
SD_BUS_PARAM(priority)
SD_BUS_PARAM(line),
0),
SD_BUS_SIGNAL_WITH_NAMES("ProgressUpdate",
"d",
SD_BUS_PARAM(progress),
0),
SD_BUS_VTABLE_END,
};

View File

@@ -462,3 +462,75 @@ int terminal_tint_color(double hue, char **ret) {
return 0;
}
void draw_progress_bar(const char *prefix, double percentage) {
fputs("\r", stderr);
if (prefix)
fputs(prefix, stderr);
if (!terminal_is_dumb()) {
size_t cols = columns();
size_t prefix_length = strlen_ptr(prefix);
size_t length = cols > prefix_length + 6 ? cols - prefix_length - 6 : 0;
fputs(ansi_highlight_green(), stderr);
if (length > 5 && percentage >= 0.0 && percentage <= 100.0) {
size_t p = (size_t) (length * percentage / 100.0);
bool separator_done = false;
for (size_t i = 0; i < length; i++) {
if (i <= p) {
if (get_color_mode() == COLOR_24BIT) {
uint8_t r8, g8, b8;
double z = i == 0 ? 0 : (((double) i / p) * 100);
hsv_to_rgb(145 /* green */, z, 33 + z*2/3, &r8, &g8, &b8);
fprintf(stderr, "\x1B[38;2;%u;%u;%um", r8, g8, b8);
}
fputs(special_glyph(SPECIAL_GLYPH_HORIZONTAL_FAT), stderr);
} else if (i+1 < length && !separator_done) {
fputs(ansi_normal(), stderr);
fputc(' ', stderr);
separator_done = true;
fputs(ansi_grey(), stderr);
} else
fputs(special_glyph(SPECIAL_GLYPH_HORIZONTAL_DOTTED), stderr);
}
fputs(ansi_normal(), stderr);
fputc(' ', stderr);
}
}
fprintf(stderr,
"%s%3.0f%%%s",
ansi_highlight(),
percentage,
ansi_normal());
if (!terminal_is_dumb())
fputs(ANSI_ERASE_TO_END_OF_LINE, stderr);
fputc('\r', stderr);
fflush(stderr);
}
void clear_progress_bar(const char *prefix) {
fputc('\r', stderr);
if (terminal_is_dumb()) {
size_t l = strlen_ptr(prefix);
for (size_t i = 0; i < l; i ++)
fputc(' ', stderr);
fputs(" ", stderr);
} else
fputs(ANSI_ERASE_TO_END_OF_LINE, stderr);
fputc('\r', stderr);
fflush(stderr);
}

View File

@@ -49,3 +49,6 @@ static inline const char *green_check_mark_internal(char buffer[static GREEN_CHE
#define COLOR_MARK_BOOL(b) ((b) ? GREEN_CHECK_MARK() : RED_CROSS_MARK())
int terminal_tint_color(double hue, char **ret);
void draw_progress_bar(const char *prefix, double percentage);
void clear_progress_bar(const char *prefix);

View File

@@ -380,6 +380,10 @@ executables += [
'sources' : files('test-process-util.c'),
'dependencies' : threads,
},
test_template + {
'sources' : files('test-progress-bar.c'),
'type' : 'manual',
},
test_template + {
'sources' : files('test-qrcode-util.c'),
'dependencies' : libdl,

View File

@@ -92,6 +92,8 @@ TEST(dump_special_glyphs) {
dump_glyph(SPECIAL_GLYPH_TREE_SPACE);
dump_glyph(SPECIAL_GLYPH_TREE_TOP);
dump_glyph(SPECIAL_GLYPH_VERTICAL_DOTTED);
dump_glyph(SPECIAL_GLYPH_HORIZONTAL_DOTTED);
dump_glyph(SPECIAL_GLYPH_HORIZONTAL_FAT);
dump_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET);
dump_glyph(SPECIAL_GLYPH_BLACK_CIRCLE);
dump_glyph(SPECIAL_GLYPH_WHITE_CIRCLE);

View File

@@ -0,0 +1,34 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "pretty-print.h"
#include "random-util.h"
#include "tests.h"
#define PROGRESS_PREFIX "test: "
TEST(progress_bar) {
draw_progress_bar(PROGRESS_PREFIX, 0);
bool paused = false;
for (double d = 0; d <= 100; d += 0.5) {
usleep_safe(random_u64_range(20 * USEC_PER_MSEC));
draw_progress_bar(PROGRESS_PREFIX, d);
if (!paused && d >= 50) {
clear_progress_bar(PROGRESS_PREFIX);
fputs("Sleeping for 1s...", stdout);
fflush(stdout);
usleep_safe(USEC_PER_SEC);
paused = true;
}
}
draw_progress_bar(PROGRESS_PREFIX, 100);
usleep_safe(300 * MSEC_PER_SEC);
clear_progress_bar(PROGRESS_PREFIX);
fputs("Done.\n", stdout);
}
DEFINE_TEST_MAIN(LOG_INFO);