/* * Copyright (c) 2014 Tim Ruehsen * Copyright (c) 2015-2021 Free Software Foundation, Inc. * * This file is part of libwget. * * Libwget is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Libwget is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libwget. If not, see . * * * Progress bar routines * * Changelog * 18.10.2014 Tim Ruehsen created from src/bar.c * */ #include #include #include #include #include #include #include #include #include #include #include #include "private.h" /** * \file * \brief Progress Bar Routines * \defgroup libwget-progress Progress Display Functions * @{ * * Methods for creating and printing a progress bar display. */ // We use enums to define the progress bar parameters because they are the // closest thing we have to defining true constants in C without using // preprocessor macros. The advantage of enums is that they will create a // symbol in the symbol table making debugging a whole lot easier. // Define the parameters for how the progress bar looks enum BAR_SIZES { BAR_FILENAME_SIZE = 20, BAR_RATIO_SIZE = 3, BAR_METER_COST = 2, BAR_DOWNBYTES_SIZE = 8, BAR_SPEED_SIZE = 8, }; // Define the cost (in number of columns) of the progress bar decorations. This // includes all the elements that are not the progress indicator itself. enum BAR_DECOR_SIZE { BAR_DECOR_COST = BAR_FILENAME_SIZE + 1 + BAR_RATIO_SIZE + 2 + BAR_METER_COST + 1 + BAR_DOWNBYTES_SIZE + 1 + BAR_SPEED_SIZE + 3 }; enum SCREEN_WIDTH { DEFAULT_SCREEN_WIDTH = 70, MINIMUM_SCREEN_WIDTH = 45, }; enum bar_slot_status { EMPTY = 0, DOWNLOADING = 1, COMPLETE = 2 }; /** The settings for drawing the progress bar. * * This includes things like how often it is updated, how many values are * stored in the speed ring, etc. */ enum BAR_SETTINGS { /// The number of values to store in the speed ring SPEED_RING_SIZE = 24, }; typedef struct { char *progress, *filename, speed_buf[BAR_SPEED_SIZE], human_size[BAR_DOWNBYTES_SIZE]; uint64_t file_size, time_ring[SPEED_RING_SIZE], bytes_ring[SPEED_RING_SIZE], bytes_downloaded; int ring_pos, ring_used, tick, numfiles; enum bar_slot_status status; bool redraw : 1; } bar_slot; struct wget_bar_st { bar_slot *slots; char *progress_mem_holder, *unknown_size, *known_size, *spaces; int nslots, max_width; wget_thread_mutex mutex; }; static wget_report_speed report_speed_type = WGET_REPORT_SPEED_BYTES; static char report_speed_type_char = 'B'; static unsigned short speed_modifier = 1000; // The progress bar may be redrawn if the window size changes. // XXX: Don't handle that case currently. Instead, later test // what happens if we don't explicitly redraw in such a case. // For fast downloads, it doesn't matter. For slow downloads, // the progress bar will maybe span across two lines till it // gets redrawn. Ideally, this should be a part of the client // code logic and not in the library. // Tl;dr: Move window size detection to client. Allow client to // specify rate at which speed stats should be updated. Speed // ring size will remain constant (Don't want second heap allocation) // - darnir 29/07/2018 static void bar_update_speed_stats(bar_slot *slotp) { int ring_pos = slotp->ring_pos; int ring_used = slotp->ring_used; int next_pos; // In case this function is called with no downloaded bytes, // exit early if (slotp->bytes_downloaded == slotp->bytes_ring[ring_pos]) { return; } uint64_t curtime = wget_get_timemillis(); // Increment the position pointer if (++ring_pos == SPEED_RING_SIZE) ring_pos = 0; slotp->bytes_ring[ring_pos] = slotp->bytes_downloaded; slotp->time_ring[ring_pos] = curtime; if (ring_used < SPEED_RING_SIZE) { ring_used++; next_pos = 1; } else { next_pos = (ring_pos + 1 == SPEED_RING_SIZE) ? 0 : ring_pos + 1; } if (ring_used < 2) { // Not enough measurements to calculate the speed wget_strlcpy(slotp->speed_buf, " --.-K", sizeof(slotp->speed_buf)); } else { size_t bytes = slotp->bytes_ring[ring_pos] - slotp->bytes_ring[next_pos]; size_t time = slotp->time_ring[ring_pos] - slotp->time_ring[next_pos]; size_t speed = (bytes * speed_modifier) / (time ? time : 1); wget_human_readable(slotp->speed_buf, sizeof(slotp->speed_buf), speed); } slotp->ring_pos = ring_pos; slotp->ring_used = ring_used; } static volatile sig_atomic_t winsize_changed; static inline WGET_GCC_ALWAYS_INLINE void restore_cursor_position(void) { // ESC 8: Restore cursor position fputs("\0338", stdout); } static inline WGET_GCC_ALWAYS_INLINE void bar_print_slot(const wget_bar *bar, int slot) { // ESC 7: Save cursor // CSI A: Cursor up // CSI G: Cursor horizontal absolute wget_fprintf(stdout, "\0337\033[%dA\033[1G", bar->nslots - slot); } static inline WGET_GCC_ALWAYS_INLINE void bar_set_progress(const wget_bar *bar, int slot) { bar_slot *slotp = &bar->slots[slot]; if (slotp->file_size > 0) { size_t bytes = slotp->bytes_downloaded; int cols = (int) ((bytes / (double) slotp->file_size) * bar->max_width); if (cols > bar->max_width) cols = bar->max_width; else if (cols <= 0) cols = 1; // Write one extra byte for \0. This has already been accounted for // when initializing the progress storage. memcpy(slotp->progress, bar->known_size, cols - 1); slotp->progress[cols - 1] = '>'; if (cols < bar->max_width) memset(slotp->progress + cols, ' ', bar->max_width - cols); } else { int ind = slotp->tick % (bar->max_width * 2 - 6); int pre_space; if (ind <= bar->max_width - 3) pre_space = ind; else pre_space = bar->max_width - (ind - bar->max_width + 5); memset(slotp->progress, ' ', bar->max_width); memcpy(slotp->progress + pre_space, "<=>", 3); } slotp->progress[bar->max_width] = 0; } /** * \param[in] s String possibly containing multibyte characters (eg UTF-8) * \param[in] available_space Number of columns available for display of s * \param[out] inspectedp where to store number of characters inspected from s * \param[out] padp where to store amount of white space padding * * Inspect that part of the multibyte string s which will consume up to * available_space columns on the screen * Each multibyte character can consume 0 or more columns on the screen * If the string as displayed is shorter than available_space, padding * will be required * * Starting with the first, each (possibly) multibyte sequence in s is * converted to the corresponding wide character. * Two values are derived in this process: * mblen: length of multi-byte sequence (eg 1 for ordinary ASCII) * wcwidth(wide): number of columns occupied by the wide character (>= 0) * The mblen values are summed up to determine how much of s has been * used in the inspection so far and the wcwidth(wide) values are summed up * to determine the position of a (virtual) cursor in the available space. */ static void bar_inspect_multibyte(char *s, size_t available_space, size_t *inspectedp, size_t *padp) { unsigned int displayed = 0; /* number of columns displayed so far */ int inspected = 0; /* total number of bytes inspected from s */ wchar_t wide; /* wide character made from initial multibyte section */ int mblen; /* length of initial multibyte section which was converted to "wide" */ size_t remaining; if (!s) { *inspectedp = inspected; *padp = available_space; return; } remaining = strlen(s); /* a slight optimization */ /* while we have another character ... */ while ((mblen = mbtowc(&wide, &s[inspected], remaining)) > 0) { int wid = wcwidth(wide); /* * If we have filled exactly "available_size" columns * and the next character is a zero-width character ... * ... or ... * if appending the wide character would exceed the given available_space ... */ if ((wid == 0 && displayed == available_space) || displayed + wid > available_space) break; /* ... we're done */ /* we're not done, so advance in s ... */ inspected += mblen; remaining -= mblen; /* ... and advance cursor */ displayed += wid; } /* * When we come here, we either have processed the entire multibyte * string, then we will need to pad, or we have filled the available * space, then there will be no padding. */ *inspectedp = inspected; *padp = available_space - displayed; } static void bar_update_slot(const wget_bar *bar, int slot) { bar_slot *slotp = &bar->slots[slot]; // We only print a progress bar for the slot if a context has been // registered for it if (slotp->status == DOWNLOADING || slotp->status == COMPLETE) { uint64_t max, cur; int ratio; size_t consumed, pad; max = slotp->file_size; cur = slotp->bytes_downloaded; ratio = max ? (int) ((100 * cur) / max) : 0; wget_human_readable(slotp->human_size, sizeof(slotp->human_size), cur); bar_update_speed_stats(slotp); bar_set_progress(bar, slot); bar_print_slot(bar, slot); // The progress bar looks like this: // // filename xxx% [======> ] xxx.xxK // // It is made of the following elements: // filename _BAR_FILENAME_SIZE Name of local file // xxx% _BAR_RATIO_SIZE + 1 Amount of file downloaded // [] _BAR_METER_COST Bar Decorations // xxx.xxK _BAR_DOWNBYTES_SIZE Number of downloaded bytes // xxx.xxKB/s _BAR_SPEED_SIZE Download speed // ===> Remaining Progress Meter bar_inspect_multibyte(slotp->filename, BAR_FILENAME_SIZE, &consumed, &pad); wget_fprintf(stdout, "%-*.*s %*d%% [%s] %*s %*s%c/s", (int) (consumed+pad), (int) (consumed+pad), slotp->filename, BAR_RATIO_SIZE, ratio, slotp->progress, BAR_DOWNBYTES_SIZE, slotp->human_size, BAR_SPEED_SIZE, slotp->speed_buf, report_speed_type_char); restore_cursor_position(); fflush(stdout); slotp->tick++; } } static int bar_get_width(void) { int width = DEFAULT_SCREEN_WIDTH; if (wget_get_screen_size(&width, NULL) == 0) { if (width < MINIMUM_SCREEN_WIDTH) width = MINIMUM_SCREEN_WIDTH; else width--; // leave one space at the end, else we see a linebreak on Windows } return width - BAR_DECOR_COST; } static void bar_update_winsize(wget_bar *bar, bool slots_changed) { if (winsize_changed || slots_changed) { char *progress_mem_holder; int max_width = bar_get_width(); if (!(progress_mem_holder = wget_calloc(bar->nslots, max_width + 1))) return; if (bar->max_width < max_width) { char *known_size = wget_malloc(max_width); char *unknown_size = wget_malloc(max_width); char *spaces = wget_malloc(max_width); if (!known_size || ! unknown_size || !spaces) { xfree(spaces); xfree(unknown_size); xfree(known_size); xfree(progress_mem_holder); return; } xfree(bar->known_size); bar->known_size = known_size; memset(bar->known_size, '=', max_width); xfree(bar->unknown_size); bar->unknown_size = unknown_size; memset(bar->unknown_size, '*', max_width); xfree(bar->spaces); bar->spaces = spaces; memset(bar->spaces, ' ', max_width); } xfree(bar->progress_mem_holder); // Add one extra byte to hold the \0 character bar->progress_mem_holder = progress_mem_holder; for (int i = 0; i < bar->nslots; i++) { bar->slots[i].progress = bar->progress_mem_holder + (i * max_width); } bar->max_width = max_width; } winsize_changed = 0; } static void bar_update(wget_bar *bar) { bar_update_winsize(bar, false); for (int i = 0; i < bar->nslots; i++) { if (bar->slots[i].redraw || winsize_changed) { bar_update_slot(bar, i); bar->slots[i].redraw = 0; } } } /** * \param[in] bar Pointer to a \p wget_bar object * \param[in] nslots Number of progress bars * \return Pointer to a \p wget_bar object * * Initialize a new progress bar instance. If \p bar is a NULL * pointer, it will be allocated on the heap and a pointer to the newly * allocated memory will be returned. To free this memory, call either the * wget_bar_deinit() or wget_bar_free() functions based on your needs. * * \p nslots is the number of screen lines to reserve for printing the progress * bars. This may be any number, but you generally want at least as many slots * as there are downloader threads. */ wget_bar *wget_bar_init(wget_bar *bar, int nslots) { /* Initialize screen_width if this hasn't been done or if it might have changed, as indicated by receiving SIGWINCH. */ int max_width = bar_get_width(); if (nslots < 1 || max_width < 1) return NULL; if (!bar) { if (!(bar = wget_calloc(1, sizeof(*bar)))) return NULL; } else memset(bar, 0, sizeof(*bar)); wget_thread_mutex_init(&bar->mutex); wget_bar_set_slots(bar, nslots); return bar; } /** * \param[in] bar Pointer to a wget_bar object * \param[in] nslots The new number of progress bars that should be drawn * * Update the number of progress bar lines that are drawn on the screen. * This is useful when the number of downloader threads changes dynamically or * to change the number of reserved lines. Calling this function will * immediately reserve \p nslots lines on the screen. However if \p nslots is * lower than the existing value, nothing will be done. */ void wget_bar_set_slots(wget_bar *bar, int nslots) { wget_thread_mutex_lock(bar->mutex); int more_slots = nslots - bar->nslots; if (more_slots > 0) { bar_slot *slots = wget_realloc(bar->slots, nslots * sizeof(bar_slot)); if (!slots) { wget_thread_mutex_unlock(bar->mutex); return; } bar->slots = slots; memset(bar->slots + bar->nslots, 0, more_slots * sizeof(bar_slot)); bar->nslots = nslots; for (int i = 0; i < more_slots; i++) fputs("\n", stdout); bar_update_winsize(bar, true); bar_update(bar); } wget_thread_mutex_unlock(bar->mutex); } /** * \param[in] bar Pointer to a wget_bar object * \param[in] slot The slot number to use * \param[in] filename The file name to display in the given \p slot * \param[in] new_file if this is the start of a download of the body of a new file * \param[in] file_size The file size that would be 100% * * Initialize the given \p slot of the \p bar object with it's (file) name to display * and the (file) size to be assumed 100%. */ void wget_bar_slot_begin(wget_bar *bar, int slot, const char *filename, int new_file, ssize_t file_size) { wget_thread_mutex_lock(bar->mutex); bar_slot *slotp = &bar->slots[slot]; xfree(slotp->filename); if (new_file) slotp->numfiles++; if (slotp->numfiles == 1) { slotp->filename = wget_strdup(filename); } else { slotp->filename = wget_aprintf("%d files", slotp->numfiles); } slotp->tick = 0; slotp->file_size += file_size; slotp->status = DOWNLOADING; slotp->redraw = 1; slotp->ring_pos = 0; slotp->ring_used = 0; memset(&slotp->time_ring, 0, sizeof(slotp->time_ring)); memset(&slotp->bytes_ring, 0, sizeof(slotp->bytes_ring)); wget_thread_mutex_unlock(bar->mutex); } /** * \param[in] bar Pointer to a wget_bar object * \param[in] slot The slot number to use * \param[in] nbytes The number of bytes downloaded since the last invocation of this function * * Set the current number of bytes for \p slot for the next update of * the bar/slot. */ void wget_bar_slot_downloaded(wget_bar *bar, int slot, size_t nbytes) { wget_thread_mutex_lock(bar->mutex); bar->slots[slot].bytes_downloaded += nbytes; bar->slots[slot].redraw = 1; wget_thread_mutex_unlock(bar->mutex); } /** * \param[in] bar Pointer to a wget_bar object * \param[in] slot The slot number to use * * Redraw the given \p slot as being completed. */ void wget_bar_slot_deregister(wget_bar *bar, int slot) { wget_thread_mutex_lock(bar->mutex); if (slot >= 0 && slot < bar->nslots) { bar_slot *slotp = &bar->slots[slot]; slotp->status = COMPLETE; bar_update_slot(bar, slot); } wget_thread_mutex_unlock(bar->mutex); } /** * \param[in] bar Pointer to a wget_bar object * * Redraw the parts of the \p bar that have been changed so far. */ void wget_bar_update(wget_bar *bar) { wget_thread_mutex_lock(bar->mutex); bar_update(bar); wget_thread_mutex_unlock(bar->mutex); } /** * \param[in] bar Pointer to \p wget_bar * * Free the various progress bar data structures * without freeing \p bar itself. */ void wget_bar_deinit(wget_bar *bar) { if (bar) { for (int i = 0; i < bar->nslots; i++) { xfree(bar->slots[i].filename); } xfree(bar->progress_mem_holder); xfree(bar->spaces); xfree(bar->known_size); xfree(bar->unknown_size); xfree(bar->slots); wget_thread_mutex_destroy(&bar->mutex); } } /** * \param[in] bar Pointer to \p wget_bar * * Free the various progress bar data structures * including the \p bar pointer itself. */ void wget_bar_free(wget_bar **bar) { if (bar) { wget_bar_deinit(*bar); xfree(*bar); } } /** * \param[in] bar Pointer to \p wget_bar * \param[in] slot The slot number to use * \param[in] display The string to be displayed in the given slot * * Displays the \p display string in the given \p slot. */ void wget_bar_print(wget_bar *bar, int slot, const char *display) { wget_thread_mutex_lock(bar->mutex); bar_print_slot(bar, slot); // CSI G: Cursor horizontal absolute wget_fprintf(stdout, "\033[27G[%-*.*s]", bar->max_width, bar->max_width, display); restore_cursor_position(); fflush(stdout); wget_thread_mutex_unlock(bar->mutex); } /** * \param[in] bar Pointer to \p wget_bar * \param[in] slot The slot number to use * \param[in] fmt Printf-like format to build the display string * \param[in] args Arguments matching the \p fmt format string * * Displays the \p string build using the printf-style \p fmt and \p args. */ void wget_bar_vprintf(wget_bar *bar, int slot, const char *fmt, va_list args) { char text[bar->max_width + 1]; wget_vsnprintf(text, sizeof(text), fmt, args); wget_bar_print(bar, slot, text); } /** * \param[in] bar Pointer to \p wget_bar * \param[in] slot The slot number to use * \param[in] fmt Printf-like format to build the display string * \param[in] ... List of arguments to match \p fmt * * Displays the \p string build using the printf-style \p fmt and the given arguments. */ void wget_bar_printf(wget_bar *bar, int slot, const char *fmt, ...) { va_list args; va_start(args, fmt); wget_bar_vprintf(bar, slot, fmt, args); va_end(args); } /** * Call this function when a resize of the screen / console has been detected. */ void wget_bar_screen_resized(void) { winsize_changed = 1; } /** * * \param[in] bar Pointer to \p wget_bar * @param buf Pointer to buffer to be displayed * @param len Number of bytes to be displayed * * Write 'above' the progress bar area, scrolls screen one line up * if needed. Currently used by Wget2 to display error messages in * color red. * * This function needs a redesign to be useful for general purposes. */ void wget_bar_write_line(wget_bar *bar, const char *buf, size_t len) { wget_thread_mutex_lock(bar->mutex); // ESC 7: Save cursor // CSI S: Scroll up whole screen // CSI A: Cursor up // CSI G: Cursor horizontal absolute // CSI 0J: Clear from cursor to end of screen // CSI 31m: Red text color wget_fprintf(stdout, "\0337\033[1S\033[%dA\033[1G\033[0J\033[31m", bar->nslots + 1); fwrite(buf, 1, len, stdout); fputs("\033[m", stdout); // reset text color restore_cursor_position(); bar_update(bar); wget_thread_mutex_unlock(bar->mutex); } /** * @param type Report speed type * * Set the progress bar report speed type to WGET_REPORT_SPEED_BYTES * or WGET_REPORT_SPEED_BITS. * * Default is WGET_REPORT_SPEED_BYTES. */ void wget_bar_set_speed_type(wget_report_speed type) { report_speed_type = type; if (type == WGET_REPORT_SPEED_BITS) { report_speed_type_char = 'b'; speed_modifier = 8; } } /** @}*/