mirror of
https://codeberg.org/dnkl/yambar.git
synced 2025-04-22 20:25:39 +02:00
Introduce a new icon particle. It follows the icon spec (https://specifications.freedesktop.org/icon-theme-spec/latest/index.html). Rendering logic is taken from fuzzel (using nanosvg + libpng), while loading logic is taken from sway. Standard usage is with `use-tag = false` which expands the provided string template and then loads the string as the icon name. There are settings to manually override the base paths, themes, etc. The second usage which is required for tray support is a special icon tag that transfers raw pixmaps. With `use-tag = true` it first expands the string, and then uses that output to find an icon pixmap tag. To reduce memory usage, themes are reference counted so they can be passed down the configuration stack without having to load them in multiple times. For programmability, a fallback particle can be specified if no icon/tag is found `fallback: ...`. And the new icon pixmap tag can be existence checked in map conditions using `+{tag_name}`. Future work to be done in follow up diffs: 1. Icon caching. Currently performs an icon lookup on each instantiation & a render on each refresh. 2. Theme caching. Changing theme directories results in a new "theme collection" being created resulting in the possibility of duplicated theme loading.
968 lines
26 KiB
C
968 lines
26 KiB
C
#include "tllist.h"
|
|
#include <ctype.h>
|
|
#include <dirent.h>
|
|
#include <limits.h>
|
|
#include <nanosvg/nanosvg.h>
|
|
#include <nanosvg/nanosvgrast.h>
|
|
#include <pixman.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <wordexp.h>
|
|
|
|
#define LOG_MODULE "icon"
|
|
#define LOG_ENABLE_DBG 0
|
|
#include "icon.h"
|
|
#include "log.h"
|
|
#include "png-yambar.h"
|
|
#include "stride.h"
|
|
#include "stringop.h"
|
|
|
|
void
|
|
icon_pixmaps_free(const struct ref *ref)
|
|
{
|
|
struct icon_pixmaps *p = (struct icon_pixmaps *)ref;
|
|
tll_free_and_free(p->list, free);
|
|
free(p);
|
|
}
|
|
|
|
void
|
|
icon_pixmaps_dec(struct icon_pixmaps *p)
|
|
{
|
|
ref_dec(&p->refcount);
|
|
}
|
|
|
|
struct icon_pixmaps *
|
|
icon_pixmaps_inc(struct icon_pixmaps *p)
|
|
{
|
|
ref_inc(&p->refcount);
|
|
return p;
|
|
}
|
|
|
|
struct icon_pixmaps *
|
|
new_icon_pixmaps()
|
|
{
|
|
struct icon_pixmaps *p = malloc(sizeof(*p));
|
|
p->refcount = (struct ref){icon_pixmaps_free, 1};
|
|
icon_pixmaps_t tmp = tll_init();
|
|
p->list = tmp;
|
|
return p;
|
|
}
|
|
|
|
void
|
|
icon_from_pixmaps(struct icon *icon, struct icon_pixmaps *p)
|
|
{
|
|
icon->pixmaps = icon_pixmaps_inc(p);
|
|
icon->type = ICON_PIXMAP;
|
|
}
|
|
|
|
bool
|
|
icon_from_png(struct icon *icon, const char *file_name)
|
|
{
|
|
pixman_image_t *png = png_load(file_name);
|
|
if (png == NULL)
|
|
return false;
|
|
|
|
icon->type = ICON_PNG;
|
|
icon->png = png;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
icon_from_svg(struct icon *icon, const char *file_name)
|
|
{
|
|
/* TODO: DPI */
|
|
NSVGimage *svg = nsvgParseFromFile(file_name, "px", 96);
|
|
if (svg == NULL)
|
|
return false;
|
|
|
|
if (svg->width == 0 || svg->height == 0) {
|
|
nsvgDelete(svg);
|
|
return false;
|
|
}
|
|
|
|
icon->type = ICON_SVG;
|
|
icon->svg = svg;
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
scale_if_necessary(pixman_image_t *img, int x, int y, int size, pixman_image_t *dest)
|
|
{
|
|
pixman_format_code_t fmt = pixman_image_get_format(img);
|
|
int height = pixman_image_get_height(img);
|
|
int width = pixman_image_get_width(img);
|
|
|
|
bool scale_img = height > size;
|
|
|
|
// Exposable is const so have to recalculate every-time...
|
|
//
|
|
if (scale_img) {
|
|
double scale = (double)size / height;
|
|
|
|
pixman_f_transform_t _scale_transform;
|
|
pixman_f_transform_init_scale(&_scale_transform, 1. / scale, 1. / scale);
|
|
|
|
pixman_transform_t scale_transform;
|
|
pixman_transform_from_pixman_f_transform(&scale_transform, &_scale_transform);
|
|
pixman_image_set_transform(img, &scale_transform);
|
|
|
|
int param_count = 0;
|
|
pixman_kernel_t kernel = PIXMAN_KERNEL_LANCZOS3;
|
|
pixman_fixed_t *params = pixman_filter_create_separable_convolution(
|
|
¶m_count, pixman_double_to_fixed(1. / scale), pixman_double_to_fixed(1. / scale), kernel, kernel,
|
|
kernel, kernel, pixman_int_to_fixed(1), pixman_int_to_fixed(1));
|
|
|
|
if (params != NULL || param_count == 0) {
|
|
pixman_image_set_filter(img, PIXMAN_FILTER_SEPARABLE_CONVOLUTION, params, param_count);
|
|
}
|
|
|
|
free(params);
|
|
|
|
width *= scale;
|
|
height *= scale;
|
|
|
|
int stride = stride_for_format_and_width(fmt, width);
|
|
uint8_t *data = malloc(height * stride);
|
|
pixman_image_t *scaled_img = pixman_image_create_bits_no_clear(fmt, width, height, (uint32_t *)data, stride);
|
|
|
|
pixman_image_composite32(PIXMAN_OP_SRC, img, NULL, scaled_img, 0, 0, 0, 0, 0, 0, width, height);
|
|
img = scaled_img;
|
|
}
|
|
|
|
pixman_image_composite32(PIXMAN_OP_OVER, img, NULL, dest, 0, 0, 0, 0, x, y, width, height);
|
|
|
|
if (scale_img) {
|
|
free(pixman_image_get_data(img));
|
|
pixman_image_unref(img);
|
|
}
|
|
}
|
|
|
|
static void
|
|
render_png(const struct icon *icon, int x, int y, int size, pixman_image_t *dest)
|
|
{
|
|
assert(icon->type == ICON_PNG);
|
|
assert(icon->png != NULL);
|
|
|
|
pixman_image_t *png = icon->png;
|
|
|
|
scale_if_necessary(png, x, y, size, dest);
|
|
}
|
|
|
|
static void
|
|
render_svg(const struct icon *icon, int x, int y, int size, pixman_image_t *dest)
|
|
{
|
|
assert(icon->type == ICON_SVG);
|
|
assert(icon->svg != NULL);
|
|
|
|
NSVGimage *svg = icon->svg;
|
|
struct NSVGrasterizer *rast = nsvgCreateRasterizer();
|
|
|
|
if (rast == NULL)
|
|
return;
|
|
|
|
float scale = svg->width > svg->height ? size / svg->width : size / svg->height;
|
|
|
|
uint8_t *data = malloc(size * size * 4);
|
|
nsvgRasterize(rast, svg, 0, 0, scale, data, size, size, size * 4);
|
|
|
|
pixman_image_t *img = pixman_image_create_bits_no_clear(PIXMAN_a8b8g8r8, size, size, (uint32_t *)data, size * 4);
|
|
|
|
/* Nanosvg produces non-premultiplied ABGR, while pixman expects
|
|
* premultiplied */
|
|
for (uint32_t *abgr = (uint32_t *)data; abgr < (uint32_t *)(data + size * size * 4); abgr++) {
|
|
uint8_t alpha = (*abgr >> 24) & 0xff;
|
|
uint8_t blue = (*abgr >> 16) & 0xff;
|
|
uint8_t green = (*abgr >> 8) & 0xff;
|
|
uint8_t red = (*abgr >> 0) & 0xff;
|
|
|
|
if (alpha == 0xff)
|
|
continue;
|
|
|
|
if (alpha == 0x00)
|
|
blue = green = red = 0x00;
|
|
else {
|
|
blue = blue * alpha / 0xff;
|
|
green = green * alpha / 0xff;
|
|
red = red * alpha / 0xff;
|
|
}
|
|
|
|
*abgr = (uint32_t)alpha << 24 | blue << 16 | green << 8 | red;
|
|
}
|
|
|
|
nsvgDeleteRasterizer(rast);
|
|
|
|
pixman_image_composite32(PIXMAN_OP_OVER, img, NULL, dest, 0, 0, 0, 0, x, y, size, size);
|
|
free(pixman_image_get_data(img));
|
|
pixman_image_unref(img);
|
|
}
|
|
|
|
static void
|
|
render_pixmap(const struct icon *icon, int x, int y, int size, pixman_image_t *dest)
|
|
{
|
|
assert(icon->type == ICON_PIXMAP);
|
|
assert(icon->pixmaps != NULL);
|
|
|
|
struct icon_pixmap *icon_pixmap = NULL;
|
|
int min_error = INT_MAX;
|
|
tll_foreach(icon->pixmaps->list, it)
|
|
{
|
|
int e = abs(size - it->item->size);
|
|
if (e < min_error) {
|
|
icon_pixmap = it->item;
|
|
min_error = e;
|
|
}
|
|
}
|
|
|
|
assert(icon_pixmap != NULL);
|
|
pixman_image_t *img = pixman_image_create_bits_no_clear(
|
|
PIXMAN_a8r8g8b8, icon_pixmap->size, icon_pixmap->size, (uint32_t *)icon_pixmap->pixels,
|
|
stride_for_format_and_width(PIXMAN_a8r8g8b8, icon_pixmap->size));
|
|
scale_if_necessary(img, x, y, size, dest);
|
|
pixman_image_unref(img);
|
|
}
|
|
|
|
void
|
|
render_icon(const struct icon *icon, int x, int y, int size, pixman_image_t *dest)
|
|
{
|
|
|
|
switch (icon->type) {
|
|
case ICON_NONE:
|
|
break;
|
|
case ICON_PNG:
|
|
render_png(icon, x, y, size, dest);
|
|
break;
|
|
case ICON_SVG:
|
|
render_svg(icon, x, y, size, dest);
|
|
break;
|
|
case ICON_PIXMAP:
|
|
render_pixmap(icon, x, y, size, dest);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void
|
|
reset_icon(struct icon *icon)
|
|
{
|
|
switch (icon->type) {
|
|
case ICON_NONE:
|
|
break;
|
|
case ICON_PNG:
|
|
free(pixman_image_get_data(icon->png));
|
|
pixman_image_unref(icon->png);
|
|
icon->png = NULL;
|
|
break;
|
|
case ICON_SVG:
|
|
nsvgDelete(icon->svg);
|
|
icon->svg = NULL;
|
|
break;
|
|
case ICON_PIXMAP:
|
|
icon_pixmaps_dec(icon->pixmaps);
|
|
icon->pixmaps = NULL;
|
|
break;
|
|
}
|
|
|
|
icon->type = ICON_NONE;
|
|
}
|
|
|
|
void
|
|
string_list_free(const struct ref *ref)
|
|
{
|
|
struct string_list *b = (struct string_list *)ref;
|
|
tll_free_and_free(b->strings, free);
|
|
free(b);
|
|
}
|
|
|
|
void
|
|
string_list_dec(struct string_list *b)
|
|
{
|
|
ref_dec(&b->refcount);
|
|
}
|
|
|
|
struct string_list *
|
|
string_list_inc(struct string_list *p)
|
|
{
|
|
ref_inc(&p->refcount);
|
|
return p;
|
|
}
|
|
|
|
struct string_list *
|
|
string_list_new()
|
|
{
|
|
string_list_t strings = tll_init();
|
|
|
|
struct string_list *out = malloc(sizeof(*out));
|
|
out->strings = strings;
|
|
out->refcount = (struct ref){string_list_free, 1};
|
|
return out;
|
|
}
|
|
|
|
bool
|
|
dir_exists(char *path)
|
|
{
|
|
struct stat sb;
|
|
return stat(path, &sb) == 0 && S_ISDIR(sb.st_mode);
|
|
}
|
|
|
|
static void
|
|
destroy_theme(struct icon_theme *theme)
|
|
{
|
|
if (!theme) {
|
|
return;
|
|
}
|
|
free(theme->name);
|
|
free(theme->comment);
|
|
tll_free_and_free(theme->inherits, free);
|
|
tll_free_and_free(theme->directories, free);
|
|
free(theme->dir);
|
|
|
|
tll_foreach(theme->subdirs, it)
|
|
{
|
|
free(it->item->name);
|
|
free(it->item);
|
|
tll_remove(theme->subdirs, it);
|
|
}
|
|
free(theme);
|
|
}
|
|
|
|
void
|
|
basedirs_free(const struct ref *ref)
|
|
{
|
|
struct basedirs *b = (struct basedirs *)ref;
|
|
tll_free_and_free(b->basedirs, free);
|
|
free(b);
|
|
}
|
|
|
|
void
|
|
basedirs_dec(struct basedirs *b)
|
|
{
|
|
ref_dec(&b->refcount);
|
|
}
|
|
|
|
struct basedirs *
|
|
basedirs_inc(struct basedirs *p)
|
|
{
|
|
ref_inc(&p->refcount);
|
|
return p;
|
|
}
|
|
|
|
struct basedirs *
|
|
basedirs_new(void)
|
|
{
|
|
string_list_t basedirs = tll_init();
|
|
|
|
struct basedirs *out = malloc(sizeof(*out));
|
|
out->basedirs = basedirs;
|
|
out->refcount = (struct ref){basedirs_free, 1};
|
|
return out;
|
|
}
|
|
|
|
struct basedirs *
|
|
get_basedirs(void)
|
|
{
|
|
string_list_t basedirs = tll_init();
|
|
|
|
// Follows freedesktop specs:
|
|
//
|
|
tll_push_back(basedirs, strdup("$HOME/.icons")); // deprecated
|
|
|
|
char *data_home = getenv("XDG_DATA_HOME");
|
|
tll_push_back(basedirs, strdup(data_home && *data_home ? "$XDG_DATA_HOME/icons" : "$HOME/.local/share/icons"));
|
|
|
|
tll_push_back(basedirs, strdup("/usr/share/pixmaps"));
|
|
|
|
char *data_dirs = getenv("XDG_DATA_DIRS");
|
|
if (!(data_dirs && *data_dirs)) {
|
|
data_dirs = "/usr/local/share:/usr/share";
|
|
}
|
|
data_dirs = strdup(data_dirs);
|
|
char *dir = strtok(data_dirs, ":");
|
|
do {
|
|
char *path = format_str("%s/icons", dir);
|
|
tll_push_back(basedirs, path);
|
|
} while ((dir = strtok(NULL, ":")));
|
|
free(data_dirs);
|
|
|
|
string_list_t basedirs_expanded = tll_init();
|
|
tll_foreach(basedirs, it)
|
|
{
|
|
wordexp_t p;
|
|
if (wordexp(it->item, &p, WRDE_UNDEF) == 0) {
|
|
if (dir_exists(p.we_wordv[0])) {
|
|
tll_push_back(basedirs_expanded, strdup(p.we_wordv[0]));
|
|
}
|
|
wordfree(&p);
|
|
}
|
|
};
|
|
|
|
tll_free_and_free(basedirs, free);
|
|
struct basedirs *out = malloc(sizeof(*out));
|
|
out->basedirs = basedirs_expanded;
|
|
out->refcount = (struct ref){basedirs_free, 1};
|
|
return out;
|
|
}
|
|
|
|
static const char *
|
|
group_handler(char *old_group, char *new_group, struct icon_theme *theme)
|
|
{
|
|
if (!old_group) {
|
|
return new_group && strcasecmp(new_group, "icon theme") == 0 ? NULL : "first group must be 'Icon Theme'";
|
|
}
|
|
|
|
if (strcasecmp(old_group, "icon theme") == 0) {
|
|
if (!theme->name) {
|
|
return "missing required key 'Name'";
|
|
} else if (!theme->comment) {
|
|
return "missing required key 'Comment'";
|
|
} else if (tll_length(theme->directories) == 0) {
|
|
return "missing required key 'Directories'";
|
|
}
|
|
} else {
|
|
if (tll_length(theme->subdirs) == 0) { // skip
|
|
return NULL;
|
|
}
|
|
|
|
struct icon_theme_subdir *subdir = tll_back(theme->subdirs);
|
|
if (!subdir->size) {
|
|
return "missing required key 'Size'";
|
|
}
|
|
|
|
switch (subdir->type) {
|
|
case ICON_DIR_FIXED:
|
|
subdir->max_size = subdir->min_size = subdir->size;
|
|
break;
|
|
case ICON_DIR_SCALABLE: {
|
|
if (!subdir->max_size)
|
|
subdir->max_size = subdir->size;
|
|
if (!subdir->min_size)
|
|
subdir->min_size = subdir->size;
|
|
break;
|
|
}
|
|
case ICON_DIR_THRESHOLD:
|
|
subdir->max_size = subdir->size + subdir->threshold;
|
|
subdir->min_size = subdir->size - subdir->threshold;
|
|
}
|
|
}
|
|
|
|
if (new_group) {
|
|
bool found = false;
|
|
tll_foreach(theme->directories, it)
|
|
{
|
|
if (strcmp(it->item, new_group)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (found) {
|
|
struct icon_theme_subdir *subdir = calloc(1, sizeof(struct icon_theme_subdir));
|
|
if (!subdir) {
|
|
return "out of memory";
|
|
}
|
|
subdir->name = strdup(new_group);
|
|
subdir->threshold = 2;
|
|
tll_push_back(theme->subdirs, subdir);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
string_list_t
|
|
split_string(const char *str, const char *delims)
|
|
{
|
|
string_list_t res = tll_init();
|
|
char *copy = strdup(str);
|
|
|
|
char *token = strtok(copy, delims);
|
|
while (token) {
|
|
tll_push_back(res, strdup(token));
|
|
token = strtok(NULL, delims);
|
|
}
|
|
free(copy);
|
|
return res;
|
|
}
|
|
|
|
#define OOM_ERROR(val) \
|
|
if (!val) \
|
|
return "out of memory";
|
|
|
|
static const char *
|
|
entry_handler(char *group, char *key, char *value, struct icon_theme *theme)
|
|
{
|
|
if (strcmp(group, "Icon Theme") == 0) {
|
|
if (strcmp(key, "Name") == 0) {
|
|
theme->name = strdup(value);
|
|
OOM_ERROR(theme->name);
|
|
} else if (strcmp(key, "Comment") == 0) {
|
|
theme->comment = strdup(value);
|
|
OOM_ERROR(theme->comment);
|
|
} else if (strcmp(key, "Inherits") == 0) {
|
|
theme->inherits = split_string(value, ",");
|
|
} else if (strcmp(key, "Directories") == 0) {
|
|
theme->directories = split_string(value, ",");
|
|
} // Ignored: ScaledDirectories, Hidden, Example
|
|
} else {
|
|
if (tll_length(theme->subdirs) == 0) { // skip
|
|
return NULL;
|
|
}
|
|
|
|
struct icon_theme_subdir *subdir = tll_back(theme->subdirs);
|
|
if (strcmp(subdir->name, group) != 0) { // skip
|
|
return NULL;
|
|
}
|
|
|
|
if (strcmp(key, "Context") == 0) {
|
|
return NULL; // ignored, but explicitly handled to not fail parsing
|
|
} else if (strcmp(key, "Type") == 0) {
|
|
if (strcmp(value, "Fixed") == 0) {
|
|
subdir->type = ICON_DIR_FIXED;
|
|
} else if (strcmp(value, "Scalable") == 0) {
|
|
subdir->type = ICON_DIR_SCALABLE;
|
|
} else if (strcmp(value, "Threshold") == 0) {
|
|
subdir->type = ICON_DIR_THRESHOLD;
|
|
} else {
|
|
return "invalid value - expected 'Fixed', 'Scalable', or 'Threshold";
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
char *end;
|
|
int n = strtol(value, &end, 10);
|
|
if (*end != '\0') {
|
|
return "invalid value - expected a number";
|
|
}
|
|
|
|
if (strcmp(key, "Size") == 0) {
|
|
subdir->size = n;
|
|
} else if (strcmp(key, "MaxSize") == 0) {
|
|
subdir->max_size = n;
|
|
} else if (strcmp(key, "MinSize") == 0) {
|
|
subdir->min_size = n;
|
|
} else if (strcmp(key, "Threshold") == 0) {
|
|
subdir->threshold = n;
|
|
} else if (strcmp(key, "Scale") == 0) {
|
|
subdir->scale = n;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* This is a Freedesktop Desktop Entry parser (essentially INI)
|
|
* It calls entry_handler for every entry
|
|
* and group_handler between every group (as well as at both ends)
|
|
* Handlers return whether an error occurred, which stops parsing
|
|
*/
|
|
struct icon_theme *
|
|
read_theme_file(char *basedir, char *theme_name)
|
|
{
|
|
struct icon_theme *theme = calloc(1, sizeof(struct icon_theme));
|
|
if (!theme) {
|
|
return NULL;
|
|
}
|
|
string_list_t tmp1 = tll_init();
|
|
subdirs_t tmp3 = tll_init();
|
|
theme->directories = tmp1;
|
|
theme->inherits = tmp1;
|
|
theme->subdirs = tmp3;
|
|
|
|
// look for index.theme file
|
|
char *path = format_str("%s/%s/index.theme", basedir, theme_name);
|
|
if (!path) {
|
|
return NULL;
|
|
}
|
|
FILE *theme_file = fopen(path, "r");
|
|
free(path);
|
|
if (!theme_file) {
|
|
free(theme);
|
|
return NULL;
|
|
}
|
|
|
|
string_list_t groups = tll_init();
|
|
char *full_line = NULL;
|
|
const char *error = NULL;
|
|
int line_no = 0;
|
|
size_t sz = 0;
|
|
while (true) {
|
|
const char *warning = NULL;
|
|
ssize_t nread = getline(&full_line, &sz, theme_file);
|
|
if (nread == -1) {
|
|
break;
|
|
}
|
|
|
|
++line_no;
|
|
|
|
char *line = full_line - 1;
|
|
while (isspace(*++line)) {
|
|
} // remove leading whitespace
|
|
if (!*line || line[0] == '#')
|
|
goto next; // ignore blank lines & comments
|
|
|
|
int len = nread - (line - full_line);
|
|
while (isspace(line[--len])) {
|
|
}
|
|
line[++len] = '\0'; // Remove trailing whitespace
|
|
|
|
if (line[0] == '[') { // Group handler
|
|
// check well-formed
|
|
int i = 1;
|
|
for (; !iscntrl(line[i]) && line[i] != '[' && line[i] != ']'; ++i) {
|
|
}
|
|
if (i != --len || line[i] != ']') {
|
|
warning = "malformed group header";
|
|
goto warn;
|
|
}
|
|
|
|
line[len] = '\0';
|
|
line = &line[1];
|
|
|
|
tll_foreach(groups, it)
|
|
{
|
|
// If duplicate move to back and continue
|
|
//
|
|
if (strcmp(it->item, line) == 0) {
|
|
tll_push_back(groups, it->item);
|
|
tll_remove(groups, it);
|
|
goto next;
|
|
}
|
|
}
|
|
|
|
char *last_group = tll_length(groups) ? tll_back(groups) : NULL;
|
|
error = group_handler(last_group, line, theme);
|
|
if (error) {
|
|
break;
|
|
}
|
|
|
|
tll_push_back(groups, strdup(line));
|
|
} else {
|
|
if (tll_length(groups) == 0) {
|
|
error = "unexpected content before first header";
|
|
break;
|
|
}
|
|
|
|
// check well-formed
|
|
int eok = 0;
|
|
for (; isalnum(line[eok]) || line[eok] == '-'; ++eok) {
|
|
}
|
|
int i = eok - 1;
|
|
while (isspace(line[++i])) {
|
|
}
|
|
if (line[i] == '[') {
|
|
// not handling localized values:
|
|
// https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html
|
|
//
|
|
goto next;
|
|
}
|
|
if (line[i] != '=') {
|
|
warning = "malformed key-value pair";
|
|
goto warn;
|
|
}
|
|
|
|
line[eok] = '\0'; // split into key-value pair
|
|
|
|
char *value = &line[i];
|
|
while (isspace(*++value)) {
|
|
}
|
|
|
|
error = entry_handler(tll_back(groups), line, value, theme);
|
|
if (error) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
next:
|
|
continue;
|
|
|
|
warn:;
|
|
assert(warning != NULL);
|
|
char *group = tll_length(groups) > 0 ? tll_back(groups) : "n/a";
|
|
LOG_INFO("Error during load of theme '%s' - parsing of file "
|
|
"'%s/%s/index.theme' encountered '%s' on line %d (group '%s') - continuing",
|
|
theme_name, basedir, theme_name, warning, line_no, group);
|
|
}
|
|
|
|
if (!error) {
|
|
if (tll_length(groups) > 0) {
|
|
error = group_handler(tll_back(groups), NULL, theme);
|
|
} else {
|
|
error = "empty file";
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
char *group = tll_length(groups) > 0 ? tll_back(groups) : "n/a";
|
|
LOG_WARN("Failed to load theme '%s' - parsing of file "
|
|
"'%s/%s/index.theme' failed on line %d (group '%s'): %s",
|
|
theme_name, basedir, theme_name, line_no, group, error);
|
|
destroy_theme(theme);
|
|
theme = NULL;
|
|
} else {
|
|
theme->dir = strdup(theme_name);
|
|
}
|
|
|
|
free(full_line);
|
|
fclose(theme_file);
|
|
tll_free_and_free(groups, free);
|
|
return theme;
|
|
}
|
|
|
|
themes_t
|
|
load_themes_in_dir(char *basedir)
|
|
{
|
|
themes_t themes = tll_init();
|
|
|
|
DIR *dir;
|
|
if (!(dir = opendir(basedir))) {
|
|
return themes;
|
|
}
|
|
|
|
struct dirent *entry;
|
|
while ((entry = readdir(dir))) {
|
|
if (entry->d_name[0] == '.')
|
|
continue;
|
|
|
|
struct icon_theme *theme = read_theme_file(basedir, entry->d_name);
|
|
if (theme) {
|
|
LOG_DBG("Found theme [%s]: dir: %s", theme->name, theme->dir);
|
|
tll_push_back(themes, theme);
|
|
}
|
|
}
|
|
closedir(dir);
|
|
return themes;
|
|
}
|
|
|
|
void
|
|
log_loaded_themes(themes_t themes)
|
|
{
|
|
LOG_INFO("Logging themes");
|
|
if (tll_length(themes) == 0) {
|
|
LOG_INFO("Warning: no icon themes loaded");
|
|
return;
|
|
}
|
|
const char sep[] = ", ";
|
|
size_t sep_len = strlen(sep);
|
|
|
|
size_t len = 0;
|
|
tll_foreach(themes, it) { len += strlen(it->item->name) + sep_len; }
|
|
|
|
char *str = malloc(len + 1);
|
|
if (!str) {
|
|
return;
|
|
}
|
|
char *p = str;
|
|
bool start = true;
|
|
tll_foreach(themes, it)
|
|
{
|
|
if (!start) {
|
|
memcpy(p, sep, sep_len);
|
|
p += sep_len;
|
|
}
|
|
start = false;
|
|
|
|
struct icon_theme *theme = it->item;
|
|
size_t name_len = strlen(theme->name);
|
|
memcpy(p, theme->name, name_len);
|
|
p += name_len;
|
|
}
|
|
*p = '\0';
|
|
|
|
LOG_INFO("Loaded icon themes: %s", str);
|
|
free(str);
|
|
}
|
|
|
|
void
|
|
themes_free(const struct ref *ref)
|
|
{
|
|
LOG_DBG("themes free");
|
|
struct themes *p = (struct themes *)ref;
|
|
tll_free_and_free(p->themes, destroy_theme);
|
|
free(p);
|
|
}
|
|
|
|
void
|
|
themes_dec(struct themes *t)
|
|
{
|
|
ref_dec(&t->refcount);
|
|
}
|
|
|
|
struct themes *
|
|
themes_inc(struct themes *t)
|
|
{
|
|
ref_inc(&t->refcount);
|
|
return t;
|
|
}
|
|
|
|
struct themes *
|
|
init_themes(struct basedirs *basedirs)
|
|
{
|
|
|
|
themes_t themes = tll_init();
|
|
tll_foreach(basedirs->basedirs, it)
|
|
{
|
|
themes_t dir_themes = load_themes_in_dir(it->item);
|
|
tll_foreach(dir_themes, it)
|
|
{
|
|
struct icon_theme *theme = it->item;
|
|
tll_remove(dir_themes, it);
|
|
tll_push_back(themes, theme);
|
|
}
|
|
}
|
|
|
|
log_loaded_themes(themes);
|
|
struct themes *out = malloc(sizeof(*out));
|
|
out->themes = themes;
|
|
out->refcount = (struct ref){themes_free, 1};
|
|
return out;
|
|
}
|
|
|
|
static char *
|
|
find_icon_in_subdir(char *name, char *basedir, char *theme, char *subdir)
|
|
{
|
|
static const char *extensions[] = {
|
|
"svg",
|
|
"png",
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(extensions) / sizeof(*extensions); ++i) {
|
|
char *path = format_str("%s/%s/%s/%s.%s", basedir, theme, subdir, name, extensions[i]);
|
|
if (path && access(path, R_OK) == 0) {
|
|
return path;
|
|
}
|
|
free(path);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static bool
|
|
theme_exists_in_basedir(char *theme, char *basedir)
|
|
{
|
|
char *path = format_str("%s/%s", basedir, theme);
|
|
bool ret = dir_exists(path);
|
|
free(path);
|
|
return ret;
|
|
}
|
|
|
|
static char *
|
|
find_icon_with_theme(string_list_t basedirs, themes_t themes, char *name, int size, char *theme_name, int *min_size,
|
|
int *max_size)
|
|
{
|
|
LOG_DBG("Looking for icon [%s] in theme [%s]", name, theme_name);
|
|
struct icon_theme *theme = NULL;
|
|
tll_foreach(themes, it)
|
|
{
|
|
theme = it->item;
|
|
if (strcmp(theme->name, theme_name) == 0) {
|
|
break;
|
|
}
|
|
theme = NULL;
|
|
}
|
|
if (!theme)
|
|
return NULL;
|
|
|
|
char *icon = NULL;
|
|
tll_foreach(basedirs, bd_it)
|
|
{
|
|
if (!theme_exists_in_basedir(theme->dir, bd_it->item)) {
|
|
continue;
|
|
}
|
|
|
|
tll_rforeach(theme->subdirs, sd_it)
|
|
{
|
|
struct icon_theme_subdir *subdir = sd_it->item;
|
|
if (size >= subdir->min_size && size <= subdir->max_size) {
|
|
if ((icon = find_icon_in_subdir(name, bd_it->item, theme->dir, subdir->name))) {
|
|
*min_size = subdir->min_size;
|
|
*max_size = subdir->max_size;
|
|
LOG_DBG("Found icon [%s] in theme [%s]", name, theme_name);
|
|
return icon;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// inexact match
|
|
unsigned smallest_error = -1; // UINT_MAX
|
|
tll_foreach(basedirs, bd_it)
|
|
{
|
|
if (!theme_exists_in_basedir(theme->dir, bd_it->item)) {
|
|
continue;
|
|
}
|
|
|
|
tll_rforeach(theme->subdirs, sd_it)
|
|
{
|
|
struct icon_theme_subdir *subdir = sd_it->item;
|
|
unsigned error = (size > subdir->max_size ? size - subdir->max_size : 0)
|
|
+ (size < subdir->min_size ? subdir->min_size - size : 0);
|
|
if (error < smallest_error) {
|
|
char *test_icon = find_icon_in_subdir(name, bd_it->item, theme->dir, subdir->name);
|
|
if (test_icon) {
|
|
if (icon)
|
|
free(icon);
|
|
icon = test_icon;
|
|
smallest_error = error;
|
|
*min_size = subdir->min_size;
|
|
*max_size = subdir->max_size;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!icon) {
|
|
tll_foreach(theme->inherits, it)
|
|
{
|
|
icon = find_icon_with_theme(basedirs, themes, name, size, it->item, min_size, max_size);
|
|
if (icon) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return icon;
|
|
}
|
|
|
|
static char *
|
|
find_fallback_icon(string_list_t basedirs, char *name, int *min_size, int *max_size)
|
|
{
|
|
tll_foreach(basedirs, it)
|
|
{
|
|
char *icon = find_icon_in_subdir(name, it->item, "", "");
|
|
if (icon) {
|
|
*min_size = 1;
|
|
*max_size = 512;
|
|
LOG_DBG("Found icon [%s] in fallback theme [%s]", name, it->item);
|
|
return icon;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
char *
|
|
find_icon(themes_t themes, string_list_t basedirs, char *name, int size, string_list_t icon_themes, int *min_size,
|
|
int *max_size)
|
|
{
|
|
// TODO https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#implementation_notes
|
|
//
|
|
char *icon = NULL;
|
|
bool seenHicolor = false;
|
|
tll_foreach(icon_themes, it)
|
|
{
|
|
icon = find_icon_with_theme(basedirs, themes, name, size, it->item, min_size, max_size);
|
|
if (icon) {
|
|
return icon;
|
|
}
|
|
seenHicolor |= strcmp(it->item, "Hicolor") == 0;
|
|
}
|
|
if (!seenHicolor) {
|
|
icon = find_icon_with_theme(basedirs, themes, name, size, "Hicolor", min_size, max_size);
|
|
}
|
|
if (!icon) {
|
|
icon = find_fallback_icon(basedirs, name, min_size, max_size);
|
|
}
|
|
|
|
return icon;
|
|
}
|