From f6483489c2a4861bc6dd6c5521fb8a153eddbcb1 Mon Sep 17 00:00:00 2001 From: PÅ™emysl Eric Janouch Date: Wed, 18 Dec 2024 11:45:25 +0100 Subject: xC: fix crash with too many topic formatting items Manually constructed formatters have no sentinel value. This is a one-line change in relay_prepare_channel_buffer_update(), however the whole block of "Relay output" code has been moved down, resolving one TODO and rendering two function prototypes unnecessary. --- xC.c | 2535 +++++++++++++++++++++++++++++++++--------------------------------- 1 file changed, 1266 insertions(+), 1269 deletions(-) (limited to 'xC.c') diff --git a/xC.c b/xC.c index fde1874..21dd846 100644 --- a/xC.c +++ b/xC.c @@ -2895,1628 +2895,1625 @@ serialize_configuration (struct config_item *root, struct str *output) config_item_write (root, true, output); } -// --- Relay output ------------------------------------------------------------ +// --- Terminal output --------------------------------------------------------- -static void -client_kill (struct client *c) -{ - struct app_context *ctx = c->ctx; - poller_fd_reset (&c->socket_event); - xclose (c->socket_fd); - c->socket_fd = -1; +/// Default colour pair +#define COLOR_DEFAULT -1 - LIST_UNLINK (ctx->clients, c); - client_destroy (c); -} +/// Bright versions of the basic colour set +#define COLOR_BRIGHT(x) (COLOR_ ## x + 8) -static void -client_update_poller (struct client *c, const struct pollfd *pfd) +/// Builds a colour pair for 256-colour terminals with a 16-colour backup value +#define COLOR_256(name, c256) \ + (((COLOR_ ## name) & 0xFFFF) | (((c256) & 0xFFFF) << 16)) + +typedef int (*terminal_printer_fn) (int); + +static int +putchar_stderr (int c) { - int new_events = POLLIN; - if (c->write_buffer.len) - new_events |= POLLOUT; + return fputc (c, stderr); +} - hard_assert (new_events != 0); - if (!pfd || pfd->events != new_events) - poller_fd_set (&c->socket_event, new_events); +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; } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// ~~~ Attribute printer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -static void -relay_send (struct client *c) +// A little tool that tries to make the most of the terminal's capabilities +// to set up text attributes. It mostly targets just terminal emulators as that +// is what people are using these days. At least no stupid ncurses limits us +// with colour pairs. + +struct attr_printer { - struct relay_event_message *m = &c->ctx->relay_message; - m->event_seq = c->event_seq++; + struct attrs *attrs; ///< Named attributes + FILE *stream; ///< Output stream + bool dirty; ///< Attributes are set +}; - // TODO: Also don't try sending anything if half-closed. - if (!c->initialized || c->socket_fd == -1) - return; +#define ATTR_PRINTER_INIT(attrs, stream) { attrs, stream, true } - // liberty has msg_{reader,writer} already, but they use 8-byte lengths. - size_t frame_len_pos = c->write_buffer.len, frame_len = 0; - str_pack_u32 (&c->write_buffer, 0); - if (!relay_event_message_serialize (m, &c->write_buffer) - || (frame_len = c->write_buffer.len - frame_len_pos - 4) > UINT32_MAX) +static void +attr_printer_filtered_puts (FILE *stream, const char *attr) +{ + for (; *attr; attr++) { - print_error ("serialization failed, killing client"); - client_kill (c); - return; - } + // sgr/set_attributes and sgr0/exit_attribute_mode like to enable or + // disable the ACS with SO/SI (e.g. for TERM=screen), however `less -R` + // does not skip over these characters and it screws up word wrapping + if (*attr == 14 /* SO */ || *attr == 15 /* SI */) + continue; - uint32_t len = htonl (frame_len); - memcpy (c->write_buffer.str + frame_len_pos, &len, sizeof len); - client_update_poller (c, NULL); + // Trivially skip delay sequences intended to be processed by tputs() + const char *end = NULL; + if (attr[0] == '$' && attr[1] == '<' && (end = strchr (attr, '>'))) + attr = end; + else + fputc (*attr, stream); + } } static void -relay_broadcast_except (struct app_context *ctx, struct client *exception) +attr_printer_tputs (struct attr_printer *self, const char *attr) { - LIST_FOR_EACH (struct client, c, ctx->clients) - if (c != exception) - relay_send (c); + terminal_printer_fn printer = get_attribute_printer (self->stream); + if (printer) + tputs (attr, 1, printer); + else + // We shouldn't really do this but we need it to output formatting + // to the pager--it should be SGR-only + attr_printer_filtered_puts (self->stream, attr); } -#define relay_broadcast(ctx) relay_broadcast_except ((ctx), NULL) - -static struct relay_event_message * -relay_prepare (struct app_context *ctx) +static void +attr_printer_reset (struct attr_printer *self) { - struct relay_event_message *m = &ctx->relay_message; - relay_event_message_free (m); - memset (m, 0, sizeof *m); - return m; + if (self->dirty) + attr_printer_tputs (self, exit_attribute_mode); + + self->dirty = false; } -static void -relay_prepare_ping (struct app_context *ctx) +// NOTE: commonly terminals have: +// 8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK) +// 16 colours (okayish, we have the full basic range guaranteed) +// 88 colours (the same plus a 4^3 RGB cube and a few shades of grey) +// 256 colours (best, like above but with a larger cube and more grey) + +/// Interpolate from the 256-colour palette to the 88-colour one +static int +attr_printer_256_to_88 (int color) { - relay_prepare (ctx)->data.event = RELAY_EVENT_PING; + // These colours are the same everywhere + if (color < 16) + return color; + + // 24 -> 8 extra shades of grey + if (color >= 232) + return 80 + (color - 232) / 3; + + // 6 * 6 * 6 cube -> 4 * 4 * 4 cube + int x[6] = { 0, 1, 1, 2, 2, 3 }; + int index = color - 16; + return 16 + + ( x[ index / 36 ] << 8 + | x[(index / 6) % 6 ] << 4 + | x[(index % 6) ] ); } -static union relay_item_data * -relay_translate_formatter (struct app_context *ctx, union relay_item_data *p, - const struct formatter_item *i) +static int +attr_printer_decode_color (int color, bool *is_bright) { - // XXX: See attr_printer_decode_color(), this is a footgun. - int16_t c16 = i->color; - int16_t c256 = i->color >> 16; + int16_t c16 = color; hard_assert (c16 < 16); + int16_t c256 = color >> 16; hard_assert (c256 < 256); - unsigned attrs = i->attribute; - switch (i->type) + *is_bright = false; + switch (max_colors) { - case FORMATTER_ITEM_TEXT: - p->text.text = str_from_cstr (i->text); - (p++)->kind = RELAY_ITEM_TEXT; - break; - case FORMATTER_ITEM_FG_COLOR: - p->fg_color.color = c256 <= 0 ? c16 : c256; - (p++)->kind = RELAY_ITEM_FG_COLOR; - break; - case FORMATTER_ITEM_BG_COLOR: - p->bg_color.color = c256 <= 0 ? c16 : c256; - (p++)->kind = RELAY_ITEM_BG_COLOR; - break; - case FORMATTER_ITEM_ATTR: - (p++)->kind = RELAY_ITEM_RESET; - if ((c256 = ctx->theme[i->attribute].fg) >= 0) - { - p->fg_color.color = c256; - (p++)->kind = RELAY_ITEM_FG_COLOR; - } - if ((c256 = ctx->theme[i->attribute].bg) >= 0) + case 8: + if (c16 >= 8) { - p->bg_color.color = c256; - (p++)->kind = RELAY_ITEM_BG_COLOR; + c16 -= 8; + *is_bright = true; } - - attrs = ctx->theme[i->attribute].attrs; // Fall-through - case FORMATTER_ITEM_SIMPLE: - if (attrs & TEXT_BOLD) - (p++)->kind = RELAY_ITEM_FLIP_BOLD; - if (attrs & TEXT_ITALIC) - (p++)->kind = RELAY_ITEM_FLIP_ITALIC; - if (attrs & TEXT_UNDERLINE) - (p++)->kind = RELAY_ITEM_FLIP_UNDERLINE; - if (attrs & TEXT_INVERSE) - (p++)->kind = RELAY_ITEM_FLIP_INVERSE; - if (attrs & TEXT_CROSSED_OUT) - (p++)->kind = RELAY_ITEM_FLIP_CROSSED_OUT; - if (attrs & TEXT_MONOSPACE) - (p++)->kind = RELAY_ITEM_FLIP_MONOSPACE; - break; - default: - break; - } - return p; -} - -static union relay_item_data * -relay_items (struct app_context *ctx, const struct formatter_item *items, - uint32_t *len) -{ - size_t items_len = 0; - for (size_t i = 0; items[i].type; i++) - items_len++; + case 16: + return c16; - // Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR. - union relay_item_data *a = xcalloc (items_len * 9, sizeof *a), *p = a; - for (const struct formatter_item *i = items; items_len--; i++) - p = relay_translate_formatter (ctx, p, i); + case 88: + return c256 <= 0 ? c16 : attr_printer_256_to_88 (c256); + case 256: + return c256 <= 0 ? c16 : c256; - *len = p - a; - return a; + default: + // Unsupported palette + return -1; + } } static void -relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer, - struct buffer_line *line, bool leak_to_active) +attr_printer_apply (struct attr_printer *self, + int text_attrs, int wanted_fg, int wanted_bg) { - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_buffer_line *e = &m->data.buffer_line; - e->event = RELAY_EVENT_BUFFER_LINE; - e->buffer_name = str_from_cstr (buffer->name); - e->is_unimportant = !!(line->flags & BUFFER_LINE_UNIMPORTANT); - e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT); - e->rendition = 1 + line->r; - e->when = line->when * 1000; - e->leak_to_active = leak_to_active; - e->items = relay_items (ctx, line->items, &e->items_len); -} + bool fg_is_bright; + int fg = attr_printer_decode_color (wanted_fg, &fg_is_bright); + bool bg_is_bright; + int bg = attr_printer_decode_color (wanted_bg, &bg_is_bright); -// TODO: Consider pushing this whole block of code much further down. -static void formatter_add (struct formatter *self, const char *format, ...); -static char *irc_to_utf8 (const char *text); + bool have_inverse = !!(text_attrs & TEXT_INVERSE); + if (have_inverse) + { + bool tmp = fg_is_bright; + fg_is_bright = bg_is_bright; + bg_is_bright = tmp; + } -static void -relay_prepare_channel_buffer_update (struct app_context *ctx, - struct buffer *buffer, struct relay_buffer_context_channel *e) -{ - struct channel *channel = buffer->channel; - struct formatter f = formatter_make (ctx, buffer->server); - if (channel->topic) - formatter_add (&f, "#m", channel->topic); - e->topic = relay_items (ctx, f.items, &e->topic_len); - formatter_free (&f); + // In 8 colour mode, some terminals don't support bright backgrounds. + // However, we can make use of the fact that the brightness change caused + // by the bold attribute is retained when inverting the colours. + // This has the downside of making the text bold when it's not supposed + // to be, and we still can't make both colours bright, so it's more of + // an interesting hack rather than anything else. + if (!fg_is_bright && bg_is_bright && have_inverse) + text_attrs |= TEXT_BOLD; + else if (!fg_is_bright && bg_is_bright + && !have_inverse && fg >= 0 && bg >= 0) + { + // As long as none of the colours is the default, we can swap them + int tmp = fg; fg = bg; bg = tmp; + text_attrs |= TEXT_BOLD | TEXT_INVERSE; + } + else + { + // This often works, however... + if (fg_is_bright) text_attrs |= TEXT_BOLD; + // this turns out to be annoying if implemented "correctly" + if (bg_is_bright) text_attrs |= TEXT_BLINK; + } - // As in make_prompt(), conceal the last known channel modes. - // XXX: This should use irc_channel_is_joined(). - if (!channel->users_len) - return; + attr_printer_reset (self); - struct str modes = str_make (); - str_append_str (&modes, &channel->no_param_modes); + // TEXT_MONOSPACE is unimplemented, for obvious reasons + if (text_attrs) + attr_printer_tputs (self, tparm (set_attributes, + 0, // standout + text_attrs & TEXT_UNDERLINE, + text_attrs & TEXT_INVERSE, + text_attrs & TEXT_BLINK, + 0, // dim + text_attrs & TEXT_BOLD, + 0, // blank + 0, // protect + 0)); // acs + if ((text_attrs & TEXT_ITALIC) && enter_italics_mode) + attr_printer_tputs (self, enter_italics_mode); - struct str params = str_make (); - struct str_map_iter iter = str_map_iter_make (&channel->param_modes); - const char *param; - while ((param = str_map_iter_next (&iter))) - { - str_append_c (&modes, iter.link->key[0]); - str_append_c (¶ms, ' '); - str_append (¶ms, param); - } + char *smxx = NULL; + if ((text_attrs & TEXT_CROSSED_OUT) + && (smxx = tigetstr ("smxx")) && smxx != (char *) -1) + attr_printer_tputs (self, smxx); - str_append_str (&modes, ¶ms); - str_free (¶ms); + if (fg >= 0) + attr_printer_tputs (self, g_terminal.color_set_fg[fg]); + if (bg >= 0) + attr_printer_tputs (self, g_terminal.color_set_bg[bg]); - char *modes_utf8 = irc_to_utf8 (modes.str); - str_free (&modes); - e->modes = str_from_cstr (modes_utf8); - free (modes_utf8); + self->dirty = true; } static void -relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer) +attr_printer_apply_named (struct attr_printer *self, int attribute) { - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_buffer_update *e = &m->data.buffer_update; - e->event = RELAY_EVENT_BUFFER_UPDATE; - e->buffer_name = str_from_cstr (buffer->name); - e->hide_unimportant = buffer->hide_unimportant; + attr_printer_reset (self); + if (attribute == ATTR_RESET) + return; - struct str *server_name = NULL; - switch (buffer->type) - { - case BUFFER_GLOBAL: - e->context.kind = RELAY_BUFFER_KIND_GLOBAL; - break; - case BUFFER_SERVER: - e->context.kind = RELAY_BUFFER_KIND_SERVER; - server_name = &e->context.server.server_name; - break; - case BUFFER_CHANNEL: - e->context.kind = RELAY_BUFFER_KIND_CHANNEL; - server_name = &e->context.channel.server_name; - relay_prepare_channel_buffer_update (ctx, buffer, &e->context.channel); - break; - case BUFFER_PM: - e->context.kind = RELAY_BUFFER_KIND_PRIVATE_MESSAGE; - server_name = &e->context.private_message.server_name; - break; - } - if (server_name) - *server_name = str_from_cstr (buffer->server->name); + // See the COLOR_256 macro or attr_printer_decode_color(). + struct attrs *a = &self->attrs[attribute]; + attr_printer_apply (self, a->attrs, + a->fg < 16 ? a->fg : (a->fg << 16 | (-1 & 0xFFFF)), + a->bg < 16 ? a->bg : (a->bg << 16 | (-1 & 0xFFFF))); + self->dirty = true; } -static void -relay_prepare_buffer_stats (struct app_context *ctx, struct buffer *buffer) -{ - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_buffer_stats *e = &m->data.buffer_stats; - e->event = RELAY_EVENT_BUFFER_STATS; - e->buffer_name = str_from_cstr (buffer->name); - e->new_messages = MIN (UINT32_MAX, - buffer->new_messages_count - buffer->new_unimportant_count); - e->new_unimportant_messages = MIN (UINT32_MAX, - buffer->new_unimportant_count); - e->highlighted = buffer->highlighted; -} +// ~~~ Logging redirect ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ static void -relay_prepare_buffer_rename (struct app_context *ctx, struct buffer *buffer, - const char *new_name) +vprint_attributed (struct app_context *ctx, + FILE *stream, intptr_t attribute, const char *fmt, va_list ap) { - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_buffer_rename *e = &m->data.buffer_rename; - e->event = RELAY_EVENT_BUFFER_RENAME; - e->buffer_name = str_from_cstr (buffer->name); - e->new = str_from_cstr (new_name); -} + terminal_printer_fn printer = get_attribute_printer (stream); + if (!attribute) + printer = NULL; -static void -relay_prepare_buffer_remove (struct app_context *ctx, struct buffer *buffer) -{ - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_buffer_remove *e = &m->data.buffer_remove; - e->event = RELAY_EVENT_BUFFER_REMOVE; - e->buffer_name = str_from_cstr (buffer->name); -} + struct attr_printer state = ATTR_PRINTER_INIT (ctx->theme, stream); + if (printer) + attr_printer_apply_named (&state, attribute); -static void -relay_prepare_buffer_activate (struct app_context *ctx, struct buffer *buffer) -{ - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_buffer_activate *e = &m->data.buffer_activate; - e->event = RELAY_EVENT_BUFFER_ACTIVATE; - e->buffer_name = str_from_cstr (buffer->name); + vfprintf (stream, fmt, ap); + + if (printer) + attr_printer_reset (&state); } static void -relay_prepare_buffer_input (struct app_context *ctx, struct buffer *buffer, - const char *input) +print_attributed (struct app_context *ctx, + FILE *stream, intptr_t attribute, const char *fmt, ...) { - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_buffer_input *e = &m->data.buffer_input; - e->event = RELAY_EVENT_BUFFER_INPUT; - e->buffer_name = str_from_cstr (buffer->name); - e->text = str_from_cstr (input); + va_list ap; + va_start (ap, fmt); + vprint_attributed (ctx, stream, attribute, fmt, ap); + va_end (ap); } static void -relay_prepare_buffer_clear (struct app_context *ctx, - struct buffer *buffer) +log_message_attributed (void *user_data, const char *quote, const char *fmt, + va_list ap) { - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_buffer_clear *e = &m->data.buffer_clear; - e->event = RELAY_EVENT_BUFFER_CLEAR; - e->buffer_name = str_from_cstr (buffer->name); + FILE *stream = stderr; + struct app_context *ctx = g_ctx; + + CALL (ctx->input, hide); + + print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote); + vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap); + fputs ("\n", stream); + + CALL (ctx->input, show); } -enum relay_server_state -relay_server_state_for_server (struct server *s) +// ~~~ Theme ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +static ssize_t +attr_by_name (const char *name) { - switch (s->state) + static const char *table[ATTR_COUNT] = { - case IRC_DISCONNECTED: return RELAY_SERVER_STATE_DISCONNECTED; - case IRC_CONNECTING: return RELAY_SERVER_STATE_CONNECTING; - case IRC_CONNECTED: return RELAY_SERVER_STATE_CONNECTED; - case IRC_REGISTERED: return RELAY_SERVER_STATE_REGISTERED; - case IRC_CLOSING: - case IRC_HALF_CLOSED: return RELAY_SERVER_STATE_DISCONNECTING; - } - return 0; + NULL, +#define XX(x, y, z) [ATTR_ ## x] = #y, + ATTR_TABLE (XX) +#undef XX + }; + + for (size_t i = 1; i < N_ELEMENTS (table); i++) + if (!strcmp (name, table[i])) + return i; + return -1; } static void -relay_prepare_server_update (struct app_context *ctx, struct server *s) +on_config_theme_change (struct config_item *item) { - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_server_update *e = &m->data.server_update; - e->event = RELAY_EVENT_SERVER_UPDATE; - e->server_name = str_from_cstr (s->name); - e->data.state = relay_server_state_for_server (s); - if (s->state == IRC_REGISTERED) + struct app_context *ctx = item->user_data; + ssize_t id = attr_by_name (item->schema->name); + if (id != -1) { - char *user_utf8 = irc_to_utf8 (s->irc_user->nickname); - e->data.registered.user = str_from_cstr (user_utf8); - free (user_utf8); - - char *user_modes_utf8 = irc_to_utf8 (s->irc_user_modes.str); - e->data.registered.user_modes = str_from_cstr (user_modes_utf8); - free (user_modes_utf8); + // TODO: There should be a validator. + ctx->theme[id] = item->type == CONFIG_ITEM_NULL + ? ctx->theme_defaults[id] + : attrs_decode (item->value.string.str); } } static void -relay_prepare_server_rename (struct app_context *ctx, struct server *s, - const char *new_name) +init_colors (struct app_context *ctx) { - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_server_rename *e = &m->data.server_rename; - e->event = RELAY_EVENT_SERVER_RENAME; - e->server_name = str_from_cstr (s->name); - e->new = str_from_cstr (new_name); -} + bool have_ti = init_terminal (); -static void -relay_prepare_server_remove (struct app_context *ctx, struct server *s) -{ - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_server_remove *e = &m->data.server_remove; - e->event = RELAY_EVENT_SERVER_REMOVE; - e->server_name = str_from_cstr (s->name); -} - -static void -relay_prepare_error (struct app_context *ctx, uint32_t seq, const char *message) -{ - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_error *e = &m->data.error; - e->event = RELAY_EVENT_ERROR; - e->command_seq = seq; - e->error = str_from_cstr (message); -} +#define INIT_ATTR(id, ...) ctx->theme[ATTR_ ## id] = \ + ctx->theme_defaults[ATTR_ ## id] = (struct attrs) { __VA_ARGS__ } -static struct relay_event_data_response * -relay_prepare_response (struct app_context *ctx, uint32_t seq) -{ - struct relay_event_message *m = relay_prepare (ctx); - struct relay_event_data_response *e = &m->data.response; - e->event = RELAY_EVENT_RESPONSE; - e->command_seq = seq; - return e; -} + INIT_ATTR (PROMPT, -1, -1, TEXT_BOLD); + INIT_ATTR (RESET, -1, -1, 0); + INIT_ATTR (DATE_CHANGE, -1, -1, TEXT_BOLD); + INIT_ATTR (READ_MARKER, COLOR_MAGENTA, -1, 0); + INIT_ATTR (WARNING, COLOR_YELLOW, -1, 0); + INIT_ATTR (ERROR, COLOR_RED, -1, 0); -// --- Terminal output --------------------------------------------------------- + INIT_ATTR (EXTERNAL, COLOR_WHITE, -1, 0); + INIT_ATTR (TIMESTAMP, COLOR_WHITE, -1, 0); + INIT_ATTR (HIGHLIGHT, COLOR_BRIGHT (YELLOW), COLOR_MAGENTA, TEXT_BOLD); + INIT_ATTR (ACTION, COLOR_RED, -1, 0); + INIT_ATTR (USERHOST, COLOR_CYAN, -1, 0); + INIT_ATTR (JOIN, COLOR_GREEN, -1, 0); + INIT_ATTR (PART, COLOR_RED, -1, 0); -/// Default colour pair -#define COLOR_DEFAULT -1 +#undef INIT_ATTR -/// Bright versions of the basic colour set -#define COLOR_BRIGHT(x) (COLOR_ ## x + 8) + // This prevents formatters from obtaining an attribute printer function + if (!have_ti) + { + g_terminal.stdout_is_tty = false; + g_terminal.stderr_is_tty = false; + } -/// Builds a colour pair for 256-colour terminals with a 16-colour backup value -#define COLOR_256(name, c256) \ - (((COLOR_ ## name) & 0xFFFF) | (((c256) & 0xFFFF) << 16)) + g_log_message_real = log_message_attributed; +} -typedef int (*terminal_printer_fn) (int); +// --- Helpers ----------------------------------------------------------------- static int -putchar_stderr (int c) +irc_server_strcmp (struct server *s, const char *a, const char *b) { - return fputc (c, stderr); + int x; + while (*a || *b) + if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++))) + return x; + return 0; } -static terminal_printer_fn -get_attribute_printer (FILE *stream) +static int +irc_server_strncmp (struct server *s, const char *a, const char *b, size_t n) { - if (stream == stdout && g_terminal.stdout_is_tty) - return putchar; - if (stream == stderr && g_terminal.stderr_is_tty) - return putchar_stderr; - return NULL; + int x; + while (n-- && (*a || *b)) + if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++))) + return x; + return 0; } -// ~~~ Attribute printer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -// A little tool that tries to make the most of the terminal's capabilities -// to set up text attributes. It mostly targets just terminal emulators as that -// is what people are using these days. At least no stupid ncurses limits us -// with colour pairs. - -struct attr_printer +static char * +irc_cut_nickname (const char *prefix) { - struct attrs *attrs; ///< Named attributes - FILE *stream; ///< Output stream - bool dirty; ///< Attributes are set -}; + return cstr_cut_until (prefix, "!@"); +} -#define ATTR_PRINTER_INIT(attrs, stream) { attrs, stream, true } +static const char * +irc_find_userhost (const char *prefix) +{ + const char *p = strchr (prefix, '!'); + return p ? p + 1 : NULL; +} -static void -attr_printer_filtered_puts (FILE *stream, const char *attr) +static bool +irc_is_this_us (struct server *s, const char *prefix) { - for (; *attr; attr++) - { - // sgr/set_attributes and sgr0/exit_attribute_mode like to enable or - // disable the ACS with SO/SI (e.g. for TERM=screen), however `less -R` - // does not skip over these characters and it screws up word wrapping - if (*attr == 14 /* SO */ || *attr == 15 /* SI */) - continue; + // This shouldn't be called before successfully registering. + // Better safe than sorry, though. + if (!s->irc_user) + return false; - // Trivially skip delay sequences intended to be processed by tputs() - const char *end = NULL; - if (attr[0] == '$' && attr[1] == '<' && (end = strchr (attr, '>'))) - attr = end; - else - fputc (*attr, stream); - } + char *nick = irc_cut_nickname (prefix); + bool result = !irc_server_strcmp (s, nick, s->irc_user->nickname); + free (nick); + return result; } -static void -attr_printer_tputs (struct attr_printer *self, const char *attr) +static bool +irc_is_channel (struct server *s, const char *ident) { - terminal_printer_fn printer = get_attribute_printer (self->stream); - if (printer) - tputs (attr, 1, printer); - else - // We shouldn't really do this but we need it to output formatting - // to the pager--it should be SGR-only - attr_printer_filtered_puts (self->stream, attr); + return *ident + && (!!strchr (s->irc_chantypes, *ident) || + !!strchr (s->irc_idchan_prefixes, *ident)); } -static void -attr_printer_reset (struct attr_printer *self) +// Message targets can be prefixed by a character filtering their targets +static const char * +irc_skip_statusmsg (struct server *s, const char *target) { - if (self->dirty) - attr_printer_tputs (self, exit_attribute_mode); - - self->dirty = false; + return target + (*target && strchr (s->irc_statusmsg, *target)); } -// NOTE: commonly terminals have: -// 8 colours (worst, bright fg often with BOLD, bg sometimes with BLINK) -// 16 colours (okayish, we have the full basic range guaranteed) -// 88 colours (the same plus a 4^3 RGB cube and a few shades of grey) -// 256 colours (best, like above but with a larger cube and more grey) - -/// Interpolate from the 256-colour palette to the 88-colour one -static int -attr_printer_256_to_88 (int color) +static bool +irc_is_extban (struct server *s, const char *target) { - // These colours are the same everywhere - if (color < 16) - return color; - - // 24 -> 8 extra shades of grey - if (color >= 232) - return 80 + (color - 232) / 3; + // Some servers have a prefix, and some support negation + if (s->irc_extban_prefix && *target++ != s->irc_extban_prefix) + return false; + if (*target == '~') + target++; - // 6 * 6 * 6 cube -> 4 * 4 * 4 cube - int x[6] = { 0, 1, 1, 2, 2, 3 }; - int index = color - 16; - return 16 + - ( x[ index / 36 ] << 8 - | x[(index / 6) % 6 ] << 4 - | x[(index % 6) ] ); + // XXX: we don't know if it's supposed to have an argument, or not + return *target && strchr (s->irc_extban_types, *target++) + && strchr (":\0", *target); } -static int -attr_printer_decode_color (int color, bool *is_bright) +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// As of 2020, everything should be in UTF-8. And if it's not, we'll decode it +// as ISO Latin 1. This function should not be called on the whole message. +static char * +irc_to_utf8 (const char *text) { - int16_t c16 = color; hard_assert (c16 < 16); - int16_t c256 = color >> 16; hard_assert (c256 < 256); + if (!text) + return NULL; - *is_bright = false; - switch (max_colors) - { - case 8: - if (c16 >= 8) - { - c16 -= 8; - *is_bright = true; - } - // Fall-through - case 16: - return c16; + // XXX: the validation may be unnecessarily harsh, could do with a lenient + // first pass, then replace any errors with the replacement character + size_t len = strlen (text) + 1; + if (utf8_validate (text, len)) + return xstrdup (text); - case 88: - return c256 <= 0 ? c16 : attr_printer_256_to_88 (c256); - case 256: - return c256 <= 0 ? c16 : c256; + // Windows 1252 redefines several silly C1 control characters as glyphs + static const char c1[32][4] = + { + "\xe2\x82\xac", "\xc2\x81", "\xe2\x80\x9a", "\xc6\x92", + "\xe2\x80\x9e", "\xe2\x80\xa6", "\xe2\x80\xa0", "\xe2\x80\xa1", + "\xcb\x86", "\xe2\x80\xb0", "\xc5\xa0", "\xe2\x80\xb9", + "\xc5\x92", "\xc2\x8d", "\xc5\xbd", "\xc2\x8f", + "\xc2\x90", "\xe2\x80\x98", "\xe2\x80\x99", "\xe2\x80\x9c", + "\xe2\x80\x9d", "\xe2\x80\xa2", "\xe2\x80\x93", "\xe2\x80\x94", + "\xcb\x9c", "\xe2\x84\xa2", "\xc5\xa1", "\xe2\x80\xba", + "\xc5\x93", "\xc2\x9d", "\xc5\xbe", "\xc5\xb8", + }; - default: - // Unsupported palette - return -1; + struct str s = str_make (); + for (const char *p = text; *p; p++) + { + int c = *(unsigned char *) p; + if (c < 0x80) + str_append_c (&s, c); + else if (c < 0xA0) + str_append (&s, c1[c & 0x1f]); + else + str_append_data (&s, + (char[]) {0xc0 | (c >> 6), 0x80 | (c & 0x3f)}, 2); } + return str_steal (&s); } -static void -attr_printer_apply (struct attr_printer *self, - int text_attrs, int wanted_fg, int wanted_bg) -{ - bool fg_is_bright; - int fg = attr_printer_decode_color (wanted_fg, &fg_is_bright); - bool bg_is_bright; - int bg = attr_printer_decode_color (wanted_bg, &bg_is_bright); +// --- Output formatter -------------------------------------------------------- - bool have_inverse = !!(text_attrs & TEXT_INVERSE); - if (have_inverse) - { - bool tmp = fg_is_bright; - fg_is_bright = bg_is_bright; - bg_is_bright = tmp; - } +// 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 (expected to be in UTF-8) +// #d inserts a signed integer +// #l inserts a locale-encoded string +// +// #S inserts a string from the server in an unknown encoding +// #m inserts an IRC-formatted string (auto-resets at boundaries) +// #n cuts the nickname from a string and automatically colours it +// #N is like #n but also appends userhost, if present +// +// #a inserts named attributes (auto-resets) +// #r resets terminal attributes +// #c sets foreground colour +// #C sets background colour +// +// Modifiers: +// & free() the string argument after using it - // In 8 colour mode, some terminals don't support bright backgrounds. - // However, we can make use of the fact that the brightness change caused - // by the bold attribute is retained when inverting the colours. - // This has the downside of making the text bold when it's not supposed - // to be, and we still can't make both colours bright, so it's more of - // an interesting hack rather than anything else. - if (!fg_is_bright && bg_is_bright && have_inverse) - text_attrs |= TEXT_BOLD; - else if (!fg_is_bright && bg_is_bright - && !have_inverse && fg >= 0 && bg >= 0) - { - // As long as none of the colours is the default, we can swap them - int tmp = fg; fg = bg; bg = tmp; - text_attrs |= TEXT_BOLD | TEXT_INVERSE; - } - else - { - // This often works, however... - if (fg_is_bright) text_attrs |= TEXT_BOLD; - // this turns out to be annoying if implemented "correctly" - if (bg_is_bright) text_attrs |= TEXT_BLINK; - } +static void +formatter_add_item (struct formatter *self, struct formatter_item template_) +{ + // Auto-resetting tends to create unnecessary items, + // which also end up being relayed to frontends, so filter them out. + bool reset = + template_.type == FORMATTER_ITEM_ATTR && + template_.attribute == ATTR_RESET; + if (self->clean && reset) + return; - attr_printer_reset (self); + self->clean = reset || + (self->clean && template_.type == FORMATTER_ITEM_TEXT); - // TEXT_MONOSPACE is unimplemented, for obvious reasons - if (text_attrs) - attr_printer_tputs (self, tparm (set_attributes, - 0, // standout - text_attrs & TEXT_UNDERLINE, - text_attrs & TEXT_INVERSE, - text_attrs & TEXT_BLINK, - 0, // dim - text_attrs & TEXT_BOLD, - 0, // blank - 0, // protect - 0)); // acs - if ((text_attrs & TEXT_ITALIC) && enter_italics_mode) - attr_printer_tputs (self, enter_italics_mode); + if (template_.text) + template_.text = xstrdup (template_.text); - char *smxx = NULL; - if ((text_attrs & TEXT_CROSSED_OUT) - && (smxx = tigetstr ("smxx")) && smxx != (char *) -1) - attr_printer_tputs (self, smxx); + if (self->items_len == self->items_alloc) + self->items = xreallocarray + (self->items, sizeof *self->items, (self->items_alloc <<= 1)); + self->items[self->items_len++] = template_; +} - if (fg >= 0) - attr_printer_tputs (self, g_terminal.color_set_fg[fg]); - if (bg >= 0) - attr_printer_tputs (self, g_terminal.color_set_bg[bg]); +#define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \ + (struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ }) - self->dirty = true; -} +#define FORMATTER_ADD_RESET(self) \ + FORMATTER_ADD_ITEM ((self), ATTR, .attribute = ATTR_RESET) +#define FORMATTER_ADD_TEXT(self, text_) \ + FORMATTER_ADD_ITEM ((self), TEXT, .text = (text_)) -static void -attr_printer_apply_named (struct attr_printer *self, int attribute) +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +enum { - attr_printer_reset (self); - if (attribute == ATTR_RESET) - return; + MIRC_WHITE, MIRC_BLACK, MIRC_BLUE, MIRC_GREEN, + MIRC_L_RED, MIRC_RED, MIRC_PURPLE, MIRC_ORANGE, + MIRC_YELLOW, MIRC_L_GREEN, MIRC_CYAN, MIRC_L_CYAN, + MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY, MIRC_L_GRAY, +}; - // See the COLOR_256 macro or attr_printer_decode_color(). - struct attrs *a = &self->attrs[attribute]; - attr_printer_apply (self, a->attrs, - a->fg < 16 ? a->fg : (a->fg << 16 | (-1 & 0xFFFF)), - a->bg < 16 ? a->bg : (a->bg << 16 | (-1 & 0xFFFF))); - self->dirty = true; -} +// We use estimates from the 16 colour terminal palette, or the 256 colour cube, +// which is not always available. The mIRC orange colour is only in the cube. -// ~~~ Logging redirect ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +static const int g_mirc_to_terminal[] = +{ + [MIRC_WHITE] = COLOR_256 (BRIGHT (WHITE), 231), + [MIRC_BLACK] = COLOR_256 (BLACK, 16), + [MIRC_BLUE] = COLOR_256 (BLUE, 19), + [MIRC_GREEN] = COLOR_256 (GREEN, 34), + [MIRC_L_RED] = COLOR_256 (BRIGHT (RED), 196), + [MIRC_RED] = COLOR_256 (RED, 124), + [MIRC_PURPLE] = COLOR_256 (MAGENTA, 127), + [MIRC_ORANGE] = COLOR_256 (BRIGHT (YELLOW), 214), + [MIRC_YELLOW] = COLOR_256 (BRIGHT (YELLOW), 226), + [MIRC_L_GREEN] = COLOR_256 (BRIGHT (GREEN), 46), + [MIRC_CYAN] = COLOR_256 (CYAN, 37), + [MIRC_L_CYAN] = COLOR_256 (BRIGHT (CYAN), 51), + [MIRC_L_BLUE] = COLOR_256 (BRIGHT (BLUE), 21), + [MIRC_L_PURPLE] = COLOR_256 (BRIGHT (MAGENTA),201), + [MIRC_GRAY] = COLOR_256 (BRIGHT (BLACK), 244), + [MIRC_L_GRAY] = COLOR_256 (WHITE, 252), +}; -static void -vprint_attributed (struct app_context *ctx, - FILE *stream, intptr_t attribute, const char *fmt, va_list ap) +// https://modern.ircdocs.horse/formatting.html +// http://anti.teamidiot.de/static/nei/*/extended_mirc_color_proposal.html +static const int16_t g_extra_to_256[100 - 16] = { - terminal_printer_fn printer = get_attribute_printer (stream); - if (!attribute) - printer = NULL; + 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89, + 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125, + 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161, + 196, 208, 226, 154, 46, 86 , 51, 75, 21, 171, 201, 198, + 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205, + 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212, + 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, -1 +}; - struct attr_printer state = ATTR_PRINTER_INIT (ctx->theme, stream); - if (printer) - attr_printer_apply_named (&state, attribute); +static const char * +irc_parse_mirc_color (const char *s, uint8_t *fg, uint8_t *bg) +{ + if (!isdigit_ascii (*s)) + { + *fg = *bg = 99; + return s; + } - vfprintf (stream, fmt, ap); + *fg = *s++ - '0'; + if (isdigit_ascii (*s)) + *fg = *fg * 10 + (*s++ - '0'); - if (printer) - attr_printer_reset (&state); + if (*s != ',' || !isdigit_ascii (s[1])) + return s; + s++; + + *bg = *s++ - '0'; + if (isdigit_ascii (*s)) + *bg = *bg * 10 + (*s++ - '0'); + return s; } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct irc_char_attrs +{ + uint8_t fg, bg; ///< {Fore,back}ground colour or 99 + uint8_t attributes; ///< TEXT_* flags, except TEXT_BLINK + uint8_t starts_at_boundary; ///< Possible to split here? +}; + static void -print_attributed (struct app_context *ctx, - FILE *stream, intptr_t attribute, const char *fmt, ...) +irc_serialize_char_attrs (const struct irc_char_attrs *attrs, struct str *out) { - va_list ap; - va_start (ap, fmt); - vprint_attributed (ctx, stream, attribute, fmt, ap); - va_end (ap); + soft_assert (attrs->fg < 100 && attrs->bg < 100); + + if (attrs->fg != 99 || attrs->bg != 99) + { + str_append_printf (out, "\x03%u", attrs->fg); + if (attrs->bg != 99) + str_append_printf (out, ",%02u", attrs->bg); + } + if (attrs->attributes & TEXT_BOLD) str_append_c (out, '\x02'); + if (attrs->attributes & TEXT_ITALIC) str_append_c (out, '\x1d'); + if (attrs->attributes & TEXT_UNDERLINE) str_append_c (out, '\x1f'); + if (attrs->attributes & TEXT_INVERSE) str_append_c (out, '\x16'); + if (attrs->attributes & TEXT_CROSSED_OUT) str_append_c (out, '\x1e'); + if (attrs->attributes & TEXT_MONOSPACE) str_append_c (out, '\x11'); } -static void -log_message_attributed (void *user_data, const char *quote, const char *fmt, - va_list ap) +static int +irc_parse_attribute (char c) { - FILE *stream = stderr; - struct app_context *ctx = g_ctx; + switch (c) + { + case '\x02' /* ^B */: return TEXT_BOLD; + case '\x11' /* ^Q */: return TEXT_MONOSPACE; + case '\x16' /* ^V */: return TEXT_INVERSE; + case '\x1d' /* ^] */: return TEXT_ITALIC; + case '\x1e' /* ^^ */: return TEXT_CROSSED_OUT; + case '\x1f' /* ^_ */: return TEXT_UNDERLINE; + case '\x0f' /* ^O */: return -1; + } + return 0; +} - CALL (ctx->input, hide); +// The text needs to be NUL-terminated, and a valid UTF-8 string +static struct irc_char_attrs * +irc_analyze_text (const char *text, size_t len) +{ + struct irc_char_attrs *attrs = xcalloc (len, sizeof *attrs), + blank = { .fg = 99, .bg = 99, .starts_at_boundary = true }, + next = blank, cur = next; - print_attributed (ctx, stream, (intptr_t) user_data, "%s", quote); - vprint_attributed (ctx, stream, (intptr_t) user_data, fmt, ap); - fputs ("\n", stream); + for (size_t i = 0; i != len; cur = next) + { + const char *start = text; + hard_assert (utf8_decode (&text, len - i) >= 0); - CALL (ctx->input, show); + int attribute = irc_parse_attribute (*start); + if (*start == '\x03') + text = irc_parse_mirc_color (text, &next.fg, &next.bg); + else if (attribute > 0) + next.attributes ^= attribute; + else if (attribute < 0) + next = blank; + + while (start++ != text) + { + attrs[i++] = cur; + cur.starts_at_boundary = false; + } + } + return attrs; } -// ~~~ Theme ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static ssize_t -attr_by_name (const char *name) +static const char * +formatter_parse_mirc_color (struct formatter *self, const char *s) { - static const char *table[ATTR_COUNT] = - { - NULL, -#define XX(x, y, z) [ATTR_ ## x] = #y, - ATTR_TABLE (XX) -#undef XX - }; + uint8_t fg = 255, bg = 255; + s = irc_parse_mirc_color (s, &fg, &bg); - for (size_t i = 1; i < N_ELEMENTS (table); i++) - if (!strcmp (name, table[i])) - return i; - return -1; + if (fg < 16) + FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]); + else if (fg < 100) + FORMATTER_ADD_ITEM (self, FG_COLOR, + .color = COLOR_256 (DEFAULT, g_extra_to_256[fg - 16])); + + if (bg < 16) + FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]); + else if (bg < 100) + FORMATTER_ADD_ITEM (self, BG_COLOR, + .color = COLOR_256 (DEFAULT, g_extra_to_256[bg - 16])); + + return s; } static void -on_config_theme_change (struct config_item *item) +formatter_parse_message (struct formatter *self, const char *s) { - struct app_context *ctx = item->user_data; - ssize_t id = attr_by_name (item->schema->name); - if (id != -1) + FORMATTER_ADD_RESET (self); + + struct str buf = str_make (); + unsigned char c; + while ((c = *s++)) { - // TODO: There should be a validator. - ctx->theme[id] = item->type == CONFIG_ITEM_NULL - ? ctx->theme_defaults[id] - : attrs_decode (item->value.string.str); + if (buf.len && c < 0x20) + { + FORMATTER_ADD_TEXT (self, buf.str); + str_reset (&buf); + } + + int attribute = irc_parse_attribute (c); + if (c == '\x03') + s = formatter_parse_mirc_color (self, s); + else if (attribute > 0) + FORMATTER_ADD_ITEM (self, SIMPLE, .attribute = attribute); + else if (attribute < 0) + FORMATTER_ADD_RESET (self); + else + str_append_c (&buf, c); } + + if (buf.len) + FORMATTER_ADD_TEXT (self, buf.str); + + str_free (&buf); + FORMATTER_ADD_RESET (self); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + static void -init_colors (struct app_context *ctx) +formatter_parse_nick (struct formatter *self, const char *s) { - bool have_ti = init_terminal (); - -#define INIT_ATTR(id, ...) ctx->theme[ATTR_ ## id] = \ - ctx->theme_defaults[ATTR_ ## id] = (struct attrs) { __VA_ARGS__ } + // For outgoing messages; maybe we should add a special #t for them + // which would also make us not cut off the userhost part, ever + if (irc_is_channel (self->s, irc_skip_statusmsg (self->s, s))) + { + char *tmp = irc_to_utf8 (s); + FORMATTER_ADD_TEXT (self, tmp); + free (tmp); + return; + } - INIT_ATTR (PROMPT, -1, -1, TEXT_BOLD); - INIT_ATTR (RESET, -1, -1, 0); - INIT_ATTR (DATE_CHANGE, -1, -1, TEXT_BOLD); - INIT_ATTR (READ_MARKER, COLOR_MAGENTA, -1, 0); - INIT_ATTR (WARNING, COLOR_YELLOW, -1, 0); - INIT_ATTR (ERROR, COLOR_RED, -1, 0); + char *nick = irc_cut_nickname (s); + int color = siphash_wrapper (nick, strlen (nick)) % 7; - INIT_ATTR (EXTERNAL, COLOR_WHITE, -1, 0); - INIT_ATTR (TIMESTAMP, COLOR_WHITE, -1, 0); - INIT_ATTR (HIGHLIGHT, COLOR_BRIGHT (YELLOW), COLOR_MAGENTA, TEXT_BOLD); - INIT_ATTR (ACTION, COLOR_RED, -1, 0); - INIT_ATTR (USERHOST, COLOR_CYAN, -1, 0); - INIT_ATTR (JOIN, COLOR_GREEN, -1, 0); - INIT_ATTR (PART, COLOR_RED, -1, 0); + // Never use the black colour, could become transparent on black terminals; + // white is similarly excluded from the range + if (color == COLOR_BLACK) + color = (uint16_t) -1; -#undef INIT_ATTR + // Use a colour from the 256-colour cube if available + color |= self->ctx->nick_palette[siphash_wrapper (nick, + strlen (nick)) % self->ctx->nick_palette_len] << 16; - // This prevents formatters from obtaining an attribute printer function - if (!have_ti) - { - g_terminal.stdout_is_tty = false; - g_terminal.stderr_is_tty = false; - } + // We always use the default colour for ourselves + if (self->s && irc_is_this_us (self->s, nick)) + color = -1; - g_log_message_real = log_message_attributed; -} + FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color); -// --- Helpers ----------------------------------------------------------------- + char *x = irc_to_utf8 (nick); + free (nick); + FORMATTER_ADD_TEXT (self, x); + free (x); -static int -irc_server_strcmp (struct server *s, const char *a, const char *b) -{ - int x; - while (*a || *b) - if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++))) - return x; - return 0; + // Need to reset the colour afterwards + FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1); } -static int -irc_server_strncmp (struct server *s, const char *a, const char *b, size_t n) +static void +formatter_parse_nick_full (struct formatter *self, const char *s) { - int x; - while (n-- && (*a || *b)) - if ((x = s->irc_tolower (*a++) - s->irc_tolower (*b++))) - return x; - return 0; -} + formatter_parse_nick (self, s); -static char * -irc_cut_nickname (const char *prefix) -{ - return cstr_cut_until (prefix, "!@"); -} + const char *userhost; + if (!(userhost = irc_find_userhost (s))) + return; -static const char * -irc_find_userhost (const char *prefix) -{ - const char *p = strchr (prefix, '!'); - return p ? p + 1 : NULL; -} + FORMATTER_ADD_TEXT (self, " ("); + FORMATTER_ADD_ITEM (self, ATTR, .attribute = ATTR_USERHOST); -static bool -irc_is_this_us (struct server *s, const char *prefix) -{ - // This shouldn't be called before successfully registering. - // Better safe than sorry, though. - if (!s->irc_user) - return false; + char *x = irc_to_utf8 (userhost); + FORMATTER_ADD_TEXT (self, x); + free (x); - char *nick = irc_cut_nickname (prefix); - bool result = !irc_server_strcmp (s, nick, s->irc_user->nickname); - free (nick); - return result; + FORMATTER_ADD_RESET (self); + FORMATTER_ADD_TEXT (self, ")"); } -static bool -irc_is_channel (struct server *s, const char *ident) +static const char * +formatter_parse_field (struct formatter *self, + const char *field, struct str *buf, va_list *ap) { - return *ident - && (!!strchr (s->irc_chantypes, *ident) || - !!strchr (s->irc_idchan_prefixes, *ident)); -} + bool free_string = false; + char *s = NULL; + char *tmp = NULL; + int c; + +restart: + switch ((c = *field++)) + { + // We can push boring text content to the caller's buffer + // and let it flush the buffer only when it's actually needed + case 'd': + tmp = xstrdup_printf ("%d", va_arg (*ap, int)); + str_append (buf, tmp); + free (tmp); + break; + case 's': + str_append (buf, (s = va_arg (*ap, char *))); + break; + case 'l': + if (!(tmp = iconv_xstrdup (self->ctx->term_to_utf8, + (s = va_arg (*ap, char *)), -1, NULL))) + print_error ("character conversion failed for: %s", "output"); + else + str_append (buf, tmp); + free (tmp); + break; + + case 'S': + tmp = irc_to_utf8 ((s = va_arg (*ap, char *))); + str_append (buf, tmp); + free (tmp); + break; + case 'm': + tmp = irc_to_utf8 ((s = va_arg (*ap, char *))); + formatter_parse_message (self, tmp); + free (tmp); + break; + case 'n': + formatter_parse_nick (self, (s = va_arg (*ap, char *))); + break; + case 'N': + formatter_parse_nick_full (self, (s = va_arg (*ap, char *))); + break; -// Message targets can be prefixed by a character filtering their targets -static const char * -irc_skip_statusmsg (struct server *s, const char *target) -{ - return target + (*target && strchr (s->irc_statusmsg, *target)); -} + case 'a': + FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int)); + break; + case 'c': + FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int)); + break; + case 'C': + FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int)); + break; + case 'r': + FORMATTER_ADD_RESET (self); + break; -static bool -irc_is_extban (struct server *s, const char *target) -{ - // Some servers have a prefix, and some support negation - if (s->irc_extban_prefix && *target++ != s->irc_extban_prefix) - return false; - if (*target == '~') - target++; + default: + if (c == '&' && !free_string) + free_string = true; + else if (c) + hard_assert (!"unexpected format specifier"); + else + hard_assert (!"unexpected end of format string"); + goto restart; + } - // XXX: we don't know if it's supposed to have an argument, or not - return *target && strchr (s->irc_extban_types, *target++) - && strchr (":\0", *target); + if (free_string) + free (s); + return field; } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// As of 2020, everything should be in UTF-8. And if it's not, we'll decode it -// as ISO Latin 1. This function should not be called on the whole message. -static char * -irc_to_utf8 (const char *text) +// I was unable to take a pointer of a bare "va_list" when it was passed in +// as a function argument, so it has to be a pointer from the beginning +static void +formatter_addv (struct formatter *self, const char *format, va_list *ap) { - if (!text) - return NULL; - - // XXX: the validation may be unnecessarily harsh, could do with a lenient - // first pass, then replace any errors with the replacement character - size_t len = strlen (text) + 1; - if (utf8_validate (text, len)) - return xstrdup (text); - - // Windows 1252 redefines several silly C1 control characters as glyphs - static const char c1[32][4] = + struct str buf = str_make (); + while (*format) { - "\xe2\x82\xac", "\xc2\x81", "\xe2\x80\x9a", "\xc6\x92", - "\xe2\x80\x9e", "\xe2\x80\xa6", "\xe2\x80\xa0", "\xe2\x80\xa1", - "\xcb\x86", "\xe2\x80\xb0", "\xc5\xa0", "\xe2\x80\xb9", - "\xc5\x92", "\xc2\x8d", "\xc5\xbd", "\xc2\x8f", - "\xc2\x90", "\xe2\x80\x98", "\xe2\x80\x99", "\xe2\x80\x9c", - "\xe2\x80\x9d", "\xe2\x80\xa2", "\xe2\x80\x93", "\xe2\x80\x94", - "\xcb\x9c", "\xe2\x84\xa2", "\xc5\xa1", "\xe2\x80\xba", - "\xc5\x93", "\xc2\x9d", "\xc5\xbe", "\xc5\xb8", - }; + if (*format != '#' || *++format == '#') + { + str_append_c (&buf, *format++); + continue; + } + if (buf.len) + { + FORMATTER_ADD_TEXT (self, buf.str); + str_reset (&buf); + } - struct str s = str_make (); - for (const char *p = text; *p; p++) - { - int c = *(unsigned char *) p; - if (c < 0x80) - str_append_c (&s, c); - else if (c < 0xA0) - str_append (&s, c1[c & 0x1f]); - else - str_append_data (&s, - (char[]) {0xc0 | (c >> 6), 0x80 | (c & 0x3f)}, 2); + format = formatter_parse_field (self, format, &buf, ap); } - return str_steal (&s); -} -// --- Output formatter -------------------------------------------------------- + if (buf.len) + FORMATTER_ADD_TEXT (self, buf.str); -// 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 (expected to be in UTF-8) -// #d inserts a signed integer -// #l inserts a locale-encoded string -// -// #S inserts a string from the server in an unknown encoding -// #m inserts an IRC-formatted string (auto-resets at boundaries) -// #n cuts the nickname from a string and automatically colours it -// #N is like #n but also appends userhost, if present -// -// #a inserts named attributes (auto-resets) -// #r resets terminal attributes -// #c sets foreground colour -// #C sets background colour -// -// Modifiers: -// & free() the string argument after using it + str_free (&buf); +} static void -formatter_add_item (struct formatter *self, struct formatter_item template_) +formatter_add (struct formatter *self, const char *format, ...) { - // Auto-resetting tends to create unnecessary items, - // which also end up being relayed to frontends, so filter them out. - bool reset = - template_.type == FORMATTER_ITEM_ATTR && - template_.attribute == ATTR_RESET; - if (self->clean && reset) - return; - - self->clean = reset || - (self->clean && template_.type == FORMATTER_ITEM_TEXT); - - if (template_.text) - template_.text = xstrdup (template_.text); - - if (self->items_len == self->items_alloc) - self->items = xreallocarray - (self->items, sizeof *self->items, (self->items_alloc <<= 1)); - self->items[self->items_len++] = template_; + va_list ap; + va_start (ap, format); + formatter_addv (self, format, &ap); + va_end (ap); } -#define FORMATTER_ADD_ITEM(self, type_, ...) formatter_add_item ((self), \ - (struct formatter_item) { .type = FORMATTER_ITEM_ ## type_, __VA_ARGS__ }) - -#define FORMATTER_ADD_RESET(self) \ - FORMATTER_ADD_ITEM ((self), ATTR, .attribute = ATTR_RESET) -#define FORMATTER_ADD_TEXT(self, text_) \ - FORMATTER_ADD_ITEM ((self), TEXT, .text = (text_)) - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -enum +struct line_char_attrs { - MIRC_WHITE, MIRC_BLACK, MIRC_BLUE, MIRC_GREEN, - MIRC_L_RED, MIRC_RED, MIRC_PURPLE, MIRC_ORANGE, - MIRC_YELLOW, MIRC_L_GREEN, MIRC_CYAN, MIRC_L_CYAN, - MIRC_L_BLUE, MIRC_L_PURPLE, MIRC_GRAY, MIRC_L_GRAY, + short named; ///< Named attribute or -1 + short text; ///< Text attributes + int fg; ///< Foreground colour (-1 for default) + int bg; ///< Background colour (-1 for default) }; -// We use estimates from the 16 colour terminal palette, or the 256 colour cube, -// which is not always available. The mIRC orange colour is only in the cube. +// We can get rid of the linked list and do this in one allocation (use strlen() +// for the upper bound)--since we only prepend and/or replace characters, add +// a member to specify the prepended character and how many times to repeat it. +// Tabs may nullify the wide character but it's not necessary. +// +// This would be slighly more optimal but it would also set the algorithm in +// stone and complicate flushing. -static const int g_mirc_to_terminal[] = +struct line_char { - [MIRC_WHITE] = COLOR_256 (BRIGHT (WHITE), 231), - [MIRC_BLACK] = COLOR_256 (BLACK, 16), - [MIRC_BLUE] = COLOR_256 (BLUE, 19), - [MIRC_GREEN] = COLOR_256 (GREEN, 34), - [MIRC_L_RED] = COLOR_256 (BRIGHT (RED), 196), - [MIRC_RED] = COLOR_256 (RED, 124), - [MIRC_PURPLE] = COLOR_256 (MAGENTA, 127), - [MIRC_ORANGE] = COLOR_256 (BRIGHT (YELLOW), 214), - [MIRC_YELLOW] = COLOR_256 (BRIGHT (YELLOW), 226), - [MIRC_L_GREEN] = COLOR_256 (BRIGHT (GREEN), 46), - [MIRC_CYAN] = COLOR_256 (CYAN, 37), - [MIRC_L_CYAN] = COLOR_256 (BRIGHT (CYAN), 51), - [MIRC_L_BLUE] = COLOR_256 (BRIGHT (BLUE), 21), - [MIRC_L_PURPLE] = COLOR_256 (BRIGHT (MAGENTA),201), - [MIRC_GRAY] = COLOR_256 (BRIGHT (BLACK), 244), - [MIRC_L_GRAY] = COLOR_256 (WHITE, 252), -}; + LIST_HEADER (struct line_char) -// https://modern.ircdocs.horse/formatting.html -// http://anti.teamidiot.de/static/nei/*/extended_mirc_color_proposal.html -static const int16_t g_extra_to_256[100 - 16] = -{ - 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89, - 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125, - 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161, - 196, 208, 226, 154, 46, 86 , 51, 75, 21, 171, 201, 198, - 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205, - 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212, - 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231, -1 + wchar_t wide; ///< The character as a wchar_t + int width; ///< Width of the character in cells + struct line_char_attrs attrs; ///< Attributes }; -static const char * -irc_parse_mirc_color (const char *s, uint8_t *fg, uint8_t *bg) +static struct line_char * +line_char_new (wchar_t wc) { - if (!isdigit_ascii (*s)) - { - *fg = *bg = 99; - return s; - } - - *fg = *s++ - '0'; - if (isdigit_ascii (*s)) - *fg = *fg * 10 + (*s++ - '0'); + struct line_char *self = xcalloc (1, sizeof *self); + self->width = wcwidth ((self->wide = wc)); - if (*s != ',' || !isdigit_ascii (s[1])) - return s; - s++; + // Typically various control characters + if (self->width < 0) + self->width = 0; - *bg = *s++ - '0'; - if (isdigit_ascii (*s)) - *bg = *bg * 10 + (*s++ - '0'); - return s; + self->attrs.bg = self->attrs.fg = -1; + self->attrs.named = ATTR_RESET; + return self; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -struct irc_char_attrs +struct line_wrap_mark +{ + struct line_char *start; ///< First character + int used; ///< Display cells used +}; + +static void +line_wrap_mark_push (struct line_wrap_mark *mark, struct line_char *c) +{ + if (!mark->start) + mark->start = c; + mark->used += c->width; +} + +struct line_wrap_state { - uint8_t fg, bg; ///< {Fore,back}ground colour or 99 - uint8_t attributes; ///< TEXT_* flags, except TEXT_BLINK - uint8_t starts_at_boundary; ///< Possible to split here? + struct line_char *result; ///< Head of result + struct line_char *result_tail; ///< Tail of result + + int line_used; ///< Line length before marks + int line_max; ///< Maximum line length + struct line_wrap_mark chunk; ///< All buffered text + struct line_wrap_mark overflow; ///< Overflowing text }; static void -irc_serialize_char_attrs (const struct irc_char_attrs *attrs, struct str *out) +line_wrap_flush_split (struct line_wrap_state *s, struct line_wrap_mark *before) { - soft_assert (attrs->fg < 100 && attrs->bg < 100); - - if (attrs->fg != 99 || attrs->bg != 99) - { - str_append_printf (out, "\x03%u", attrs->fg); - if (attrs->bg != 99) - str_append_printf (out, ",%02u", attrs->bg); - } - if (attrs->attributes & TEXT_BOLD) str_append_c (out, '\x02'); - if (attrs->attributes & TEXT_ITALIC) str_append_c (out, '\x1d'); - if (attrs->attributes & TEXT_UNDERLINE) str_append_c (out, '\x1f'); - if (attrs->attributes & TEXT_INVERSE) str_append_c (out, '\x16'); - if (attrs->attributes & TEXT_CROSSED_OUT) str_append_c (out, '\x1e'); - if (attrs->attributes & TEXT_MONOSPACE) str_append_c (out, '\x11'); + struct line_char *nl = line_char_new (L'\n'); + LIST_INSERT_WITH_TAIL (s->result, s->result_tail, nl, before->start); + s->line_used = before->used; } -static int -irc_parse_attribute (char c) +static void +line_wrap_flush (struct line_wrap_state *s, bool force_split) { - switch (c) + if (!s->overflow.start) + s->line_used += s->chunk.used; + else if (force_split || s->chunk.used > s->line_max) { - case '\x02' /* ^B */: return TEXT_BOLD; - case '\x11' /* ^Q */: return TEXT_MONOSPACE; - case '\x16' /* ^V */: return TEXT_INVERSE; - case '\x1d' /* ^] */: return TEXT_ITALIC; - case '\x1e' /* ^^ */: return TEXT_CROSSED_OUT; - case '\x1f' /* ^_ */: return TEXT_UNDERLINE; - case '\x0f' /* ^O */: return -1; +#ifdef WRAP_UNNECESSARILY + // When the line wraps at the end of the screen and a background colour + // is set, the terminal paints the entire new line with that colour. + // Explicitly inserting a newline with the default attributes fixes it. + line_wrap_flush_split (s, &s->overflow); +#else + // Splitting here breaks link searching mechanisms in some terminals, + // though, so we make a trade-off and let the chunk wrap naturally. + // Fuck terminals, really. + s->line_used = s->overflow.used; +#endif } - return 0; + else + // Print the chunk in its entirety on a new line + line_wrap_flush_split (s, &s->chunk); + + memset (&s->chunk, 0, sizeof s->chunk); + memset (&s->overflow, 0, sizeof s->overflow); } -// The text needs to be NUL-terminated, and a valid UTF-8 string -static struct irc_char_attrs * -irc_analyze_text (const char *text, size_t len) +static void +line_wrap_nl (struct line_wrap_state *s) { - struct irc_char_attrs *attrs = xcalloc (len, sizeof *attrs), - blank = { .fg = 99, .bg = 99, .starts_at_boundary = true }, - next = blank, cur = next; - - for (size_t i = 0; i != len; cur = next) - { - const char *start = text; - hard_assert (utf8_decode (&text, len - i) >= 0); - - int attribute = irc_parse_attribute (*start); - if (*start == '\x03') - text = irc_parse_mirc_color (text, &next.fg, &next.bg); - else if (attribute > 0) - next.attributes ^= attribute; - else if (attribute < 0) - next = blank; - - while (start++ != text) - { - attrs[i++] = cur; - cur.starts_at_boundary = false; - } - } - return attrs; + line_wrap_flush (s, true); + struct line_char *nl = line_char_new (L'\n'); + LIST_APPEND_WITH_TAIL (s->result, s->result_tail, nl); + s->line_used = 0; } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static const char * -formatter_parse_mirc_color (struct formatter *self, const char *s) +static void +line_wrap_tab (struct line_wrap_state *s, struct line_char *c) { - uint8_t fg = 255, bg = 255; - s = irc_parse_mirc_color (s, &fg, &bg); - - if (fg < 16) - FORMATTER_ADD_ITEM (self, FG_COLOR, .color = g_mirc_to_terminal[fg]); - else if (fg < 100) - FORMATTER_ADD_ITEM (self, FG_COLOR, - .color = COLOR_256 (DEFAULT, g_extra_to_256[fg - 16])); + line_wrap_flush (s, true); + if (s->line_used >= s->line_max) + line_wrap_nl (s); - if (bg < 16) - FORMATTER_ADD_ITEM (self, BG_COLOR, .color = g_mirc_to_terminal[bg]); - else if (bg < 100) - FORMATTER_ADD_ITEM (self, BG_COLOR, - .color = COLOR_256 (DEFAULT, g_extra_to_256[bg - 16])); + // Compute the number of characters needed to get to the next tab stop + int tab_width = ((s->line_used + 8) & ~7) - s->line_used; + // On overflow just fill the rest of the line with spaces + if (s->line_used + tab_width > s->line_max) + tab_width = s->line_max - s->line_used; - return s; + s->line_used += tab_width; + while (tab_width--) + { + struct line_char *space = line_char_new (L' '); + space->attrs = c->attrs; + LIST_APPEND_WITH_TAIL (s->result, s->result_tail, space); + } } static void -formatter_parse_message (struct formatter *self, const char *s) +line_wrap_push_char (struct line_wrap_state *s, struct line_char *c) { - FORMATTER_ADD_RESET (self); + // Note that when processing whitespace here, any non-WS chunk has already + // been flushed, and thus it matters little if we flush with force split + if (wcschr (L"\r\f\v", c->wide)) + /* Skip problematic characters */; + else if (c->wide == L'\n') + line_wrap_nl (s); + else if (c->wide == L'\t') + line_wrap_tab (s, c); + else + goto use_as_is; + free (c); + return; - struct str buf = str_make (); - unsigned char c; - while ((c = *s++)) +use_as_is: + if (s->overflow.start + || s->line_used + s->chunk.used + c->width > s->line_max) { - if (buf.len && c < 0x20) + if (s->overflow.used + c->width > s->line_max) { - FORMATTER_ADD_TEXT (self, buf.str); - str_reset (&buf); +#ifdef WRAP_UNNECESSARILY + // If the overflow overflows, restart on a new line + line_wrap_nl (s); +#else + // See line_wrap_flush(), we would end up on a new line anyway + line_wrap_flush (s, true); + s->line_used = 0; +#endif } - - int attribute = irc_parse_attribute (c); - if (c == '\x03') - s = formatter_parse_mirc_color (self, s); - else if (attribute > 0) - FORMATTER_ADD_ITEM (self, SIMPLE, .attribute = attribute); - else if (attribute < 0) - FORMATTER_ADD_RESET (self); else - str_append_c (&buf, c); + line_wrap_mark_push (&s->overflow, c); } - - if (buf.len) - FORMATTER_ADD_TEXT (self, buf.str); - - str_free (&buf); - FORMATTER_ADD_RESET (self); + line_wrap_mark_push (&s->chunk, c); + LIST_APPEND_WITH_TAIL (s->result, s->result_tail, c); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -formatter_parse_nick (struct formatter *self, const char *s) +/// Basic word wrapping that respects wcwidth(3) and expands tabs. +/// Besides making text easier to read, it also fixes the problem with +/// formatting spilling over the entire new line on line wrap. +static struct line_char * +line_wrap (struct line_char *line, int max_width) { - // For outgoing messages; maybe we should add a special #t for them - // which would also make us not cut off the userhost part, ever - if (irc_is_channel (self->s, irc_skip_statusmsg (self->s, s))) + struct line_wrap_state s = { .line_max = max_width }; + bool last_was_word_char = false; + LIST_FOR_EACH (struct line_char, c, line) { - char *tmp = irc_to_utf8 (s); - FORMATTER_ADD_TEXT (self, tmp); - free (tmp); - return; - } - - char *nick = irc_cut_nickname (s); - int color = siphash_wrapper (nick, strlen (nick)) % 7; - - // Never use the black colour, could become transparent on black terminals; - // white is similarly excluded from the range - if (color == COLOR_BLACK) - color = (uint16_t) -1; + // Act on the right boundary of (\s*\S+) chunks + bool this_is_word_char = !wcschr (L" \t\r\n\f\v", c->wide); + if (last_was_word_char && !this_is_word_char) + line_wrap_flush (&s, false); + last_was_word_char = this_is_word_char; - // Use a colour from the 256-colour cube if available - color |= self->ctx->nick_palette[siphash_wrapper (nick, - strlen (nick)) % self->ctx->nick_palette_len] << 16; + LIST_UNLINK (line, c); + line_wrap_push_char (&s, c); + } - // We always use the default colour for ourselves - if (self->s && irc_is_this_us (self->s, nick)) - color = -1; + // Make sure to process the last word and return the modified list + line_wrap_flush (&s, false); + return s.result; +} - FORMATTER_ADD_ITEM (self, FG_COLOR, .color = color); +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - char *x = irc_to_utf8 (nick); - free (nick); - FORMATTER_ADD_TEXT (self, x); - free (x); +struct exploder +{ + struct app_context *ctx; ///< Application context + struct line_char *result; ///< Result + struct line_char *result_tail; ///< Tail of result + struct line_char_attrs attrs; ///< Current attributes +}; - // Need to reset the colour afterwards - FORMATTER_ADD_ITEM (self, FG_COLOR, .color = -1); +static bool +explode_formatter_attr (struct exploder *self, struct formatter_item *item) +{ + switch (item->type) + { + case FORMATTER_ITEM_ATTR: + self->attrs.named = item->attribute; + self->attrs.text = 0; + self->attrs.fg = -1; + self->attrs.bg = -1; + return true; + case FORMATTER_ITEM_SIMPLE: + self->attrs.named = -1; + self->attrs.text ^= item->attribute; + return true; + case FORMATTER_ITEM_FG_COLOR: + self->attrs.named = -1; + self->attrs.fg = item->color; + return true; + case FORMATTER_ITEM_BG_COLOR: + self->attrs.named = -1; + self->attrs.bg = item->color; + return true; + default: + return false; + } } static void -formatter_parse_nick_full (struct formatter *self, const char *s) +explode_text (struct exploder *self, const char *text) { - formatter_parse_nick (self, s); + // Throw away any potentially harmful control characters first + struct str filtered = str_make (); + for (const char *p = text; *p; p++) + if (!strchr ("\a\b\x0e\x0f\x1b" /* BEL BS SO SI ESC */, *p)) + str_append_c (&filtered, *p); - const char *userhost; - if (!(userhost = irc_find_userhost (s))) - return; + size_t term_len = 0, processed = 0, len; + char *term = iconv_xstrdup (self->ctx->term_from_utf8, + filtered.str, filtered.len + 1, &term_len); + str_free (&filtered); - FORMATTER_ADD_TEXT (self, " ("); - FORMATTER_ADD_ITEM (self, ATTR, .attribute = ATTR_USERHOST); + mbstate_t ps; + memset (&ps, 0, sizeof ps); - char *x = irc_to_utf8 (userhost); - FORMATTER_ADD_TEXT (self, x); - free (x); + wchar_t wch; + while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps))) + { + hard_assert (len != (size_t) -2 && len != (size_t) -1); + hard_assert ((processed += len) <= term_len); - FORMATTER_ADD_RESET (self); - FORMATTER_ADD_TEXT (self, ")"); + struct line_char *c = line_char_new (wch); + c->attrs = self->attrs; + LIST_APPEND_WITH_TAIL (self->result, self->result_tail, c); + } + free (term); } -static const char * -formatter_parse_field (struct formatter *self, - const char *field, struct str *buf, va_list *ap) +static struct line_char * +formatter_to_chars (struct formatter *formatter) { - bool free_string = false; - char *s = NULL; - char *tmp = NULL; - int c; + struct exploder self = { .ctx = formatter->ctx }; + self.attrs.fg = self.attrs.bg = self.attrs.named = -1; -restart: - switch ((c = *field++)) + int attribute_ignore = 0; + for (size_t i = 0; i < formatter->items_len; i++) { - // We can push boring text content to the caller's buffer - // and let it flush the buffer only when it's actually needed - case 'd': - tmp = xstrdup_printf ("%d", va_arg (*ap, int)); - str_append (buf, tmp); - free (tmp); - break; - case 's': - str_append (buf, (s = va_arg (*ap, char *))); - break; - case 'l': - if (!(tmp = iconv_xstrdup (self->ctx->term_to_utf8, - (s = va_arg (*ap, char *)), -1, NULL))) - print_error ("character conversion failed for: %s", "output"); - else - str_append (buf, tmp); - free (tmp); - break; - - case 'S': - tmp = irc_to_utf8 ((s = va_arg (*ap, char *))); - str_append (buf, tmp); - free (tmp); - break; - case 'm': - tmp = irc_to_utf8 ((s = va_arg (*ap, char *))); - formatter_parse_message (self, tmp); - free (tmp); - break; - case 'n': - formatter_parse_nick (self, (s = va_arg (*ap, char *))); - break; - case 'N': - formatter_parse_nick_full (self, (s = va_arg (*ap, char *))); - break; - - case 'a': - FORMATTER_ADD_ITEM (self, ATTR, .attribute = va_arg (*ap, int)); - break; - case 'c': - FORMATTER_ADD_ITEM (self, FG_COLOR, .color = va_arg (*ap, int)); - break; - case 'C': - FORMATTER_ADD_ITEM (self, BG_COLOR, .color = va_arg (*ap, int)); - break; - case 'r': - FORMATTER_ADD_RESET (self); - break; - - default: - if (c == '&' && !free_string) - free_string = true; - else if (c) - hard_assert (!"unexpected format specifier"); - else - hard_assert (!"unexpected end of format string"); - goto restart; + struct formatter_item *iter = &formatter->items[i]; + if (iter->type == FORMATTER_ITEM_TEXT) + explode_text (&self, iter->text); + else if (iter->type == FORMATTER_ITEM_IGNORE_ATTR) + attribute_ignore += iter->attribute; + else if (attribute_ignore <= 0 + && !explode_formatter_attr (&self, iter)) + hard_assert (!"unhandled formatter item type"); } + return self.result; +} - if (free_string) - free (s); - return field; +enum +{ + FLUSH_OPT_RAW = (1 << 0), ///< Print raw attributes + FLUSH_OPT_NOWRAP = (1 << 1) ///< Do not wrap +}; + +/// The input is a bunch of wide characters--respect shift state encodings +static void +formatter_putc (struct line_char *c, FILE *stream) +{ + static mbstate_t mb; + char buf[MB_LEN_MAX] = {}; + size_t len = wcrtomb (buf, c ? c->wide : L'\0', &mb); + if (len != (size_t) -1 && len) + fwrite (buf, len - !c, 1, stream); + free (c); } -// I was unable to take a pointer of a bare "va_list" when it was passed in -// as a function argument, so it has to be a pointer from the beginning static void -formatter_addv (struct formatter *self, const char *format, va_list *ap) +formatter_flush (struct formatter *self, FILE *stream, int flush_opts) { - struct str buf = str_make (); - while (*format) + struct line_char *line = formatter_to_chars (self); + + bool is_tty = !!get_attribute_printer (stream); + if (!is_tty && !(flush_opts & FLUSH_OPT_RAW)) { - if (*format != '#' || *++format == '#') - { - str_append_c (&buf, *format++); - continue; - } - if (buf.len) + LIST_FOR_EACH (struct line_char, c, line) + formatter_putc (c, stream); + formatter_putc (NULL, stream); + return; + } + + if (self->ctx->word_wrapping && !(flush_opts & FLUSH_OPT_NOWRAP)) + line = line_wrap (line, g_terminal.columns); + + struct attr_printer state = ATTR_PRINTER_INIT (self->ctx->theme, stream); + struct line_char_attrs attrs = {}; // Won't compare equal to anything + LIST_FOR_EACH (struct line_char, c, line) + { + if (attrs.fg != c->attrs.fg + || attrs.bg != c->attrs.bg + || attrs.named != c->attrs.named + || attrs.text != c->attrs.text) { - FORMATTER_ADD_TEXT (self, buf.str); - str_reset (&buf); + formatter_putc (NULL, stream); + + attrs = c->attrs; + if (attrs.named == -1) + attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg); + else + attr_printer_apply_named (&state, attrs.named); } - format = formatter_parse_field (self, format, &buf, ap); + formatter_putc (c, stream); } + formatter_putc (NULL, stream); + attr_printer_reset (&state); +} - if (buf.len) - FORMATTER_ADD_TEXT (self, buf.str); +// --- Relay output ------------------------------------------------------------ - str_free (&buf); +static void +client_kill (struct client *c) +{ + struct app_context *ctx = c->ctx; + poller_fd_reset (&c->socket_event); + xclose (c->socket_fd); + c->socket_fd = -1; + + LIST_UNLINK (ctx->clients, c); + client_destroy (c); } static void -formatter_add (struct formatter *self, const char *format, ...) +client_update_poller (struct client *c, const struct pollfd *pfd) { - va_list ap; - va_start (ap, format); - formatter_addv (self, format, &ap); - va_end (ap); + int new_events = POLLIN; + if (c->write_buffer.len) + new_events |= POLLOUT; + + hard_assert (new_events != 0); + if (!pfd || pfd->events != new_events) + poller_fd_set (&c->socket_event, new_events); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -struct line_char_attrs +static void +relay_send (struct client *c) { - short named; ///< Named attribute or -1 - short text; ///< Text attributes - int fg; ///< Foreground colour (-1 for default) - int bg; ///< Background colour (-1 for default) -}; + struct relay_event_message *m = &c->ctx->relay_message; + m->event_seq = c->event_seq++; -// We can get rid of the linked list and do this in one allocation (use strlen() -// for the upper bound)--since we only prepend and/or replace characters, add -// a member to specify the prepended character and how many times to repeat it. -// Tabs may nullify the wide character but it's not necessary. -// -// This would be slighly more optimal but it would also set the algorithm in -// stone and complicate flushing. + // TODO: Also don't try sending anything if half-closed. + if (!c->initialized || c->socket_fd == -1) + return; -struct line_char -{ - LIST_HEADER (struct line_char) + // liberty has msg_{reader,writer} already, but they use 8-byte lengths. + size_t frame_len_pos = c->write_buffer.len, frame_len = 0; + str_pack_u32 (&c->write_buffer, 0); + if (!relay_event_message_serialize (m, &c->write_buffer) + || (frame_len = c->write_buffer.len - frame_len_pos - 4) > UINT32_MAX) + { + print_error ("serialization failed, killing client"); + client_kill (c); + return; + } - wchar_t wide; ///< The character as a wchar_t - int width; ///< Width of the character in cells - struct line_char_attrs attrs; ///< Attributes -}; + uint32_t len = htonl (frame_len); + memcpy (c->write_buffer.str + frame_len_pos, &len, sizeof len); + client_update_poller (c, NULL); +} -static struct line_char * -line_char_new (wchar_t wc) +static void +relay_broadcast_except (struct app_context *ctx, struct client *exception) { - struct line_char *self = xcalloc (1, sizeof *self); - self->width = wcwidth ((self->wide = wc)); - - // Typically various control characters - if (self->width < 0) - self->width = 0; - - self->attrs.bg = self->attrs.fg = -1; - self->attrs.named = ATTR_RESET; - return self; + LIST_FOR_EACH (struct client, c, ctx->clients) + if (c != exception) + relay_send (c); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +#define relay_broadcast(ctx) relay_broadcast_except ((ctx), NULL) -struct line_wrap_mark +static struct relay_event_message * +relay_prepare (struct app_context *ctx) { - struct line_char *start; ///< First character - int used; ///< Display cells used -}; + struct relay_event_message *m = &ctx->relay_message; + relay_event_message_free (m); + memset (m, 0, sizeof *m); + return m; +} static void -line_wrap_mark_push (struct line_wrap_mark *mark, struct line_char *c) +relay_prepare_ping (struct app_context *ctx) { - if (!mark->start) - mark->start = c; - mark->used += c->width; + relay_prepare (ctx)->data.event = RELAY_EVENT_PING; } -struct line_wrap_state +static union relay_item_data * +relay_translate_formatter (struct app_context *ctx, union relay_item_data *p, + const struct formatter_item *i) { - struct line_char *result; ///< Head of result - struct line_char *result_tail; ///< Tail of result + // XXX: See attr_printer_decode_color(), this is a footgun. + int16_t c16 = i->color; + int16_t c256 = i->color >> 16; - int line_used; ///< Line length before marks - int line_max; ///< Maximum line length - struct line_wrap_mark chunk; ///< All buffered text - struct line_wrap_mark overflow; ///< Overflowing text -}; + unsigned attrs = i->attribute; + switch (i->type) + { + case FORMATTER_ITEM_TEXT: + p->text.text = str_from_cstr (i->text); + (p++)->kind = RELAY_ITEM_TEXT; + break; + case FORMATTER_ITEM_FG_COLOR: + p->fg_color.color = c256 <= 0 ? c16 : c256; + (p++)->kind = RELAY_ITEM_FG_COLOR; + break; + case FORMATTER_ITEM_BG_COLOR: + p->bg_color.color = c256 <= 0 ? c16 : c256; + (p++)->kind = RELAY_ITEM_BG_COLOR; + break; + case FORMATTER_ITEM_ATTR: + (p++)->kind = RELAY_ITEM_RESET; + if ((c256 = ctx->theme[i->attribute].fg) >= 0) + { + p->fg_color.color = c256; + (p++)->kind = RELAY_ITEM_FG_COLOR; + } + if ((c256 = ctx->theme[i->attribute].bg) >= 0) + { + p->bg_color.color = c256; + (p++)->kind = RELAY_ITEM_BG_COLOR; + } -static void -line_wrap_flush_split (struct line_wrap_state *s, struct line_wrap_mark *before) -{ - struct line_char *nl = line_char_new (L'\n'); - LIST_INSERT_WITH_TAIL (s->result, s->result_tail, nl, before->start); - s->line_used = before->used; + attrs = ctx->theme[i->attribute].attrs; + // Fall-through + case FORMATTER_ITEM_SIMPLE: + if (attrs & TEXT_BOLD) + (p++)->kind = RELAY_ITEM_FLIP_BOLD; + if (attrs & TEXT_ITALIC) + (p++)->kind = RELAY_ITEM_FLIP_ITALIC; + if (attrs & TEXT_UNDERLINE) + (p++)->kind = RELAY_ITEM_FLIP_UNDERLINE; + if (attrs & TEXT_INVERSE) + (p++)->kind = RELAY_ITEM_FLIP_INVERSE; + if (attrs & TEXT_CROSSED_OUT) + (p++)->kind = RELAY_ITEM_FLIP_CROSSED_OUT; + if (attrs & TEXT_MONOSPACE) + (p++)->kind = RELAY_ITEM_FLIP_MONOSPACE; + break; + default: + break; + } + return p; } -static void -line_wrap_flush (struct line_wrap_state *s, bool force_split) +static union relay_item_data * +relay_items (struct app_context *ctx, const struct formatter_item *items, + uint32_t *len) { - if (!s->overflow.start) - s->line_used += s->chunk.used; - else if (force_split || s->chunk.used > s->line_max) - { -#ifdef WRAP_UNNECESSARILY - // When the line wraps at the end of the screen and a background colour - // is set, the terminal paints the entire new line with that colour. - // Explicitly inserting a newline with the default attributes fixes it. - line_wrap_flush_split (s, &s->overflow); -#else - // Splitting here breaks link searching mechanisms in some terminals, - // though, so we make a trade-off and let the chunk wrap naturally. - // Fuck terminals, really. - s->line_used = s->overflow.used; -#endif - } - else - // Print the chunk in its entirety on a new line - line_wrap_flush_split (s, &s->chunk); + size_t items_len = 0; + for (size_t i = 0; items[i].type; i++) + items_len++; - memset (&s->chunk, 0, sizeof s->chunk); - memset (&s->overflow, 0, sizeof s->overflow); + // Beware of the upper bound, currently dominated by FORMATTER_ITEM_ATTR. + union relay_item_data *a = xcalloc (items_len * 9, sizeof *a), *p = a; + for (const struct formatter_item *i = items; items_len--; i++) + p = relay_translate_formatter (ctx, p, i); + + *len = p - a; + return a; } static void -line_wrap_nl (struct line_wrap_state *s) +relay_prepare_buffer_line (struct app_context *ctx, struct buffer *buffer, + struct buffer_line *line, bool leak_to_active) { - line_wrap_flush (s, true); - struct line_char *nl = line_char_new (L'\n'); - LIST_APPEND_WITH_TAIL (s->result, s->result_tail, nl); - s->line_used = 0; + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_buffer_line *e = &m->data.buffer_line; + e->event = RELAY_EVENT_BUFFER_LINE; + e->buffer_name = str_from_cstr (buffer->name); + e->is_unimportant = !!(line->flags & BUFFER_LINE_UNIMPORTANT); + e->is_highlight = !!(line->flags & BUFFER_LINE_HIGHLIGHT); + e->rendition = 1 + line->r; + e->when = line->when * 1000; + e->leak_to_active = leak_to_active; + e->items = relay_items (ctx, line->items, &e->items_len); } static void -line_wrap_tab (struct line_wrap_state *s, struct line_char *c) +relay_prepare_channel_buffer_update (struct app_context *ctx, + struct buffer *buffer, struct relay_buffer_context_channel *e) { - line_wrap_flush (s, true); - if (s->line_used >= s->line_max) - line_wrap_nl (s); + struct channel *channel = buffer->channel; + struct formatter f = formatter_make (ctx, buffer->server); + if (channel->topic) + formatter_add (&f, "#m", channel->topic); + FORMATTER_ADD_ITEM (&f, END); + e->topic = relay_items (ctx, f.items, &e->topic_len); + formatter_free (&f); - // Compute the number of characters needed to get to the next tab stop - int tab_width = ((s->line_used + 8) & ~7) - s->line_used; - // On overflow just fill the rest of the line with spaces - if (s->line_used + tab_width > s->line_max) - tab_width = s->line_max - s->line_used; + // As in make_prompt(), conceal the last known channel modes. + // XXX: This should use irc_channel_is_joined(). + if (!channel->users_len) + return; - s->line_used += tab_width; - while (tab_width--) + struct str modes = str_make (); + str_append_str (&modes, &channel->no_param_modes); + + struct str params = str_make (); + struct str_map_iter iter = str_map_iter_make (&channel->param_modes); + const char *param; + while ((param = str_map_iter_next (&iter))) { - struct line_char *space = line_char_new (L' '); - space->attrs = c->attrs; - LIST_APPEND_WITH_TAIL (s->result, s->result_tail, space); + str_append_c (&modes, iter.link->key[0]); + str_append_c (¶ms, ' '); + str_append (¶ms, param); } + + str_append_str (&modes, ¶ms); + str_free (¶ms); + + char *modes_utf8 = irc_to_utf8 (modes.str); + str_free (&modes); + e->modes = str_from_cstr (modes_utf8); + free (modes_utf8); } static void -line_wrap_push_char (struct line_wrap_state *s, struct line_char *c) +relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer) { - // Note that when processing whitespace here, any non-WS chunk has already - // been flushed, and thus it matters little if we flush with force split - if (wcschr (L"\r\f\v", c->wide)) - /* Skip problematic characters */; - else if (c->wide == L'\n') - line_wrap_nl (s); - else if (c->wide == L'\t') - line_wrap_tab (s, c); - else - goto use_as_is; - free (c); - return; + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_buffer_update *e = &m->data.buffer_update; + e->event = RELAY_EVENT_BUFFER_UPDATE; + e->buffer_name = str_from_cstr (buffer->name); + e->hide_unimportant = buffer->hide_unimportant; -use_as_is: - if (s->overflow.start - || s->line_used + s->chunk.used + c->width > s->line_max) + struct str *server_name = NULL; + switch (buffer->type) { - if (s->overflow.used + c->width > s->line_max) - { -#ifdef WRAP_UNNECESSARILY - // If the overflow overflows, restart on a new line - line_wrap_nl (s); -#else - // See line_wrap_flush(), we would end up on a new line anyway - line_wrap_flush (s, true); - s->line_used = 0; -#endif - } - else - line_wrap_mark_push (&s->overflow, c); + case BUFFER_GLOBAL: + e->context.kind = RELAY_BUFFER_KIND_GLOBAL; + break; + case BUFFER_SERVER: + e->context.kind = RELAY_BUFFER_KIND_SERVER; + server_name = &e->context.server.server_name; + break; + case BUFFER_CHANNEL: + e->context.kind = RELAY_BUFFER_KIND_CHANNEL; + server_name = &e->context.channel.server_name; + relay_prepare_channel_buffer_update (ctx, buffer, &e->context.channel); + break; + case BUFFER_PM: + e->context.kind = RELAY_BUFFER_KIND_PRIVATE_MESSAGE; + server_name = &e->context.private_message.server_name; + break; } - line_wrap_mark_push (&s->chunk, c); - LIST_APPEND_WITH_TAIL (s->result, s->result_tail, c); + if (server_name) + *server_name = str_from_cstr (buffer->server->name); } -/// Basic word wrapping that respects wcwidth(3) and expands tabs. -/// Besides making text easier to read, it also fixes the problem with -/// formatting spilling over the entire new line on line wrap. -static struct line_char * -line_wrap (struct line_char *line, int max_width) +static void +relay_prepare_buffer_stats (struct app_context *ctx, struct buffer *buffer) { - struct line_wrap_state s = { .line_max = max_width }; - bool last_was_word_char = false; - LIST_FOR_EACH (struct line_char, c, line) - { - // Act on the right boundary of (\s*\S+) chunks - bool this_is_word_char = !wcschr (L" \t\r\n\f\v", c->wide); - if (last_was_word_char && !this_is_word_char) - line_wrap_flush (&s, false); - last_was_word_char = this_is_word_char; - - LIST_UNLINK (line, c); - line_wrap_push_char (&s, c); - } - - // Make sure to process the last word and return the modified list - line_wrap_flush (&s, false); - return s.result; + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_buffer_stats *e = &m->data.buffer_stats; + e->event = RELAY_EVENT_BUFFER_STATS; + e->buffer_name = str_from_cstr (buffer->name); + e->new_messages = MIN (UINT32_MAX, + buffer->new_messages_count - buffer->new_unimportant_count); + e->new_unimportant_messages = MIN (UINT32_MAX, + buffer->new_unimportant_count); + e->highlighted = buffer->highlighted; } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -struct exploder +static void +relay_prepare_buffer_rename (struct app_context *ctx, struct buffer *buffer, + const char *new_name) { - struct app_context *ctx; ///< Application context - struct line_char *result; ///< Result - struct line_char *result_tail; ///< Tail of result - struct line_char_attrs attrs; ///< Current attributes -}; + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_buffer_rename *e = &m->data.buffer_rename; + e->event = RELAY_EVENT_BUFFER_RENAME; + e->buffer_name = str_from_cstr (buffer->name); + e->new = str_from_cstr (new_name); +} -static bool -explode_formatter_attr (struct exploder *self, struct formatter_item *item) +static void +relay_prepare_buffer_remove (struct app_context *ctx, struct buffer *buffer) { - switch (item->type) - { - case FORMATTER_ITEM_ATTR: - self->attrs.named = item->attribute; - self->attrs.text = 0; - self->attrs.fg = -1; - self->attrs.bg = -1; - return true; - case FORMATTER_ITEM_SIMPLE: - self->attrs.named = -1; - self->attrs.text ^= item->attribute; - return true; - case FORMATTER_ITEM_FG_COLOR: - self->attrs.named = -1; - self->attrs.fg = item->color; - return true; - case FORMATTER_ITEM_BG_COLOR: - self->attrs.named = -1; - self->attrs.bg = item->color; - return true; - default: - return false; - } + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_buffer_remove *e = &m->data.buffer_remove; + e->event = RELAY_EVENT_BUFFER_REMOVE; + e->buffer_name = str_from_cstr (buffer->name); } static void -explode_text (struct exploder *self, const char *text) +relay_prepare_buffer_activate (struct app_context *ctx, struct buffer *buffer) { - // Throw away any potentially harmful control characters first - struct str filtered = str_make (); - for (const char *p = text; *p; p++) - if (!strchr ("\a\b\x0e\x0f\x1b" /* BEL BS SO SI ESC */, *p)) - str_append_c (&filtered, *p); + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_buffer_activate *e = &m->data.buffer_activate; + e->event = RELAY_EVENT_BUFFER_ACTIVATE; + e->buffer_name = str_from_cstr (buffer->name); +} - size_t term_len = 0, processed = 0, len; - char *term = iconv_xstrdup (self->ctx->term_from_utf8, - filtered.str, filtered.len + 1, &term_len); - str_free (&filtered); +static void +relay_prepare_buffer_input (struct app_context *ctx, struct buffer *buffer, + const char *input) +{ + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_buffer_input *e = &m->data.buffer_input; + e->event = RELAY_EVENT_BUFFER_INPUT; + e->buffer_name = str_from_cstr (buffer->name); + e->text = str_from_cstr (input); +} - mbstate_t ps; - memset (&ps, 0, sizeof ps); +static void +relay_prepare_buffer_clear (struct app_context *ctx, + struct buffer *buffer) +{ + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_buffer_clear *e = &m->data.buffer_clear; + e->event = RELAY_EVENT_BUFFER_CLEAR; + e->buffer_name = str_from_cstr (buffer->name); +} - wchar_t wch; - while ((len = mbrtowc (&wch, term + processed, term_len - processed, &ps))) +enum relay_server_state +relay_server_state_for_server (struct server *s) +{ + switch (s->state) { - hard_assert (len != (size_t) -2 && len != (size_t) -1); - hard_assert ((processed += len) <= term_len); - - struct line_char *c = line_char_new (wch); - c->attrs = self->attrs; - LIST_APPEND_WITH_TAIL (self->result, self->result_tail, c); + case IRC_DISCONNECTED: return RELAY_SERVER_STATE_DISCONNECTED; + case IRC_CONNECTING: return RELAY_SERVER_STATE_CONNECTING; + case IRC_CONNECTED: return RELAY_SERVER_STATE_CONNECTED; + case IRC_REGISTERED: return RELAY_SERVER_STATE_REGISTERED; + case IRC_CLOSING: + case IRC_HALF_CLOSED: return RELAY_SERVER_STATE_DISCONNECTING; } - free (term); + return 0; } -static struct line_char * -formatter_to_chars (struct formatter *formatter) +static void +relay_prepare_server_update (struct app_context *ctx, struct server *s) { - struct exploder self = { .ctx = formatter->ctx }; - self.attrs.fg = self.attrs.bg = self.attrs.named = -1; - - int attribute_ignore = 0; - for (size_t i = 0; i < formatter->items_len; i++) + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_server_update *e = &m->data.server_update; + e->event = RELAY_EVENT_SERVER_UPDATE; + e->server_name = str_from_cstr (s->name); + e->data.state = relay_server_state_for_server (s); + if (s->state == IRC_REGISTERED) { - struct formatter_item *iter = &formatter->items[i]; - if (iter->type == FORMATTER_ITEM_TEXT) - explode_text (&self, iter->text); - else if (iter->type == FORMATTER_ITEM_IGNORE_ATTR) - attribute_ignore += iter->attribute; - else if (attribute_ignore <= 0 - && !explode_formatter_attr (&self, iter)) - hard_assert (!"unhandled formatter item type"); + char *user_utf8 = irc_to_utf8 (s->irc_user->nickname); + e->data.registered.user = str_from_cstr (user_utf8); + free (user_utf8); + + char *user_modes_utf8 = irc_to_utf8 (s->irc_user_modes.str); + e->data.registered.user_modes = str_from_cstr (user_modes_utf8); + free (user_modes_utf8); } - return self.result; } -enum +static void +relay_prepare_server_rename (struct app_context *ctx, struct server *s, + const char *new_name) { - FLUSH_OPT_RAW = (1 << 0), ///< Print raw attributes - FLUSH_OPT_NOWRAP = (1 << 1) ///< Do not wrap -}; + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_server_rename *e = &m->data.server_rename; + e->event = RELAY_EVENT_SERVER_RENAME; + e->server_name = str_from_cstr (s->name); + e->new = str_from_cstr (new_name); +} -/// The input is a bunch of wide characters--respect shift state encodings static void -formatter_putc (struct line_char *c, FILE *stream) +relay_prepare_server_remove (struct app_context *ctx, struct server *s) { - static mbstate_t mb; - char buf[MB_LEN_MAX] = {}; - size_t len = wcrtomb (buf, c ? c->wide : L'\0', &mb); - if (len != (size_t) -1 && len) - fwrite (buf, len - !c, 1, stream); - free (c); + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_server_remove *e = &m->data.server_remove; + e->event = RELAY_EVENT_SERVER_REMOVE; + e->server_name = str_from_cstr (s->name); } static void -formatter_flush (struct formatter *self, FILE *stream, int flush_opts) +relay_prepare_error (struct app_context *ctx, uint32_t seq, const char *message) { - struct line_char *line = formatter_to_chars (self); - - bool is_tty = !!get_attribute_printer (stream); - if (!is_tty && !(flush_opts & FLUSH_OPT_RAW)) - { - LIST_FOR_EACH (struct line_char, c, line) - formatter_putc (c, stream); - formatter_putc (NULL, stream); - return; - } - - if (self->ctx->word_wrapping && !(flush_opts & FLUSH_OPT_NOWRAP)) - line = line_wrap (line, g_terminal.columns); - - struct attr_printer state = ATTR_PRINTER_INIT (self->ctx->theme, stream); - struct line_char_attrs attrs = {}; // Won't compare equal to anything - LIST_FOR_EACH (struct line_char, c, line) - { - if (attrs.fg != c->attrs.fg - || attrs.bg != c->attrs.bg - || attrs.named != c->attrs.named - || attrs.text != c->attrs.text) - { - formatter_putc (NULL, stream); - - attrs = c->attrs; - if (attrs.named == -1) - attr_printer_apply (&state, attrs.text, attrs.fg, attrs.bg); - else - attr_printer_apply_named (&state, attrs.named); - } + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_error *e = &m->data.error; + e->event = RELAY_EVENT_ERROR; + e->command_seq = seq; + e->error = str_from_cstr (message); +} - formatter_putc (c, stream); - } - formatter_putc (NULL, stream); - attr_printer_reset (&state); +static struct relay_event_data_response * +relay_prepare_response (struct app_context *ctx, uint32_t seq) +{ + struct relay_event_message *m = relay_prepare (ctx); + struct relay_event_data_response *e = &m->data.response; + e->event = RELAY_EVENT_RESPONSE; + e->command_seq = seq; + return e; } // --- Buffers ----------------------------------------------------------------- -- cgit v1.2.3-70-g09d2