Add colour code support with a %{sgr:...} format sequence.
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
/* Debugging support enabled */
|
||||
#undef ENABLE_DEBUGGING
|
||||
|
||||
/* Build with ncurses support */
|
||||
#undef ENABLE_NCURSES
|
||||
|
||||
/* Define to 1 if translation of program messages to the user's native
|
||||
language is requested. */
|
||||
#undef ENABLE_NLS
|
||||
@@ -203,6 +206,9 @@
|
||||
/* Define to 1 if you have the <termios.h> header file. */
|
||||
#undef HAVE_TERMIOS_H
|
||||
|
||||
/* Define to 1 if you have the <term.h> header file. */
|
||||
#undef HAVE_TERM_H
|
||||
|
||||
/* Define to 1 if you have the <unistd.h> header file. */
|
||||
#undef HAVE_UNISTD_H
|
||||
|
||||
|
||||
@@ -186,6 +186,8 @@ struct pvstate_s {
|
||||
bool discard_input; /* write nothing to stdout */
|
||||
bool show_stats; /* show statistics on exit */
|
||||
bool can_display_utf8; /* whether UTF-8 output is permitted */
|
||||
bool can_display_colour; /* whether the terminal supports colour */
|
||||
bool checked_colour_support; /* whether we have checked colour support yet */
|
||||
bool width_set_manually; /* width was set manually, not detected */
|
||||
bool height_set_manually; /* height was set manually, not detected */
|
||||
} control;
|
||||
@@ -237,10 +239,12 @@ struct pvstate_s {
|
||||
/* See pv__format_init() for more details. */
|
||||
int type; /* component type, -1 for static string */
|
||||
int parameter; /* component parameter, such as bar style index */
|
||||
uint8_t string_parameter_bytes; /* number of bytes in string_parameter */
|
||||
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 offset; /* start offset of this segment in the build buffer */
|
||||
size_t bytes; /* length of segment in bytes in the build buffer */
|
||||
size_t width; /* displayed width of segment */
|
||||
/*@dependent@*/ /*@null@*/ const char *string_parameter; /* parameter after colon in %{x:} */
|
||||
} format[PV_FORMAT_ARRAY_MAX];
|
||||
|
||||
struct pvbarstyle_s barstyle[PV_BARSTYLE_MAX];
|
||||
@@ -273,6 +277,9 @@ struct pvstate_s {
|
||||
bool showing_last_written; /* set if displaying the last few bytes written */
|
||||
bool showing_previous_line; /* set if displaying the previously output line */
|
||||
|
||||
bool format_uses_colour; /* set if the format string uses colours */
|
||||
bool colour_permitted; /* whether colour is permitted for this display */
|
||||
bool sgr_code_active; /* set while SGR code is active in a display line */
|
||||
bool final_update; /* set internally on the final update */
|
||||
bool display_visible; /* set once anything written to terminal */
|
||||
|
||||
@@ -511,6 +518,7 @@ size_t pv_formatter_buffer_percent(pvformatter_args_t);
|
||||
size_t pv_formatter_last_written(pvformatter_args_t);
|
||||
size_t pv_formatter_previous_line(pvformatter_args_t);
|
||||
size_t pv_formatter_name(pvformatter_args_t);
|
||||
size_t pv_formatter_sgr(pvformatter_args_t);
|
||||
|
||||
bool pv_format(pvstate_t, /*@null@*/ const char *, pvdisplay_t, bool, bool);
|
||||
void pv_display(pvstate_t, bool);
|
||||
|
||||
+115
-9
@@ -22,6 +22,13 @@
|
||||
#include <termios.h>
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_NCURSES
|
||||
#ifdef HAVE_TERM_H
|
||||
#include <term.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
* We need sys/ioctl.h for ioctl() regardless of whether TIOCGWINSZ is
|
||||
* defined in termios.h, so we no longer use AC_HEADER_TIOCGWINSZ in
|
||||
@@ -566,6 +573,7 @@ static bool pv__format_numeric(pvstate_t state, pvdisplay_t display)
|
||||
{ "{previous-line}", &pv_formatter_previous_line, true },
|
||||
{ "N", &pv_formatter_name, false },
|
||||
{ "{name}", &pv_formatter_name, false },
|
||||
{ "{sgr:colour,...}", &pv_formatter_sgr, false },
|
||||
{ NULL, NULL, false }
|
||||
};
|
||||
return format_component_array;
|
||||
@@ -636,6 +644,7 @@ static void pv__format_init(pvstate_t state, /*@null@ */ const char *format_supp
|
||||
display->showing_rate = false;
|
||||
display->showing_last_written = false;
|
||||
display->showing_previous_line = false;
|
||||
display->format_uses_colour = false;
|
||||
|
||||
display_format = NULL == format_supplied ? state->control.default_format : format_supplied;
|
||||
|
||||
@@ -666,6 +675,8 @@ static void pv__format_init(pvstate_t state, /*@null@ */ const char *format_supp
|
||||
for (strpos = 0; display_format[strpos] != '\0' && segment < PV_FORMAT_ARRAY_MAX; strpos++, segment++) {
|
||||
int component_type, component_idx;
|
||||
size_t str_start, str_bytes, chosen_size;
|
||||
const char *string_parameter = NULL;
|
||||
size_t string_parameter_bytes = 0;
|
||||
|
||||
str_start = strpos;
|
||||
str_bytes = 0;
|
||||
@@ -674,7 +685,7 @@ static void pv__format_init(pvstate_t state, /*@null@ */ const char *format_supp
|
||||
|
||||
if ('%' == display_format[strpos]) {
|
||||
unsigned long number_prefix;
|
||||
size_t percent_sign_offset, sequence_start, sequence_length;
|
||||
size_t percent_sign_offset, sequence_start, sequence_length, sequence_colon_offset;
|
||||
#if HAVE_STRTOUL
|
||||
char *number_end_ptr;
|
||||
#endif
|
||||
@@ -705,11 +716,14 @@ static void pv__format_init(pvstate_t state, /*@null@ */ const char *format_supp
|
||||
|
||||
sequence_start = strpos;
|
||||
sequence_length = 0;
|
||||
sequence_colon_offset = 0;
|
||||
if ('\0' != display_format[strpos])
|
||||
sequence_length = 1;
|
||||
if ('{' == display_format[strpos]) {
|
||||
while ('\0' != display_format[strpos] && '}' != display_format[strpos]
|
||||
&& '%' != display_format[strpos]) {
|
||||
if (':' == display_format[strpos])
|
||||
sequence_colon_offset = sequence_length;
|
||||
strpos++;
|
||||
sequence_length++;
|
||||
}
|
||||
@@ -718,15 +732,40 @@ static void pv__format_init(pvstate_t state, /*@null@ */ const char *format_supp
|
||||
component_type = -1;
|
||||
for (component_idx = 0; NULL != format_component_array[component_idx].match; component_idx++) {
|
||||
size_t component_sequence_length = strlen(format_component_array[component_idx].match); /* flawfinder: ignore */
|
||||
char *component_colon_pointer =
|
||||
strchr(format_component_array[component_idx].match, (int) ':');
|
||||
|
||||
/* flawfinder - static strings, guaranteed null-terminated. */
|
||||
if (component_sequence_length != sequence_length)
|
||||
continue;
|
||||
if (0 !=
|
||||
strncmp(format_component_array[component_idx].match,
|
||||
&(display_format[sequence_start]), sequence_length))
|
||||
continue;
|
||||
component_type = component_idx;
|
||||
break;
|
||||
|
||||
if ((component_sequence_length == sequence_length)
|
||||
&& (0 ==
|
||||
strncmp(format_component_array[component_idx].match,
|
||||
&(display_format[sequence_start]), sequence_length))
|
||||
) {
|
||||
component_type = component_idx;
|
||||
break;
|
||||
}
|
||||
|
||||
if (sequence_colon_offset > 0 && NULL != component_colon_pointer) {
|
||||
size_t component_colon_offset =
|
||||
(size_t) (1 + component_colon_pointer -
|
||||
format_component_array[component_idx].match);
|
||||
if ((component_colon_offset == sequence_colon_offset)
|
||||
&& (0 ==
|
||||
strncmp(format_component_array[component_idx].match,
|
||||
&(display_format[sequence_start]), sequence_colon_offset))
|
||||
) {
|
||||
component_type = component_idx;
|
||||
string_parameter =
|
||||
&(display_format[sequence_start + sequence_colon_offset]);
|
||||
string_parameter_bytes = sequence_length - sequence_colon_offset;
|
||||
if (string_parameter_bytes > 0)
|
||||
string_parameter_bytes--; /* the closing '}' */
|
||||
if (string_parameter_bytes > 255)
|
||||
string_parameter_bytes = 255;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-1 == component_type) {
|
||||
@@ -770,6 +809,8 @@ static void pv__format_init(pvstate_t state, /*@null@ */ const char *format_supp
|
||||
|
||||
display->format[segment].type = component_type;
|
||||
display->format[segment].chosen_size = chosen_size;
|
||||
display->format[segment].string_parameter = string_parameter;
|
||||
display->format[segment].string_parameter_bytes = string_parameter_bytes;
|
||||
|
||||
if (-1 == component_type) {
|
||||
if (0 == str_bytes)
|
||||
@@ -824,6 +865,51 @@ static void pv__format_init(pvstate_t state, /*@null@ */ const char *format_supp
|
||||
|
||||
display->format_segment_count++;
|
||||
}
|
||||
|
||||
if (display->format_uses_colour && !state->control.checked_colour_support) {
|
||||
state->control.checked_colour_support = true;
|
||||
#ifdef ENABLE_NCURSES
|
||||
/*
|
||||
* If we have terminal info support, check whether the
|
||||
* current terminal supports colour - or just assume it's
|
||||
* supported if we're forcing output.
|
||||
*/
|
||||
if (true == state->control.force) {
|
||||
state->control.can_display_colour = true;
|
||||
debug("%s", "force mode - assuming terminal supports colour");
|
||||
} else {
|
||||
char *term_env = NULL;
|
||||
|
||||
term_env = getenv("TERM"); /* flawfinder: ignore */
|
||||
/*
|
||||
* flawfinder - here we pass responsibility to the
|
||||
* ncurses library to behave OK with $TERM.
|
||||
*/
|
||||
state->control.can_display_colour = false;
|
||||
if (NULL != term_env) {
|
||||
int setup_err = 0;
|
||||
|
||||
if ((0 == setupterm(term_env, STDERR_FILENO, &setup_err))
|
||||
&& (tigetnum("colors") > 1)
|
||||
) {
|
||||
state->control.can_display_colour = true;
|
||||
debug("%s: %s", term_env, "terminal supports colour");
|
||||
}
|
||||
} else {
|
||||
/* If TERM is unset, disable colour. */
|
||||
state->control.can_display_colour = false;
|
||||
}
|
||||
}
|
||||
#else /* ! ENABLE_NCURSES */
|
||||
/*
|
||||
* Without terminal info support, always assume colour is
|
||||
* supported.
|
||||
*/
|
||||
/* TODO: call "tput colors" with popen() instead. */
|
||||
state->control.can_display_colour = true;
|
||||
debug("%s", "no terminal info support - assuming terminal supports colour");
|
||||
#endif /* ENABLE_NCURSES */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -933,6 +1019,9 @@ bool pv_format(pvstate_t state, /*@null@ */ const char *format_supplied, pvdispl
|
||||
return pv__format_numeric(state, display);
|
||||
}
|
||||
|
||||
/* Clear the SGR active codes flag, for the SGR formatter. */
|
||||
display->sgr_code_active = false;
|
||||
|
||||
/*
|
||||
* Populate the internal segments buffer with each component's
|
||||
* output, in two passes.
|
||||
@@ -1066,6 +1155,16 @@ bool pv_format(pvstate_t state, /*@null@ */ const char *format_supplied, pvdispl
|
||||
segment->bytes, display->display_buffer + display_buffer_offset - segment->bytes);
|
||||
}
|
||||
|
||||
/* If the SGR active codes flag is set, we need to emit an SGR reset. */
|
||||
if (display->sgr_code_active) {
|
||||
debug("%s", "SGR codes still active - adding reset");
|
||||
(void) pv_strlcat(display->display_buffer, "\033[m", display->display_buffer_size);
|
||||
new_display_string_bytes += 3;
|
||||
if (new_display_string_bytes > display->display_buffer_size)
|
||||
new_display_string_bytes = display->display_buffer_size;
|
||||
display->sgr_code_active = false;
|
||||
}
|
||||
|
||||
debug("%s: %d", "new display string length in bytes", (int) new_display_string_bytes);
|
||||
debug("%s: %d", "new display string width", (int) new_display_string_width);
|
||||
|
||||
@@ -1117,6 +1216,13 @@ void pv_display(pvstate_t state, bool final)
|
||||
|
||||
pv_calculate_transfer_rate(state, final);
|
||||
|
||||
/*
|
||||
* Enable colour on the main display, and disable it on the extra
|
||||
* display (process title, window title).
|
||||
*/
|
||||
state->display.colour_permitted = true;
|
||||
state->extra_display.colour_permitted = false;
|
||||
|
||||
/*
|
||||
* If the display options need reparsing, do so to generate new
|
||||
* formatting parameters.
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Formatter function for ECMA-48 SGR codes.
|
||||
*
|
||||
* 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>
|
||||
|
||||
struct sgr_keyword_map_s {
|
||||
/*@null@ */ const char *keyword;
|
||||
uint8_t bytes;
|
||||
uint8_t code;
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Return an array mapping keywords to ECMA-48 SGR code numbers.
|
||||
*/
|
||||
/*@keep@ */ static struct sgr_keyword_map_s *sgr_keywords(void)
|
||||
{
|
||||
/*@keep@ */ static struct sgr_keyword_map_s keywords[] = {
|
||||
{ "reset", 0, 0 },
|
||||
{ "none", 0, 0 },
|
||||
{ "bold", 0, 1 },
|
||||
{ "dim", 0, 2 },
|
||||
{ "italic", 0, 3 },
|
||||
{ "underscore", 0, 4 },
|
||||
{ "underline", 0, 4 },
|
||||
{ "blink", 0, 5 },
|
||||
{ "reverse", 0, 7 },
|
||||
{ "no-bold", 0, 22 }, /* same as no-dim */
|
||||
{ "no-dim", 0, 22 },
|
||||
{ "no-italic", 0, 23 },
|
||||
{ "no-underscore", 0, 24 },
|
||||
{ "no-underline", 0, 24 },
|
||||
{ "no-blink", 0, 25 },
|
||||
{ "no-reverse", 0, 27 },
|
||||
{ "black", 0, 30 },
|
||||
{ "red", 0, 31 },
|
||||
{ "green", 0, 32 },
|
||||
{ "brown", 0, 33 },
|
||||
{ "yellow", 0, 33 },
|
||||
{ "blue", 0, 34 },
|
||||
{ "magenta", 0, 35 },
|
||||
{ "cyan", 0, 36 },
|
||||
{ "white", 0, 37 },
|
||||
{ "fg-black", 0, 30 },
|
||||
{ "fg-red", 0, 31 },
|
||||
{ "fg-green", 0, 32 },
|
||||
{ "fg-brown", 0, 33 },
|
||||
{ "fg-yellow", 0, 33 },
|
||||
{ "fg-blue", 0, 34 },
|
||||
{ "fg-magenta", 0, 35 },
|
||||
{ "fg-cyan", 0, 36 },
|
||||
{ "fg-white", 0, 37 },
|
||||
{ "fg-default", 0, 39 },
|
||||
{ "bg-black", 0, 40 },
|
||||
{ "bg-red", 0, 41 },
|
||||
{ "bg-green", 0, 42 },
|
||||
{ "bg-brown", 0, 43 },
|
||||
{ "bg-yellow", 0, 43 },
|
||||
{ "bg-blue", 0, 44 },
|
||||
{ "bg-magenta", 0, 45 },
|
||||
{ "bg-cyan", 0, 46 },
|
||||
{ "bg-white", 0, 47 },
|
||||
{ "bg-default", 0, 49 },
|
||||
{ NULL, 0, 0 }
|
||||
};
|
||||
int keyword_index;
|
||||
|
||||
/* Calculate the lengths of each keyword on the first call. */
|
||||
if (0 == keywords[0].bytes) {
|
||||
for (keyword_index = 0; NULL != keywords[keyword_index].keyword; keyword_index++) {
|
||||
keywords[keyword_index].bytes = strlen(keywords[keyword_index].keyword); /* flawfinder: ignore */
|
||||
/* flawfinder - strlen() on a static null-terminated string is OK. */
|
||||
}
|
||||
}
|
||||
|
||||
/*@-compmempass@ */
|
||||
return keywords;
|
||||
/*@+compmempass@ */
|
||||
/*
|
||||
* splint - found no other way to pass static back without a false
|
||||
* positive warning about a memory leak.
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display SGR codes if colour output is supported.
|
||||
*/
|
||||
size_t pv_formatter_sgr(pvformatter_args_t args)
|
||||
{
|
||||
/*@keep@ */ static struct sgr_keyword_map_s *keywords;
|
||||
char content[1024]; /* flawfinder: ignore */
|
||||
size_t write_position, read_position, keyword_start, keyword_length;
|
||||
int numeric_value, code_count, most_recent_code;
|
||||
|
||||
/* flawfinder - null-terminated and bounded with pv_snprintf(). */
|
||||
|
||||
args->display->format_uses_colour = true;
|
||||
|
||||
if (!args->display->colour_permitted)
|
||||
return 0;
|
||||
if (!args->state->control.can_display_colour)
|
||||
return 0;
|
||||
if (NULL == args->segment->string_parameter)
|
||||
return 0;
|
||||
if (0 == args->segment->string_parameter_bytes)
|
||||
return 0;
|
||||
|
||||
keywords = sgr_keywords();
|
||||
|
||||
write_position = 0;
|
||||
content[0] = '\0';
|
||||
|
||||
debug("%p+%d [%.*s]", args->segment->string_parameter, args->segment->string_parameter_bytes,
|
||||
args->segment->string_parameter_bytes, args->segment->string_parameter);
|
||||
|
||||
read_position = 0;
|
||||
keyword_start = 0;
|
||||
keyword_length = 0;
|
||||
numeric_value = -1;
|
||||
code_count = 0;
|
||||
most_recent_code = -1;
|
||||
|
||||
while (read_position < args->segment->string_parameter_bytes) {
|
||||
char read_char = args->segment->string_parameter[read_position++];
|
||||
if ((',' == read_char) || (';' == read_char)) {
|
||||
keyword_length = read_position - keyword_start;
|
||||
if (keyword_length > 0)
|
||||
keyword_length--;
|
||||
} else {
|
||||
if ((read_char >= '0' && read_char <= '9')
|
||||
&& (numeric_value >= 0 || keyword_start == read_position - 1)) {
|
||||
if (keyword_start == read_position - 1)
|
||||
numeric_value = 0;
|
||||
numeric_value = numeric_value * 10 + (int) (read_char - '0');
|
||||
} else {
|
||||
numeric_value = -1;
|
||||
}
|
||||
if (read_position >= args->segment->string_parameter_bytes) {
|
||||
keyword_length = read_position - keyword_start;
|
||||
}
|
||||
}
|
||||
if (keyword_length > 0) {
|
||||
int code = -1;
|
||||
|
||||
debug("keyword@%d+%d: [%.*s]; numeric=%d", keyword_start, keyword_length, keyword_length,
|
||||
&(args->segment->string_parameter[keyword_start]), numeric_value);
|
||||
|
||||
if (numeric_value >= 0 && numeric_value < 255) {
|
||||
code = numeric_value;
|
||||
} else {
|
||||
int keyword_index;
|
||||
for (keyword_index = 0; -1 == code && NULL != keywords[keyword_index].keyword;
|
||||
keyword_index++) {
|
||||
if (keyword_length != keywords[keyword_index].bytes)
|
||||
continue;
|
||||
if (0 !=
|
||||
strncmp(keywords[keyword_index].keyword,
|
||||
&(args->segment->string_parameter[keyword_start]), keyword_length))
|
||||
continue;
|
||||
code = (int) (keywords[keyword_index].code);
|
||||
}
|
||||
}
|
||||
|
||||
debug("code=%d", code);
|
||||
|
||||
if (code >= 0) {
|
||||
if (code_count > 15) {
|
||||
write_position +=
|
||||
pv_snprintf(content + write_position, sizeof(content) - write_position,
|
||||
"%s", "m");
|
||||
code_count = 0;
|
||||
}
|
||||
if (0 == code_count) {
|
||||
write_position +=
|
||||
pv_snprintf(content + write_position, sizeof(content) - write_position,
|
||||
"%s", "\033[");
|
||||
} else {
|
||||
write_position +=
|
||||
pv_snprintf(content + write_position, sizeof(content) - write_position,
|
||||
"%s", ";");
|
||||
}
|
||||
write_position +=
|
||||
pv_snprintf(content + write_position, sizeof(content) - write_position, "%d", code);
|
||||
code_count++;
|
||||
most_recent_code = code;
|
||||
}
|
||||
|
||||
keyword_length = 0;
|
||||
keyword_start = read_position;
|
||||
}
|
||||
}
|
||||
|
||||
if (code_count > 0)
|
||||
write_position += pv_snprintf(content + write_position, sizeof(content) - write_position, "%s", "m");
|
||||
|
||||
if (most_recent_code > 0) {
|
||||
args->display->sgr_code_active = true;
|
||||
} else if (0 == most_recent_code) {
|
||||
args->display->sgr_code_active = false;
|
||||
}
|
||||
|
||||
return pv_formatter_segmentcontent(content, args);
|
||||
}
|
||||
Reference in New Issue
Block a user