From 864be7cfc54e310ffbd471ce9fefd6230daba468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Sun, 26 Apr 2015 18:23:43 +0200 Subject: [PATCH] degesch: add output text formatting --- degesch.c | 470 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 384 insertions(+), 86 deletions(-) diff --git a/degesch.c b/degesch.c index 095e05e..d1b7dbc 100644 --- a/degesch.c +++ b/degesch.c @@ -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 # and #0 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, ¤t)) 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