New bar formats - default, block, granular, shaded - with the granular format providing a smoother bar using Unicode block characters (#15)

This commit is contained in:
Andrew Wood
2024-12-13 20:02:36 +00:00
parent c49abf1608
commit 60563f3e3c
13 changed files with 439 additions and 127 deletions
+61
View File
@@ -9,6 +9,9 @@
#ifndef _PV_INTERNAL_H
#define _PV_INTERNAL_H 1
#ifdef HAVE_STDINT_H
#include <stdint.h>
#endif
#include <stdlib.h>
#include <signal.h>
#include <time.h>
@@ -45,6 +48,10 @@ extern "C" {
#define PV_SIZEOF_FILE_FDPATH 4096
#define PV_SIZEOF_DISPLAY_NAME 512
#define PV_BARSTYLE_MAX 4 /* number of different styles allowed in a format */
#define PV_BARSTYLE_SIZEOF_STRING 10 /* max length of a bar constituent component in bytes */
#define PV_BARSTYLE_MAX_FILLERS 10 /* max number of bar filler strings */
#define PV_DISPLAY_WINDOWTITLE 1
#define PV_DISPLAY_PROCESSTITLE 2
@@ -67,6 +74,50 @@ typedef enum {
} pvtransfercount_t;
/*
* Structure describing a short string used as part of a progress bar, whose
* width in display characters may not be the same as its length in bytes.
*/
struct pvbarstring_spec_s {
char string[PV_BARSTYLE_SIZEOF_STRING];
uint8_t width;
uint8_t bytes;
};
/*
* Structure describing a style of progress bar.
*
* The part that moves back and forth when the size isn't known, such as
* "<=>", is "indicator".
*
* The string used to populate the rest of the bar is one of "filler", which
* contains "filler_entries" array items. The first item "filler[0]" is
* used for an empty part such as " ". The last item
* "filler[filler_entries-1]" is used for a 100% full part, such as "=".
*
* If there are more than the minimum of 2 entries, then the intervening
* entries are used for partial fills.
*
* When there are only 2 entries and the bar isn't completely empty or full,
* the last filled portion of the bar is filled with "tip" instead of the
* last filler item to indicate the tip of the bar, such as ">".
*
* Note that only the indicator is expected to ever be wider than 1 display
* character. All other items are expected to have a width of 0 or 1.
*
* The "style_id" is an opaque identifier that should only be used to
* determine whether two styles are the same. It is always nonzero.
*/
struct pvbarstyle_s {
uint8_t style_id;
uint8_t filler_entries;
struct pvbarstring_spec_s indicator;
struct pvbarstring_spec_s tip;
struct pvbarstring_spec_s filler[PV_BARSTYLE_MAX_FILLERS];
};
typedef struct pvbarstyle_s *pvbarstyle_t;
/*
* Structure for holding PV internal state. Opaque outside the PV library.
*
@@ -183,12 +234,15 @@ struct pvstate_s {
struct pvdisplay_segment_s { /* format string broken into segments */
/* See pv__format_init() for more details. */
int type; /* component type, -1 for static string */
int parameter; /* component parameter, such as bar style index */
size_t chosen_size; /* "n" from %<n>A, or 0 */
size_t offset; /* start offset of this segment */
size_t bytes; /* length of segment in bytes */
size_t width; /* displayed width of segment */
} format[PV_FORMAT_ARRAY_MAX];
struct pvbarstyle_s barstyle[PV_BARSTYLE_MAX];
/* The last-written "n" bytes. */
char lastwritten_buffer[PV_SIZEOF_LASTWRITTEN_BUFFER];
@@ -412,6 +466,9 @@ long pv_seconds_remaining(const off_t, const off_t, const long double);
void pv_si_prefix(long double *, char *, const long double, pvtransfercount_t);
void pv_describe_amount(char *, size_t, char *, long double, char *, char *, pvtransfercount_t);
bool pv_barstyle(pvbarstyle_t, const char *);
int pv_display_barstyle_index(pvdisplay_t, const char *);
size_t pv_formatter_segmentcontent(char *, pvformatter_args_t);
/*
@@ -438,6 +495,10 @@ size_t pv_formatter_segmentcontent(char *, pvformatter_args_t);
size_t pv_formatter_progress(pvformatter_args_t);
size_t pv_formatter_progress_bar_only(pvformatter_args_t);
size_t pv_formatter_progress_amount_only(pvformatter_args_t);
size_t pv_formatter_bar_default(pvformatter_args_t);
size_t pv_formatter_bar_block(pvformatter_args_t);
size_t pv_formatter_bar_granular(pvformatter_args_t);
size_t pv_formatter_bar_shaded(pvformatter_args_t);
size_t pv_formatter_timer(pvformatter_args_t);
size_t pv_formatter_eta(pvformatter_args_t);
size_t pv_formatter_fineta(pvformatter_args_t);
+6 -1
View File
@@ -539,8 +539,13 @@ static bool pv__format_numeric(pvstate_t state, pvdisplay_t display)
/*@keep@ */ static struct pvdisplay_component_s format_component_array[] = {
{ "p", &pv_formatter_progress, true },
{ "{progress}", &pv_formatter_progress, true },
{ "{progress-bar-only}", &pv_formatter_progress_bar_only, true },
{ "{progress-amount-only}", &pv_formatter_progress_amount_only, false },
{ "{progress-bar-only}", &pv_formatter_bar_default, true },
{ "{bar}", &pv_formatter_bar_default, true },
{ "{bar-default}", &pv_formatter_bar_default, true },
{ "{bar-block}", &pv_formatter_bar_block, true },
{ "{bar-granular}", &pv_formatter_bar_granular, true },
{ "{bar-shaded}", &pv_formatter_bar_shaded, true },
{ "t", &pv_formatter_timer, false },
{ "{timer}", &pv_formatter_timer, false },
{ "e", &pv_formatter_eta, false },
+188
View File
@@ -0,0 +1,188 @@
/*
* Formatter functions for styled progress bars.
*
* Copyright 2024 Andrew Wood
*
* License GPLv3+: GNU GPL version 3 or later; see `docs/COPYING'.
*/
#include "config.h"
#include "pv.h"
#include "pv-internal.h"
#include <string.h>
#if defined(ENABLE_NLS) && defined(HAVE_WCHAR_H)
#include <wchar.h>
#if defined(HAVE_WCTYPE_H)
#include <wctype.h>
#endif
#endif
/*
* Populate "style" with the named bar style, falling back to the default if
* the name was not recognised. Returns true if the named style was found.
*
* Note that strings are copied into the structure, rather than just
* updating pointers, to maintain separation of concern between different
* parts of the code - and because on 64-bit architectures, the sizes of the
* data types and their associated alignment padding tend to outweigh the
* memory savings from using pointers and shared strings.
*/
bool pv_barstyle(pvbarstyle_t style, const char *name)
{
#define populate_string(item, str, w) { \
item.width = w; \
item.bytes = strlen(str); /* flawfinder: ignore */ \
if (item.bytes > 0 && item.bytes <= PV_BARSTYLE_SIZEOF_STRING) \
memcpy(item.string, str, item.bytes); /* flawfinder: ignore */ \
}
/*
* flawfinder - strlen() on null-terminated static strings is OK,
* and with the memcpy(), we check the buffer is big enough.
*/
memset(style, 0, sizeof(*style));
if (0 == strcmp(name, "block")) {
style->style_id = 2;
populate_string(style->indicator, "◀▶", 2);
populate_string(style->tip, "", 0);
populate_string(style->filler[0], " ", 1);
populate_string(style->filler[1], "", 1);
style->filler_entries = 2;
return true;
} else if (0 == strcmp(name, "granular")) {
style->style_id = 3;
populate_string(style->indicator, "◀▶", 2);
populate_string(style->tip, "", 0);
populate_string(style->filler[0], " ", 1);
populate_string(style->filler[1], "", 1);
populate_string(style->filler[2], "", 1);
populate_string(style->filler[3], "", 1);
populate_string(style->filler[4], "", 1);
populate_string(style->filler[5], "", 1);
populate_string(style->filler[6], "", 1);
populate_string(style->filler[7], "", 1);
populate_string(style->filler[8], "", 1);
style->filler_entries = 9;
return true;
} else if (0 == strcmp(name, "shaded")) {
style->style_id = 4;
populate_string(style->indicator, "▒▓▒", 3);
populate_string(style->tip, "", 0);
populate_string(style->filler[0], "", 1);
populate_string(style->filler[1], "", 1);
populate_string(style->filler[2], "", 1);
populate_string(style->filler[3], "", 1);
style->filler_entries = 4;
return true;
}
/* Default style. */
style->style_id = 1;
populate_string(style->indicator, "<=>", 3);
populate_string(style->tip, ">", 1);
populate_string(style->filler[0], " ", 1);
populate_string(style->filler[1], "=", 1);
style->filler_entries = 2;
if (0 == strcmp(name, "default"))
return true;
return false;
}
/*
* Return the index into display->barstyle for the style with the given
* name, adding that style to the array if it's not there already and
* there's room.
*
* If there is no room, returns zero, so the first style is re-used.
*/
int pv_display_barstyle_index(pvdisplay_t display, const char *name)
{
struct pvbarstyle_s style;
int barstyle_index;
#ifdef ENABLE_DEBUGGING
bool found;
#endif
memset(&style, 0, sizeof(style));
#ifdef ENABLE_DEBUGGING
found = pv_barstyle(&style, name);
if (!found)
debug("%s: %s", name, "bar style not found, using default");
#else
(void) pv_barstyle(&style, name);
#endif
for (barstyle_index = 0; barstyle_index < PV_BARSTYLE_MAX && display->barstyle[barstyle_index].style_id > 0;
barstyle_index++) {
if (display->barstyle[barstyle_index].style_id == style.style_id) {
debug("%s: %s: %d", name, "found in bar style array", barstyle_index);
return barstyle_index;
}
}
if (barstyle_index >= PV_BARSTYLE_MAX) {
debug("%s: %s", name, "no room to add another bar style - returning 0");
return 0;
}
memcpy(&(display->barstyle[barstyle_index]), &style, sizeof(style)); /* flawfinder: ignore */
/* flawfinder - the destination is an array element of the right size. */
debug("%s: %s: %d", name, "added to bar style array", barstyle_index);
return barstyle_index;
}
size_t pv_formatter_bar_default(pvformatter_args_t args)
{
if (0 == args->segment->parameter)
args->segment->parameter = 1 + pv_display_barstyle_index(args->display, "default");
return pv_formatter_progress_bar_only(args);
}
size_t pv_formatter_bar_block(pvformatter_args_t args)
{
if (0 == args->segment->parameter)
args->segment->parameter = 1 + pv_display_barstyle_index(args->display, "block");
return pv_formatter_progress_bar_only(args);
}
size_t pv_formatter_bar_granular(pvformatter_args_t args)
{
if (0 == args->segment->parameter)
args->segment->parameter = 1 + pv_display_barstyle_index(args->display, "granular");
return pv_formatter_progress_bar_only(args);
}
size_t pv_formatter_bar_shaded(pvformatter_args_t args)
{
if (0 == args->segment->parameter)
args->segment->parameter = 1 + pv_display_barstyle_index(args->display, "shaded");
return pv_formatter_progress_bar_only(args);
}
+119 -62
View File
@@ -12,6 +12,18 @@
#include <string.h>
/* Convenience macro for appending a string to the buffer. */
#define append_to_buffer(x) { \
if (buffer_offset < (buffer_size - x.bytes)) { \
memcpy(&(buffer[buffer_offset]), x.string, x.bytes); /* flawfinder: ignore */ \
buffer_offset += x.bytes; \
} \
}
/*
* flawfinder - we are checking that there is room in the destination
* buffer, given its size and our current offset.
*/
/*
* Write a progress bar to a buffer, in known-size or rate-gauge mode - a
@@ -29,38 +41,55 @@
*
* This is only called by pv_formatter_progress().
*/
static size_t pv_formatter_progress_knownsize(pvstate_t state, pvdisplay_t display, char *buffer, size_t buffer_size,
size_t width, bool bar_sides, bool include_bar, bool include_amount)
static size_t pv_formatter_progress_knownsize(pvformatter_args_t args, char *buffer, size_t buffer_size, bool bar_sides,
bool include_bar, bool include_amount)
{
char after_bar[32]; /* flawfinder: ignore - only populated by pv_snprintf(). */
size_t after_bar_bytes, after_bar_width;
size_t bar_area_width, filled_bar_width, buffer_offset, pad_count;
int bar_percentage;
double bar_percentage;
pvbarstyle_t style;
size_t full_cell_index;
bool has_tip = false;
buffer[0] = '\0';
if (args->segment->parameter > 0 && args->segment->parameter <= PV_BARSTYLE_MAX) {
style = &(args->display->barstyle[args->segment->parameter - 1]);
} else {
style = &(args->display->barstyle[0]);
}
full_cell_index = style->filler_entries;
if (full_cell_index > 0)
full_cell_index--;
if (1 == full_cell_index && style->tip.width > 0)
has_tip = true;
memset(after_bar, 0, sizeof(after_bar));
if (state->control.size > 0) {
if (args->state->control.size > 0) {
/* Percentage of data transferred. */
bar_percentage = state->calc.percentage;
(void) pv_snprintf(after_bar, sizeof(after_bar), " %3ld%%", bar_percentage);
bar_percentage = (double) (args->state->calc.percentage);
(void) pv_snprintf(after_bar, sizeof(after_bar), " %3ld%%", (int) bar_percentage);
} else {
/* Current rate vs max rate. */
bar_percentage = 0;
if (state->calc.rate_max > 0) {
bar_percentage = (int) (100.0 * state->calc.transfer_rate / state->calc.rate_max);
bar_percentage = 0.0;
if (args->state->calc.rate_max > 0) {
bar_percentage =
(double) (100.0 * args->state->calc.transfer_rate / args->state->calc.rate_max);
}
/*@-mustfreefresh@ */
if (state->control.bits && !state->control.linemode) {
if (args->state->control.bits && !args->state->control.linemode) {
/* bits per second */
pv_describe_amount(after_bar, sizeof(after_bar), "/%s",
8.0 * state->calc.rate_max, "", _("b/s"), display->count_type);
8.0 * args->state->calc.rate_max, "", _("b/s"), args->display->count_type);
} else {
/* bytes or lines per second */
pv_describe_amount(after_bar, sizeof(after_bar),
"/%s", state->calc.rate_max, _("/s"), _("B/s"), display->count_type);
"/%s", args->state->calc.rate_max, _("/s"), _("B/s"),
args->display->count_type);
}
/*@+mustfreefresh@ *//* splint: see above about gettext(). */
}
@@ -73,6 +102,10 @@ static size_t pv_formatter_progress_knownsize(pvstate_t state, pvdisplay_t displ
after_bar_width = pv_strwidth(after_bar, after_bar_bytes);
if (!include_bar) {
/*
* Only returning the "after bar" portion - the amount
* (progress or max rate).
*/
if (buffer_size < after_bar_bytes)
return 0;
if (after_bar_bytes > 1) {
@@ -85,24 +118,28 @@ static size_t pv_formatter_progress_knownsize(pvstate_t state, pvdisplay_t displ
}
if (bar_sides) {
if (width < (after_bar_width + 2))
if (args->segment->width < (after_bar_width + 2))
return 0;
bar_area_width = width - after_bar_width - 2;
bar_area_width = args->segment->width - after_bar_width - 2;
} else {
if (width < after_bar_width)
if (args->segment->width < after_bar_width)
return 0;
bar_area_width = width - after_bar_width;
bar_area_width = args->segment->width - after_bar_width;
}
if (bar_area_width > buffer_size - 16)
bar_area_width = buffer_size - 16;
/*
* Make sure there is enough room in the buffer, even if all of the
* bar characters are very wide.
*/
if (4 * bar_area_width > buffer_size - 16)
bar_area_width = (buffer_size - 16) / 4;
filled_bar_width = (size_t) (bar_area_width * bar_percentage) / 100;
filled_bar_width = (size_t) ((bar_area_width * bar_percentage) / 100);
/* Leave room for the tip of the bar. */
if (filled_bar_width > 0)
if (has_tip && filled_bar_width > 0)
filled_bar_width--;
debug("width=%d bar_area_width=%d filled_bar_width=%d", width, bar_area_width, filled_bar_width);
debug("width=%d bar_area_width=%d filled_bar_width=%d", args->segment->width, bar_area_width, filled_bar_width);
buffer_offset = 0;
@@ -112,22 +149,35 @@ static size_t pv_formatter_progress_knownsize(pvstate_t state, pvdisplay_t displ
}
/* The bar portion. */
for (pad_count = 0; pad_count < filled_bar_width && buffer_offset < buffer_size - 1; pad_count++) {
if (pad_count < bar_area_width)
buffer[buffer_offset++] = '=';
for (pad_count = 0; pad_count < filled_bar_width && buffer_offset < buffer_size - 4; pad_count++) {
if (pad_count < bar_area_width) {
append_to_buffer(style->filler[full_cell_index]);
}
}
/* The tip of the bar, if not at 100%. */
if (pad_count < bar_area_width) {
if (buffer_offset < buffer_size - 1)
buffer[buffer_offset++] = '>';
if (has_tip && pad_count < bar_area_width) {
append_to_buffer(style->tip);
pad_count++;
}
/* A partial cell, if there are intermediates and we're not at 100%. */
if (pad_count < bar_area_width && full_cell_index > 1 && !has_tip) {
double exact_width = (((double) bar_area_width) * bar_percentage) / 100.0;
double cell_portion = exact_width - (double) filled_bar_width;
double cell_index_double = ((double) full_cell_index) * cell_portion;
size_t cell_index = (size_t) cell_index_double;
if (cell_index > full_cell_index)
cell_index = full_cell_index;
append_to_buffer(style->filler[cell_index]);
pad_count++;
}
/* The spaces after the bar. */
for (; pad_count < bar_area_width; pad_count++) {
if (buffer_offset < buffer_size - 1)
buffer[buffer_offset++] = ' ';
append_to_buffer(style->filler[0]);
}
if (bar_sides) {
@@ -158,27 +208,37 @@ static size_t pv_formatter_progress_knownsize(pvstate_t state, pvdisplay_t displ
*
* This is only called by pv_formatter_progress().
*/
static size_t pv_formatter_progress_unknownsize(pvstate_t state, /*@unused@ */
__attribute__((unused)) pvdisplay_t display, char *buffer,
size_t buffer_size, size_t width, bool bar_sides)
static size_t pv_formatter_progress_unknownsize(pvformatter_args_t args, char *buffer,
size_t buffer_size, bool bar_sides)
{
size_t bar_area_width, buffer_offset, pad_count;
size_t indicator_position;
pvbarstyle_t style;
buffer[0] = '\0';
if (bar_sides) {
if (width < 6)
return 0;
bar_area_width = width - 5;
if (args->segment->parameter > 0 && args->segment->parameter <= PV_BARSTYLE_MAX) {
style = &(args->display->barstyle[args->segment->parameter - 1]);
} else {
if (width < 5)
return 0;
bar_area_width = width - 3;
style = &(args->display->barstyle[0]);
}
if (bar_area_width > buffer_size - 16)
bar_area_width = buffer_size - 16;
if (bar_sides) {
if (args->segment->width < (style->indicator.width + 3))
return 0;
bar_area_width = args->segment->width - (style->indicator.width + 2);
} else {
if (args->segment->width < (style->indicator.width + 2))
return 0;
bar_area_width = args->segment->width - style->indicator.width;
}
/*
* Make sure there is enough room in the buffer, even if all of the
* bar characters are very wide.
*/
if (4 * bar_area_width > buffer_size - 16)
bar_area_width = (buffer_size - 16) / 4;
/*
* Note that pv_calculate_transfer_rate() sets the percentage when
@@ -186,7 +246,7 @@ static size_t pv_formatter_progress_unknownsize(pvstate_t state, /*@unused@ */
* here we make values above 100 send the indicator back down again,
* so it moves back and forth.
*/
indicator_position = (size_t) (state->calc.percentage);
indicator_position = (size_t) (args->state->calc.percentage);
if (indicator_position > 200)
indicator_position = indicator_position % 200;
if (indicator_position > 100 && indicator_position <= 200)
@@ -201,21 +261,19 @@ static size_t pv_formatter_progress_unknownsize(pvstate_t state, /*@unused@ */
/* The spaces before the indicator. */
for (pad_count = 0; pad_count < (bar_area_width * indicator_position) / 100; pad_count++) {
if (pad_count < bar_area_width && buffer_offset < buffer_size - 1)
buffer[buffer_offset++] = ' ';
if (pad_count < bar_area_width) {
append_to_buffer(style->filler[0]);
}
}
/* The indicator. */
if (buffer_offset < buffer_size - 4) {
buffer[buffer_offset++] = '<';
buffer[buffer_offset++] = '=';
buffer[buffer_offset++] = '>';
if (buffer_offset < buffer_size - style->indicator.bytes) {
append_to_buffer(style->indicator);
}
/* The spaces after the indicator. */
for (; pad_count < bar_area_width; pad_count++) {
if (pad_count < bar_area_width && buffer_offset < buffer_size - 1)
buffer[buffer_offset++] = ' ';
append_to_buffer(style->filler[0]);
}
if (bar_sides) {
@@ -240,17 +298,18 @@ size_t pv_formatter_progress(pvformatter_args_t args)
content[0] = '\0';
if (0 == args->segment->parameter)
args->segment->parameter = 1 + pv_display_barstyle_index(args->display, "default");
if (0 == args->buffer_size)
return 0;
if (args->state->control.size > 0 || args->state->control.rate_gauge) {
/* Known size or rate gauge - bar with percentage. */
bytes =
pv_formatter_progress_knownsize(args->state, args->display, content, sizeof(content), args->segment->width, true, true,
true);
bytes = pv_formatter_progress_knownsize(args, content, sizeof(content), true, true, true);
} else {
/* Unknown size - back-and-forth moving indicator. */
bytes = pv_formatter_progress_unknownsize(args->state, args->display, content, sizeof(content), args->segment->width, true);
bytes = pv_formatter_progress_unknownsize(args, content, sizeof(content), true);
}
content[bytes] = '\0';
@@ -269,18 +328,18 @@ size_t pv_formatter_progress_bar_only(pvformatter_args_t args)
content[0] = '\0';
if (0 == args->segment->parameter)
args->segment->parameter = 1 + pv_display_barstyle_index(args->display, "default");
if (0 == args->buffer_size)
return 0;
if (args->state->control.size > 0 || args->state->control.rate_gauge) {
/* Known size or rate gauge - bar with percentage. */
bytes =
pv_formatter_progress_knownsize(args->state, args->display, content, sizeof(content), args->segment->width, false, true,
false);
bytes = pv_formatter_progress_knownsize(args, content, sizeof(content), false, true, false);
} else {
/* Unknown size - back-and-forth moving indicator. */
bytes =
pv_formatter_progress_unknownsize(args->state, args->display, content, sizeof(content), args->segment->width, false);
bytes = pv_formatter_progress_unknownsize(args, content, sizeof(content), false);
}
content[bytes] = '\0';
@@ -304,9 +363,7 @@ size_t pv_formatter_progress_amount_only(pvformatter_args_t args)
if (args->state->control.size > 0 || args->state->control.rate_gauge) {
/* Known size or rate gauge - percentage or rate. */
bytes =
pv_formatter_progress_knownsize(args->state, args->display, content, sizeof(content), args->segment->width, false,
false, true);
bytes = pv_formatter_progress_knownsize(args, content, sizeof(content), false, false, true);
} else {
/* Unknown size - no number. */
return 0;