From dfbbf7f6e1b22ccf9e5a45d77ee10995577fb4fc Mon Sep 17 00:00:00 2001 From: Hiltjo Posthuma Date: Wed, 8 Mar 2023 21:20:52 +0100 Subject: [PATCH 01/10] readstdin: reduce memory-usage by duplicating the line from getline() Improves upon commit 32db2b125190d366be472ccb7cad833248696144 The getline() implementation often uses a more greedy way of allocating memory. Using this buffer directly and forcing an allocation (by setting it to NULL) would waste a bit of extra space, depending on the implementation of course. Tested on musl libc and glibc. The current glibc version allocates a minimum of 120 bytes per line. For smaller lines musl libc seems less wasteful but still wastes a few bytes per line. On a dmenu_path listing on my system the memory usage was about 350kb (old) vs 30kb (new) on Void Linux glibc. Side-note that getline() also reads NUL bytes in lines, while strdup() would read until the NUL byte. Since dmenu reads text lines either is probably fine(tm). Also rename junk to linesiz. --- dmenu.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dmenu.c b/dmenu.c index 27b7a30..4e7df12 100644 --- a/dmenu.c +++ b/dmenu.c @@ -550,11 +550,11 @@ static void readstdin(void) { char *line = NULL; - size_t i, junk, itemsiz = 0; + size_t i, itemsiz = 0, linesiz = 0; ssize_t len; /* read each line from stdin and add it to the item list */ - for (i = 0; (len = getline(&line, &junk, stdin)) != -1; i++) { + for (i = 0; (len = getline(&line, &linesiz, stdin)) != -1; i++) { if (i + 1 >= itemsiz) { itemsiz += 256; if (!(items = realloc(items, itemsiz * sizeof(*items)))) @@ -562,9 +562,10 @@ readstdin(void) } if (line[len - 1] == '\n') line[len - 1] = '\0'; - items[i].text = line; + if (!(items[i].text = strdup(line))) + die("strdup:"); + items[i].out = 0; - line = NULL; /* next call of getline() allocates a new line */ } free(line); if (items) From 0fe460dbd469a1d5b6a7140d0e1801935e4a923b Mon Sep 17 00:00:00 2001 From: Lucas de Sena Date: Wed, 5 Apr 2023 17:11:49 -0300 Subject: [PATCH 02/10] fix BadMatch error when embedding on some windows When embedded into another window, dmenu will fail with the BadMatch error if that window have not the same colormap/depth/visual as the root window. That happens because dmenu inherits the colormap/depth/visual from its parent, but draws on a pixmap created based on the root window using a GC created for the root window (see drw.c). A BadMatch will occur when copying the content of the pixmap into dmenu's window. A solution is to create dmenu's window inside root and then reparent it if embeded. See this mail[1] on ports@openbsd.org mailing list for context. [1]: https://marc.info/?l=openbsd-ports&m=168072150814664&w=2 --- dmenu.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dmenu.c b/dmenu.c index 4e7df12..62f1089 100644 --- a/dmenu.c +++ b/dmenu.c @@ -685,7 +685,7 @@ setup(void) swa.override_redirect = True; swa.background_pixel = scheme[SchemeNorm][ColBg].pixel; swa.event_mask = ExposureMask | KeyPressMask | VisibilityChangeMask; - win = XCreateWindow(dpy, parentwin, x, y, mw, mh, 0, + win = XCreateWindow(dpy, root, x, y, mw, mh, 0, CopyFromParent, CopyFromParent, CopyFromParent, CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); XSetClassHint(dpy, win, &ch); @@ -700,6 +700,7 @@ setup(void) XMapRaised(dpy, win); if (embed) { + XReparentWindow(dpy, win, parentwin, x, y); XSelectInput(dpy, parentwin, FocusChangeMask | SubstructureNotifyMask); if (XQueryTree(dpy, parentwin, &dw, &w, &dws, &du) && dws) { for (i = 0; i < du && dws[i] != win; ++i) From 7ab0cb5ef0e19352fc5d64ae0d57a5cf4540acbf Mon Sep 17 00:00:00 2001 From: NRK Date: Fri, 7 Jul 2023 17:00:42 +0600 Subject: [PATCH 03/10] drw: minor improvement to the nomatches cache 1. use `unsigned int` to store the codepoints, this avoids waste on common case where `long` is 64bits. and POSIX guarantees `int` to be at least 32bits so there's no risk of truncation. 2. since switching to `unsigned int` cuts down the memory requirement by half, double the cache size from 64 to 128. 3. instead of a linear search, use a simple hash-table for O(1) lookups. --- dmenu.c | 1 - drw.c | 23 ++++++++++++----------- util.h | 1 + 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/dmenu.c b/dmenu.c index 62f1089..40f93e0 100644 --- a/dmenu.c +++ b/dmenu.c @@ -22,7 +22,6 @@ /* macros */ #define INTERSECT(x,y,w,h,r) (MAX(0, MIN((x)+(w),(r).x_org+(r).width) - MAX((x),(r).x_org)) \ * MAX(0, MIN((y)+(h),(r).y_org+(r).height) - MAX((y),(r).y_org))) -#define LENGTH(X) (sizeof X / sizeof X[0]) #define TEXTW(X) (drw_fontset_getwidth(drw, (X)) + lrpad) /* enums */ diff --git a/drw.c b/drw.c index a58a2b4..78a2b27 100644 --- a/drw.c +++ b/drw.c @@ -238,8 +238,8 @@ drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert) { - int i, ty, ellipsis_x = 0; - unsigned int tmpw, ew, ellipsis_w = 0, ellipsis_len; + int ty, ellipsis_x = 0; + unsigned int tmpw, ew, ellipsis_w = 0, ellipsis_len, hash, h0, h1; XftDraw *d = NULL; Fnt *usedfont, *curfont, *nextfont; int utf8strlen, utf8charlen, render = x || y || w || h; @@ -251,9 +251,7 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp XftResult result; int charexists = 0, overflow = 0; /* keep track of a couple codepoints for which we have no match. */ - enum { nomatches_len = 64 }; - static struct { long codepoint[nomatches_len]; unsigned int idx; } nomatches; - static unsigned int ellipsis_width = 0; + static unsigned int nomatches[128], ellipsis_width; if (!drw || (render && (!drw->scheme || !w)) || !text || !drw->fonts) return 0; @@ -338,11 +336,14 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp * character must be drawn. */ charexists = 1; - for (i = 0; i < nomatches_len; ++i) { - /* avoid calling XftFontMatch if we know we won't find a match */ - if (utf8codepoint == nomatches.codepoint[i]) - goto no_match; - } + hash = (unsigned int)utf8codepoint; + hash = ((hash >> 16) ^ hash) * 0x21F0AAAD; + hash = ((hash >> 15) ^ hash) * 0xD35A2D97; + h0 = ((hash >> 15) ^ hash) % LENGTH(nomatches); + h1 = (hash >> 17) % LENGTH(nomatches); + /* avoid expensive XftFontMatch call when we know we won't find a match */ + if (nomatches[h0] == utf8codepoint || nomatches[h1] == utf8codepoint) + goto no_match; fccharset = FcCharSetCreate(); FcCharSetAddChar(fccharset, utf8codepoint); @@ -371,7 +372,7 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp curfont->next = usedfont; } else { xfont_free(usedfont); - nomatches.codepoint[++nomatches.idx % nomatches_len] = utf8codepoint; + nomatches[nomatches[h0] ? h1 : h0] = utf8codepoint; no_match: usedfont = drw->fonts; } diff --git a/util.h b/util.h index f633b51..c0a50d4 100644 --- a/util.h +++ b/util.h @@ -3,6 +3,7 @@ #define MAX(A, B) ((A) > (B) ? (A) : (B)) #define MIN(A, B) ((A) < (B) ? (A) : (B)) #define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B)) +#define LENGTH(X) (sizeof (X) / sizeof (X)[0]) void die(const char *fmt, ...); void *ecalloc(size_t nmemb, size_t size); From 8df553e0048733bab4bc85a6b76bcfd44c046e71 Mon Sep 17 00:00:00 2001 From: Hiltjo Posthuma Date: Fri, 22 Sep 2023 15:16:44 +0200 Subject: [PATCH 04/10] Makefile: remove the options target The Makefile used to suppress output (by using @), so this target made sense at the time. But the Makefile should be simple and make debugging with less abstractions or fancy printing. The Makefile was made verbose and doesn't hide the build output, so remove this target. Prompted by a question on the mailing list about the options target. --- Makefile | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index a03a95c..458c524 100644 --- a/Makefile +++ b/Makefile @@ -6,13 +6,7 @@ include config.mk SRC = drw.c dmenu.c stest.c util.c OBJ = $(SRC:.c=.o) -all: options dmenu stest - -options: - @echo dmenu build options: - @echo "CFLAGS = $(CFLAGS)" - @echo "LDFLAGS = $(LDFLAGS)" - @echo "CC = $(CC)" +all: dmenu stest .c.o: $(CC) -c $(CFLAGS) $< @@ -61,4 +55,4 @@ uninstall: $(DESTDIR)$(MANPREFIX)/man1/dmenu.1\ $(DESTDIR)$(MANPREFIX)/man1/stest.1 -.PHONY: all options clean dist install uninstall +.PHONY: all clean dist install uninstall From 7be720cc88ed2294338f7182600df10f21c575ce Mon Sep 17 00:00:00 2001 From: Hiltjo Posthuma Date: Tue, 19 Mar 2024 12:12:52 +0100 Subject: [PATCH 05/10] bump version to 5.3 --- config.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.mk b/config.mk index 566348b..137f7c8 100644 --- a/config.mk +++ b/config.mk @@ -1,5 +1,5 @@ # dmenu version -VERSION = 5.2 +VERSION = 5.3 # paths PREFIX = /usr/local From 51e32d49b56c86cd288c64fccf6cd765547781b9 Mon Sep 17 00:00:00 2001 From: NRK Date: Thu, 4 Jul 2024 21:25:37 +0000 Subject: [PATCH 06/10] overhaul utf8decode() this changes the utf8decode function to: * report when an error occurs * report how many bytes to advance on error these will be useful in the next commit to render invalid utf8 sequences. the new implementation is also shorter and more direct. --- drw.c | 76 ++++++++++++++++++++++++----------------------------------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/drw.c b/drw.c index 78a2b27..eb71da7 100644 --- a/drw.c +++ b/drw.c @@ -9,54 +9,40 @@ #include "util.h" #define UTF_INVALID 0xFFFD -#define UTF_SIZ 4 -static const unsigned char utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; -static const unsigned char utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; -static const long utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; -static const long utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; - -static long -utf8decodebyte(const char c, size_t *i) +static int +utf8decode(const char *s_in, long *u, int *err) { - for (*i = 0; *i < (UTF_SIZ + 1); ++(*i)) - if (((unsigned char)c & utfmask[*i]) == utfbyte[*i]) - return (unsigned char)c & ~utfmask[*i]; - return 0; -} - -static size_t -utf8validate(long *u, size_t i) -{ - if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF)) - *u = UTF_INVALID; - for (i = 1; *u > utfmax[i]; ++i) - ; - return i; -} - -static size_t -utf8decode(const char *c, long *u, size_t clen) -{ - size_t i, j, len, type; - long udecoded; + static const unsigned char lens[] = { + /* 0XXXX */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* 10XXX */ 0, 0, 0, 0, 0, 0, 0, 0, /* invalid */ + /* 110XX */ 2, 2, 2, 2, + /* 1110X */ 3, 3, + /* 11110 */ 4, + /* 11111 */ 0, /* invalid */ + }; + static const unsigned char leading_mask[] = { 0x7F, 0x1F, 0x0F, 0x07 }; + static const unsigned int overlong[] = { 0x0, 0x80, 0x0800, 0x10000 }; + const unsigned char *s = (const unsigned char *)s_in; + int len = lens[*s >> 3]; *u = UTF_INVALID; - if (!clen) - return 0; - udecoded = utf8decodebyte(c[0], &len); - if (!BETWEEN(len, 1, UTF_SIZ)) + *err = 1; + if (len == 0) return 1; - for (i = 1, j = 1; i < clen && j < len; ++i, ++j) { - udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type); - if (type) - return j; - } - if (j < len) - return 0; - *u = udecoded; - utf8validate(u, len); + long cp = s[0] & leading_mask[len - 1]; + for (int i = 1; i < len; ++i) { + if (s[i] == '\0' || (s[i] & 0xC0) != 0x80) + return i; + cp = (cp << 6) | (s[i] & 0x3F); + } + /* out of range, surrogate, overlong encoding */ + if (cp > 0x10FFFF || (cp >> 11) == 0x1B || cp < overlong[len - 1]) + return len; + + *err = 0; + *u = cp; return len; } @@ -242,7 +228,7 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp unsigned int tmpw, ew, ellipsis_w = 0, ellipsis_len, hash, h0, h1; XftDraw *d = NULL; Fnt *usedfont, *curfont, *nextfont; - int utf8strlen, utf8charlen, render = x || y || w || h; + int utf8strlen, utf8charlen, utf8err, render = x || y || w || h; long utf8codepoint = 0; const char *utf8str; FcCharSet *fccharset; @@ -272,11 +258,11 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp if (!ellipsis_width && render) ellipsis_width = drw_fontset_getwidth(drw, "..."); while (1) { - ew = ellipsis_len = utf8strlen = 0; + ew = ellipsis_len = utf8err = utf8charlen = utf8strlen = 0; utf8str = text; nextfont = NULL; while (*text) { - utf8charlen = utf8decode(text, &utf8codepoint, UTF_SIZ); + utf8charlen = utf8decode(text, &utf8codepoint, &utf8err); for (curfont = drw->fonts; curfont; curfont = curfont->next) { charexists = charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint); if (charexists) { From 59936c7d972587a47d61161279bb8e8abc0b02f3 Mon Sep 17 00:00:00 2001 From: NRK Date: Thu, 4 Jul 2024 21:27:47 +0000 Subject: [PATCH 07/10] render invalid utf8 sequences as U+FFFD previously drw_text would do the width calculations as if invalid utf8 sequences were replaced with U+FFFD but would pass the invalid utf8 sequence to xft to render where xft would just cut it off at the first invalid byte. this change makes invalid utf8 render as U+FFFD and avoids sending invalid sequences to xft. the following can be used to check the behavior before and after the patch: $ printf "0\xef1234567\ntest" | dmenu Ref: https://lists.suckless.org/dev/2407/35646.html --- drw.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/drw.c b/drw.c index eb71da7..f151ae5 100644 --- a/drw.c +++ b/drw.c @@ -237,7 +237,8 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp XftResult result; int charexists = 0, overflow = 0; /* keep track of a couple codepoints for which we have no match. */ - static unsigned int nomatches[128], ellipsis_width; + static unsigned int nomatches[128], ellipsis_width, invalid_width; + static const char invalid[] = "�"; if (!drw || (render && (!drw->scheme || !w)) || !text || !drw->fonts) return 0; @@ -257,6 +258,10 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp usedfont = drw->fonts; if (!ellipsis_width && render) ellipsis_width = drw_fontset_getwidth(drw, "..."); + if (!invalid_width) { + invalid_width = -1; /* stop infinite recursion */ + invalid_width = drw_fontset_getwidth(drw, invalid); + } while (1) { ew = ellipsis_len = utf8err = utf8charlen = utf8strlen = 0; utf8str = text; @@ -284,9 +289,9 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp else utf8strlen = ellipsis_len; } else if (curfont == usedfont) { - utf8strlen += utf8charlen; text += utf8charlen; - ew += tmpw; + utf8strlen += utf8err ? 0 : utf8charlen; + ew += utf8err ? 0 : tmpw; } else { nextfont = curfont; } @@ -294,7 +299,7 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp } } - if (overflow || !charexists || nextfont) + if (overflow || !charexists || nextfont || utf8err) break; else charexists = 0; @@ -309,6 +314,12 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp x += ew; w -= ew; } + if (utf8err && (!render || invalid_width < w)) { + if (render) + drw_text(drw, x, y, w, h, 0, invalid, invert); + x += invalid_width; + w -= invalid_width; + } if (render && overflow) drw_text(drw, ellipsis_x, y, ellipsis_w, h, 0, "...", invert); From 475d8093cb8d29d5756937bfa9e0b3b9e415f632 Mon Sep 17 00:00:00 2001 From: Hiltjo Posthuma Date: Sun, 14 Jul 2024 11:40:20 +0200 Subject: [PATCH 08/10] drw.c: use the same pattern as ellipsis_width to check for infinite recursion --- drw.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/drw.c b/drw.c index f151ae5..344de61 100644 --- a/drw.c +++ b/drw.c @@ -258,10 +258,8 @@ drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lp usedfont = drw->fonts; if (!ellipsis_width && render) ellipsis_width = drw_fontset_getwidth(drw, "..."); - if (!invalid_width) { - invalid_width = -1; /* stop infinite recursion */ + if (!invalid_width && render) invalid_width = drw_fontset_getwidth(drw, invalid); - } while (1) { ew = ellipsis_len = utf8err = utf8charlen = utf8strlen = 0; utf8str = text; From 64ffc39ed1531cd53b511172c5bbba975e76387f Mon Sep 17 00:00:00 2001 From: Jarkko Toivanen Date: Tue, 16 Jul 2024 02:55:56 +0300 Subject: [PATCH 09/10] Default configuration --- .gitignore | 2 ++ config.h | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 .gitignore create mode 100644 config.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0031bb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +/dmenu diff --git a/config.h b/config.h new file mode 100644 index 0000000..1edb647 --- /dev/null +++ b/config.h @@ -0,0 +1,23 @@ +/* See LICENSE file for copyright and license details. */ +/* Default settings; can be overriden by command line. */ + +static int topbar = 1; /* -b option; if 0, dmenu appears at bottom */ +/* -fn option overrides fonts[0]; default X11 font or font set */ +static const char *fonts[] = { + "monospace:size=10" +}; +static const char *prompt = NULL; /* -p option; prompt to the left of input field */ +static const char *colors[SchemeLast][2] = { + /* fg bg */ + [SchemeNorm] = { "#bbbbbb", "#222222" }, + [SchemeSel] = { "#eeeeee", "#005577" }, + [SchemeOut] = { "#000000", "#00ffff" }, +}; +/* -l option; if nonzero, dmenu uses vertical list with given number of lines */ +static unsigned int lines = 0; + +/* + * Characters not considered part of a word while deleting words + * for example: " /?\"&[]" + */ +static const char worddelimiters[] = " "; From c72737a4b89e82beca05e253efea1c39e26d9238 Mon Sep 17 00:00:00 2001 From: Jarkko Toivanen Date: Tue, 16 Jul 2024 03:17:16 +0300 Subject: [PATCH 10/10] Configuring default colours --- config.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.h b/config.h index 1edb647..4f484cd 100644 --- a/config.h +++ b/config.h @@ -9,8 +9,8 @@ static const char *fonts[] = { static const char *prompt = NULL; /* -p option; prompt to the left of input field */ static const char *colors[SchemeLast][2] = { /* fg bg */ - [SchemeNorm] = { "#bbbbbb", "#222222" }, - [SchemeSel] = { "#eeeeee", "#005577" }, + [SchemeNorm] = { "#ffc000", "#000000" }, + [SchemeSel] = { "#000000", "#ffc000" }, [SchemeOut] = { "#000000", "#00ffff" }, }; /* -l option; if nonzero, dmenu uses vertical list with given number of lines */