commit 7b13656e3ef0f51754993932e6c95f5a3d71f894 from: Brett Fisher date: Tue May 26 03:44:10 2026 UTC bxnotify.c: Implement dynamic window geometry and text word-wrapping. - Refractor input handling and rendering logic to support multi- line input and automatic window resizing. - Add TextBlock struct replacing fixec 1024-char buffer with dynamic heap allocation for flexible text processing. - Implement calc_layout() as a layout pass that calculates necessary window height and performs word-wrapping based on WIN_WIDTH before window is mapped. - Add measure_text() helper. - Split out display and font setup into init_display() to satisfy dependencies for early layout calculation. - Updated draw() loop for rendiering to iterate through wrapped lines applying alignment and vertical offsets dynamically. - Improved cleanup() adding heap-memory management for TextBlock lines to prevent memory leaks. commit - c243e7413d92458368975c45ee74d5f45aec5337 commit + 7b13656e3ef0f51754993932e6c95f5a3d71f894 blob - 711c03363990f9fabb4fefa46e01655a51784b0f blob + c6c6d155c3a77956ad8ec22c5b60a818ede9a9ed --- bxnotify.c +++ bxnotify.c @@ -19,6 +19,11 @@ #include "config.h" +typedef struct { + char **lines; + int count; +} TextBlock; + /* Global Variables */ static Display *dpy; static int screen; @@ -27,14 +32,18 @@ static XftFont *font; static XftDraw *xft_draw; static XftColor fg, bg, border; static int running = 1; -static char text[1024]; +static TextBlock tb; +static int total_height; static time_t start_time; /* Forward Declarations */ static void die(const char *fmt, ...); static void cleanup(void); static void sigusr1(int sig); -static void setup(void); +static int measure_text(const char *text); +static void init_display(void); +static void calc_layout(void); +static void setup(int win_h); static void readstdin(void); static void draw(void); static void run(void); @@ -47,7 +56,10 @@ main(int argc, char *argv[]) /* TODO: parse cli flags, override config.h */ readstdin(); - setup(); + init_display(); + calc_layout(); + setup(total_height); + start_time = time(NULL); run(); cleanup(); @@ -55,7 +67,6 @@ main(int argc, char *argv[]) return 0; } - static void die(const char *fmt, ...) { @@ -69,10 +80,14 @@ die(const char *fmt, ...) static void cleanup(void) { - if (draw) XftDrawDestroy(xft_draw); - if (font) XftFontClose(dpy, font); - if (win) XDestroyWindow(dpy, win); - if (dpy) XCloseDisplay(dpy); + for (int i = 0; i < tb.count; i++) + free(tb.lines[i]); + free(tb.lines); + + if (xft_draw) XftDrawDestroy(xft_draw); + if (font) XftFontClose(dpy, font); + if (win) XDestroyWindow(dpy, win); + if (dpy) XCloseDisplay(dpy); } static void @@ -82,110 +97,150 @@ sigusr1(int sig) running = 0; } +static int +measure_text(const char *text) +{ + XGlyphInfo ext; + XftTextExtentsUtf8(dpy, font, (const FcChar8 *)text, strlen(text), &ext); + return ext.width; +} + static void -setup(void) +readstdin(void) { - XSetWindowAttributes swa; - int x, y, height; + char *line = NULL; + size_t linesize = 0; + ssize_t len; - /* open display */ + tb.lines = NULL; + tb.count = 0; + + while ((len = getline(&line, &linesize, stdin)) != -1) { + if (len > 0 && line[len - 1] == '\n') + line[len - 1] = '\0'; + + tb.lines = realloc(tb.lines, (tb.count + 1) * sizeof(char *)); + if (!tb.lines) + die("bxnotify: realloc failed\n"); + + tb.lines[tb.count] = strdup(line); + if (!tb.lines[tb.count]) + die("bxnotify: strdup failed\n"); + + tb.count++; + } + + free(line); + + if (tb.count == 0) + die("bxnotify: no input received\n"); +} + +static void +init_display(void) +{ if (!(dpy = XOpenDisplay(NULL))) die("bxnotify: cannot open display\n"); screen = DefaultScreen(dpy); - /* load font first — needed for height calculation */ if (!(font = XftFontOpenName(dpy, screen, FONT))) die("bxnotify: cannot load font\n"); +} - /* auto-height: font ascent + descent + padding top/bottom */ - height = font->ascent + font->descent + PADDING * 2; +static void +calc_layout(void) +{ + int line_h = font->ascent + font->descent; + int wrapped_count = 0; + char **wrapped = NULL; - /* position: top-right, below bxbar */ + for (int i = 0; i < tb.count; i++) { + char *line = tb.lines[i]; + + while (measure_text(line) > WIN_WIDTH) { + /* Find break point */ + char *p = line + strlen(line); + while (p > line && measure_text(line) > WIN_WIDTH) { + while (p > line && *p != ' ') p--; + if (p == line) break; /* Cannot wrap, forced break */ + *p = '\0'; + } + /* Add chunk */ + wrapped = realloc(wrapped, (++wrapped_count) * sizeof(char *)); + wrapped[wrapped_count - 1] = strdup(line); + line = p + 1; + } + /* Add remainder */ + wrapped = realloc(wrapped, (++wrapped_count) * sizeof(char *)); + wrapped[wrapped_count - 1] = strdup(line); + } + + /* Update global state */ + for (int i = 0; i < tb.count; i++) free(tb.lines[i]); + free(tb.lines); + tb.lines = wrapped; + tb.count = wrapped_count; + total_height = (tb.count * line_h) + ((tb.count + 1) * PADDING) + (2 * BORDER_WIDTH); +} + +static void +setup(int win_h) +{ + XSetWindowAttributes swa; + int x, y; int win_w = WIN_WIDTH + 2 * BORDER_WIDTH; - int win_h = height + 2 * BORDER_WIDTH; x = DisplayWidth(dpy, screen) - win_w - WIN_X; y = WIN_Y; - /* create window — override_redirect so bxwm ignores it */ swa.override_redirect = True; swa.background_pixel = bg.pixel; swa.border_pixel = 0; swa.event_mask = ExposureMask; win = XCreateWindow(dpy, RootWindow(dpy, screen), x, y, - win_w, win_h, 0, /* border_width = 0 */ + win_w, win_h, 0, CopyFromParent, CopyFromParent, CopyFromParent, CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask, &swa); - /* allocate colors */ - if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), - DefaultColormap(dpy, screen), FG_COLOR, &fg)) + /* Allocate colors and draw context... */ + if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen), FG_COLOR, &fg)) die("bxnotify: cannot allocate fg color\n"); - if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), - DefaultColormap(dpy, screen), BG_COLOR, &bg)) + if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen), BG_COLOR, &bg)) die("bxnotify: cannot allocate bg color\n"); - if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), - DefaultColormap(dpy, screen), BORDER_COLOR, &border)) + if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen), BORDER_COLOR, &border)) die("bxnotify: cannot allocate border color\n"); - /* create draw context */ - xft_draw = XftDrawCreate(dpy, win, DefaultVisual(dpy, screen), - DefaultColormap(dpy, screen)); + xft_draw = XftDrawCreate(dpy, win, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen)); XMapWindow(dpy, win); } static void -readstdin(void) -{ - if (fgets(text, sizeof(text), stdin) == NULL) { - die("bxnotify: no input received\n"); - } - /* remove trailing newline */ - text[strcspn(text, "\n")] = '\0'; -} - -static void draw(void) { - int height = font->ascent + font->descent + PADDING * 2; - int win_w = WIN_WIDTH + 2 * BORDER_WIDTH; - int win_h = height + 2 * BORDER_WIDTH; - int x, y; - XGlyphInfo ext; + int line_h = font->ascent + font->descent; + /* Redraw background... */ + XftDrawRect(xft_draw, &border, 0, 0, WIN_WIDTH + 2 * BORDER_WIDTH, total_height); + XftDrawRect(xft_draw, &bg, BORDER_WIDTH, BORDER_WIDTH, WIN_WIDTH, total_height - 2 * BORDER_WIDTH); - /* draw border (full window) */ - XftDrawRect(xft_draw, &border, 0, 0, win_w, win_h); - - /* draw background (inner area) */ - XftDrawRect(xft_draw, &bg, BORDER_WIDTH, BORDER_WIDTH, WIN_WIDTH, height); - - switch (ALIGN) { - case 0: /* left */ - x = PADDING; - break; - case 1: /* center */ - XftTextExtentsUtf8(dpy, font, (XftChar8 *)text, strlen(text), &ext); - x = (WIN_WIDTH - ext.width) / 2; - break; - case 2: /* right */ - XftTextExtentsUtf8(dpy, font, (XftChar8 *)text, strlen(text), &ext); - x = WIN_WIDTH - ext.width - PADDING; - break; + for (int i = 0; i < tb.count; i++) { + /* Calculate Y for this specific line */ + int y = PADDING + BORDER_WIDTH + (i * line_h) + font->ascent; + int x; + XGlyphInfo ext; + XftTextExtentsUtf8(dpy, font, (XftChar8 *)tb.lines[i], strlen(tb.lines[i]), &ext); + + switch (ALIGN) { + case 0: x = BORDER_WIDTH + PADDING; break; + case 1: x = BORDER_WIDTH + (WIN_WIDTH - ext.width) / 2; break; + case 2: x = BORDER_WIDTH + (WIN_WIDTH - ext.width - PADDING); break; + default: x = BORDER_WIDTH + PADDING; break; + } + XftDrawStringUtf8(xft_draw, &fg, font, x, y, (XftChar8 *)tb.lines[i], strlen(tb.lines[i])); } - - y = (height - (font->ascent + font->descent)) / 2 + font->ascent; - - /* offset text into inner content area */ - x += BORDER_WIDTH; - y += BORDER_WIDTH; - - XftDrawStringUtf8(xft_draw, &fg, font, x, y, - (XftChar8 *)text, strlen(text)); - XFlush(dpy); } @@ -201,7 +256,7 @@ run(void) draw(); } - /* check duration timeout */ + /* Check duration timeout */ if (DURATION > 0 && (time(NULL) - start_time) >= DURATION) running = 0;