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