From 855d02acab5a2f679a736fcaa0b88de068b0922b Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Sun, 22 Feb 2015 19:15:06 +0100 Subject: Add support for attributed output Colours, colours, colours. Configurable. --- CMakeLists.txt | 6 +- json-rpc-shell.c | 423 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- utils.c | 86 ++++++++--- 3 files changed, 474 insertions(+), 41 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dbef75..560ebda 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,11 +20,13 @@ set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}") set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) # Dependencies +find_package (Curses REQUIRED) find_package (PkgConfig REQUIRED) pkg_check_modules (dependencies REQUIRED libcurl jansson) find_package (LibEV REQUIRED) -include_directories (${dependencies_INCLUDE_DIRS} ${LIBEV_INCLUDE_DIRS}) +include_directories (${CURSES_INCLUDE_DIR} + ${dependencies_INCLUDE_DIRS} ${LIBEV_INCLUDE_DIRS}) # Generate a configuration file configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h) @@ -32,7 +34,7 @@ include_directories (${PROJECT_BINARY_DIR}) # Build the main executable and link it add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c siphash.c) -target_link_libraries (${PROJECT_NAME} +target_link_libraries (${PROJECT_NAME} ${CURSES_LIBRARY} ${dependencies_LIBRARIES} ${LIBEV_LIBRARIES} readline) # The files to be installed 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; } diff --git a/utils.c b/utils.c index 00772f7..7a7392b 100644 --- a/utils.c +++ b/utils.c @@ -59,8 +59,10 @@ // --- Logging ----------------------------------------------------------------- static void -log_message_stdio (const char *quote, const char *fmt, va_list ap) +log_message_stdio (void *user_data, const char *quote, const char *fmt, + va_list ap) { + (void) user_data; FILE *stream = stderr; fputs (quote, stream); @@ -68,25 +70,48 @@ log_message_stdio (const char *quote, const char *fmt, va_list ap) fputs ("\n", stream); } +static void (*g_log_message_real) (void *, const char *, const char *, va_list) + = log_message_stdio; + static void -log_message (const char *quote, const char *fmt, ...) ATTRIBUTE_PRINTF (2, 3); +log_message (void *user_data, const char *quote, const char *fmt, ...) + ATTRIBUTE_PRINTF (3, 4); static void -log_message (const char *quote, const char *fmt, ...) +log_message (void *user_data, const char *quote, const char *fmt, ...) { va_list ap; va_start (ap, fmt); - log_message_stdio (quote, fmt, ap); + g_log_message_real (user_data, quote, fmt, ap); va_end (ap); } // `fatal' is reserved for unexpected failures that would harm further operation -// TODO: colors (probably copy over from stracepkg) -#define print_fatal(...) log_message ("fatal: ", __VA_ARGS__) -#define print_error(...) log_message ("error: ", __VA_ARGS__) -#define print_warning(...) log_message ("warning: ", __VA_ARGS__) -#define print_status(...) log_message ("-- ", __VA_ARGS__) +#ifndef print_fatal_data +#define print_fatal_data NULL +#endif + +#ifndef print_error_data +#define print_error_data NULL +#endif + +#ifndef print_warning_data +#define print_warning_data NULL +#endif + +#ifndef print_status_data +#define print_status_data NULL +#endif + +#define print_fatal(...) \ + log_message (print_fatal_data, "fatal: ", __VA_ARGS__) +#define print_error(...) \ + log_message (print_error_data, "error: ", __VA_ARGS__) +#define print_warning(...) \ + log_message (print_warning_data, "warning: ", __VA_ARGS__) +#define print_status(...) \ + log_message (print_status_data, "-- ", __VA_ARGS__) #define exit_fatal(...) \ BLOCK_START \ @@ -479,6 +504,16 @@ struct str_map size_t (*key_xfrm) (char *dest, const char *src, size_t n); }; +// As long as you don't remove the current entry, you can modify the map. +// Use `link' directly to access the data. + +struct str_map_iter +{ + struct str_map *map; ///< The map we're iterating + size_t next_index; ///< Next table index to search + struct str_map_link *link; ///< Current link +}; + #define STR_MAP_MIN_ALLOC 16 typedef void (*str_map_free_fn) (void *); @@ -512,6 +547,29 @@ str_map_free (struct str_map *self) self->map = NULL; } +static void +str_map_iter_init (struct str_map_iter *self, struct str_map *map) +{ + self->map = map; + self->next_index = 0; + self->link = NULL; +} + +static void * +str_map_iter_next (struct str_map_iter *self) +{ + struct str_map *map = self->map; + if (self->link) + self->link = self->link->next; + while (!self->link) + { + if (self->next_index >= map->alloc) + return NULL; + self->link = map->map[self->next_index++]; + } + return self->link->data; +} + static uint64_t str_map_hash (const char *s, size_t len) { @@ -866,16 +924,6 @@ struct config_item const char *description; }; -static void -load_config_defaults (struct str_map *config, const struct config_item *table) -{ - for (; table->key != NULL; table++) - if (table->default_value) - str_map_set (config, table->key, xstrdup (table->default_value)); - else - str_map_set (config, table->key, NULL); -} - static bool read_config_file (struct str_map *config, struct error **e) { -- cgit v1.2.3-70-g09d2