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