diff --git a/degesch.c b/degesch.c index 02af1d9..7b9d141 100644 --- a/degesch.c +++ b/degesch.c @@ -44,11 +44,18 @@ #include #include + +// Literally cancer +#undef lines +#undef columns + #include #include // --- Configuration (application-specific) ------------------------------------ +// TODO: reject all junk present in the configuration; there can be newlines + static struct config_item g_config_table[] = { { ATTR_PROMPT, NULL, "Terminal attributes for the prompt" }, @@ -81,6 +88,9 @@ static struct config_item g_config_table[] = // --- Application data -------------------------------------------------------- +// All text stored in our data structures is encoded in UTF-8. +// Or at least should be. + /// Shorthand to set an error and return failure from the function #define FAIL(...) \ BLOCK_START \ @@ -97,23 +107,47 @@ enum buffer_line_flags enum buffer_line_type { - BUFFER_LINE_TEXT, ///< PRIVMSG + BUFFER_LINE_PRIVMSG, ///< PRIVMSG + BUFFER_LINE_ACTION, ///< PRIVMSG ACTION BUFFER_LINE_NOTICE, ///< NOTICE - BUFFER_LINE_STATUS ///< JOIN, PART, QUIT + BUFFER_LINE_JOIN, ///< JOIN + BUFFER_LINE_PART, ///< PART + BUFFER_LINE_KICK, ///< KICK + BUFFER_LINE_QUIT, ///< QUIT + BUFFER_LINE_STATUS ///< Whatever status messages }; struct buffer_line { LIST_HEADER (struct buffer_line) + // We use the "type" and "flags" mostly just as formatting hints + enum buffer_line_type type; ///< Type of the event int flags; ///< Flags time_t when; ///< Time of the event - char *origin; ///< Name of the origin - char *text; ///< The text of the message + char *who; ///< Name of the origin or NULL (user) + char *object; ///< Text of message, object of action + char *reason; ///< Reason for PART, KICK, QUIT }; +struct buffer_line * +buffer_line_new (void) +{ + struct buffer_line *self = xcalloc (1, sizeof *self); + return self; +} + +static void +buffer_line_destroy (struct buffer_line *self) +{ + free (self->who); + free (self->object); + free (self->reason); + free (self); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - struct nick_info @@ -143,6 +177,9 @@ enum buffer_type BUFFER_PM ///< Private messages (query) }; +// TODO: now I can't just print messages with print_status() etc., +// all that stuff has to go in a buffer now + struct buffer { LIST_HEADER (struct buffer) @@ -150,12 +187,15 @@ struct buffer enum buffer_type type; ///< Type of the buffer char *name; ///< The name of the buffer - unsigned unseen_messages; ///< # messages since last visited + // Buffer contents: - // TODO: now I can't just print messages with print_status() etc., - // all that stuff has to go into a buffer now + struct buffer_line *lines; ///< All lines in this buffer + struct buffer_line *lines_tail; ///< The tail of buffer lines + unsigned lines_count; ///< How many lines we have - // Channels: + unsigned unseen_messages_count; ///< # messages since last visited + + // Channel information: char *topic; ///< Channel topic struct str_map nicks; ///< Maps nicks to "nick_info" @@ -191,29 +231,41 @@ enum color_mode struct app_context { + // Configuration: + struct str_map config; ///< User configuration enum color_mode color_mode; ///< Colour output mode bool reconnect; ///< Whether to reconnect on conn. fail. unsigned long reconnect_delay; ///< Reconnect delay in seconds + // Server connection: + int irc_fd; ///< Socket FD of the server struct str read_buffer; ///< Input yet to be processed struct poller_fd irc_event; ///< IRC FD event bool irc_ready; ///< Whether we may send messages now + SSL_CTX *ssl_ctx; ///< SSL context + SSL *ssl; ///< SSL connection + + // Events: + struct poller_fd tty_event; ///< Terminal input event struct poller_fd signal_event; ///< Signal FD event struct poller_timer ping_tmr; ///< We should send a ping struct poller_timer timeout_tmr; ///< Connection seems to be dead struct poller_timer reconnect_tmr; ///< We should reconnect now - SSL_CTX *ssl_ctx; ///< SSL context - SSL *ssl; ///< SSL connection + struct poller poller; ///< Manages polled descriptors + bool quitting; ///< User requested quitting + bool polling; ///< The event loop is running + + // Buffers: struct buffer *buffers; ///< All our buffers in order struct buffer *buffers_tail; ///< The tail of our buffers - // TODO: a name -> buffer map that excludes GLOBAL and SERVER + struct str_map buffers_by_name; ///< Excludes GLOBAL and SERVER struct buffer *global_buffer; ///< The global buffer struct buffer *server_buffer; ///< The server buffer @@ -222,14 +274,16 @@ struct app_context // TODO: So that we always output proper date change messages time_t last_displayed_msg_time; ///< Time of last displayed message - struct poller poller; ///< Manages polled descriptors - bool quitting; ///< User requested quitting - bool polling; ///< The event loop is running + // Terminal: iconv_t term_to_utf8; ///< Terminal encoding to UTF-8 iconv_t term_from_utf8; ///< UTF-8 to terminal encoding + // XXX: shouldn't it be rather UTF-8 from Latin 1? iconv_t term_from_latin1; ///< ISO Latin 1 to terminal encoding + int lines; ///< Current terminal height + int columns; ///< Current ternimal width + char *readline_prompt; ///< The prompt we use for readline bool readline_prompt_shown; ///< Whether the prompt is shown now } @@ -252,6 +306,9 @@ app_context_init (struct app_context *self) str_init (&self->read_buffer); self->irc_ready = false; + str_map_init (&self->buffers_by_name); + self->buffers_by_name.key_xfrm = irc_strxfrm; + self->last_displayed_msg_time = time (NULL); poller_init (&self->poller); @@ -291,12 +348,17 @@ app_context_free (struct app_context *self) if (self->ssl_ctx) SSL_CTX_free (self->ssl_ctx); + LIST_FOR_EACH (struct buffer, iter, self->buffers) + buffer_destroy (iter); + str_map_free (&self->buffers_by_name); + poller_free (&self->poller); - free (self->readline_prompt); iconv_close (self->term_from_latin1); iconv_close (self->term_from_utf8); iconv_close (self->term_to_utf8); + + free (self->readline_prompt); } // --- Attributed output ------------------------------------------------------- @@ -566,14 +628,192 @@ setup_signal_handlers (void) // --- Buffers ----------------------------------------------------------------- -static void -send_to_buffer (struct app_context *ctx, struct buffer *buffer, - enum buffer_line_type type, int flags, - const char *origin, const char *format, ...); +static void buffer_send (struct app_context *ctx, struct buffer *buffer, + enum buffer_line_type type, int flags, const char *origin, + const char *reason, const char *format, ...) ATTRIBUTE_PRINTF (7, 8); static void -prepare_buffers (struct app_context *ctx) +buffer_update_time (struct app_context *ctx, time_t now) { + struct tm last, current; + if (!localtime_r (&ctx->last_displayed_msg_time, &last) + || !localtime_r (&now, ¤t)) + { + // Strange but nonfatal + print_error ("%s: %s", "localtime_r", strerror (errno)); + return; + } + + ctx->last_displayed_msg_time = now; + if (last.tm_year == current.tm_year + && last.tm_mon == current.tm_mon + && last.tm_mday == current.tm_mday) + return; + + char buf[32] = ""; + if (soft_assert (strftime (buf, sizeof buf, "%F", ¤t))) + print_status ("%s", buf); + // Else the buffer was too small, which is pretty weird +} + +static void +buffer_line_display (struct app_context *ctx, struct buffer_line *line) +{ + // 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 text; + str_init (&text); + + struct tm current; + if (!localtime_r (&line->when, ¤t)) + print_error ("%s: %s", "localtime_r", strerror (errno)); + else + str_append_printf (&text, "%02d:%02d:%02d ", + current.tm_hour, current.tm_min, current.tm_sec); + + char *who = iconv_xstrdup (ctx->term_from_utf8, line->who, -1, NULL); + char *object = iconv_xstrdup (ctx->term_from_utf8, line->object, -1, NULL); + char *reason = iconv_xstrdup (ctx->term_from_utf8, line->reason, -1, NULL); + + switch (line->type) + { + case BUFFER_LINE_PRIVMSG: + str_append_printf (&text, "<%s> %s", who, object); + break; + case BUFFER_LINE_ACTION: + str_append_printf (&text, " * %s %s", who, object); + break; + case BUFFER_LINE_NOTICE: + str_append_printf (&text, " - Notice(%s): %s", who, object); + break; + case BUFFER_LINE_JOIN: + if (who) + str_append_printf (&text, "--> %s has joined %s", who, object); + else + str_append_printf (&text, "--> You have joined %s", object); + break; + case BUFFER_LINE_PART: + if (who) + str_append_printf (&text, "<-- %s has left %s (%s)", + who, object, reason); + else + str_append_printf (&text, "<-- You have left %s (%s)", + object, reason); + break; + case BUFFER_LINE_KICK: + if (who) + str_append_printf (&text, "<-- %s has kicked %s (%s)", + who, object, reason); + else + str_append_printf (&text, "<-- You have kicked %s (%s)", + object, reason); + break; + case BUFFER_LINE_QUIT: + if (who) + str_append_printf (&text, "<-- %s has quit (%s)", who, reason); + else + str_append_printf (&text, "<-- You have quit (%s)", reason); + break; + case BUFFER_LINE_STATUS: + str_append_printf (&text, " - %s", object); + } + + free (who); + free (object); + free (reason); + + // TODO: hide readline if needed + + printf ("%s\n", text.str); + str_free (&text); + + // TODO: unhide readline if hidden +} + +static void +buffer_send (struct app_context *ctx, struct buffer *buffer, + enum buffer_line_type type, int flags, + const char *origin, const char *reason, const char *format, ...) +{ + va_list ap; + va_start (ap, format); + struct str text; + str_init (&text); + str_append_vprintf (&text, format, ap); + va_end (ap); + + struct buffer_line *line = buffer_line_new (); + line->type = type; + line->flags = flags; + line->when = time (NULL); + line->who = xstrdup (origin); + line->object = str_steal (&text); + line->reason = xstrdup (reason); + + LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line); + buffer->lines_count++; + + if (buffer == ctx->current_buffer) + buffer_line_display (ctx, line); +} + +static struct buffer * +buffer_by_name (struct app_context *ctx, const char *name) +{ + return str_map_find (&ctx->buffers_by_name, name); +} + +static void +buffer_add (struct app_context *ctx, struct buffer *buffer) +{ + hard_assert (!buffer_by_name (ctx, buffer->name)); + + str_map_set (&ctx->buffers_by_name, buffer->name, buffer); + LIST_APPEND_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); + + // TODO: refresh the prompt? +} + +static void +buffer_remove (struct app_context *ctx, struct buffer *buffer) +{ + hard_assert (buffer != ctx->current_buffer); + + str_map_set (&ctx->buffers_by_name, buffer->name, NULL); + LIST_UNLINK_WITH_TAIL (ctx->buffers, ctx->buffers_tail, buffer); + buffer_destroy (buffer); + + // It's not a good idea to remove these buffers, but it's even a worse + // one to leave the pointers point to invalid memory + if (buffer == ctx->global_buffer) + ctx->global_buffer = NULL; + if (buffer == ctx->server_buffer) + ctx->server_buffer = NULL; + + // TODO: refresh the prompt? +} + +static void +buffer_activate (struct app_context *ctx, struct buffer *buffer) +{ + ctx->current_buffer = buffer; + print_status ("%s", buffer->name); + + // That is, minus the buffer switch line and the readline prompt + int to_display = ctx->lines - 2; + // TODO: find the to_display-th line from the back + // TODO: print all the lines in order + + // TODO: switch readline history stack + // TODO: refresh the prompt? +} + +static void +init_buffers (struct app_context *ctx) +{ + // At the moment we have only two global everpresent buffers struct buffer *global = ctx->global_buffer = buffer_new (); struct buffer *server = ctx->server_buffer = buffer_new (); @@ -1348,10 +1588,13 @@ on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx) } if (g_winch_received) + { // This fucks up big time on terminals with automatic wrapping such as // rxvt-unicode or newer VTE when the current line overflows, however we // can't do much about that rl_resize_terminal (); + rl_get_screen_size (&ctx->lines, &ctx->columns); + } } static void @@ -1657,9 +1900,14 @@ main (int argc, char *argv[]) using_history (); stifle_history (HISTORY_LIMIT); + setup_signal_handlers (); + init_colors (&ctx); init_poller_events (&ctx); + init_buffers (&ctx); + refresh_prompt (&ctx); + // TODO: connect asynchronously (first step towards multiple servers) struct error *e = NULL; if (!load_config (&ctx, &e) || !irc_connect (&ctx, &e)) @@ -1669,10 +1917,6 @@ main (int argc, char *argv[]) exit (EXIT_FAILURE); } - setup_signal_handlers (); - prepare_buffers (&ctx); - refresh_prompt (&ctx); - // TODO: maybe use rl_make_bare_keymap() and start from there // XXX: Since readline() installs a set of default key bindings the first @@ -1690,8 +1934,9 @@ main (int argc, char *argv[]) rl_bind_keyseq ("M-n", rl_named_function ("next-history")); rl_catch_sigwinch = false; - ctx.readline_prompt_shown = true; rl_callback_handler_install (ctx.readline_prompt, on_readline_input); + rl_get_screen_size (&ctx.lines, &ctx.columns); + ctx.readline_prompt_shown = true; ctx.polling = true; while (ctx.polling)