aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2024-02-10 08:46:11 +0100
committerPřemysl Eric Janouch <p@janouch.name>2024-02-10 10:07:09 +0100
commitf8c6ac2ed10ed7a3ecb03868a3b0b97deca5c2db (patch)
tree0e18b360b5e44a084406a4a350955eed89451fa8
parentf32bcbd7f471799befdacf36ac8864c4ce328dfb (diff)
downloadliberty-f8c6ac2ed10ed7a3ecb03868a3b0b97deca5c2db.tar.gz
liberty-f8c6ac2ed10ed7a3ecb03868a3b0b97deca5c2db.tar.xz
liberty-f8c6ac2ed10ed7a3ecb03868a3b0b97deca5c2db.zip
Make liberty-xui load PNG program icons
X11 applications now have a dependency on libpng. This makes use of a new related liberty-xdg module, which can be used separately.
-rw-r--r--liberty-xdg.c766
-rw-r--r--liberty-xui.c50
2 files changed, 808 insertions, 8 deletions
diff --git a/liberty-xdg.c b/liberty-xdg.c
new file mode 100644
index 0000000..2c95668
--- /dev/null
+++ b/liberty-xdg.c
@@ -0,0 +1,766 @@
+/*
+ * liberty-xdg.c: the ultimate C unlibrary: freedesktop.org specifications
+ *
+ * Copyright (c) 2023 - 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.
+ *
+ * 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.
+ *
+ */
+
+// This files assumes you've already included liberty.c.
+
+#ifdef LIBERTY_XDG_WANT_X11
+#include <X11/Xatom.h>
+#include <X11/Xlib.h>
+
+#include <png.h>
+#endif // LIBERTY_XDG_WANT_X11
+
+// --- XSettings ---------------------------------------------------------------
+
+#ifdef LIBERTY_XDG_WANT_X11
+
+struct xdg_xsettings_setting
+{
+ enum xdg_xsettings_type
+ {
+ XDG_XSETTINGS_INTEGER,
+ XDG_XSETTINGS_STRING,
+ XDG_XSETTINGS_COLOR,
+ }
+ type; ///< What's stored in the union
+ uint32_t serial; ///< Serial of the last change
+ union
+ {
+ int32_t integer;
+ struct str string;
+ struct { uint16_t red, green, blue, alpha; } color;
+ };
+};
+
+static void
+xdg_xsettings_setting_destroy (struct xdg_xsettings_setting *self)
+{
+ if (self->type == XDG_XSETTINGS_STRING)
+ str_free (&self->string);
+ free (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct xdg_xsettings
+{
+ struct str_map settings; ///< Name -> xdg_xsettings_setting
+};
+
+static void
+xdg_xsettings_free (struct xdg_xsettings *self)
+{
+ str_map_free (&self->settings);
+}
+
+static struct xdg_xsettings
+xdg_xsettings_make (void)
+{
+ return (struct xdg_xsettings)
+ {
+ .settings =
+ str_map_make ((str_map_free_fn) xdg_xsettings_setting_destroy),
+ };
+}
+
+static void
+xdg_xsettings_update (struct xdg_xsettings *self, Display *dpy)
+{
+ // TODO: We're supposed to lock the server.
+ // TODO: We're supposed to trap X errors.
+ char *selection = xstrdup_printf ("_XSETTINGS_S%d", DefaultScreen (dpy));
+ Window owner
+ = XGetSelectionOwner (dpy, XInternAtom (dpy, selection, True));
+ free (selection);
+ if (!owner)
+ return;
+
+ Atom actual_type = None;
+ int actual_format = 0;
+ unsigned long nitems = 0, bytes_after = 0;
+ unsigned char *buffer = NULL;
+ Atom xsettings = XInternAtom (dpy, "_XSETTINGS_SETTINGS", True);
+ int status = XGetWindowProperty (dpy,
+ owner,
+ xsettings,
+ 0L,
+ LONG_MAX,
+ False,
+ xsettings,
+ &actual_type,
+ &actual_format,
+ &nitems,
+ &bytes_after,
+ &buffer);
+ if (status != Success || !buffer)
+ return;
+
+ if (actual_type != xsettings
+ || actual_format != 8
+ || nitems < 12)
+ goto fail;
+
+ const struct peeker *peeker = NULL;
+ if (buffer[0] == LSBFirst)
+ peeker = &peeker_le;
+ else if (buffer[0] == MSBFirst)
+ peeker = &peeker_be;
+ else
+ goto fail;
+
+ // We're ignoring the serial for now.
+ uint32_t n_settings = peeker->u32 (buffer + 8);
+ size_t offset = 12;
+ struct str name = str_make ();
+ struct xdg_xsettings_setting *setting = xcalloc (1, sizeof *setting);
+ while (n_settings--)
+ {
+ if (nitems < offset + 4)
+ goto fail_item;
+
+ setting->type = buffer[offset];
+ uint16_t name_len = peeker->u16 (buffer + offset + 2);
+ offset += 4;
+ if (nitems < offset + name_len)
+ goto fail_item;
+
+ str_append_data (&name, buffer + offset, name_len);
+ offset += ((name_len + 3) & ~3);
+ if (nitems < offset + 4)
+ goto fail_item;
+
+ setting->serial = peeker->u32 (buffer + offset);
+ offset += 4;
+ switch (setting->type)
+ {
+ case XDG_XSETTINGS_INTEGER:
+ if (nitems < offset + 4)
+ goto fail_item;
+
+ setting->integer = (int32_t) peeker->u32 (buffer + offset);
+ offset += 4;
+ break;
+ case XDG_XSETTINGS_STRING:
+ {
+ if (nitems < offset + 4)
+ goto fail_item;
+
+ uint32_t value_len = peeker->u32 (buffer + offset);
+ offset += 4;
+ if (nitems < offset + value_len)
+ goto fail_item;
+
+ setting->string = str_make ();
+ str_append_data (&setting->string, buffer + offset, value_len);
+ offset += ((value_len + 3) & ~3);
+ break;
+ }
+ case XDG_XSETTINGS_COLOR:
+ if (nitems < offset + 8)
+ goto fail_item;
+
+ setting->color.red = peeker->u16 (buffer + offset);
+ setting->color.green = peeker->u16 (buffer + offset + 2);
+ setting->color.blue = peeker->u16 (buffer + offset + 4);
+ setting->color.alpha = peeker->u16 (buffer + offset + 6);
+ offset += 8;
+ break;
+ default:
+ goto fail_item;
+ }
+
+ // TODO(p): Change detection, by comparing existence and serials.
+ str_map_set (&self->settings, name.str, setting);
+ setting = xcalloc (1, sizeof *setting);
+ str_reset (&name);
+ }
+fail_item:
+ xdg_xsettings_setting_destroy (setting);
+ str_free (&name);
+fail:
+ XFree (buffer);
+}
+
+#endif // LIBERTY_XDG_WANT_X11
+
+// --- Desktop file parser -----------------------------------------------------
+
+// Useful for parsing desktop-file-spec, icon-theme-spec, trash-spec,
+// mime-apps-spec. This code is not designed for making changes to the files.
+
+struct desktop_file
+{
+ struct str_map groups; ///< Group name → Key → Value
+};
+
+static void
+desktop_file_free_group (void *value)
+{
+ str_map_free (value);
+ free (value);
+}
+
+static void
+desktop_file_free (struct desktop_file *self)
+{
+ str_map_free (&self->groups);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+desktop_file_parse_line (struct desktop_file *self,
+ char **group_name, const char *line, const char *end)
+{
+ struct str_map *group = NULL;
+ if (*group_name)
+ group = str_map_find (&self->groups, *group_name);
+
+ if (*line == '[')
+ {
+ bool ok = *--end == ']';
+ for (const char *p = ++line; ok && p != end; p++)
+ ok = (unsigned char) *p >= 32 && (unsigned char) *p <= 127
+ && *p != '[' && *p != ']';
+ if (!ok)
+ {
+ cstr_set (group_name, NULL);
+ print_debug ("invalid desktop file group header");
+ return;
+ }
+
+ cstr_set (group_name, xstrndup (line, end - line));
+ if (str_map_find (&self->groups, *group_name))
+ {
+ print_debug ("duplicate desktop file group: %s", *group_name);
+ return;
+ }
+
+ group = xcalloc (1, sizeof *group);
+ *group = str_map_make (free);
+ str_map_set (&self->groups, *group_name, group);
+ return;
+ }
+ if (!group)
+ {
+ print_debug ("unexpected desktop file entry outside of a group");
+ return;
+ }
+
+ const char *key_end = line;
+ while (key_end != end && (isalnum_ascii (*key_end) || *key_end == '-'))
+ key_end++;
+
+ // We could validate these further, but we just search in them anyway.
+ if (key_end != end && *key_end == '[')
+ {
+ while (++key_end != end && *key_end != ']')
+ ;
+ if (key_end != end && *key_end == ']')
+ key_end++;
+ }
+
+ const char *value = key_end;
+ while (value != end && *value == ' ')
+ value++;
+ if (value == end || *value++ != '=')
+ {
+ print_debug ("invalid desktop file entry");
+ return;
+ }
+ while (value != end && *value == ' ')
+ value++;
+
+ char *key = xstrndup (line, key_end - line);
+ if (str_map_find (group, key))
+ print_debug ("duplicate desktop file entry for: %s", key);
+ else
+ str_map_set (group, key, xstrndup (value, end - value));
+ free (key);
+}
+
+static struct desktop_file
+desktop_file_make (const char *data, size_t len)
+{
+ struct desktop_file self = (struct desktop_file)
+ { .groups = str_map_make (desktop_file_free_group) };
+
+ char *group_name = NULL;
+ const char *p = data, *data_end = p + len;
+ while (p != data_end)
+ {
+ const char *line = p, *line_end = line;
+ while (line_end != data_end && *line_end != '\n')
+ line_end++;
+ if ((p = line_end) != data_end && *p == '\n')
+ p++;
+
+ if (line != line_end && *line != '#')
+ desktop_file_parse_line (&self, &group_name, line, line_end);
+ }
+ free (group_name);
+ return self;
+}
+
+static const char *
+desktop_file_get (struct desktop_file *self, const char *group, const char *key)
+{
+ // TODO(p): Ideally, also implement localised keys.
+ struct str_map *group_map = str_map_find (&self->groups, group);
+ if (!group_map)
+ return NULL;
+
+ return str_map_find (group_map, key);
+}
+
+static struct strv
+desktop_file_unescape (const char *value, bool is_list)
+{
+ struct strv result = strv_make ();
+ struct str s = str_make ();
+
+ // XXX: The unescaping behaviour is underspecified.
+ // It might make sense to warn about unrecognised escape sequences.
+ bool escape = false;
+ for (const char *p = value; *p; p++)
+ {
+ if (escape)
+ {
+ switch (*p)
+ {
+ break; case 's': str_append_c (&s, ' ');
+ break; case 'n': str_append_c (&s, '\n');
+ break; case 't': str_append_c (&s, '\t');
+ break; case 'r': str_append_c (&s, '\r');
+ break; default: str_append_c (&s, *p);
+ }
+ }
+ else if (*p == '\\' && p[1])
+ escape = true;
+ else if (*p == ';' && is_list)
+ {
+ strv_append_owned (&result, str_steal (&s));
+ s = str_make ();
+ }
+ else
+ str_append_c (&s, *p);
+ }
+
+ if (!is_list || s.len != 0)
+ strv_append_owned (&result, str_steal (&s));
+ else
+ str_free (&s);
+ return result;
+}
+
+static char *
+desktop_file_get_string (struct desktop_file *self,
+ const char *group, const char *key)
+{
+ const char *value = desktop_file_get (self, group, key);
+ if (!value)
+ return NULL;
+
+ struct strv values = desktop_file_unescape (value, false /* is_list */);
+ char *unescaped = strv_steal (&values, 0);
+ strv_free (&values);
+ return unescaped;
+}
+
+static struct strv
+desktop_file_get_stringv (struct desktop_file *self,
+ const char *group, const char *key)
+{
+ const char *value = desktop_file_get (self, group, key);
+ if (!value)
+ return strv_make ();
+
+ return desktop_file_unescape (value, true /* is_list */);
+}
+
+static bool
+desktop_file_get_bool (struct desktop_file *self,
+ const char *group, const char *key)
+{
+ const char *value = desktop_file_get (self, group, key);
+ if (!value)
+ return false;
+
+ // Let's be compatible with pre-1.0 files when it costs us so little.
+ if (!strcmp (value, "true")
+ || !strcmp (value, "1"))
+ return true;
+ if (!strcmp (value, "false")
+ || !strcmp (value, "0"))
+ return false;
+
+ print_debug ("invalid desktop file boolean for '%s': %s", key, value);
+ return false;
+}
+
+// Nothing uses the "numeric" type.
+// "icon-theme-spec" uses "integer" and doesn't say what it is.
+static long
+desktop_file_get_integer (struct desktop_file *self,
+ const char *group, const char *key)
+{
+ const char *value = desktop_file_get (self, group, key);
+ if (!value)
+ return 0;
+
+ char *end = NULL;
+ long parsed = (errno = 0, strtol (value, &end, 10));
+ if (errno != 0 || *end)
+ print_debug ("invalid desktop file integer for '%s': %s", key, value);
+ return parsed;
+}
+
+// --- Icon themes -------------------------------------------------------------
+
+// This implements part of the Icon Theme Specification.
+
+struct icon_theme_icon
+{
+ uint32_t width; ///< Width of argb in pixels
+ uint32_t height; ///< Height of argb in pixels
+ uint32_t argb[]; ///< ARGB32 data, unassociated alpha
+};
+
+static void
+icon_theme_open_on_error (png_structp pngp, const char *error)
+{
+ print_debug ("%s: %s", (const char *) png_get_error_ptr (pngp), error);
+ png_longjmp (pngp, 1);
+}
+
+static void
+icon_theme_open_on_warning (png_structp pngp, const char *warning)
+{
+ (void) pngp;
+ (void) warning;
+ // Fuck your "gamma value does not match libpng estimate".
+}
+
+// For simplicity, only support PNG icons, using the most popular library.
+static struct icon_theme_icon *
+icon_theme_open (const char *path)
+{
+ volatile png_bytep buffer = NULL;
+ volatile png_bytepp row_pointers = NULL;
+ volatile struct icon_theme_icon *result = NULL;
+ FILE *fp = fopen (path, "rb");
+ if (!fp)
+ {
+ if (errno != ENOENT)
+ print_debug ("%s: %s", path, strerror (errno));
+ return NULL;
+ }
+
+ // The simplified and high-level APIs aren't powerful enough.
+ png_structp pngp = png_create_read_struct (PNG_LIBPNG_VER_STRING,
+ (png_voidp) path, icon_theme_open_on_error, icon_theme_open_on_warning);
+ png_infop infop = png_create_info_struct (pngp);
+ if (!infop)
+ {
+ print_debug ("%s: %s", path, strerror (errno));
+ goto fail;
+ }
+ if (setjmp (png_jmpbuf (pngp)))
+ goto fail;
+
+ png_init_io (pngp, fp);
+ png_read_info (pngp, infop);
+
+ // Asking for at least 8-bit channels. This call is a superset of:
+ // - png_set_palette_to_rgb(),
+ // - png_set_tRNS_to_alpha(),
+ // - png_set_expand_gray_1_2_4_to_8().
+ png_set_expand (pngp);
+
+ // Reduce the possibilities further to RGB or RGBA...
+ png_set_gray_to_rgb (pngp);
+
+ // ...and /exactly/ 8-bit channels.
+ // Alternatively, use png_set_expand_16() above to obtain 16-bit channels.
+ png_set_scale_16 (pngp);
+
+ // PNG uses RGBA order, let's change that to ARGB (both in memory order).
+ // This doesn't change a row's `color_type` in png_do_read_filler(),
+ // and the following transformation thus ignores it.
+ png_set_add_alpha (pngp, 0xFFFF, PNG_FILLER_BEFORE);
+ png_set_swap_alpha (pngp);
+
+ (void) png_set_interlace_handling (pngp);
+
+ png_read_update_info (pngp, infop);
+ if (png_get_bit_depth (pngp, infop) != 8
+ || png_get_channels (pngp, infop) != 4
+ || png_get_color_type (pngp, infop) != PNG_COLOR_TYPE_RGB_ALPHA)
+ png_error (pngp, "result not A8R8G8B8");
+
+ size_t row_bytes = png_get_rowbytes (pngp, infop);
+ size_t height = png_get_image_height (pngp, infop);
+ buffer = xcalloc (row_bytes, height);
+ row_pointers = xcalloc (height, sizeof buffer);
+ for (size_t y = 0; y < height; y++)
+ row_pointers[y] = buffer + y * row_bytes;
+
+ png_read_image (pngp, row_pointers);
+
+ result = xcalloc (1, sizeof *result + row_bytes * height);
+ result->width = png_get_image_width (pngp, infop);
+ result->height = height;
+
+ uint32_t *dst = (uint32_t *) result->argb, *src = (uint32_t *) buffer;
+ for (size_t pixels = result->width * result->height; pixels--; )
+ *dst++ = ntohl (*src++);
+
+fail:
+ free (buffer);
+ free (row_pointers);
+ png_destroy_read_struct (&pngp, &infop, NULL);
+ fclose (fp);
+ return (struct icon_theme_icon *) result;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct icon_theme_find_context
+{
+ struct strv base; ///< Base directories
+ struct str_map visited; ///< Cycle prevention
+
+ ARRAY (struct icon_theme_icon *, icons)
+};
+
+static void
+icon_theme_find__fallback (struct icon_theme_find_context *ctx,
+ const char *name)
+{
+ for (size_t i = 0; i < ctx->base.len; i++)
+ {
+ char *path = xstrdup_printf ("%s/%s.png", ctx->base.vector[i], name);
+ struct icon_theme_icon *icon = icon_theme_open (path);
+ free (path);
+ if (icon)
+ {
+ ARRAY_RESERVE (ctx->icons, 1);
+ ctx->icons[ctx->icons_len++] = icon;
+ return;
+ }
+ }
+}
+
+static struct desktop_file
+icon_theme_find__index (struct icon_theme_find_context *ctx, const char *theme)
+{
+ struct str data = str_make ();
+ for (size_t i = 0; i < ctx->base.len; i++)
+ {
+ struct error *e = NULL;
+ char *path = xstrdup_printf ("%s/%s/index.theme",
+ ctx->base.vector[i], theme);
+ read_file (path, &data, &e);
+ free (path);
+ if (!e)
+ break;
+
+ if (errno != ENOENT)
+ print_debug ("%s", e->message);
+ error_free (e);
+ }
+
+ struct desktop_file index = desktop_file_make (data.str, data.len);
+ str_free (&data);
+ return index;
+}
+
+static void
+icon_theme_find__named (struct icon_theme_find_context *ctx,
+ const char *theme, const char *name)
+{
+ // Either a cycle, or a common ancestor of inherited themes, which is valid.
+ if (str_map_find (&ctx->visited, theme))
+ return;
+
+ str_map_set (&ctx->visited, theme, (void *) (intptr_t) 1);
+ struct desktop_file index = icon_theme_find__index (ctx, theme);
+
+ char *directories =
+ desktop_file_get_string (&index, "Icon Theme", "Directories");
+ if (!directories)
+ goto out;
+
+ // NOTE: The sizes are not deduplicated, and priorities are uncertain.
+ struct strv dirs = strv_make ();
+ cstr_split (directories, ",", true, &dirs);
+ free (directories);
+ for (size_t d = 0; d < dirs.len; d++)
+ {
+ // The hicolor icon theme stuffs everything in Directories.
+ if (desktop_file_get (&index, dirs.vector[d], "Scale")
+ && desktop_file_get_integer (&index, dirs.vector[d], "Scale") != 1)
+ continue;
+
+ for (size_t i = 0; i < ctx->base.len; i++)
+ {
+ char *path = xstrdup_printf ("%s/%s/%s/%s.png",
+ ctx->base.vector[i], theme, dirs.vector[d], name);
+ struct icon_theme_icon *icon = icon_theme_open (path);
+ free (path);
+ if (icon)
+ {
+ ARRAY_RESERVE (ctx->icons, 1);
+ ctx->icons[ctx->icons_len++] = icon;
+ break;
+ }
+ }
+ }
+ strv_free (&dirs);
+ if (ctx->icons_len)
+ goto out;
+
+ char *inherits =
+ desktop_file_get_string (&index, "Icon Theme", "Inherits");
+ if (inherits)
+ {
+ struct strv parents = strv_make ();
+ cstr_split (inherits, ",", true, &parents);
+ free (inherits);
+ for (size_t i = 0; i < parents.len; i++)
+ {
+ icon_theme_find__named (ctx, parents.vector[i], name);
+ if (ctx->icons_len)
+ break;
+ }
+ strv_free (&parents);
+ }
+
+out:
+ desktop_file_free (&index);
+}
+
+/// Return all base directories appropriate for icon search.
+static struct strv
+icon_theme_get_base_directories (void)
+{
+ struct strv dirs = strv_make ();
+ struct str icons = str_make ();
+ (void) str_append_env_path (&icons, "HOME", false);
+ str_append (&icons, "/.icons");
+ strv_append_owned (&dirs, str_steal (&icons));
+
+ // Note that we use XDG_CONFIG_HOME as well, which might be intended.
+ struct strv xdg = strv_make ();
+ get_xdg_data_dirs (&xdg);
+ for (size_t i = 0; i < xdg.len; i++)
+ strv_append_owned (&dirs, xstrdup_printf ("%s/icons", xdg.vector[i]));
+ strv_free (&xdg);
+
+ strv_append (&dirs, "/usr/share/pixmaps");
+ return dirs;
+}
+
+static int
+icon_theme_find__compare (const void *a, const void *b)
+{
+ const struct icon_theme_icon **ia = (const struct icon_theme_icon **) a;
+ const struct icon_theme_icon **ib = (const struct icon_theme_icon **) b;
+ double pa = (double) (*ia)->width * (*ia)->height;
+ double pb = (double) (*ib)->width * (*ib)->height;
+ return (pa > pb) - (pa < pb);
+}
+
+/// Return all sizes of the named icon. When the theme name is not NULL,
+/// use it as the preferred theme. Always consult fallbacks locations.
+/// Ignore icon scales other than 1.
+static struct icon_theme_icon **
+icon_theme_find (const char *theme, const char *name, size_t *len)
+{
+ struct icon_theme_find_context ctx = {};
+ ctx.base = icon_theme_get_base_directories ();
+ ctx.visited = str_map_make (NULL);
+ ARRAY_INIT (ctx.icons);
+
+ if (theme)
+ icon_theme_find__named (&ctx, theme, name);
+ if (!ctx.icons_len)
+ icon_theme_find__named (&ctx, "hicolor", name);
+ if (!ctx.icons_len)
+ icon_theme_find__fallback (&ctx, name);
+
+ strv_free (&ctx.base);
+ str_map_free (&ctx.visited);
+
+ ARRAY_RESERVE (ctx.icons, 1);
+ ctx.icons[ctx.icons_len] = NULL;
+ if (!ctx.icons_len)
+ {
+ free (ctx.icons);
+ return NULL;
+ }
+
+ qsort (ctx.icons,
+ ctx.icons_len, sizeof *ctx.icons, icon_theme_find__compare);
+ *len = ctx.icons_len;
+ return ctx.icons;
+}
+
+static void
+icon_theme_free (struct icon_theme_icon **icons)
+{
+ for (struct icon_theme_icon **p = icons; *p; p++)
+ free (*p);
+ free (icons);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#ifdef LIBERTY_XDG_WANT_X11
+
+static void
+icon_theme_set_window_icon (Display *dpy,
+ Window window, const char *theme, const char *name)
+{
+ size_t icons_len = 0;
+ struct icon_theme_icon **icons = icon_theme_find (theme, name, &icons_len);
+ if (!icons)
+ return;
+
+ size_t n = 0;
+ for (size_t i = 0; i < icons_len; i++)
+ n += 2 + icons[i]->width * icons[i]->height;
+
+ unsigned long *data = xcalloc (n, sizeof *data), *p = data;
+ for (size_t i = 0; i < icons_len; i++)
+ {
+ *p++ = icons[i]->width;
+ *p++ = icons[i]->height;
+
+ uint32_t *q = icons[i]->argb;
+ for (size_t k = icons[i]->width * icons[i]->height; k--; )
+ *p++ = *q++;
+ }
+
+ XChangeProperty (dpy, window, XInternAtom (dpy, "_NET_WM_ICON", False),
+ XA_CARDINAL, 32, PropModeReplace, (const unsigned char *) data, n);
+ free (data);
+ icon_theme_free (icons);
+}
+
+#endif // LIBERTY_XDG_WANT_X11
diff --git a/liberty-xui.c b/liberty-xui.c
index 45a6d90..761d7c6 100644
--- a/liberty-xui.c
+++ b/liberty-xui.c
@@ -1,7 +1,7 @@
/*
* liberty-xui.c: the ultimate C unlibrary: hybrid terminal/X11 UI
*
- * Copyright (c) 2016 - 2023, Přemysl Eric Janouch <p@janouch.name>
+ * Copyright (c) 2016 - 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.
@@ -17,6 +17,7 @@
*/
// This file includes some common stuff to build terminal/X11 applications with.
+// It assumes you've already included liberty.c, and may include liberty-xdg.c.
#include <ncurses.h>
@@ -66,6 +67,9 @@ enum { XUI_KEYMOD_DOUBLE_CLICK = 1 << 15 };
#include <X11/keysym.h>
#include <X11/XKBlib.h>
#include <X11/Xft/Xft.h>
+
+#define LIBERTY_XDG_WANT_X11
+#include "liberty-xdg.c"
#endif // LIBERTY_XUI_WANT_X11
// The application needs to implement these.
@@ -690,7 +694,10 @@ struct xui
XftDraw *xft_draw; ///< Xft rendering context
struct x11_font *xft_fonts; ///< Font collection
char *x11_selection; ///< CLIPBOARD selection
+ struct xdg_xsettings x11_xsettings; ///< XSETTINGS
+ int32_t x11_double_click_time; ///< Maximum delay for double clicks
+ int32_t x11_double_click_distance; ///< Maximum distance for double clicks
const char *x11_fontname; ///< Fontconfig font name
const char *x11_fontname_monospace; ///< Fontconfig monospace font name
XRenderColor *x_fg; ///< Foreground per attribute
@@ -1375,6 +1382,7 @@ x11_destroy (void)
LIST_FOR_EACH (struct x11_font, font, g_xui.xft_fonts)
x11_font_destroy (font);
cstr_set (&g_xui.x11_selection, NULL);
+ xdg_xsettings_free (&g_xui.x11_xsettings);
free (g_xui.x_fg);
free (g_xui.x_bg);
@@ -1623,9 +1631,10 @@ on_x11_input_event (XEvent *ev)
unsigned int button = ev->xbutton.button;
int modifiers = x11_state_to_modifiers (ev->xbutton.state);
if (ev->type == ButtonPress
- && ev->xbutton.time - last_press_event.xbutton.time < 500
- && abs (last_press_event.xbutton.x - x) < 5
- && abs (last_press_event.xbutton.y - y) < 5
+ && ev->xbutton.time - last_press_event.xbutton.time
+ < (Time) g_xui.x11_double_click_time
+ && abs (last_press_event.xbutton.x - x) < g_xui.x11_double_click_distance
+ && abs (last_press_event.xbutton.y - y) < g_xui.x11_double_click_distance
&& last_press_event.xbutton.button == button)
{
modifiers |= XUI_KEYMOD_DOUBLE_CLICK;
@@ -1891,6 +1900,13 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len)
x11_init_attributes (app_attrs, app_attrs_len);
+ // https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/
+ // TODO: Try to use Xft/{Antialias,DPI,HintStyle,Hinting,RGBA}
+ // from XSETTINGS. Sadly, Gtk/FontName is in the Pango format,
+ // which is rather difficult to parse.
+ g_xui.x11_xsettings = xdg_xsettings_make ();
+ xdg_xsettings_update (&g_xui.x11_xsettings, g_xui.dpy);
+
if (!FcInit ())
print_warning ("Fontconfig initialization failed");
if (!(g_xui.xft_fonts = x11_font_open (0)))
@@ -1942,6 +1958,25 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len)
XSetWMName (g_xui.dpy, g_xui.x11_window, &prop);
XFree (prop.value);
+ // It should not be outlandish to expect to find a program icon,
+ // although it should be possible to use a "DBus well-known name".
+ const char *icon_theme_name = NULL;
+ const struct xdg_xsettings_setting *setting =
+ str_map_find (&g_xui.x11_xsettings.settings, "Net/IconThemeName");
+ if (setting != NULL && setting->type == XDG_XSETTINGS_STRING)
+ icon_theme_name = setting->string.str;
+ icon_theme_set_window_icon (g_xui.dpy,
+ g_xui.x11_window, icon_theme_name, name);
+
+ if ((setting = str_map_find (&g_xui.x11_xsettings.settings,
+ "Net/DoubleClickTime"))
+ && setting->type == XDG_XSETTINGS_INTEGER && setting->integer >= 0)
+ g_xui.x11_double_click_time = setting->integer;
+ if ((setting = str_map_find (&g_xui.x11_xsettings.settings,
+ "Net/DoubleClickDistance"))
+ && setting->type == XDG_XSETTINGS_INTEGER && setting->integer >= 0)
+ g_xui.x11_double_click_distance = setting->integer;
+
// TODO: It is possible to do, e.g., on-the-spot.
XIMStyle im_style = XIMPreeditNothing | XIMStatusNothing;
XIMStyles *im_styles = NULL;
@@ -2147,11 +2182,10 @@ xui_preinit (void)
// Presumably, although not necessarily; unsure if queryable at all.
g_xui.focused = true;
- // TODO: Try to use Gtk/FontName from the _XSETTINGS_S%d selection,
- // as well as Net/DoubleClick*. See the XSETTINGS proposal for details.
- // https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/
- // Note that this needs an X11 connection already.
#ifdef LIBERTY_XUI_WANT_X11
+ // Note that XSETTINGS overrides some values in the init.
+ g_xui.x11_double_click_time = 500;
+ g_xui.x11_double_click_distance = 5;
g_xui.x11_fontname = "sans\\-serif-11";
g_xui.x11_fontname_monospace = "monospace-11";
#endif // LIBERTY_XUI_WANT_X11