Add colour code support with a %{sgr:...} format sequence.

This commit is contained in:
Andrew Wood
2024-12-15 23:25:20 +00:00
parent 43e2eaa4db
commit 3c09b31f92
18 changed files with 564 additions and 133 deletions
+6
View File
@@ -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
+10 -2
View File
@@ -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
View File
@@ -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.
+213
View File
@@ -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);
}