From 855d02acab5a2f679a736fcaa0b88de068b0922b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Sun, 22 Feb 2015 19:15:06 +0100 Subject: Add support for attributed output Colours, colours, colours. Configurable. --- json-rpc-shell.c | 423 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 403 insertions(+), 20 deletions(-) (limited to 'json-rpc-shell.c') diff --git a/json-rpc-shell.c b/json-rpc-shell.c index 9fb0205..6e8a4f6 100644 --- a/json-rpc-shell.c +++ b/json-rpc-shell.c @@ -21,11 +21,25 @@ /// Some arbitrary limit for the history file #define HISTORY_LIMIT 10000 +// String constants for all attributes we use for output +#define ATTR_PROMPT "attr_prompt" +#define ATTR_RESET "attr_reset" +#define ATTR_WARNING "attr_warning" +#define ATTR_ERROR "attr_error" +#define ATTR_INCOMING "attr_incoming" +#define ATTR_OUTGOING "attr_outgoing" + +// User data for logger functions to enable formatted logging +#define print_fatal_data ATTR_ERROR +#define print_error_data ATTR_ERROR +#define print_warning_data ATTR_WARNING + #include "config.h" #include "utils.c" #include #include +#include #include #include @@ -33,13 +47,38 @@ #include #include +#include +#include + +// --- Configuration (application-specific) ------------------------------------ + +static struct config_item g_config_table[] = +{ + { ATTR_PROMPT, NULL, "Terminal attributes for the prompt" }, + { ATTR_RESET, NULL, "String to reset terminal attributes" }, + { ATTR_WARNING, NULL, "Terminal attributes for warnings" }, + { ATTR_ERROR, NULL, "Terminal attributes for errors" }, + { ATTR_INCOMING, NULL, "Terminal attributes for incoming traffic" }, + { ATTR_OUTGOING, NULL, "Terminal attributes for outgoing traffic" }, + { NULL, NULL, NULL } +}; + // --- Main program ------------------------------------------------------------ +enum color_mode +{ + COLOR_AUTO, ///< Autodetect if colours are available + COLOR_ALWAYS, ///< Always use coloured output + COLOR_NEVER ///< Never use coloured output +}; + static struct app_context { CURL *curl; ///< cURL handle char curl_error[CURL_ERROR_SIZE]; ///< cURL error info buffer + struct str_map config; ///< Program configuration + enum color_mode color_mode; ///< Colour output mode bool pretty_print; ///< Whether to pretty print bool verbose; ///< Print requests bool trust_all; ///< Don't verify peer certificates @@ -52,6 +91,321 @@ static struct app_context } g_ctx; +// --- Attributed output ------------------------------------------------------- + +static struct +{ + bool initialized; ///< Terminal is available + bool stdout_is_tty; ///< `stdout' is a terminal + bool stderr_is_tty; ///< `stderr' is a terminal + + char *color_set[8]; ///< Codes to set the foreground colour +} +g_terminal; + +static bool +init_terminal (void) +{ + int tty_fd = -1; + if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO))) + tty_fd = STDERR_FILENO; + if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO))) + tty_fd = STDOUT_FILENO; + + int err; + if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR) + return false; + + // Make sure all terminal features used by us are supported + if (!set_a_foreground || !enter_bold_mode || !exit_attribute_mode) + { + del_curterm (cur_term); + return false; + } + + for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++) + g_terminal.color_set[i] = xstrdup (tparm (set_a_foreground, + i, 0, 0, 0, 0, 0, 0, 0, 0)); + + return g_terminal.initialized = true; +} + +static void +free_terminal (void) +{ + if (!g_terminal.initialized) + return; + + for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++) + free (g_terminal.color_set[i]); + del_curterm (cur_term); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +typedef int (*terminal_printer_fn) (int); + +static int +putchar_stderr (int c) +{ + return fputc (c, stderr); +} + +static terminal_printer_fn +get_attribute_printer (FILE *stream) +{ + if (stream == stdout && g_terminal.stdout_is_tty) + return putchar; + if (stream == stderr && g_terminal.stderr_is_tty) + return putchar_stderr; + return NULL; +} + +static void +vprint_attributed (struct app_context *ctx, + FILE *stream, const char *attribute, const char *fmt, va_list ap) +{ + terminal_printer_fn printer = get_attribute_printer (stream); + if (!attribute) + printer = NULL; + + const char *value; + value = str_map_find (&ctx->config, attribute); + if (printer && soft_assert (value)) + tputs (value, 1, printer); + + vfprintf (stream, fmt, ap); + + value = str_map_find (&ctx->config, ATTR_RESET); + if (printer && soft_assert (value)) + tputs (value, 1, printer); +} + +static void +print_attributed (struct app_context *ctx, + FILE *stream, const char *attribute, const char *fmt, ...) +{ + va_list ap; + va_start (ap, fmt); + vprint_attributed (ctx, stream, attribute, fmt, ap); + va_end (ap); +} + +static void +log_message_attributed (void *user_data, const char *quote, const char *fmt, + va_list ap) +{ + FILE *stream = stderr; + + print_attributed (&g_ctx, stream, user_data, "%s", quote); + vprint_attributed (&g_ctx, stream, user_data, fmt, ap); + fputs ("\n", stream); +} + +static void +init_colors (struct app_context *ctx) +{ + // Use escape sequences from terminfo if possible, and SGR as a fallback + if (init_terminal ()) + { + const char *attrs[][2] = + { + { ATTR_PROMPT, enter_bold_mode }, + { ATTR_RESET, exit_attribute_mode }, + { ATTR_WARNING, g_terminal.color_set[3] }, + { ATTR_ERROR, g_terminal.color_set[1] }, + { ATTR_INCOMING, "" }, + { ATTR_OUTGOING, "" }, + }; + for (size_t i = 0; i < N_ELEMENTS (attrs); i++) + str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1])); + } + else + { + const char *attrs[][2] = + { + { ATTR_PROMPT, "\x1b[1m" }, + { ATTR_RESET, "\x1b[0m" }, + { ATTR_WARNING, "\x1b[33m" }, + { ATTR_ERROR, "\x1b[31m" }, + { ATTR_INCOMING, "" }, + { ATTR_OUTGOING, "" }, + }; + for (size_t i = 0; i < N_ELEMENTS (attrs); i++) + str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1])); + } + + switch (ctx->color_mode) + { + case COLOR_ALWAYS: + g_terminal.stdout_is_tty = true; + g_terminal.stderr_is_tty = true; + break; + case COLOR_AUTO: + if (!g_terminal.initialized) + { + case COLOR_NEVER: + g_terminal.stdout_is_tty = false; + g_terminal.stderr_is_tty = false; + } + } + + g_log_message_real = log_message_attributed; +} + +// --- Configuration loading --------------------------------------------------- + +static bool +read_hexa_escape (const char **cursor, struct str *output) +{ + int i; + char c, code = 0; + + for (i = 0; i < 2; i++) + { + c = tolower (*(*cursor)); + if (c >= '0' && c <= '9') + code = (code << 4) | (c - '0'); + else if (c >= 'a' && c <= 'f') + code = (code << 4) | (c - 'a' + 10); + else + break; + + (*cursor)++; + } + + if (!i) + return false; + + str_append_c (output, code); + return true; +} + +static bool +read_octal_escape (const char **cursor, struct str *output) +{ + int i; + char c, code = 0; + + for (i = 0; i < 3; i++) + { + c = *(*cursor); + if (c < '0' || c > '7') + break; + + code = (code << 3) | (c - '0'); + (*cursor)++; + } + + if (!i) + return false; + + str_append_c (output, code); + return true; +} + +static bool +read_string_escape_sequence (const char **cursor, + struct str *output, struct error **e) +{ + int c; + switch ((c = *(*cursor)++)) + { + case '?': str_append_c (output, '?'); break; + case '"': str_append_c (output, '"'); break; + case '\\': str_append_c (output, '\\'); break; + case 'a': str_append_c (output, '\a'); break; + case 'b': str_append_c (output, '\b'); break; + case 'f': str_append_c (output, '\f'); break; + case 'n': str_append_c (output, '\n'); break; + case 'r': str_append_c (output, '\r'); break; + case 't': str_append_c (output, '\t'); break; + case 'v': str_append_c (output, '\v'); break; + + case 'e': + case 'E': + str_append_c (output, '\x1b'); + break; + + case 'x': + case 'X': + if (!read_hexa_escape (cursor, output)) + { + error_set (e, "invalid hexadecimal escape"); + return false; + } + break; + + case '\0': + error_set (e, "premature end of escape sequence"); + return false; + + default: + (*cursor)--; + if (!read_octal_escape (cursor, output)) + { + error_set (e, "unknown escape sequence"); + return false; + } + } + return true; +} + +static bool +unescape_string (const char *s, struct str *output, struct error **e) +{ + int c; + while ((c = *s++)) + { + if (c != '\\') + str_append_c (output, c); + else if (!read_string_escape_sequence (&s, output, e)) + return false; + } + return true; +} + +static void +load_config (struct app_context *ctx) +{ + // TODO: employ a better configuration file format, so that we don't have + // to do this convoluted post-processing anymore. + + struct str_map map; + str_map_init (&map); + map.free = free; + + struct error *e = NULL; + if (!read_config_file (&map, &e)) + { + print_error ("error loading configuration: %s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + struct str_map_iter iter; + str_map_iter_init (&iter, &map); + while (str_map_iter_next (&iter)) + { + struct error *e = NULL; + struct str value; + str_init (&value); + if (!unescape_string (iter.link->data, &value, &e)) + { + print_error ("error reading configuration: %s: %s", + iter.link->key, e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + str_map_set (&ctx->config, iter.link->key, str_steal (&value)); + } + + str_map_free (&map); +} + +// --- Main program ------------------------------------------------------------ + #define PARSE_FAIL(...) \ BLOCK_START \ print_error (__VA_ARGS__); \ @@ -146,7 +500,7 @@ parse_response (struct app_context *ctx, struct str *buf) if (!s) print_error ("character conversion failed for `%s'", "result"); else - printf ("%s\n", s); + print_attributed (ctx, stdout, ATTR_INCOMING, "%s\n", s); free (s); } @@ -250,7 +604,7 @@ make_json_rpc_call (struct app_context *ctx, if (!req_term) print_error ("%s: %s", "verbose", "character conversion failed"); else - printf ("%s\n", req_term); + print_attributed (ctx, stdout, ATTR_OUTGOING, "%s\n", req_term); free (req_term); } @@ -458,6 +812,11 @@ parse_program_arguments (struct app_context *ctx, int argc, char **argv, { 'p', "pretty", NULL, 0, "pretty-print the responses" }, { 't', "trust-all", NULL, 0, "don't care about SSL/TLS certificates" }, { 'v', "verbose", NULL, 0, "print the request before sending" }, + { 'c', "color", "WHEN", OPT_LONG_ONLY, + "colorize output: never, always, or auto" }, + { 'w', "write-default-cfg", "FILENAME", + OPT_OPTIONAL_ARG | OPT_LONG_ONLY, + "write a default configuration file and exit" }, { 0, NULL, NULL, 0, NULL } }; @@ -478,21 +837,29 @@ parse_program_arguments (struct app_context *ctx, int argc, char **argv, case 'V': printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); exit (EXIT_SUCCESS); - case 'a': - ctx->auto_id = true; - break; - case 'o': - *origin = optarg; - break; - case 'p': - ctx->pretty_print = true; - break; - case 't': - ctx->trust_all = true; - break; - case 'v': - ctx->verbose = true; + + case 'o': *origin = optarg; break; + case 'a': ctx->auto_id = true; break; + case 'p': ctx->pretty_print = true; break; + case 't': ctx->trust_all = true; break; + case 'v': ctx->verbose = true; break; + + case 'c': + if (!strcasecmp (optarg, "never")) + ctx->color_mode = COLOR_NEVER; + else if (!strcasecmp (optarg, "always")) + ctx->color_mode = COLOR_ALWAYS; + else if (!strcasecmp (optarg, "auto")) + ctx->color_mode = COLOR_AUTO; + else + { + print_error ("`%s' is not a valid value for `%s'", optarg, "color"); + exit (EXIT_FAILURE); + } break; + case 'w': + call_write_default_config (optarg, g_config_table); + exit (EXIT_SUCCESS); default: print_error ("wrong options"); @@ -516,10 +883,16 @@ parse_program_arguments (struct app_context *ctx, int argc, char **argv, int main (int argc, char *argv[]) { + str_map_init (&g_ctx.config); + g_ctx.config.free = free; + char *origin = NULL; char *endpoint = NULL; parse_program_arguments (&g_ctx, argc, argv, &origin, &endpoint); + init_colors (&g_ctx); + load_config (&g_ctx); + if (strncmp (endpoint, "http://", 7) && strncmp (endpoint, "https://", 8)) exit_fatal ("the endpoint address must begin with" @@ -582,10 +955,18 @@ main (int argc, char *argv[]) xstrdup_printf ("%s/" PROGRAM_NAME "/history", data_home); (void) read_history (history_path); - // XXX: we should use termcap/terminfo for the codes but who cares - char *prompt = xstrdup_printf ("%c\x1b[1m%cjson-rpc> %c\x1b[0m%c", - RL_PROMPT_START_IGNORE, RL_PROMPT_END_IGNORE, - RL_PROMPT_START_IGNORE, RL_PROMPT_END_IGNORE); + char *prompt; + if (!get_attribute_printer (stdout)) + prompt = xstrdup_printf ("json-rpc> "); + else + { + // XXX: to be completely correct, we should use tputs, but we cannot + const char *prompt_attrs = str_map_find (&g_ctx.config, ATTR_PROMPT); + const char *reset_attrs = str_map_find (&g_ctx.config, ATTR_RESET); + prompt = xstrdup_printf ("%c%s%cjson-rpc> %c%s%c", + RL_PROMPT_START_IGNORE, prompt_attrs, RL_PROMPT_END_IGNORE, + RL_PROMPT_START_IGNORE, reset_attrs, RL_PROMPT_END_IGNORE); + } // readline 6.3 doesn't immediately redraw the terminal upon reception // of SIGWINCH, so we must run it in an event loop to remediate that @@ -625,5 +1006,7 @@ main (int argc, char *argv[]) curl_slist_free_all (headers); free (origin); curl_easy_cleanup (curl); + str_map_free (&g_ctx.config); + free_terminal (); return EXIT_SUCCESS; } -- cgit v1.2.3