/* * 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; }