From e5f6ac40b7dd5c3b9b6d7fbc3e54d71126eda220 Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Mon, 12 Jun 2023 21:26:25 +0200 Subject: WIP: xF: show something through actually using X11 WIP: - Convert formatter items to an inner representation - Basic text layout with basic splitting - Conclude the commit as purely read-only but otherwise pretty --- xF.c | 1088 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 1046 insertions(+), 42 deletions(-) diff --git a/xF.c b/xF.c index 054871d..98dfbf8 100644 --- a/xF.c +++ b/xF.c @@ -1,7 +1,7 @@ /* * xF.c: a toothless IRC client frontend * - * Copyright (c) 2022, Přemysl Eric Janouch + * Copyright (c) 2022 - 2024, Přemysl Eric Janouch * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. @@ -22,21 +22,556 @@ #include "common.c" #include "xC-proto.c" +#define LIBERTY_XDG_WANT_X11 +#define LIBERTY_XDG_WANT_ICONS +#include "liberty/liberty-xdg.c" + #include #include #include #include #include +// --- Frontend ---------------------------------------------------------------- + +// See: struct relay_event_data_buffer_line +struct buffer_line +{ + LIST_HEADER (struct buffer_line) + + /// Leaked from another buffer, but temporarily staying in another one. + bool leaked; + + bool is_unimportant; + bool is_highlight; + enum relay_rendition rendition; + uint64_t when; + char text[]; +}; + +// See: struct relay_event_data_buffer_update +struct buffer +{ + LIST_HEADER (struct buffer) + + char *buffer_name; + enum relay_buffer_kind kind; + char *server_name; + struct buffer_line *lines; + struct buffer_line *lines_tail; + + // Stats: + + uint32_t new_messages; + uint32_t new_unimportant_messages; + bool highlighted; +}; + +static void +buffer_destroy (struct buffer *self) +{ + free (self->buffer_name); + free (self->server_name); + LIST_FOR_EACH (struct buffer_line, iter, self->lines) + free (iter); + free (self); +} + +// See: struct relay_event_data_server_update +struct server +{ + enum relay_server_state state; + char *user; + char *user_modes; +}; + +static void +server_destroy (struct server *self) +{ + free (self->user); + free (self->user_modes); + free (self); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// Wraps Xft fonts into a linked list with fallbacks. +struct x11_font_link +{ + struct x11_font_link *next; + XftFont *font; +}; + +enum +{ + X11_FONT_BOLD = 1 << 0, + X11_FONT_ITALIC = 1 << 1, + X11_FONT_MONOSPACE = 1 << 2, +}; + +struct x11_font +{ + struct x11_font *next; ///< Next in a linked list + + struct x11_font_link *list; ///< Fonts of varying Unicode coverage + unsigned style; ///< X11_FONT_* flags + FcPattern *pattern; ///< Original unsubstituted pattern + FcCharSet *unavailable; ///< Couldn't find a font for these +}; + static struct { - bool polling; + struct poller poller; ///< Poller + bool polling; ///< The event loop is running + + // Relay plumbing: + struct connector connector; - int socket; + int socket_fd; ///< Backend TCP socket + struct poller_fd socket_event; ///< The socket can be read/written to + struct str read_buffer; ///< Unprocessed input + struct str write_buffer; ///< Output yet to be sent out + + uint32_t command_seq; ///< Outgoing message counter + + // Relay state: + + struct buffer *buffers; ///< Ordered list of all buffers + struct buffer *buffers_tail; ///< The tail of all buffers + struct buffer *buffer_current; ///< The current buffer + struct buffer *buffer_last; ///< Last used buffer + + struct str_map servers; ///< All servers + + // User interface: + + int ui_width; ///< Window width + int ui_height; ///< Window height + bool ui_focused; ///< Whether the window has focus + + XIM x11_im; ///< Input method + XIC x11_ic; ///< Input method context + Display *dpy; ///< X display handle + struct poller_fd x11_event; ///< X11 events on wire + struct poller_idle xpending_event; ///< X11 events possibly in I/O queues + int xkb_base_event_code; ///< Xkb base event code + Window x11_window; ///< Application window + Pixmap x11_pixmap; ///< Off-screen bitmap + Region x11_clip; ///< Invalidated region + Picture x11_pixmap_picture; ///< XRender wrap for x11_pixmap + XftDraw *xft_draw; ///< Xft rendering context + struct x11_font *xft_fonts; ///< Font collection + char *x11_selection; ///< CLIPBOARD selection + + const char *x11_fontname; ///< Fontconfig font name + const char *x11_fontname_monospace; ///< Fontconfig monospace font name + + struct poller_idle refresh_event; ///< Refresh the window's contents + struct poller_idle flip_event; ///< Draw rendered widgets on screen +} +g = +{ + .x11_fontname = "sans\\-serif-11", + .x11_fontname_monospace = "monospace-11", +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +app_init_context (void) +{ + poller_init (&g.poller); + + g.socket_fd = -1; + g.read_buffer = str_make (); + g.write_buffer = str_make (); + + g.servers = str_map_make ((str_map_free_fn) server_destroy); + + // Presumably, although not necessarily; unsure if queryable at all + g.ui_focused = true; +} + +static void +app_quit (void) +{ + // So far there's nothing for us to wait on, so let's just stop looping. + g.polling = false; +} + +static void +app_invalidate (void) +{ + poller_idle_set (&g.refresh_event); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static struct buffer * +buffer_find (const char *name) +{ + LIST_FOR_EACH (struct buffer, buffer, g.buffers) + if (!strcmp (buffer->buffer_name, name)) + return buffer; + return NULL; +} + +static struct buffer_line * +buffer_line_new (struct relay_event_data_buffer_line *m) +{ + struct str s = str_make (); + for (uint32_t i = 0; i < m->items_len; i++) + if (m->items[i].kind == RELAY_ITEM_TEXT) + str_append_str (&s, &m->items[i].text.text); + + struct buffer_line *self = xcalloc (1, sizeof *self + s.len + 1); + memcpy (self->text, s.str, s.len + 1); + str_free (&s); + + self->is_unimportant = m->is_unimportant; + self->is_highlight = m->is_highlight; + self->rendition = m->rendition; + self->when = m->when; + return self; +} + +static void +relay_process_buffer_line (struct buffer *buffer, + struct relay_event_data_buffer_line *m) +{ + // Initial sync: skip all other processing, let highlights be. + if (!g.buffer_current) + { + struct buffer_line *line = buffer_line_new (m); + LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line); + return; + } + + // TODO: Track window on-screen visibility. + bool visible = buffer == g.buffer_current || m->leak_to_active; + + // TODO: Port the rest from xP.js. + struct buffer_line *line = buffer_line_new (m); + LIST_APPEND_WITH_TAIL (buffer->lines, buffer->lines_tail, line); + if (!(visible || m->leak_to_active) + || buffer->new_messages || buffer->new_unimportant_messages) + { + if (line->is_unimportant || m->leak_to_active) + buffer->new_unimportant_messages++; + else + buffer->new_messages++; + } + + if (m->leak_to_active) + { + struct buffer *bc = g.buffer_current; + struct buffer_line *line = buffer_line_new (m); + line->leaked = true; + LIST_APPEND_WITH_TAIL (bc->lines, bc->lines_tail, line); + if (!visible || bc->new_messages || bc->new_unimportant_messages) + { + if (line->is_unimportant) + bc->new_unimportant_messages++; + else + bc->new_messages++; + } + } + + if (line->is_highlight || (!visible && !line->is_unimportant + && buffer->kind == RELAY_BUFFER_KIND_PRIVATE_MESSAGE)) + { + // TODO: Play a beep sample. + if (!visible) + buffer->highlighted = true; + } +} + +static const char * +relay_message_buffer_name (const struct relay_event_message *m) +{ + switch (m->data.event) + { + case RELAY_EVENT_BUFFER_LINE: + return m->data.buffer_line.buffer_name.str; + case RELAY_EVENT_BUFFER_UPDATE: + return m->data.buffer_update.buffer_name.str; + case RELAY_EVENT_BUFFER_STATS: + return m->data.buffer_stats.buffer_name.str; + case RELAY_EVENT_BUFFER_RENAME: + return m->data.buffer_rename.buffer_name.str; + case RELAY_EVENT_BUFFER_REMOVE: + return m->data.buffer_remove.buffer_name.str; + case RELAY_EVENT_BUFFER_ACTIVATE: + return m->data.buffer_activate.buffer_name.str; + case RELAY_EVENT_BUFFER_INPUT: + return m->data.buffer_input.buffer_name.str; + case RELAY_EVENT_BUFFER_CLEAR: + return m->data.buffer_clear.buffer_name.str; + default: + return NULL; + } +} + +static const char * +relay_message_server_name (const struct relay_event_message *m) +{ + switch (m->data.event) + { + case RELAY_EVENT_SERVER_UPDATE: + return m->data.server_update.server_name.str; + case RELAY_EVENT_SERVER_RENAME: + return m->data.server_rename.server_name.str; + case RELAY_EVENT_SERVER_REMOVE: + return m->data.server_remove.server_name.str; + default: + return NULL; + } +} + +static const char * +relay_buffer_update_server_name (const struct relay_event_data_buffer_update *e) +{ + switch (e->context.kind) + { + case RELAY_BUFFER_KIND_SERVER: + return e->context.server.server_name.str; + case RELAY_BUFFER_KIND_CHANNEL: + return e->context.channel.server_name.str; + case RELAY_BUFFER_KIND_PRIVATE_MESSAGE: + return e->context.private_message.server_name.str; + default: + return NULL; + } +} + +static bool +relay_process_message (struct msg_unpacker *r, struct relay_event_message *m) +{ + if (!relay_event_message_deserialize (m, r) + || msg_unpacker_get_available (r)) + { + print_error ("deserialization failed"); + return false; + } + + const char *buffer_name = relay_message_buffer_name (m); + struct buffer *buffer = NULL; + if (buffer_name && !(buffer = buffer_find (buffer_name))) + { + // TODO: Maybe handle BUFFER_ACTIVATE the way xP does. + if (m->data.event != RELAY_EVENT_BUFFER_UPDATE) + { + print_warning ("unknown buffer: %s", buffer_name); + return true; + } + + buffer = xcalloc (1, sizeof *buffer); + buffer->buffer_name = xstrdup (buffer_name); + LIST_APPEND_WITH_TAIL (g.buffers, g.buffers_tail, buffer); + } + + const char *server_name = relay_message_server_name (m); + struct server *server = NULL; + if (server_name && !(server = str_map_find (&g.servers, server_name))) + { + if (m->data.event != RELAY_EVENT_SERVER_UPDATE) + { + print_warning ("unknown server: %s", server_name); + return true; + } + + server = xcalloc (1, sizeof *server); + str_map_set (&g.servers, server_name, server); + } + + switch (m->data.event) + { + case RELAY_EVENT_PING: + // TODO: While not important, we should implement it. + return true; + + case RELAY_EVENT_BUFFER_LINE: + relay_process_buffer_line (buffer, &m->data.buffer_line); + break; + case RELAY_EVENT_BUFFER_UPDATE: + buffer->kind = m->data.buffer_update.context.kind; + + server_name = relay_buffer_update_server_name (&m->data.buffer_update); + cstr_set (&buffer->server_name, NULL); + if (server_name) + buffer->server_name = xstrdup (server_name); + + // TODO: More kind-dependent state. + break; + case RELAY_EVENT_BUFFER_STATS: + buffer->new_messages + = m->data.buffer_stats.new_messages; + buffer->new_unimportant_messages + = m->data.buffer_stats.new_unimportant_messages; + buffer->highlighted + = m->data.buffer_stats.highlighted; + break; + case RELAY_EVENT_BUFFER_RENAME: + free (buffer->buffer_name); + buffer->buffer_name = xstrdup (m->data.buffer_rename.new.str); + break; + case RELAY_EVENT_BUFFER_REMOVE: + LIST_UNLINK_WITH_TAIL (g.buffers, g.buffers_tail, buffer); + if (g.buffer_current == buffer) + g.buffer_current = NULL; + if (g.buffer_last == buffer) + g.buffer_last = NULL; + buffer_destroy (buffer); + break; + case RELAY_EVENT_BUFFER_ACTIVATE: + if (g.buffer_current) + { + g.buffer_current->new_messages = 0; + g.buffer_current->new_unimportant_messages = 0; + g.buffer_current->highlighted = false; + } + + g.buffer_last = g.buffer_current; + g.buffer_current = buffer; + // TODO: Switch the input line. + break; + case RELAY_EVENT_BUFFER_CLEAR: + LIST_FOR_EACH (struct buffer_line, iter, buffer->lines) + free (iter); + buffer->lines = NULL; + break; + + case RELAY_EVENT_SERVER_UPDATE: + server->state = m->data.server_update.data.state; + + cstr_set (&server->user, NULL); + cstr_set (&server->user_modes, NULL); + if (server->state == RELAY_SERVER_STATE_REGISTERED) + { + server->user = + xstrdup (m->data.server_update.data.registered.user.str); + server->user_modes = + xstrdup (m->data.server_update.data.registered.user_modes.str); + } + break; + case RELAY_EVENT_SERVER_RENAME: + str_map_set (&g.servers, m->data.server_rename.new.str, + str_map_steal (&g.servers, server_name)); + break; + case RELAY_EVENT_SERVER_REMOVE: + str_map_set (&g.servers, server_name, NULL); + break; + + default: + return true; + } + app_invalidate (); + return true; +} + +static bool +relay_process_buffer (void) +{ + struct str *buf = &g.read_buffer; + size_t offset = 0; + while (true) + { + uint32_t frame_len = 0; + struct msg_unpacker r = + msg_unpacker_make (buf->str + offset, buf->len - offset); + if (!msg_unpacker_u32 (&r, &frame_len)) + break; + + r.len = MIN (r.len, sizeof frame_len + frame_len); + if (msg_unpacker_get_available (&r) < frame_len) + break; + + struct relay_event_message m = {}; + bool ok = relay_process_message (&r, &m); + relay_event_message_free (&m); + if (!ok) + return false; + + offset += r.offset; + } + + str_remove_slice (buf, 0, offset); + return true; +} + +static bool +relay_try_read (void) +{ + struct str *buf = &g.read_buffer; + ssize_t n_read; + + while ((n_read = read (g.socket_fd, buf->str + buf->len, + buf->alloc - buf->len - 1 /* null byte */)) > 0) + { + buf->len += n_read; + if (!relay_process_buffer ()) + break; + str_reserve (buf, 512); + } + + if (n_read < 0) + { + if (errno == EAGAIN || errno == EINTR) + return true; + + print_debug ("%s: %s: %s", __func__, "read", strerror (errno)); + } + return false; } -g; + +static bool +relay_try_write (void) +{ + struct str *buf = &g.write_buffer; + ssize_t n_written; + + while (buf->len) + { + n_written = write (g.socket_fd, buf->str, buf->len); + if (n_written >= 0) + { + str_remove_slice (buf, 0, n_written); + continue; + } + if (errno == EAGAIN || errno == EINTR) + return true; + + print_debug ("%s: %s: %s", __func__, "write", strerror (errno)); + return false; + } + return true; +} + +static void +relay_update_poller (const struct pollfd *pfd) +{ + int new_events = POLLIN; + if (g.write_buffer.len) + new_events |= POLLOUT; + + hard_assert (new_events != 0); + if (!pfd || pfd->events != new_events) + poller_fd_set (&g.socket_event, new_events); +} + +static void +on_relay_ready (const struct pollfd *pfd, void *user_data) +{ + if (relay_try_read () && relay_try_write ()) + relay_update_poller (pfd); + else + { + // TODO: Probably autoreconnect. + exit_fatal ("disconnected"); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void on_connector_connecting (void *user_data, const char *address) @@ -64,65 +599,526 @@ on_connector_connected (void *user_data, int socket, const char *hostname) { (void) user_data; (void) hostname; - g.polling = false; - g.socket = socket; + connector_free (&g.connector); + + set_blocking (socket, false); + set_cloexec (socket); + + // We already buffer our output, so reduce latencies. + int yes = 1; + soft_assert (setsockopt (socket, IPPROTO_TCP, TCP_NODELAY, + &yes, sizeof yes) != -1); + + g.socket_fd = socket; + g.socket_event = poller_fd_make (&g.poller, g.socket_fd); + g.socket_event.dispatcher = (poller_fd_fn) on_relay_ready; + + str_reset (&g.read_buffer); + str_reset (&g.write_buffer); + + struct str *s = &g.write_buffer; + str_pack_u32 (s, 0); + struct relay_command_message m = {}; + m.command_seq = ++g.command_seq; + m.data.hello.command = RELAY_COMMAND_HELLO; + m.data.hello.version = RELAY_VERSION; + if (!relay_command_message_serialize (&m, s)) + exit_fatal ("serialization failed"); + + uint32_t len = htonl (s->len - sizeof len); + memcpy (s->str, &len, sizeof len); + + relay_update_poller (NULL); } static void -protocol_test (const char *host, const char *port) +relay_connect (const char *host, const char *port) { - struct poller poller = {}; - poller_init (&poller); - - connector_init (&g.connector, &poller); + connector_init (&g.connector, &g.poller); g.connector.on_connecting = on_connector_connecting; g.connector.on_error = on_connector_error; g.connector.on_connected = on_connector_connected; g.connector.on_failure = on_connector_failure; connector_add_target (&g.connector, host, port); +} - g.polling = true; - while (g.polling) - poller_run (&poller); +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - connector_free (&g.connector); +static XRenderColor x11_default_fg = { .alpha = 0xffff }; +static XRenderColor x11_default_bg = { 0xffff, 0xffff, 0xffff, 0xffff }; +static XErrorHandler x11_default_error_handler; - struct str s = str_make (); - str_pack_u32 (&s, 0); - struct relay_command_message m = {}; - m.data.hello.command = RELAY_COMMAND_HELLO; - m.data.hello.version = RELAY_VERSION; - if (!relay_command_message_serialize (&m, &s)) - exit_fatal ("serialization failed"); +static struct x11_font_link * +x11_font_link_new (XftFont *font) +{ + struct x11_font_link *self = xcalloc (1, sizeof *self); + self->font = font; + return self; +} - uint32_t len = htonl (s.len - sizeof len); - memcpy (s.str, &len, sizeof len); - if (errno = 0, write (g.socket, s.str, s.len) != (ssize_t) s.len) - exit_fatal ("short send or error: %s", strerror (errno)); +static void +x11_font_link_destroy (struct x11_font_link *self) +{ + XftFontClose (g.dpy, self->font); + free (self); +} - char buf[1 << 20] = ""; - while (errno = 0, read (g.socket, &len, sizeof len) == sizeof len) +static struct x11_font_link * +x11_font_link_open (FcPattern *pattern) +{ + XftFont *font = XftFontOpenPattern (g.dpy, pattern); + if (!font) { - len = ntohl (len); - if (errno = 0, read (g.socket, buf, MIN (len, sizeof buf)) != len) - exit_fatal ("short read or error: %s", strerror (errno)); + FcPatternDestroy (pattern); + return NULL; + } + return x11_font_link_new (font); +} - struct msg_unpacker r = msg_unpacker_make (buf, len); - struct relay_event_message m = {}; - if (!relay_event_message_deserialize (&m, &r)) - exit_fatal ("deserialization failed"); - if (msg_unpacker_get_available (&r)) - exit_fatal ("trailing data"); +static struct x11_font * +x11_font_open (unsigned style) +{ + FcPattern *pattern = (style & X11_FONT_MONOSPACE) + ? FcNameParse ((const FcChar8 *) g.x11_fontname_monospace) + : FcNameParse ((const FcChar8 *) g.x11_fontname); + if (style & X11_FONT_BOLD) + FcPatternAdd (pattern, FC_STYLE, (FcValue) { + .type = FcTypeString, .u.s = (FcChar8 *) "Bold" }, FcFalse); + if (style & X11_FONT_ITALIC) + FcPatternAdd (pattern, FC_STYLE, (FcValue) { + .type = FcTypeString, .u.s = (FcChar8 *) "Italic" }, FcFalse); - printf ("event: %d\n", m.data.event); - relay_event_message_free (&m); + FcPattern *substituted = FcPatternDuplicate (pattern); + FcConfigSubstitute (NULL, substituted, FcMatchPattern); + + FcResult result = 0; + FcPattern *match = XftFontMatch (g.dpy, + DefaultScreen (g.dpy), substituted, &result); + FcPatternDestroy (substituted); + struct x11_font_link *link = NULL; + if (!match || !(link = x11_font_link_open (match))) + { + FcPatternDestroy (pattern); + return NULL; } - exit_fatal ("short read or error: %s", strerror (errno)); + + struct x11_font *self = xcalloc (1, sizeof *self); + self->list = link; + self->style = style; + self->pattern = pattern; + self->unavailable = FcCharSetCreate (); + return self; +} + +static void +x11_font_destroy (struct x11_font *self) +{ + FcPatternDestroy (self->pattern); + FcCharSetDestroy (self->unavailable); + LIST_FOR_EACH (struct x11_font_link, iter, self->list) + x11_font_link_destroy (iter); + free (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +static void +x11_init_pixmap (void) +{ + int screen = DefaultScreen (g.dpy); + g.x11_pixmap = XCreatePixmap (g.dpy, g.x11_window, + MAX (1, g.ui_width), MAX (1, g.ui_height), + DefaultDepth (g.dpy, screen)); + + Visual *visual = DefaultVisual (g.dpy, screen); + XRenderPictFormat *format = XRenderFindVisualFormat (g.dpy, visual); + g.x11_pixmap_picture + = XRenderCreatePicture (g.dpy, g.x11_pixmap, format, 0, NULL); +} + +static void +on_x11_selection_request (XSelectionRequestEvent *ev) +{ + Atom xa_targets = XInternAtom (g.dpy, "TARGETS", False); + Atom xa_compound_text = XInternAtom (g.dpy, "COMPOUND_TEXT", False); + Atom xa_utf8 = XInternAtom (g.dpy, "UTF8_STRING", False); + Atom targets[] = { xa_targets, XA_STRING, xa_compound_text, xa_utf8 }; + + XEvent response = {}; + bool ok = false; + Atom property = ev->property ? ev->property : ev->target; + if (!g.x11_selection) + goto out; + + XICCEncodingStyle style = 0; + if ((ok = ev->target == xa_targets)) + { + XChangeProperty (g.dpy, ev->requestor, property, + XA_ATOM, 32, PropModeReplace, + (const unsigned char *) targets, N_ELEMENTS (targets)); + goto out; + } + else if (ev->target == XA_STRING) + style = XStringStyle; + else if (ev->target == xa_compound_text) + style = XCompoundTextStyle; + else if (ev->target == xa_utf8) + style = XUTF8StringStyle; + else + goto out; + + // XXX: We let it crash us with BadLength, but we may, e.g., use INCR. + XTextProperty text = {}; + if ((ok = !Xutf8TextListToTextProperty + (g.dpy, &g.x11_selection, 1, style, &text))) + { + XChangeProperty (g.dpy, ev->requestor, property, + text.encoding, text.format, PropModeReplace, + text.value, text.nitems); + } + XFree (text.value); + +out: + response.xselection.type = SelectionNotify; + // XXX: We should check it against the event causing XSetSelectionOwner(). + response.xselection.time = ev->time; + response.xselection.requestor = ev->requestor; + response.xselection.selection = ev->selection; + response.xselection.target = ev->target; + response.xselection.property = ok ? property : None; + XSendEvent (g.dpy, ev->requestor, False, 0, &response); +} + +static bool +on_x11_input_event (XEvent *e) +{ + if (e->type != KeyPress) + return false; + + // A kibibyte long buffer will have to suffice for anyone. + XKeyEvent *ev = &e->xkey; + char buf[1 << 10] = {}, *p = buf; + KeySym keysym = None; + Status status = 0; + int len = Xutf8LookupString + (g.x11_ic, ev, buf, sizeof buf, &keysym, &status); + if (status == XBufferOverflow) + print_warning ("input method overflow"); + + // TODO: Implement an input line. + switch (keysym) + { + case XK_q: + app_quit (); + return true; + } + return false; +} + +static void +on_x11_event (XEvent *ev) +{ + switch (ev->type) + { + case Expose: + { + XRectangle r = { ev->xexpose.x, ev->xexpose.y, + ev->xexpose.width, ev->xexpose.height }; + XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip); + poller_idle_set (&g.flip_event); + break; + } + case ConfigureNotify: + if (g.ui_width == ev->xconfigure.width + && g.ui_height == ev->xconfigure.height) + break; + + g.ui_width = ev->xconfigure.width; + g.ui_height = ev->xconfigure.height; + + XRenderFreePicture (g.dpy, g.x11_pixmap_picture); + XFreePixmap (g.dpy, g.x11_pixmap); + x11_init_pixmap (); + XftDrawChange (g.xft_draw, g.x11_pixmap); + app_invalidate (); + break; + case SelectionRequest: + on_x11_selection_request (&ev->xselectionrequest); + break; + case SelectionClear: + cstr_set (&g.x11_selection, NULL); + break; + // UnmapNotify can be received when restarting the window manager. + // Should this turn out to be unreliable (window not destroyed by WM + // upon closing), opt for the WM_DELETE_WINDOW protocol as well. + case DestroyNotify: + app_quit (); + break; + case FocusIn: + g.ui_focused = true; + app_invalidate (); + break; + case FocusOut: + g.ui_focused = false; + app_invalidate (); + break; + case KeyPress: + case ButtonPress: + case ButtonRelease: + case MotionNotify: + if (!on_x11_input_event (ev)) + XkbBell (g.dpy, ev->xany.window, 0, None); + } +} + +static void +on_x11_pending (void *user_data) +{ + (void) user_data; + + XkbEvent ev; + while (XPending (g.dpy)) + { + if (XNextEvent (g.dpy, &ev.core)) + exit_fatal ("XNextEvent returned non-zero"); + if (XFilterEvent (&ev.core, None)) + continue; + + on_x11_event (&ev.core); + } + + poller_idle_reset (&g.xpending_event); +} + +static void +on_x11_ready (const struct pollfd *pfd, void *user_data) +{ + (void) pfd; + on_x11_pending (user_data); +} + +static int +on_x11_error (Display *dpy, XErrorEvent *event) +{ + // Without opting for WM_DELETE_WINDOW, this window can become destroyed + // and hence invalid at any time. We don't use the Window much, + // so we should be fine ignoring these errors. + if ((event->error_code == BadWindow && event->resourceid == g.x11_window) + || (event->error_code == BadDrawable && event->resourceid == g.x11_window)) + return app_quit (), 0; + + // XXX: The simplest possible way of discarding selection management errors. + // XCB would be a small win here, but it is a curse at the same time. + if (event->error_code == BadWindow && event->resourceid != g.x11_window) + return 0; + + return x11_default_error_handler (dpy, event); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +x11_init (void) +{ + // https://tedyin.com/posts/a-brief-intro-to-linux-input-method-framework/ + if (!XSupportsLocale ()) + print_warning ("locale not supported by Xlib"); + XSetLocaleModifiers (""); + + if (!(g.dpy = XkbOpenDisplay + (NULL, &g.xkb_base_event_code, NULL, NULL, NULL, NULL))) + exit_fatal ("cannot open display"); + if (!XftDefaultHasRender (g.dpy)) + exit_fatal ("XRender is not supported"); + if (!(g.x11_im = XOpenIM (g.dpy, NULL, NULL, NULL))) + exit_fatal ("failed to open an input method"); + + x11_default_error_handler = XSetErrorHandler (on_x11_error); + + set_cloexec (ConnectionNumber (g.dpy)); + g.x11_event = poller_fd_make (&g.poller, ConnectionNumber (g.dpy)); + g.x11_event.dispatcher = on_x11_ready; + poller_fd_set (&g.x11_event, POLLIN); + + // Whenever something causes Xlib to read its socket, it can make + // the I/O event above fail to trigger for whatever might have ended up + // in its queue. So always use this instead of XSync: + g.xpending_event = poller_idle_make (&g.poller); + g.xpending_event.dispatcher = on_x11_pending; + poller_idle_set (&g.xpending_event); + + struct xdg_xsettings settings = xdg_xsettings_make (); + xdg_xsettings_update (&settings, g.dpy); + + if (!FcInit ()) + print_warning ("Fontconfig initialization failed"); + if (!(g.xft_fonts = x11_font_open (0))) + exit_fatal ("cannot open a font"); + + int screen = DefaultScreen (g.dpy); + Colormap cmap = DefaultColormap (g.dpy, screen); + XColor default_bg = + { + .red = x11_default_bg.red, + .green = x11_default_bg.green, + .blue = x11_default_bg.blue, + }; + if (!XAllocColor (g.dpy, cmap, &default_bg)) + exit_fatal ("X11 setup failed"); + + XSetWindowAttributes attrs = + { + .event_mask = StructureNotifyMask | ExposureMask | FocusChangeMask + | KeyPressMask | ButtonPressMask | ButtonReleaseMask + | Button1MotionMask, + .bit_gravity = NorthWestGravity, + .background_pixel = default_bg.pixel, + }; + + // Base the window's size on the regular font size. + // Roughly trying to match the 80x24 default dimensions of terminals. + g.ui_height = 24 * g.xft_fonts->list->font->height; + g.ui_width = g.ui_height * 4 / 3; + + long im_event_mask = 0; + if (!XGetIMValues (g.x11_im, XNFilterEvents, &im_event_mask, NULL)) + attrs.event_mask |= im_event_mask; + + Visual *visual = DefaultVisual (g.dpy, screen); + g.x11_window = XCreateWindow (g.dpy, RootWindow (g.dpy, screen), 100, 100, + g.ui_width, g.ui_height, 0, CopyFromParent, InputOutput, visual, + CWEventMask | CWBackPixel | CWBitGravity, &attrs); + g.x11_clip = XCreateRegion (); + + XTextProperty prop = {}; + char *name = PROGRAM_NAME; + if (!Xutf8TextListToTextProperty (g.dpy, &name, 1, XUTF8StringStyle, &prop)) + XSetWMName (g.dpy, g.x11_window, &prop); + XFree (prop.value); + + // This is a rather GNOME-centric mechanism, but it's better than nothing. + const char *icon_theme_name = NULL; + const struct xdg_xsettings_setting *setting = + str_map_find (&settings.settings, "Net/IconThemeName"); + if (setting != NULL && setting->type == XDG_XSETTINGS_STRING) + icon_theme_name = setting->string.str; + icon_theme_set_window_icon (g.dpy, g.x11_window, icon_theme_name, name); + xdg_xsettings_free (&settings); + + // TODO: It is possible to do, e.g., on-the-spot. + XIMStyle im_style = XIMPreeditNothing | XIMStatusNothing; + XIMStyles *im_styles = NULL; + bool im_style_found = false; + if (!XGetIMValues (g.x11_im, XNQueryInputStyle, &im_styles, NULL) + && im_styles) + { + for (unsigned i = 0; i < im_styles->count_styles; i++) + im_style_found |= im_styles->supported_styles[i] == im_style; + XFree (im_styles); + } + if (!im_style_found) + print_warning ("failed to find the desired input method style"); + if (!(g.x11_ic = XCreateIC (g.x11_im, + XNInputStyle, im_style, + XNClientWindow, g.x11_window, + NULL))) + exit_fatal ("failed to open an input context"); + + XSetICFocus (g.x11_ic); + + x11_init_pixmap (); + g.xft_draw = XftDrawCreate (g.dpy, g.x11_pixmap, visual, cmap); + + XMapWindow (g.dpy, g.x11_window); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +render_line (int *bottom, const char *text) +{ + XftFont *font = g.xft_fonts->list->font; + XftColor color = { .color = x11_default_fg }; + XftDrawStringUtf8 (g.xft_draw, &color, font, 0, *bottom - font->descent, + (const FcChar8 *) text, strlen (text)); + return (*bottom -= font->height) > 0; +} + +static void +x11_render (void) +{ + XRenderFillRectangle (g.dpy, PictOpSrc, g.x11_pixmap_picture, + &x11_default_bg, 0, 0, g.ui_width, g.ui_height); + + int bottom = g.ui_height; + render_line (&bottom, "xF"); + if (!g.buffer_current) + render_line (&bottom, "-"); + else + { + struct buffer *buffer = g.buffer_current; + render_line (&bottom, buffer->buffer_name); + struct buffer_line *line = buffer->lines_tail; + for (; line; line = line->prev) + if (!render_line (&bottom, line->text)) + break; + } + + XRectangle r = { 0, 0, g.ui_width, g.ui_height }; + XUnionRectWithRegion (&r, g.x11_clip, g.x11_clip); + poller_idle_set (&g.xpending_event); +} + +static void +x11_flip (void) +{ + // This exercise in futility doesn't seem to affect CPU usage much. + XRectangle r = {}; + XClipBox (g.x11_clip, &r); + XCopyArea (g.dpy, g.x11_pixmap, g.x11_window, + DefaultGC (g.dpy, DefaultScreen (g.dpy)), + r.x, r.y, r.width, r.height, r.x, r.y); + + XSubtractRegion (g.x11_clip, g.x11_clip, g.x11_clip); + poller_idle_set (&g.xpending_event); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +app_on_flip (void *user_data) +{ + (void) user_data; + poller_idle_reset (&g.flip_event); + + // Waste of time, and may cause X11 to render uninitialised pixmaps. + if (g.polling && !g.refresh_event.active) + x11_flip (); +} + +static void +app_on_refresh (void *user_data) +{ + (void) user_data; + poller_idle_reset (&g.refresh_event); + + x11_render (); + poller_idle_set (&g.flip_event); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +app_init_poller_events (void) +{ + g.refresh_event = poller_idle_make (&g.poller); + g.refresh_event.dispatcher = app_on_refresh; + + g.flip_event = poller_idle_make (&g.poller); + g.flip_event.dispatcher = app_on_flip; +} + int main (int argc, char *argv[]) { @@ -166,7 +1162,15 @@ main (int argc, char *argv[]) if (!port) exit_fatal ("missing port number/service name"); - // TODO: Actually implement an X11-based user interface. - protocol_test (host, port); + app_init_context (); + app_init_poller_events (); + x11_init (); + + relay_connect (host, port); + free (address); + + g.polling = true; + while (g.polling) + poller_run (&g.poller); return 0; } -- cgit v1.2.3-70-g09d2