diff options
author | 2018-12-31 15:43:23 -0500 | |
---|---|---|
committer | 2018-12-31 15:43:23 -0500 | |
commit | 3d7c20f062bebe39199d3872e5b64d5e8d61d100 (patch) | |
tree | 93b8ddb51873cae14db8d5f07db74ca96236f184 /swaybar/tray/icon.c | |
parent | Apply implicit fallback seat config (diff) | |
parent | swaybar: use KDE's SNI IconThemePath property (diff) | |
download | sway-3d7c20f062bebe39199d3872e5b64d5e8d61d100.tar.gz sway-3d7c20f062bebe39199d3872e5b64d5e8d61d100.tar.zst sway-3d7c20f062bebe39199d3872e5b64d5e8d61d100.zip |
Merge pull request #3249 from ianyfan/tray
Swaybar tray
Diffstat (limited to 'swaybar/tray/icon.c')
-rw-r--r-- | swaybar/tray/icon.c | 462 |
1 files changed, 462 insertions, 0 deletions
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 @@ | |||
1 | #define _POSIX_C_SOURCE 200809L | ||
2 | #include <ctype.h> | ||
3 | #include <dirent.h> | ||
4 | #include <stdbool.h> | ||
5 | #include <stdio.h> | ||
6 | #include <stdlib.h> | ||
7 | #include <string.h> | ||
8 | #include <sys/stat.h> | ||
9 | #include <unistd.h> | ||
10 | #include <wordexp.h> | ||
11 | #include "swaybar/tray/icon.h" | ||
12 | #include "config.h" | ||
13 | #include "list.h" | ||
14 | #include "log.h" | ||
15 | #include "stringop.h" | ||
16 | |||
17 | static bool dir_exists(char *path) { | ||
18 | struct stat sb; | ||
19 | return stat(path, &sb) == 0 && S_ISDIR(sb.st_mode); | ||
20 | } | ||
21 | |||
22 | static list_t *get_basedirs(void) { | ||
23 | list_t *basedirs = create_list(); | ||
24 | list_add(basedirs, strdup("$HOME/.icons")); // deprecated | ||
25 | |||
26 | char *data_home = getenv("XDG_DATA_HOME"); | ||
27 | list_add(basedirs, strdup(data_home && *data_home ? | ||
28 | "$XDG_DATA_HOME/icons" : "$HOME/.local/share/icons")); | ||
29 | |||
30 | list_add(basedirs, strdup("/usr/share/pixmaps")); | ||
31 | |||
32 | char *data_dirs = getenv("XDG_DATA_DIRS"); | ||
33 | if (!(data_dirs && *data_dirs)) { | ||
34 | data_dirs = "/usr/local/share:/usr/share"; | ||
35 | } | ||
36 | data_dirs = strdup(data_dirs); | ||
37 | char *dir = strtok(data_dirs, ":"); | ||
38 | do { | ||
39 | size_t path_len = snprintf(NULL, 0, "%s/icons", dir) + 1; | ||
40 | char *path = malloc(path_len); | ||
41 | snprintf(path, path_len, "%s/icons", dir); | ||
42 | list_add(basedirs, path); | ||
43 | } while ((dir = strtok(NULL, ":"))); | ||
44 | free(data_dirs); | ||
45 | |||
46 | list_t *basedirs_expanded = create_list(); | ||
47 | for (int i = 0; i < basedirs->length; ++i) { | ||
48 | wordexp_t p; | ||
49 | if (wordexp(basedirs->items[i], &p, WRDE_UNDEF) == 0) { | ||
50 | if (dir_exists(p.we_wordv[0])) { | ||
51 | list_add(basedirs_expanded, strdup(p.we_wordv[0])); | ||
52 | } | ||
53 | wordfree(&p); | ||
54 | } | ||
55 | } | ||
56 | |||
57 | list_free_items_and_destroy(basedirs); | ||
58 | |||
59 | return basedirs_expanded; | ||
60 | } | ||
61 | |||
62 | static void destroy_theme(struct icon_theme *theme) { | ||
63 | if (!theme) { | ||
64 | return; | ||
65 | } | ||
66 | free(theme->name); | ||
67 | free(theme->comment); | ||
68 | free(theme->inherits); | ||
69 | list_free_items_and_destroy(theme->directories); | ||
70 | free(theme->dir); | ||
71 | |||
72 | for (int i = 0; i < theme->subdirs->length; ++i) { | ||
73 | struct icon_theme_subdir *subdir = theme->subdirs->items[i]; | ||
74 | free(subdir->name); | ||
75 | free(subdir); | ||
76 | } | ||
77 | list_free(theme->subdirs); | ||
78 | free(theme); | ||
79 | } | ||
80 | |||
81 | static int cmp_group(const void *item, const void *cmp_to) { | ||
82 | return strcmp(item, cmp_to); | ||
83 | } | ||
84 | |||
85 | static bool group_handler(char *old_group, char *new_group, | ||
86 | struct icon_theme *theme) { | ||
87 | if (!old_group) { // first group must be "Icon Theme" | ||
88 | return strcmp(new_group, "Icon Theme"); | ||
89 | } | ||
90 | |||
91 | if (strcmp(old_group, "Icon Theme") == 0) { | ||
92 | if (!(theme->name && theme->comment && theme->directories)) { | ||
93 | return true; | ||
94 | } | ||
95 | } else { | ||
96 | if (theme->subdirs->length == 0) { // skip | ||
97 | return false; | ||
98 | } | ||
99 | |||
100 | struct icon_theme_subdir *subdir = | ||
101 | theme->subdirs->items[theme->subdirs->length - 1]; | ||
102 | if (!subdir->size) return true; | ||
103 | |||
104 | switch (subdir->type) { | ||
105 | case FIXED: subdir->max_size = subdir->min_size = subdir->size; | ||
106 | break; | ||
107 | case SCALABLE: { | ||
108 | if (!subdir->max_size) subdir->max_size = subdir->size; | ||
109 | if (!subdir->min_size) subdir->min_size = subdir->size; | ||
110 | break; | ||
111 | } | ||
112 | case THRESHOLD: | ||
113 | subdir->max_size = subdir->size + subdir->threshold; | ||
114 | subdir->min_size = subdir->size - subdir->threshold; | ||
115 | } | ||
116 | } | ||
117 | |||
118 | if (new_group && list_seq_find(theme->directories, cmp_group, new_group) != -1) { | ||
119 | struct icon_theme_subdir *subdir = calloc(1, sizeof(struct icon_theme_subdir)); | ||
120 | if (!subdir) { | ||
121 | return true; | ||
122 | } | ||
123 | subdir->name = strdup(new_group); | ||
124 | subdir->threshold = 2; | ||
125 | list_add(theme->subdirs, subdir); | ||
126 | } | ||
127 | |||
128 | return false; | ||
129 | } | ||
130 | |||
131 | static int entry_handler(char *group, char *key, char *value, | ||
132 | struct icon_theme *theme) { | ||
133 | if (strcmp(group, "Icon Theme") == 0) { | ||
134 | if (strcmp(key, "Name") == 0) { | ||
135 | theme->name = strdup(value); | ||
136 | } else if (strcmp(key, "Comment") == 0) { | ||
137 | theme->comment = strdup(value); | ||
138 | } else if (strcmp(key, "Inherists") == 0) { | ||
139 | theme->inherits = strdup(value); | ||
140 | } else if (strcmp(key, "Directories") == 0) { | ||
141 | theme->directories = split_string(value, ","); | ||
142 | } // Ignored: ScaledDirectories, Hidden, Example | ||
143 | } else { | ||
144 | if (theme->subdirs->length == 0) { // skip | ||
145 | return false; | ||
146 | } | ||
147 | |||
148 | struct icon_theme_subdir *subdir = | ||
149 | theme->subdirs->items[theme->subdirs->length - 1]; | ||
150 | if (strcmp(subdir->name, group) != 0) { // skip | ||
151 | return false; | ||
152 | } | ||
153 | |||
154 | char *end; | ||
155 | int n = strtol(value, &end, 10); | ||
156 | if (strcmp(key, "Size") == 0) { | ||
157 | subdir->size = n; | ||
158 | return *end != '\0'; | ||
159 | } else if (strcmp(key, "Type") == 0) { | ||
160 | if (strcmp(value, "Fixed") == 0) { | ||
161 | subdir->type = FIXED; | ||
162 | } else if (strcmp(value, "Scalable") == 0) { | ||
163 | subdir->type = SCALABLE; | ||
164 | } else if (strcmp(value, "Threshold") == 0) { | ||
165 | subdir->type = THRESHOLD; | ||
166 | } else { | ||
167 | return true; | ||
168 | } | ||
169 | } else if (strcmp(key, "MaxSize") == 0) { | ||
170 | subdir->max_size = n; | ||
171 | return *end != '\0'; | ||
172 | } else if (strcmp(key, "MinSize") == 0) { | ||
173 | subdir->min_size = n; | ||
174 | return *end != '\0'; | ||
175 | } else if (strcmp(key, "Threshold") == 0) { | ||
176 | subdir->threshold = n; | ||
177 | return *end != '\0'; | ||
178 | } // Ignored: Scale, Applications | ||
179 | } | ||
180 | return false; | ||
181 | } | ||
182 | |||
183 | /* | ||
184 | * This is a Freedesktop Desktop Entry parser (essentially INI) | ||
185 | * It calls entry_handler for every entry | ||
186 | * and group_handler between every group (as well as at both ends) | ||
187 | * Handlers return whether an error occured, which stops parsing | ||
188 | */ | ||
189 | static struct icon_theme *read_theme_file(char *basedir, char *theme_name) { | ||
190 | // look for index.theme file | ||
191 | size_t path_len = snprintf(NULL, 0, "%s/%s/index.theme", basedir, | ||
192 | theme_name) + 1; | ||
193 | char *path = malloc(path_len); | ||
194 | snprintf(path, path_len, "%s/%s/index.theme", basedir, theme_name); | ||
195 | FILE *theme_file = fopen(path, "r"); | ||
196 | if (!theme_file) { | ||
197 | return NULL; | ||
198 | } | ||
199 | |||
200 | struct icon_theme *theme = calloc(1, sizeof(struct icon_theme)); | ||
201 | if (!theme) { | ||
202 | return NULL; | ||
203 | } | ||
204 | theme->subdirs = create_list(); | ||
205 | |||
206 | bool error = false; | ||
207 | char *group = NULL; | ||
208 | char *full_line = NULL; | ||
209 | size_t full_len = 0; | ||
210 | ssize_t nread; | ||
211 | while ((nread = getline(&full_line, &full_len, theme_file)) != -1) { | ||
212 | char *line = full_line - 1; | ||
213 | while (isspace(*++line)) {} // remove leading whitespace | ||
214 | if (!*line || line[0] == '#') continue; // ignore blank lines & comments | ||
215 | |||
216 | int len = nread - (line - full_line); | ||
217 | while (isspace(line[--len])) {} | ||
218 | line[++len] = '\0'; // remove trailing whitespace | ||
219 | |||
220 | if (line[0] == '[') { // group header | ||
221 | // check well-formed | ||
222 | if (line[--len] != ']') { | ||
223 | error = true; | ||
224 | break; | ||
225 | } | ||
226 | int i = 1; | ||
227 | for (; !iscntrl(line[i]) && line[i] != '[' && line[i] != ']'; ++i) {} | ||
228 | if (i < len) { | ||
229 | error = true; | ||
230 | break; | ||
231 | } | ||
232 | |||
233 | // call handler | ||
234 | line[len] = '\0'; | ||
235 | error = group_handler(group, &line[1], theme); | ||
236 | if (error) { | ||
237 | break; | ||
238 | } | ||
239 | free(group); | ||
240 | group = strdup(&line[1]); | ||
241 | } else { // key-value pair | ||
242 | // check well-formed | ||
243 | int eok = 0; | ||
244 | for (; isalnum(line[eok]) || line[eok] == '-'; ++eok) {} // TODO locale? | ||
245 | int i = eok - 1; | ||
246 | while (isspace(line[++i])) {} | ||
247 | if (line[i] != '=') { | ||
248 | error = true; | ||
249 | break; | ||
250 | } | ||
251 | |||
252 | line[eok] = '\0'; // split into key-value pair | ||
253 | char *value = &line[i]; | ||
254 | while (isspace(*++value)) {} | ||
255 | // TODO unescape value | ||
256 | error = entry_handler(group, line, value, theme); | ||
257 | if (error) { | ||
258 | break; | ||
259 | } | ||
260 | } | ||
261 | } | ||
262 | |||
263 | if (!error && group) { | ||
264 | error = group_handler(group, NULL, theme); | ||
265 | } | ||
266 | |||
267 | free(group); | ||
268 | free(full_line); | ||
269 | fclose(theme_file); | ||
270 | |||
271 | if (!error) { | ||
272 | theme->dir = strdup(theme_name); | ||
273 | return theme; | ||
274 | } else { | ||
275 | destroy_theme(theme); | ||
276 | return NULL; | ||
277 | } | ||
278 | } | ||
279 | |||
280 | static list_t *load_themes_in_dir(char *basedir) { | ||
281 | DIR *dir; | ||
282 | if (!(dir = opendir(basedir))) { | ||
283 | return NULL; | ||
284 | } | ||
285 | |||
286 | list_t *themes = create_list(); | ||
287 | struct dirent *entry; | ||
288 | while ((entry = readdir(dir))) { | ||
289 | if (entry->d_name[0] == '.') continue; | ||
290 | |||
291 | struct icon_theme *theme = read_theme_file(basedir, entry->d_name); | ||
292 | if (theme) { | ||
293 | list_add(themes, theme); | ||
294 | } | ||
295 | } | ||
296 | return themes; | ||
297 | } | ||
298 | |||
299 | void init_themes(list_t **themes, list_t **basedirs) { | ||
300 | *basedirs = get_basedirs(); | ||
301 | |||
302 | *themes = create_list(); | ||
303 | for (int i = 0; i < (*basedirs)->length; ++i) { | ||
304 | list_t *dir_themes = load_themes_in_dir((*basedirs)->items[i]); | ||
305 | list_cat(*themes, dir_themes); | ||
306 | list_free(dir_themes); | ||
307 | } | ||
308 | |||
309 | list_t *theme_names = create_list(); | ||
310 | for (int i = 0; i < (*themes)->length; ++i) { | ||
311 | struct icon_theme *theme = (*themes)->items[i]; | ||
312 | list_add(theme_names, theme->name); | ||
313 | } | ||
314 | wlr_log(WLR_DEBUG, "Loaded themes: %s", join_list(theme_names, ", ")); | ||
315 | list_free(theme_names); | ||
316 | } | ||
317 | |||
318 | void finish_themes(list_t *themes, list_t *basedirs) { | ||
319 | for (int i = 0; i < themes->length; ++i) { | ||
320 | destroy_theme(themes->items[i]); | ||
321 | } | ||
322 | list_free(themes); | ||
323 | list_free_items_and_destroy(basedirs); | ||
324 | } | ||
325 | |||
326 | static char *find_icon_in_subdir(char *name, char *basedir, char *theme, | ||
327 | char *subdir) { | ||
328 | static const char *extensions[] = { | ||
329 | #if HAVE_GDK_PIXBUF | ||
330 | "svg", | ||
331 | #endif | ||
332 | "png", | ||
333 | #if HAVE_GDK_PIXBUF | ||
334 | "xpm" | ||
335 | #endif | ||
336 | }; | ||
337 | |||
338 | size_t path_len = snprintf(NULL, 0, "%s/%s/%s/%s.EXT", basedir, theme, | ||
339 | subdir, name) + 1; | ||
340 | char *path = malloc(path_len); | ||
341 | |||
342 | for (size_t i = 0; i < sizeof(extensions) / sizeof(*extensions); ++i) { | ||
343 | snprintf(path, path_len, "%s/%s/%s/%s.%s", basedir, theme, subdir, | ||
344 | name, extensions[i]); | ||
345 | if (access(path, R_OK) == 0) { | ||
346 | return path; | ||
347 | } | ||
348 | } | ||
349 | |||
350 | free(path); | ||
351 | return NULL; | ||
352 | } | ||
353 | |||
354 | static bool theme_exists_in_basedir(char *theme, char *basedir) { | ||
355 | size_t path_len = snprintf(NULL, 0, "%s/%s", basedir, theme) + 1; | ||
356 | char *path = malloc(path_len); | ||
357 | snprintf(path, path_len, "%s/%s", basedir, theme); | ||
358 | bool ret = dir_exists(path); | ||
359 | free(path); | ||
360 | return ret; | ||
361 | } | ||
362 | |||
363 | static char *find_icon_with_theme(list_t *basedirs, list_t *themes, char *name, | ||
364 | int size, char *theme_name, int *min_size, int *max_size) { | ||
365 | struct icon_theme *theme = NULL; | ||
366 | for (int i = 0; i < themes->length; ++i) { | ||
367 | theme = themes->items[i]; | ||
368 | if (strcmp(theme->name, theme_name) == 0) { | ||
369 | break; | ||
370 | } | ||
371 | theme = NULL; | ||
372 | } | ||
373 | if (!theme) return NULL; | ||
374 | |||
375 | char *icon = NULL; | ||
376 | for (int i = 0; i < basedirs->length; ++i) { | ||
377 | if (!theme_exists_in_basedir(theme->dir, basedirs->items[i])) { | ||
378 | continue; | ||
379 | } | ||
380 | // search backwards to hopefully hit scalable/larger icons first | ||
381 | for (int j = theme->subdirs->length - 1; j >= 0; --j) { | ||
382 | struct icon_theme_subdir *subdir = theme->subdirs->items[j]; | ||
383 | if (size >= subdir->min_size && size <= subdir->max_size) { | ||
384 | if ((icon = find_icon_in_subdir(name, basedirs->items[i], | ||
385 | theme->dir, subdir->name))) { | ||
386 | *min_size = subdir->min_size; | ||
387 | *max_size = subdir->max_size; | ||
388 | return icon; | ||
389 | } | ||
390 | } | ||
391 | } | ||
392 | } | ||
393 | |||
394 | // inexact match | ||
395 | unsigned smallest_error = -1; // UINT_MAX | ||
396 | for (int i = 0; i < basedirs->length; ++i) { | ||
397 | if (!theme_exists_in_basedir(theme->dir, basedirs->items[i])) { | ||
398 | continue; | ||
399 | } | ||
400 | for (int j = theme->subdirs->length - 1; j >= 0; --j) { | ||
401 | struct icon_theme_subdir *subdir = theme->subdirs->items[j]; | ||
402 | unsigned error = (size > subdir->max_size ? size - subdir->max_size : 0) | ||
403 | + (size < subdir->min_size ? subdir->min_size - size : 0); | ||
404 | if (error < smallest_error) { | ||
405 | char *test_icon = find_icon_in_subdir(name, basedirs->items[i], | ||
406 | theme->dir, subdir->name); | ||
407 | if (test_icon) { | ||
408 | icon = test_icon; | ||
409 | smallest_error = error; | ||
410 | *min_size = subdir->min_size; | ||
411 | *max_size = subdir->max_size; | ||
412 | } | ||
413 | } | ||
414 | } | ||
415 | } | ||
416 | |||
417 | if (!icon && theme->inherits) { | ||
418 | icon = find_icon_with_theme(basedirs, themes, name, size, | ||
419 | theme->inherits, min_size, max_size); | ||
420 | } | ||
421 | |||
422 | return icon; | ||
423 | } | ||
424 | |||
425 | char *find_icon_in_dir(char *name, char *dir, int *min_size, int *max_size) { | ||
426 | char *icon = find_icon_in_subdir(name, dir, "", ""); | ||
427 | if (icon) { | ||
428 | *min_size = 1; | ||
429 | *max_size = 512; | ||
430 | } | ||
431 | return icon; | ||
432 | |||
433 | } | ||
434 | |||
435 | static char *find_fallback_icon(list_t *basedirs, char *name, int *min_size, | ||
436 | int *max_size) { | ||
437 | for (int i = 0; i < basedirs->length; ++i) { | ||
438 | char *icon = find_icon_in_dir(name, basedirs->items[i], min_size, max_size); | ||
439 | if (icon) { | ||
440 | return icon; | ||
441 | } | ||
442 | } | ||
443 | return NULL; | ||
444 | } | ||
445 | |||
446 | char *find_icon(list_t *themes, list_t *basedirs, char *name, int size, | ||
447 | char *theme, int *min_size, int *max_size) { | ||
448 | // TODO https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#implementation_notes | ||
449 | char *icon = NULL; | ||
450 | if (theme) { | ||
451 | icon = find_icon_with_theme(basedirs, themes, name, size, theme, | ||
452 | min_size, max_size); | ||
453 | } | ||
454 | if (!icon) { | ||
455 | icon = find_icon_with_theme(basedirs, themes, name, size, "Hicolor", | ||
456 | min_size, max_size); | ||
457 | } | ||
458 | if (!icon) { | ||
459 | icon = find_fallback_icon(basedirs, name, min_size, max_size); | ||
460 | } | ||
461 | return icon; | ||
462 | } | ||