diff options
| -rw-r--r-- | xF.c | 1088 | 
1 files changed, 1046 insertions, 42 deletions
| @@ -1,7 +1,7 @@  /*   * xF.c: a toothless IRC client frontend   * - * Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name> + * Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>   *   * 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 <X11/Xatom.h>  #include <X11/Xlib.h>  #include <X11/keysym.h>  #include <X11/XKBlib.h>  #include <X11/Xft/Xft.h> +// --- 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;  } | 
