degesch: add output text formatting

This commit is contained in:
Přemysl Eric Janouch 2015-04-26 18:23:43 +02:00
parent 4393e48145
commit 864be7cfc5

470
degesch.c
View File

@ -27,6 +27,11 @@
#define ATTR_WARNING "attr_warning"
#define ATTR_ERROR "attr_error"
#define ATTR_TIMESTAMP "attr_timestamp"
#define ATTR_ACTION "attr_action"
#define ATTR_JOIN "attr_join"
#define ATTR_PART "attr_part"
// User data for logger functions to enable formatted logging
#define print_fatal_data ATTR_ERROR
#define print_error_data ATTR_ERROR
@ -86,6 +91,11 @@ static struct config_item g_config_table[] =
{ ATTR_WARNING, NULL, "Terminal attributes for warnings" },
{ ATTR_ERROR, NULL, "Terminal attributes for errors" },
{ ATTR_TIMESTAMP, NULL, "Terminal attributes for timestamps" },
{ ATTR_ACTION, NULL, "Terminal attributes for user actions" },
{ ATTR_JOIN, NULL, "Terminal attributes for joins" },
{ ATTR_PART, NULL, "Terminal attributes for parts" },
{ NULL, NULL, NULL }
};
@ -94,6 +104,12 @@ static struct config_item g_config_table[] =
// All text stored in our data structures is encoded in UTF-8.
// Or at least should be. The exception is IRC identifiers.
static bool
isdigit_ascii (int c)
{
return c >= '0' && c <= '9';
}
/// Shorthand to set an error and return failure from the function
#define FAIL(...) \
BLOCK_START \
@ -575,7 +591,8 @@ static struct
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
char *color_set_fg[8]; ///< Codes to set the foreground colour
char *color_set_bg[8]; ///< Codes to set the background colour
}
g_terminal;
@ -593,15 +610,20 @@ init_terminal (void)
return false;
// Make sure all terminal features used by us are supported
if (!set_a_foreground || !enter_bold_mode || !exit_attribute_mode)
if (!set_a_foreground || !set_a_background
|| !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,
for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set_fg); i++)
{
g_terminal.color_set_fg[i] = xstrdup (tparm (set_a_foreground,
i, 0, 0, 0, 0, 0, 0, 0, 0));
g_terminal.color_set_bg[i] = xstrdup (tparm (set_a_background,
i, 0, 0, 0, 0, 0, 0, 0, 0));
}
return g_terminal.initialized = true;
}
@ -612,8 +634,11 @@ 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]);
for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set_fg); i++)
{
free (g_terminal.color_set_fg[i]);
free (g_terminal.color_set_bg[i]);
}
del_curterm (cur_term);
}
@ -738,10 +763,15 @@ init_colors (struct app_context *ctx)
#define INIT_ATTR(id, ti, vt100) \
str_map_set (&ctx->config, (id), xstrdup (have_ti ? (ti) : (vt100)));
INIT_ATTR (ATTR_PROMPT, enter_bold_mode, "\x1b[1m");
INIT_ATTR (ATTR_RESET, exit_attribute_mode, "\x1b[0m");
INIT_ATTR (ATTR_WARNING, g_terminal.color_set[3], "\x1b[33m");
INIT_ATTR (ATTR_ERROR, g_terminal.color_set[1], "\x1b[31m");
INIT_ATTR (ATTR_PROMPT, enter_bold_mode, "\x1b[1m");
INIT_ATTR (ATTR_RESET, exit_attribute_mode, "\x1b[0m");
INIT_ATTR (ATTR_WARNING, g_terminal.color_set_fg[3], "\x1b[33m");
INIT_ATTR (ATTR_ERROR, g_terminal.color_set_fg[1], "\x1b[31m");
INIT_ATTR (ATTR_TIMESTAMP, g_terminal.color_set_fg[7], "\x1b[37m");
INIT_ATTR (ATTR_ACTION, g_terminal.color_set_fg[1], "\x1b[31m");
INIT_ATTR (ATTR_JOIN, g_terminal.color_set_fg[2], "\x1b[32m");
INIT_ATTR (ATTR_PART, g_terminal.color_set_fg[1], "\x1b[31m");
#undef INIT_ATTR
@ -829,6 +859,274 @@ setup_signal_handlers (void)
exit_fatal ("sigaction: %s", strerror (errno));
}
// --- Output formatter --------------------------------------------------------
// This complicated piece of code makes attributed text formatting simple.
// We use a printf-inspired syntax to push attributes and text to the object,
// then flush it either to a terminal, or a log file with formatting stripped.
//
// Format strings use a #-quoted notation, to differentiate from printf:
// #s inserts a string
// #d inserts a signed integer; also supports the #<N> and #0<N> notation
//
// #a inserts named attributes (auto-resets)
// #r resets terminal attributes
// #c sets foreground color
// #C sets background color
enum formatter_item_type
{
FORMATTER_ITEM_TEXT, ///< Text
FORMATTER_ITEM_ATTR, ///< Named formatting attributes
FORMATTER_ITEM_FG_COLOR, ///< Foreground color
FORMATTER_ITEM_BG_COLOR ///< Background color
};
struct formatter_item
{
LIST_HEADER (struct formatter_item)
enum formatter_item_type type; ///< Type of this item
int color; ///< Color
char *data; ///< Either text or an attribute string
};
static struct formatter_item *
formatter_item_new (void)
{
struct formatter_item *self = xcalloc (1, sizeof *self);
return self;
}
static void
formatter_item_destroy (struct formatter_item *self)
{
free (self->data);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct formatter
{
struct app_context *ctx; ///< Application context
struct formatter_item *items; ///< Items
struct formatter_item *items_tail; ///< Tail of items
};
static void
formatter_init (struct formatter *self, struct app_context *ctx)
{
memset (self, 0, sizeof *self);
self->ctx = ctx;
}
static void
formatter_free (struct formatter *self)
{
LIST_FOR_EACH (struct formatter_item, iter, self->items)
formatter_item_destroy (iter);
}
static struct formatter_item *
formatter_add_blank (struct formatter *self)
{
struct formatter_item *item = formatter_item_new ();
LIST_APPEND_WITH_TAIL (self->items, self->items_tail, item);
return item;
}
static void
formatter_add_attr (struct formatter *self, const char *attr_name)
{
struct formatter_item *item = formatter_add_blank (self);
item->type = FORMATTER_ITEM_ATTR;
item->data = xstrdup (str_map_find (&self->ctx->config, attr_name));
}
static void
formatter_add_reset (struct formatter *self)
{
struct formatter_item *item = formatter_add_blank (self);
item->type = FORMATTER_ITEM_ATTR;
item->data = NULL;
}
static void
formatter_add_text (struct formatter *self, const char *text)
{
struct formatter_item *item = formatter_add_blank (self);
item->type = FORMATTER_ITEM_TEXT;
item->data = xstrdup (text);
}
static void
formatter_add_fg_color (struct formatter *self, int color)
{
struct formatter_item *item = formatter_add_blank (self);
item->type = FORMATTER_ITEM_FG_COLOR;
item->color = color;
}
static void
formatter_add_bg_color (struct formatter *self, int color)
{
struct formatter_item *item = formatter_add_blank (self);
item->type = FORMATTER_ITEM_BG_COLOR;
item->color = color;
}
static const char *
formatter_parse_field (struct formatter *self,
const char *field, struct str *buf, va_list *ap)
{
size_t width = 0;
bool zero_padded = false;
int c;
restart:
switch ((c = *field++))
{
char *s;
// We can push boring text content to the caller's buffer
// and let it flush the buffer only when it's actually needed
case 's':
s = va_arg (*ap, char *);
for (size_t len = strlen (s); len < width; len++)
str_append_c (buf, ' ');
str_append (buf, s);
break;
case 'd':
s = xstrdup_printf ("%d", va_arg (*ap, int));
for (size_t len = strlen (s); len < width; len++)
str_append_c (buf, " 0"[zero_padded]);
str_append (buf, s);
free (s);
break;
case 'a':
formatter_add_attr (self, va_arg (*ap, const char *));
break;
case 'c':
formatter_add_fg_color (self, va_arg (*ap, int));
break;
case 'C':
formatter_add_bg_color (self, va_arg (*ap, int));
break;
case 'r':
formatter_add_reset (self);
break;
default:
if (c == '0' && !zero_padded)
zero_padded = true;
else if (isdigit_ascii (c))
width = width * 10 + (c - '0');
else if (c)
hard_assert (!"unexpected format specifier");
else
hard_assert (!"unexpected end of format string");
goto restart;
}
return field;
}
static void
formatter_add (struct formatter *self, const char *format, ...)
{
struct str buf;
str_init (&buf);
va_list ap;
va_start (ap, format);
while (*format)
{
if (*format != '#' || *++format == '#')
{
str_append_c (&buf, *format++);
continue;
}
if (buf.len)
{
formatter_add_text (self, buf.str);
str_reset (&buf);
}
format = formatter_parse_field (self, format, &buf, &ap);
}
if (buf.len)
formatter_add_text (self, buf.str);
str_free (&buf);
va_end (ap);
}
static void
formatter_flush (struct formatter *self, FILE *stream)
{
terminal_printer_fn printer = get_attribute_printer (stream);
const char *attr_reset = str_map_find (&self->ctx->config, ATTR_RESET);
if (printer)
tputs (attr_reset, 1, printer);
bool is_attributed = false;
bool is_tty = isatty (fileno (stream));
LIST_FOR_EACH (struct formatter_item, iter, self->items)
{
switch (iter->type)
{
case FORMATTER_ITEM_TEXT:
if (is_tty)
{
char *term = iconv_xstrdup
(self->ctx->term_from_utf8, iter->data, -1, NULL);
fputs (term, stream);
free (term);
}
else
fputs (iter->data, stream);
break;
case FORMATTER_ITEM_ATTR:
if (!printer)
continue;
if (is_attributed)
{
tputs (attr_reset, 1, printer);
is_attributed = false;
}
if (iter->data)
{
tputs (iter->data, 1, printer);
is_attributed = true;
}
break;
case FORMATTER_ITEM_FG_COLOR:
if (!printer)
continue;
tputs (g_terminal.color_set_fg[iter->color], 1, printer);
is_attributed = true;
break;
case FORMATTER_ITEM_BG_COLOR:
if (!printer)
continue;
tputs (g_terminal.color_set_bg[iter->color], 1, printer);
is_attributed = true;
break;
}
}
if (is_attributed)
tputs (attr_reset, 1, printer);
}
// --- Buffers -----------------------------------------------------------------
static void
@ -856,117 +1154,117 @@ buffer_update_time (struct app_context *ctx, time_t now)
}
static void
buffer_line_display (struct app_context *ctx, struct buffer_line *line)
buffer_line_display (struct app_context *ctx,
struct buffer_line *line, bool is_external)
{
// Normal timestamps don't include the date, this way the user won't be
// confused as to when an event has happened
buffer_update_time (ctx, line->when);
struct str output;
str_init (&output);
struct buffer_line_args *a = &line->args;
char *nick = NULL;
const char *userhost = NULL;
int nick_color = -1;
int object_color = -1;
if (a->who)
{
nick = irc_cut_nickname (a->who);
userhost = irc_find_userhost (a->who);
nick_color = str_map_hash (nick, strlen (nick)) % 8;
}
if (a->object)
object_color = str_map_hash (a->object, strlen (a->object)) % 8;
struct formatter f;
formatter_init (&f, ctx);
struct tm current;
if (!localtime_r (&line->when, &current))
print_error ("%s: %s", "localtime_r", strerror (errno));
else
str_append_printf (&output, "%02d:%02d:%02d ",
current.tm_hour, current.tm_min, current.tm_sec);
formatter_add (&f, "#a#02d:#02d:#02d#r ",
ATTR_TIMESTAMP, current.tm_hour, current.tm_min, current.tm_sec);
#define GET_FIELD(name) char *name = line->args.name \
? iconv_xstrdup (ctx->term_from_utf8, line->args.name, -1, NULL) : NULL
// TODO: when this comes from a different buffer (is_external),
// ignore all attributes and instead print it with ATTR_OTHER
GET_FIELD (who);
GET_FIELD (object);
GET_FIELD (text);
GET_FIELD (reason);
#undef GET_FIELD
// TODO: colorize the output, note that we shouldn't put everything through
// tputs but only the attribute strings. That might prove a bit
// challenging. Maybe we could create a helper object to pust text
// and formatting into. We could have a varargs function to make it a bit
// more friendly, e.g. push(&x, ATTR_JOIN, "--> ", ATTR_RESET, who, NULL)
char *nick = NULL;
const char *userhost = NULL;
if (who)
{
nick = irc_cut_nickname (who);
userhost = irc_find_userhost (who);
}
// TODO: try to decode as much as possible using mIRC formatting;
// could either add a #m format specifier, or write a separate function
// to translate the formatting into formatter API calls
switch (line->type)
{
case BUFFER_LINE_PRIVMSG:
str_append_printf (&output, "<%s> %s", nick, text);
formatter_add (&f, "<#c#s#r> #s", nick_color, nick, a->text);
break;
case BUFFER_LINE_ACTION:
str_append_printf (&output, " * %s %s", nick, text);
formatter_add (&f, " #a*#r ", ATTR_ACTION);
formatter_add (&f, "#c#s#r #s", nick_color, nick, a->text);
break;
case BUFFER_LINE_NOTICE:
str_append_printf (&output, " - Notice(%s): %s", nick, text);
formatter_add (&f, " - ");
formatter_add (&f, "#s(#c#s#r): #s",
"Notice", nick_color, nick, a->text);
break;
case BUFFER_LINE_JOIN:
if (who)
str_append_printf (&output, "--> %s (%s) has joined %s",
nick, userhost, object);
else
str_append_printf (&output, "--> You have joined %s", object);
formatter_add (&f, "#a-->#r ", ATTR_JOIN);
formatter_add (&f, "#c#s#r (#s) #a#s#r #s",
nick_color, nick, userhost,
ATTR_JOIN, "has joined", a->object);
break;
case BUFFER_LINE_PART:
if (who)
str_append_printf (&output, "<-- %s (%s) has left %s (%s)",
nick, userhost, object, reason);
else
str_append_printf (&output, "<-- You have left %s (%s)",
object, reason);
formatter_add (&f, "#a<--#r ", ATTR_PART);
formatter_add (&f, "#c#s#r (#s) #a#s#r #s",
nick_color, nick, userhost,
ATTR_PART, "has left", a->object);
if (a->reason)
formatter_add (&f, " (#s)", a->reason);
break;
case BUFFER_LINE_KICK:
if (who)
str_append_printf (&output, "<-- %s has kicked %s (%s)",
nick, object, reason);
else
str_append_printf (&output, "<-- You have kicked %s (%s)",
object, reason);
formatter_add (&f, "#a<--#r ", ATTR_PART);
formatter_add (&f, "#c#s#r (#s) #a#s#r #c#s#r",
nick_color, nick, userhost,
ATTR_PART, "has kicked", object_color, a->object);
if (a->reason)
formatter_add (&f, " (#s)", a->reason);
break;
case BUFFER_LINE_NICK:
if (who)
str_append_printf (&output, " - %s is now known as %s",
nick, object);
formatter_add (&f, " - ");
if (a->who)
formatter_add (&f, "#c#s#r #s #c#s#r",
nick_color, nick,
"is now known as", object_color, a->object);
else
str_append_printf (&output, " - You are now known as %s", object);
formatter_add (&f, "#s #s",
"You are now known as", a->object);
break;
case BUFFER_LINE_TOPIC:
if (who)
str_append_printf (&output,
" - %s has changed the topic to: %s", nick, text);
else
str_append_printf (&output,
" - You have changed the topic to: %s", text);
formatter_add (&f, " - ");
formatter_add (&f, "#c#s#r #s \"#s\"",
nick_color, nick,
"has changed the topic to", a->text);
break;
case BUFFER_LINE_QUIT:
if (who)
str_append_printf (&output, "<-- %s (%s) has quit (%s)",
nick, userhost, reason);
else
str_append_printf (&output, "<-- You have quit (%s)", reason);
formatter_add (&f, "#a<--#r ", ATTR_PART);
formatter_add (&f, "#c#s#r (%s) #a#s#r",
nick_color, nick, userhost,
ATTR_PART, "has quit");
if (a->reason)
formatter_add (&f, " (#s)", a->reason);
break;
case BUFFER_LINE_STATUS:
str_append_printf (&output, " - %s", text);
formatter_add (&f, " - ");
formatter_add (&f, "#s", a->text);
break;
case BUFFER_LINE_ERROR:
str_append_printf (&output, "=!= %s", text);
formatter_add (&f, "#a=!=#r ", ATTR_ERROR);
formatter_add (&f, "#s", a->text);
}
free (nick);
free (who);
free (object);
free (text);
free (reason);
struct app_readline_state state;
if (ctx->readline_prompt_shown)
app_readline_hide (&state);
@ -974,8 +1272,9 @@ buffer_line_display (struct app_context *ctx, struct buffer_line *line)
// TODO: write the line to a log file; note that the global and server
// buffers musn't collide with filenames
printf ("%s\n", output.str);
str_free (&output);
formatter_add (&f, "\n");
formatter_flush (&f, stdout);
formatter_free (&f);
if (ctx->readline_prompt_shown)
app_readline_restore (&state, ctx->readline_prompt);
@ -996,11 +1295,10 @@ buffer_send_internal (struct app_context *ctx, struct buffer *buffer,
buffer->lines_count++;
if (buffer == ctx->current_buffer)
buffer_line_display (ctx, line);
buffer_line_display (ctx, line, false);
else if (!ctx->isolate_buffers &&
(buffer == ctx->global_buffer || buffer == ctx->server_buffer))
// TODO: show this in another color or something
buffer_line_display (ctx, line);
buffer_line_display (ctx, line, true);
else
{
buffer->unseen_messages_count++;
@ -1100,7 +1398,7 @@ buffer_activate (struct app_context *ctx, struct buffer *buffer)
// Once we've found where we want to start with the backlog, print it
for (; line; line = line->next)
buffer_line_display (ctx, line);
buffer_line_display (ctx, line, false);
buffer->unseen_messages_count = 0;
// The following part shows you why it's not a good idea to use