diff options
| author | Přemysl Janouch <p.janouch@gmail.com> | 2015-04-26 18:23:43 +0200 | 
|---|---|---|
| committer | Přemysl Janouch <p.janouch@gmail.com> | 2015-04-26 18:23:43 +0200 | 
| commit | 864be7cfc54e310ffbd471ce9fefd6230daba468 (patch) | |
| tree | aaaad986fbccd91805100b152c3c040ba8d4948e /degesch.c | |
| parent | 4393e48145989906ac05cc622a6fcbac6d414e8f (diff) | |
| download | xK-864be7cfc54e310ffbd471ce9fefd6230daba468.tar.gz xK-864be7cfc54e310ffbd471ce9fefd6230daba468.tar.xz xK-864be7cfc54e310ffbd471ce9fefd6230daba468.zip  | |
degesch: add output text formatting
Diffstat (limited to 'degesch.c')
| -rw-r--r-- | degesch.c | 470 | 
1 files changed, 384 insertions, 86 deletions
@@ -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, ¤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  | 
