From ccc6d0502ee55233f02310b8455db80c77c8eaf5 Mon Sep 17 00:00:00 2001 From: Rumen Date: Fri, 24 Oct 2025 16:20:39 +0200 Subject: [PATCH] appicons patch --- patches/appicons/README.md | 26 +++ patches/appicons/appicons.patch | 286 ++++++++++++++++++++++++++++++++ patches/appicons/example.png | Bin 0 -> 5608 bytes 3 files changed, 312 insertions(+) create mode 100644 patches/appicons/README.md create mode 100644 patches/appicons/appicons.patch create mode 100644 patches/appicons/example.png diff --git a/patches/appicons/README.md b/patches/appicons/README.md new file mode 100644 index 0000000..adc70e1 --- /dev/null +++ b/patches/appicons/README.md @@ -0,0 +1,26 @@ +![dwl appicons patch example](example.png) + +### Description +Adds support for app icons that can replace the tag indicator and tag name. +This feature is configurable through an additional option in `rules`. + +Icons should work out of the box. Emojis require a special font like +[Noto Color Emoji](https://fonts.google.com/noto/specimen/Noto+Color+Emoji). + +When one or more app icons are present in a tag, the tag name will be enclosed +by the outer separators (`outer_separator_beg` and `outer_separator_end`). +Additionally, the icons within the tag will be separated by `inner_separator`. + +Each tag can display a maximum of `truncate_icons_after` icons, after which the +`truncate_symbol` will be shown. + +**Inspiration:** [XMonad's DynamicIcons](https://hackage.haskell.org/package/xmonad-contrib-0.18.1/docs/XMonad-Hooks-DynamicIcons.html) + +### Prerequisites +Make sure you have the [bar](/dwl/dwl-patches/raw/branch/main/patches/bar) patch. + +### Download +- [0.8](/dwl/dwl-patches/raw/branch/main/patches/appicons/appicons.patch) +- [main 2025-10-24](/dwl/dwl-patches/raw/branch/main/patches/appicons/appicons.patch) +### Authors +- [rumenmitov](https://codeberg.org/rumenmitov) diff --git a/patches/appicons/appicons.patch b/patches/appicons/appicons.patch new file mode 100644 index 0000000..c3a24d6 --- /dev/null +++ b/patches/appicons/appicons.patch @@ -0,0 +1,286 @@ +From 04d84cccbfb0fa324f960942359c3edd92f89071 Mon Sep 17 00:00:00 2001 +From: Rumen +Date: Fri, 24 Oct 2025 16:00:58 +0200 +Subject: [PATCH] App Icons in the dwl bar! + +Adds support for app icons that can replace the tag indicator and tag name. +This feature is configurable through an additional option in rules. + +Icons should work out of the box. +Emojis require a special font like Noto Color Emoji. + +When one or more app icons are present in a tag, the tag name will be enclosed +by the outer separators (`outer_separator_beg` and `outer_separator_end`). +Additionally, the icons within the tag will be separated by inner_separator. + +Each tag can display a maximum of `truncate_icons_after` icons, after which the +`truncate_symbol` will be shown. + +Inspired by XMonad's DynamicIcons. +--- + config.def.h | 14 +++-- + dwl.c | 141 +++++++++++++++++++++++++++++++++++++++++++++++++-- + 2 files changed, 148 insertions(+), 7 deletions(-) + +diff --git a/config.def.h b/config.def.h +index 1b7472d..a48b78d 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -26,12 +26,20 @@ static char *tags[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9" }; + /* logging */ + static int log_level = WLR_ERROR; + ++/* appicons */ ++/* NOTE: set to 0 to set to default (whitespace) */ ++static char outer_separator_beg = '['; ++static char outer_separator_end = ']'; ++static char inner_separator = ' '; ++static unsigned truncate_icons_after = 2; /* will default to 1, that is the min */ ++static char truncate_symbol[] = "..."; ++ + /* NOTE: ALWAYS keep a rule declared even if you don't use rules (e.g leave at least one example) */ + static const Rule rules[] = { +- /* app_id title tags mask isfloating monitor */ ++ /* app_id title tags mask isfloating monitor appicon*/ + /* examples: */ +- { "Gimp_EXAMPLE", NULL, 0, 1, -1 }, /* Start on currently visible tags floating, not tiled */ +- { "firefox_EXAMPLE", NULL, 1 << 8, 0, -1 }, /* Start on ONLY tag "9" */ ++ { "Gimp_EXAMPLE", NULL, 0, 1, -1, NULL }, /* Start on currently visible tags floating, not tiled */ ++ { "firefox_EXAMPLE", NULL, 1 << 8, 0, -1, "" }, /* Start on ONLY tag "9" */ + }; + + /* layout(s) */ +diff --git a/dwl.c b/dwl.c +index bf340d8..33a76d9 100644 +--- a/dwl.c ++++ b/dwl.c +@@ -143,6 +143,7 @@ typedef struct { + struct wl_listener set_hints; + #endif + unsigned int bw; ++ char *appicon; + uint32_t tags; + int isfloating, isurgent, isfullscreen; + uint32_t resize; /* configure serial of a pending resize */ +@@ -221,6 +222,7 @@ struct Monitor { + unsigned int seltags; + unsigned int sellt; + uint32_t tagset[2]; ++ char **tag_icons; + float mfact; + int gamma_lut_changed; + int nmaster; +@@ -252,6 +254,7 @@ typedef struct { + uint32_t tags; + int isfloating; + int monitor; ++ const char *appicon; + } Rule; + + typedef struct { +@@ -313,6 +316,9 @@ static void destroypointerconstraint(struct wl_listener *listener, void *data); + static void destroysessionlock(struct wl_listener *listener, void *data); + static void destroykeyboardgroup(struct wl_listener *listener, void *data); + static Monitor *dirtomon(enum wlr_direction dir); ++static void remove_outer_separators(char **str); ++static void appiconsappend(char **str, const char *appicon, size_t new_size); ++static void applyappicon(char *tag_icons[], int *icons_per_tag, const Client *c); + static void drawbar(Monitor *m); + static void drawbars(void); + static void focusclient(Client *c, int lift); +@@ -520,6 +526,11 @@ applybounds(Client *c, struct wlr_box *bbox) + void + applyrules(Client *c) + { ++ outer_separator_beg = outer_separator_beg ? outer_separator_beg : ' '; ++ outer_separator_end = outer_separator_end ? outer_separator_end : ' '; ++ inner_separator = inner_separator ? inner_separator : ' '; ++ truncate_icons_after = truncate_icons_after > 0 ? truncate_icons_after : 1; ++ + /* rule matching */ + const char *appid, *title; + uint32_t newtags = 0; +@@ -533,6 +544,8 @@ applyrules(Client *c) + for (r = rules; r < END(rules); r++) { + if ((!r->title || strstr(title, r->title)) + && (!r->id || strstr(appid, r->id))) { ++ /* r->appicon is static, so lifetime is sufficient */ ++ c->appicon = (char*) r->appicon; + c->isfloating = r->isfloating; + newtags |= r->tags; + i = 0; +@@ -775,7 +788,7 @@ buttonpress(struct wl_listener *listener, void *data) + (buffer = wlr_scene_buffer_from_node(node)) && buffer == selmon->scene_buffer) { + cx = (cursor->x - selmon->m.x) * selmon->wlr_output->scale; + do +- x += TEXTW(selmon, tags[i]); ++ x += TEXTW(selmon, selmon->tag_icons[i]); + while (cx >= x && ++i < LENGTH(tags)); + if (i < LENGTH(tags)) { + click = ClkTagBar; +@@ -905,6 +918,16 @@ cleanupmon(struct wl_listener *listener, void *data) + wlr_output_layout_remove(output_layout, m->wlr_output); + wlr_scene_output_destroy(m->scene_output); + ++ for (int i = 0; i < LENGTH(tags); i++) { ++ if (m->tag_icons[i]) free(m->tag_icons[i]); ++ m->tag_icons[i] = NULL; ++ } ++ ++ if (m->tag_icons) { ++ free(m->tag_icons); ++ m->tag_icons = NULL; ++ } ++ + closemon(m); + wlr_scene_node_destroy(&m->fullscreen_bg->node); + wlr_scene_node_destroy(&m->scene_buffer->node); +@@ -1227,6 +1250,13 @@ createmon(struct wl_listener *listener, void *data) + m->lt[0] = r->lt; + m->lt[1] = &layouts[LENGTH(layouts) > 1 && r->lt != &layouts[1]]; + strncpy(m->ltsymbol, m->lt[m->sellt]->symbol, sizeof(m->ltsymbol)); ++ ++ m->tag_icons = (char**) malloc(LENGTH(tags) * sizeof(char*)); ++ if (m->tag_icons == NULL) perror("dwm: malloc()"); ++ for (int i = 0; i < LENGTH(tags); i++) { ++ m->tag_icons[i] = NULL; ++ } ++ + wlr_output_state_set_scale(&state, r->scale); + wlr_output_state_set_transform(&state, r->rr); + break; +@@ -1566,6 +1596,97 @@ dirtomon(enum wlr_direction dir) + return selmon; + } + ++void ++remove_outer_separators(char **str) ++{ ++ size_t clean_tag_name_len = strlen(*str) - 2; ++ ++ char *temp_tag_name = (char*) ++ malloc(clean_tag_name_len + 1); ++ ++ if (temp_tag_name == NULL) perror("dwm: malloc()"); ++ ++ memset(temp_tag_name, 0, clean_tag_name_len + 1); ++ ++ char *clean_tag_name_beg = *str + 1; ++ strncpy(temp_tag_name, ++ clean_tag_name_beg, ++ clean_tag_name_len); ++ ++ if (*str) free(*str); ++ *str = temp_tag_name; ++} ++ ++void ++appiconsappend(char **str, const char *appicon, size_t new_size) ++{ ++ char *temp_tag_name = (char*) malloc(new_size); ++ if (temp_tag_name == NULL) perror("dwm: malloc()"); ++ ++ /* NOTE: Example format of temp_tag_name (with two appicons): ++ * ++ */ ++ temp_tag_name = memset(temp_tag_name, 0, new_size); ++ ++ temp_tag_name[0] = outer_separator_beg; ++ temp_tag_name[new_size - 2] = outer_separator_end; ++ ++ strncpy(temp_tag_name + 1, *str, strlen(*str)); ++ temp_tag_name[strlen(temp_tag_name)] = inner_separator; ++ ++ strncpy(temp_tag_name + strlen(temp_tag_name), ++ appicon, strlen(appicon)); ++ ++ if (*str) free(*str); ++ *str = temp_tag_name; ++} ++ ++void ++applyappicon(char *tag_icons[], int *icons_per_tag, const Client *c) ++{ ++ for (unsigned t = 1, i = 0; ++ i < LENGTH(tags); ++ t <<= 1, i++) ++ { ++ if (c->tags & t) { ++ if (icons_per_tag[i] == 0) { ++ if (tag_icons[i]) free(tag_icons[i]); ++ tag_icons[i] = strndup(c->appicon, strlen(c->appicon)); ++ } else { ++ char *icon = NULL; ++ if (icons_per_tag[i] < truncate_icons_after) ++ icon = c->appicon; ++ else if (icons_per_tag[i] == truncate_icons_after) ++ icon = truncate_symbol; ++ else { ++ icons_per_tag[i]++; ++ continue; ++ } ++ ++ /* remove outer separators from previous iterations ++ * otherwise they get applied recursively */ ++ if (icons_per_tag[i] > 1) { ++ remove_outer_separators(&tag_icons[i]); ++ } ++ ++ size_t outer_separators_size = 2; ++ size_t inner_separator_size = 1; ++ ++ size_t new_size = strlen(tag_icons[i]) ++ + outer_separators_size ++ + inner_separator_size ++ + strlen(icon) ++ + 1; ++ ++ appiconsappend(&tag_icons[i], icon, new_size); ++ } ++ ++ icons_per_tag[i]++; ++ } ++ } ++} ++ ++ + void + drawbar(Monitor *m) + { +@@ -1588,7 +1709,19 @@ drawbar(Monitor *m) + drwl_text(m->drw, m->b.width - tw, 0, tw, m->b.height, 0, stext, 0); + } + ++ int icons_per_tag[LENGTH(tags)]; ++ memset(icons_per_tag, 0, LENGTH(tags) * sizeof(int)); ++ ++ for (int i = 0; i < LENGTH(tags); i++) { ++ /* set each tag to default value */ ++ m->tag_icons[i] = strndup(tags[i], strlen(tags[i])); ++ } ++ + wl_list_for_each(c, &clients, link) { ++ if (c->appicon && strlen(c->appicon) > 0) { ++ applyappicon(m->tag_icons, icons_per_tag, c); ++ } ++ + if (c->mon != m) + continue; + occ |= c->tags; +@@ -1598,10 +1731,10 @@ drawbar(Monitor *m) + x = 0; + c = focustop(m); + for (i = 0; i < LENGTH(tags); i++) { +- w = TEXTW(m, tags[i]); ++ w = TEXTW(m, m->tag_icons[i]); + drwl_setscheme(m->drw, colors[m->tagset[m->seltags] & 1 << i ? SchemeSel : SchemeNorm]); +- drwl_text(m->drw, x, 0, w, m->b.height, m->lrpad / 2, tags[i], urg & 1 << i); +- if (occ & 1 << i) ++ drwl_text(m->drw, x, 0, w, m->b.height, m->lrpad / 2, m->tag_icons[i], urg & 1 << i); ++ if (occ & 1 << i && icons_per_tag[i] ==0) + drwl_rect(m->drw, x + boxs, boxs, boxw, boxw, + m == selmon && c && c->tags & 1 << i, + urg & 1 << i); +-- +2.51.1 + diff --git a/patches/appicons/example.png b/patches/appicons/example.png new file mode 100644 index 0000000000000000000000000000000000000000..313864cd244d51567713b0bf2443e1cac171c8e3 GIT binary patch literal 5608 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 literal 0 HcmV?d00001