Add support for attributed output

Colours, colours, colours.  Configurable.
This commit is contained in:
Přemysl Eric Janouch 2015-02-22 19:15:06 +01:00
parent 66cf41f89f
commit 855d02acab
3 changed files with 475 additions and 42 deletions

View File

@ -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

View File

@ -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 <langinfo.h>
#include <signal.h>
#include <strings.h>
#include <ev.h>
#include <readline/readline.h>
@ -33,13 +47,38 @@
#include <curl/curl.h>
#include <jansson.h>
#include <curses.h>
#include <term.h>
// --- 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;
}

88
utils.c
View File

@ -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
log_message (const char *quote, const char *fmt, ...) ATTRIBUTE_PRINTF (2, 3);
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, ...)
log_message (void *user_data, const char *quote, const char *fmt, ...)
ATTRIBUTE_PRINTF (3, 4);
static void
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)
{