diff --git a/include/swaybar/tray/icon.h b/include/swaybar/tray/icon.h new file mode 100644 index 00000000..7a6c400c --- /dev/null +++ b/include/swaybar/tray/icon.h @@ -0,0 +1,44 @@ +#ifndef _SWAYBAR_TRAY_ICON_H +#define _SWAYBAR_TRAY_ICON_H + +#include "list.h" + +enum subdir_type { + THRESHOLD, + SCALABLE, + FIXED +}; + +struct icon_theme_subdir { + char *name; + int size; + enum subdir_type type; + int max_size; + int min_size; + int threshold; +}; + +struct icon_theme { + char *name; + char *comment; + char *inherits; + list_t *directories; // char * + + char *dir; + list_t *subdirs; // struct icon_theme_subdir * +}; + +void init_themes(list_t **themes, list_t **basedirs); +void finish_themes(list_t *themes, list_t *basedirs); + +/* + * Finds an icon of a specified size given a list of themes and base directories. + * If the icon is found, the pointers min_size & max_size are set to minimum & + * maximum size that the icon can be scaled to, respectively. + * Returns: path of icon (which should be freed), or NULL if the icon is not found. + */ +char *find_icon(list_t *themes, list_t *basedirs, char *name, int size, + char *theme, int *min_size, int *max_size); +char *find_icon_in_dir(char *name, char *dir, int *min_size, int *max_size); + +#endif diff --git a/include/swaybar/tray/tray.h b/include/swaybar/tray/tray.h index cc3e8133..8e15fb86 100644 --- a/include/swaybar/tray/tray.h +++ b/include/swaybar/tray/tray.h @@ -9,6 +9,7 @@ #endif #include #include +#include "list.h" struct swaybar; struct swaybar_output; @@ -22,6 +23,9 @@ struct swaybar_tray { struct swaybar_watcher *watcher_xdg; struct swaybar_watcher *watcher_kde; + + list_t *basedirs; // char * + list_t *themes; // struct swaybar_theme * }; struct swaybar_tray *create_tray(struct swaybar *bar); diff --git a/swaybar/meson.build b/swaybar/meson.build index ef64f33a..b8ed2c23 100644 --- a/swaybar/meson.build +++ b/swaybar/meson.build @@ -1,4 +1,5 @@ tray_files = get_option('enable-tray') ? [ + 'tray/icon.c', 'tray/tray.c', 'tray/watcher.c' ] : [] diff --git a/swaybar/tray/icon.c b/swaybar/tray/icon.c new file mode 100644 index 00000000..67805858 --- /dev/null +++ b/swaybar/tray/icon.c @@ -0,0 +1,462 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "swaybar/tray/icon.h" +#include "config.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +static bool dir_exists(char *path) { + struct stat sb; + return stat(path, &sb) == 0 && S_ISDIR(sb.st_mode); +} + +static list_t *get_basedirs(void) { + list_t *basedirs = create_list(); + list_add(basedirs, strdup("$HOME/.icons")); // deprecated + + char *data_home = getenv("XDG_DATA_HOME"); + list_add(basedirs, strdup(data_home && *data_home ? + "$XDG_DATA_HOME/icons" : "$HOME/.local/share/icons")); + + list_add(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 { + size_t path_len = snprintf(NULL, 0, "%s/icons", dir) + 1; + char *path = malloc(path_len); + snprintf(path, path_len, "%s/icons", dir); + list_add(basedirs, path); + } while ((dir = strtok(NULL, ":"))); + free(data_dirs); + + list_t *basedirs_expanded = create_list(); + for (int i = 0; i < basedirs->length; ++i) { + wordexp_t p; + if (wordexp(basedirs->items[i], &p, WRDE_UNDEF) == 0) { + if (dir_exists(p.we_wordv[0])) { + list_add(basedirs_expanded, strdup(p.we_wordv[0])); + } + wordfree(&p); + } + } + + list_free_items_and_destroy(basedirs); + + return basedirs_expanded; +} + +static void destroy_theme(struct icon_theme *theme) { + if (!theme) { + return; + } + free(theme->name); + free(theme->comment); + free(theme->inherits); + list_free_items_and_destroy(theme->directories); + free(theme->dir); + + for (int i = 0; i < theme->subdirs->length; ++i) { + struct icon_theme_subdir *subdir = theme->subdirs->items[i]; + free(subdir->name); + free(subdir); + } + list_free(theme->subdirs); + free(theme); +} + +static int cmp_group(const void *item, const void *cmp_to) { + return strcmp(item, cmp_to); +} + +static bool group_handler(char *old_group, char *new_group, + struct icon_theme *theme) { + if (!old_group) { // first group must be "Icon Theme" + return strcmp(new_group, "Icon Theme"); + } + + if (strcmp(old_group, "Icon Theme") == 0) { + if (!(theme->name && theme->comment && theme->directories)) { + return true; + } + } else { + if (theme->subdirs->length == 0) { // skip + return false; + } + + struct icon_theme_subdir *subdir = + theme->subdirs->items[theme->subdirs->length - 1]; + if (!subdir->size) return true; + + switch (subdir->type) { + case FIXED: subdir->max_size = subdir->min_size = subdir->size; + break; + case SCALABLE: { + if (!subdir->max_size) subdir->max_size = subdir->size; + if (!subdir->min_size) subdir->min_size = subdir->size; + break; + } + case THRESHOLD: + subdir->max_size = subdir->size + subdir->threshold; + subdir->min_size = subdir->size - subdir->threshold; + } + } + + if (new_group && list_seq_find(theme->directories, cmp_group, new_group) != -1) { + struct icon_theme_subdir *subdir = calloc(1, sizeof(struct icon_theme_subdir)); + if (!subdir) { + return true; + } + subdir->name = strdup(new_group); + subdir->threshold = 2; + list_add(theme->subdirs, subdir); + } + + return false; +} + +static int 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); + } else if (strcmp(key, "Comment") == 0) { + theme->comment = strdup(value); + } else if (strcmp(key, "Inherists") == 0) { + theme->inherits = strdup(value); + } else if (strcmp(key, "Directories") == 0) { + theme->directories = split_string(value, ","); + } // Ignored: ScaledDirectories, Hidden, Example + } else { + if (theme->subdirs->length == 0) { // skip + return false; + } + + struct icon_theme_subdir *subdir = + theme->subdirs->items[theme->subdirs->length - 1]; + if (strcmp(subdir->name, group) != 0) { // skip + return false; + } + + char *end; + int n = strtol(value, &end, 10); + if (strcmp(key, "Size") == 0) { + subdir->size = n; + return *end != '\0'; + } else if (strcmp(key, "Type") == 0) { + if (strcmp(value, "Fixed") == 0) { + subdir->type = FIXED; + } else if (strcmp(value, "Scalable") == 0) { + subdir->type = SCALABLE; + } else if (strcmp(value, "Threshold") == 0) { + subdir->type = THRESHOLD; + } else { + return true; + } + } else if (strcmp(key, "MaxSize") == 0) { + subdir->max_size = n; + return *end != '\0'; + } else if (strcmp(key, "MinSize") == 0) { + subdir->min_size = n; + return *end != '\0'; + } else if (strcmp(key, "Threshold") == 0) { + subdir->threshold = n; + return *end != '\0'; + } // Ignored: Scale, Applications + } + return false; +} + +/* + * 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 occured, which stops parsing + */ +static struct icon_theme *read_theme_file(char *basedir, char *theme_name) { + // look for index.theme file + size_t path_len = snprintf(NULL, 0, "%s/%s/index.theme", basedir, + theme_name) + 1; + char *path = malloc(path_len); + snprintf(path, path_len, "%s/%s/index.theme", basedir, theme_name); + FILE *theme_file = fopen(path, "r"); + if (!theme_file) { + return NULL; + } + + struct icon_theme *theme = calloc(1, sizeof(struct icon_theme)); + if (!theme) { + return NULL; + } + theme->subdirs = create_list(); + + bool error = false; + char *group = NULL; + char *full_line = NULL; + size_t full_len = 0; + ssize_t nread; + while ((nread = getline(&full_line, &full_len, theme_file)) != -1) { + char *line = full_line - 1; + while (isspace(*++line)) {} // remove leading whitespace + if (!*line || line[0] == '#') continue; // ignore blank lines & comments + + int len = nread - (line - full_line); + while (isspace(line[--len])) {} + line[++len] = '\0'; // remove trailing whitespace + + if (line[0] == '[') { // group header + // check well-formed + if (line[--len] != ']') { + error = true; + break; + } + int i = 1; + for (; !iscntrl(line[i]) && line[i] != '[' && line[i] != ']'; ++i) {} + if (i < len) { + error = true; + break; + } + + // call handler + line[len] = '\0'; + error = group_handler(group, &line[1], theme); + if (error) { + break; + } + free(group); + group = strdup(&line[1]); + } else { // key-value pair + // check well-formed + int eok = 0; + for (; isalnum(line[eok]) || line[eok] == '-'; ++eok) {} // TODO locale? + int i = eok - 1; + while (isspace(line[++i])) {} + if (line[i] != '=') { + error = true; + break; + } + + line[eok] = '\0'; // split into key-value pair + char *value = &line[i]; + while (isspace(*++value)) {} + // TODO unescape value + error = entry_handler(group, line, value, theme); + if (error) { + break; + } + } + } + + if (!error && group) { + error = group_handler(group, NULL, theme); + } + + free(group); + free(full_line); + fclose(theme_file); + + if (!error) { + theme->dir = strdup(theme_name); + return theme; + } else { + destroy_theme(theme); + return NULL; + } +} + +static list_t *load_themes_in_dir(char *basedir) { + DIR *dir; + if (!(dir = opendir(basedir))) { + return NULL; + } + + list_t *themes = create_list(); + 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) { + list_add(themes, theme); + } + } + return themes; +} + +void init_themes(list_t **themes, list_t **basedirs) { + *basedirs = get_basedirs(); + + *themes = create_list(); + for (int i = 0; i < (*basedirs)->length; ++i) { + list_t *dir_themes = load_themes_in_dir((*basedirs)->items[i]); + list_cat(*themes, dir_themes); + list_free(dir_themes); + } + + list_t *theme_names = create_list(); + for (int i = 0; i < (*themes)->length; ++i) { + struct icon_theme *theme = (*themes)->items[i]; + list_add(theme_names, theme->name); + } + wlr_log(WLR_DEBUG, "Loaded themes: %s", join_list(theme_names, ", ")); + list_free(theme_names); +} + +void finish_themes(list_t *themes, list_t *basedirs) { + for (int i = 0; i < themes->length; ++i) { + destroy_theme(themes->items[i]); + } + list_free(themes); + list_free_items_and_destroy(basedirs); +} + +static char *find_icon_in_subdir(char *name, char *basedir, char *theme, + char *subdir) { + static const char *extensions[] = { +#if HAVE_GDK_PIXBUF + "svg", +#endif + "png", +#if HAVE_GDK_PIXBUF + "xpm" +#endif + }; + + size_t path_len = snprintf(NULL, 0, "%s/%s/%s/%s.EXT", basedir, theme, + subdir, name) + 1; + char *path = malloc(path_len); + + for (size_t i = 0; i < sizeof(extensions) / sizeof(*extensions); ++i) { + snprintf(path, path_len, "%s/%s/%s/%s.%s", basedir, theme, subdir, + name, extensions[i]); + if (access(path, R_OK) == 0) { + return path; + } + } + + free(path); + return NULL; +} + +static bool theme_exists_in_basedir(char *theme, char *basedir) { + size_t path_len = snprintf(NULL, 0, "%s/%s", basedir, theme) + 1; + char *path = malloc(path_len); + snprintf(path, path_len, "%s/%s", basedir, theme); + bool ret = dir_exists(path); + free(path); + return ret; +} + +static char *find_icon_with_theme(list_t *basedirs, list_t *themes, char *name, + int size, char *theme_name, int *min_size, int *max_size) { + struct icon_theme *theme = NULL; + for (int i = 0; i < themes->length; ++i) { + theme = themes->items[i]; + if (strcmp(theme->name, theme_name) == 0) { + break; + } + theme = NULL; + } + if (!theme) return NULL; + + char *icon = NULL; + for (int i = 0; i < basedirs->length; ++i) { + if (!theme_exists_in_basedir(theme->dir, basedirs->items[i])) { + continue; + } + // search backwards to hopefully hit scalable/larger icons first + for (int j = theme->subdirs->length - 1; j >= 0; --j) { + struct icon_theme_subdir *subdir = theme->subdirs->items[j]; + if (size >= subdir->min_size && size <= subdir->max_size) { + if ((icon = find_icon_in_subdir(name, basedirs->items[i], + theme->dir, subdir->name))) { + *min_size = subdir->min_size; + *max_size = subdir->max_size; + return icon; + } + } + } + } + + // inexact match + unsigned smallest_error = -1; // UINT_MAX + for (int i = 0; i < basedirs->length; ++i) { + if (!theme_exists_in_basedir(theme->dir, basedirs->items[i])) { + continue; + } + for (int j = theme->subdirs->length - 1; j >= 0; --j) { + struct icon_theme_subdir *subdir = theme->subdirs->items[j]; + 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, basedirs->items[i], + theme->dir, subdir->name); + if (test_icon) { + icon = test_icon; + smallest_error = error; + *min_size = subdir->min_size; + *max_size = subdir->max_size; + } + } + } + } + + if (!icon && theme->inherits) { + icon = find_icon_with_theme(basedirs, themes, name, size, + theme->inherits, min_size, max_size); + } + + return icon; +} + +char *find_icon_in_dir(char *name, char *dir, int *min_size, int *max_size) { + char *icon = find_icon_in_subdir(name, dir, "", ""); + if (icon) { + *min_size = 1; + *max_size = 512; + } + return icon; + +} + +static char *find_fallback_icon(list_t *basedirs, char *name, int *min_size, + int *max_size) { + for (int i = 0; i < basedirs->length; ++i) { + char *icon = find_icon_in_dir(name, basedirs->items[i], min_size, max_size); + if (icon) { + return icon; + } + } + return NULL; +} + +char *find_icon(list_t *themes, list_t *basedirs, char *name, int size, + char *theme, int *min_size, int *max_size) { + // TODO https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#implementation_notes + char *icon = NULL; + if (theme) { + icon = find_icon_with_theme(basedirs, themes, name, size, theme, + min_size, max_size); + } + if (!icon) { + 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; +} diff --git a/swaybar/tray/tray.c b/swaybar/tray/tray.c index 36ee3c30..c1d3b50b 100644 --- a/swaybar/tray/tray.c +++ b/swaybar/tray/tray.c @@ -3,6 +3,7 @@ #include #include #include "swaybar/bar.h" +#include "swaybar/tray/icon.h" #include "swaybar/tray/tray.h" #include "swaybar/tray/watcher.h" #include "log.h" @@ -28,6 +29,8 @@ struct swaybar_tray *create_tray(struct swaybar *bar) { tray->watcher_xdg = create_watcher("freedesktop", tray->bus); tray->watcher_kde = create_watcher("kde", tray->bus); + init_themes(&tray->themes, &tray->basedirs); + return tray; } @@ -38,6 +41,7 @@ void destroy_tray(struct swaybar_tray *tray) { destroy_watcher(tray->watcher_xdg); destroy_watcher(tray->watcher_kde); sd_bus_flush_close_unref(tray->bus); + finish_themes(tray->themes, tray->basedirs); free(tray); }