Everybody loves pretty terminal progress bar.
[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] = "*",
/* 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"○",
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,
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) {
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) {
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) {
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"));
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,
}
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;
&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");
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");
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));
unsigned n_canceled;
unsigned progress_percent;
+ unsigned progress_percent_sent;
int stdin_fd;
int stdout_fd;
.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;
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);
t->progress_percent = (unsigned) r;
log_debug("Got percentage from client: %u%%", t->progress_percent);
+
+ transfer_send_progress_update(t);
return 0;
}
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,
};
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);
+}
#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);
'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,
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);
--- /dev/null
+/* 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);