From 864be7cfc54e310ffbd471ce9fefd6230daba468 Mon Sep 17 00:00:00 2001
From: Přemysl Janouch
Date: Sun, 26 Apr 2015 18:23:43 +0200
Subject: 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);
-
-#define GET_FIELD(name) char *name = line->args.name \
- ? iconv_xstrdup (ctx->term_from_utf8, line->args.name, -1, NULL) : NULL
+ formatter_add (&f, "#a#02d:#02d:#02d#r ",
+ ATTR_TIMESTAMP, current.tm_hour, current.tm_min, current.tm_sec);
- 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;
+ // TODO: when this comes from a different buffer (is_external),
+ // ignore all attributes and instead print it with ATTR_OTHER
- 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
--
cgit v1.2.3-70-g09d2