/*
* xF.c: a toothless IRC client frontend
*
* 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.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include "config.h"
#define PROGRAM_NAME "xF"
#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
{
struct poller poller; ///< Poller
bool polling; ///< The event loop is running
// Relay plumbing:
struct connector connector;
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;
}
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)
{
(void) user_data;
print_status ("connecting to %s...", address);
}
static void
on_connector_error (void *user_data, const char *error)
{
(void) user_data;
print_status ("connection failed: %s", error);
}
static void
on_connector_failure (void *user_data)
{
(void) user_data;
exit_fatal ("giving up");
}
static void
on_connector_connected (void *user_data, int socket, const char *hostname)
{
(void) user_data;
(void) hostname;
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
relay_connect (const char *host, const char *port)
{
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);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static XRenderColor x11_default_fg = { .alpha = 0xffff };
static XRenderColor x11_default_bg = { 0xffff, 0xffff, 0xffff, 0xffff };
static XErrorHandler x11_default_error_handler;
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;
}
static void
x11_font_link_destroy (struct x11_font_link *self)
{
XftFontClose (g.dpy, self->font);
free (self);
}
static struct x11_font_link *
x11_font_link_open (FcPattern *pattern)
{
XftFont *font = XftFontOpenPattern (g.dpy, pattern);
if (!font)
{
FcPatternDestroy (pattern);
return NULL;
}
return x11_font_link_new (font);
}
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);
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;
}
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[])
{
static const struct opt opts[] =
{
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 0, NULL, NULL, 0, NULL }
};
struct opt_handler oh = opt_handler_make (argc, argv, opts,
"HOST:PORT", "X11 frontend for xC.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
switch (c)
{
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
case 'V':
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS);
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
argc -= optind;
argv += optind;
if (argc != 1)
{
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
opt_handler_free (&oh);
char *address = xstrdup (argv[0]);
const char *port = NULL, *host = tokenize_host_port (address, &port);
if (!port)
exit_fatal ("missing port number/service name");
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;
}