1 #include <ctype.h>
2 #include <err.h>
3 #include <errno.h>
4 #include <fcntl.h>
5 #include <limits.h>
6 #include <poll.h>
7 #include <signal.h>
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <time.h>
11 #include <unistd.h>
12 #include <X11/Xlib.h>
13 #include <X11/Xatom.h>
14 #include <X11/Xresource.h>
15 #include <X11/Xft/Xft.h>
16 #include <X11/extensions/Xinerama.h>
17 #include <Imlib2.h>
18 #include "xnotify.h"
19
20 /* macros */
21 #define DEFWIDTH 350 /* default width of a notification */
22 #define MIN(x,y) ((x)<(y)?(x):(y))
23 #define MAX(x,y) ((x)>(y)?(x):(y))
24 #define BETWEEN(x, a, b) ((a) <= (x) && (x) <= (b))
25
26 /* X stuff */
27 static Display *dpy;
28 static Colormap colormap;
29 static Visual *visual;
30 static Window root;
31 static XrmDatabase xdb;
32 static char *xrm;
33 static int screen;
34 static int depth;
35 static int xfd;
36 static struct Monitor mon;
37 static Atom utf8string;
38 static Atom netatom[NetLast];
39 static struct DC dc;
40 static struct Fonts titlefnt, bodyfnt;
41
42 /* flags */
43 static int oflag = 0; /* whether only one notification must exist at a time */
44 volatile sig_atomic_t usrflag; /* 1 if for SIGUSR1, 2 for SIGUSR2, 0 otherwise */
45
46 /* include configuration structure */
47 #include "config.h"
48
49 /* show usage */
50 void
usage(void)51 usage(void)
52 {
53 (void)fprintf(stderr, "usage: xnotify [-o] [-G gravity] [-b button] [-g geometry] [-m monitor] [-s seconds]\n");
54 exit(1);
55 }
56
57 /* get configuration from X resources */
58 static void
getresources(void)59 getresources(void)
60 {
61 XrmValue xval;
62 unsigned long n;
63 char *type;
64
65 if (xrm == NULL || xdb == NULL)
66 return;
67
68 if (XrmGetResource(xdb, "xnotify.title.font", "*", &type, &xval) == True)
69 config.titlefont = xval.addr;
70 if (XrmGetResource(xdb, "xnotify.body.font", "*", &type, &xval) == True)
71 config.bodyfont = xval.addr;
72 if (XrmGetResource(xdb, "xnotify.background", "*", &type, &xval) == True)
73 config.background_color = xval.addr;
74 if (XrmGetResource(xdb, "xnotify.foreground", "*", &type, &xval) == True)
75 config.foreground_color = xval.addr;
76 if (XrmGetResource(xdb, "xnotify.border", "*", &type, &xval) == True)
77 config.border_color = xval.addr;
78 if (XrmGetResource(xdb, "xnotify.geometry", "*", &type, &xval) == True)
79 config.geometryspec = xval.addr;
80 if (XrmGetResource(xdb, "xnotify.gravity", "*", &type, &xval) == True)
81 config.gravityspec = xval.addr;
82 if (XrmGetResource(xdb, "xnotify.borderWidth", "*", &type, &xval) == True)
83 if ((n = strtoul(xval.addr, NULL, 10)) < INT_MAX)
84 config.border_pixels = n;
85 if (XrmGetResource(xdb, "xnotify.gap", "*", &type, &xval) == True)
86 if ((n = strtoul(xval.addr, NULL, 10)) < INT_MAX)
87 config.gap_pixels = n;
88 if (XrmGetResource(xdb, "xnotify.imageWidth", "*", &type, &xval) == True)
89 if ((n = strtoul(xval.addr, NULL, 10)) < INT_MAX)
90 config.image_pixels = n;
91 if (XrmGetResource(xdb, "xnotify.leading", "*", &type, &xval) == True)
92 if ((n = strtoul(xval.addr, NULL, 10)) < INT_MAX)
93 config.leading_pixels = n;
94 if (XrmGetResource(xdb, "xnotify.padding", "*", &type, &xval) == True)
95 if ((n = strtoul(xval.addr, NULL, 10)) < INT_MAX)
96 config.padding_pixels = n;
97 if (XrmGetResource(xdb, "xnotify.shrink", "*", &type, &xval) == True)
98 config.shrink = (strcasecmp(xval.addr, "true") == 0 ||
99 strcasecmp(xval.addr, "on") == 0 ||
100 strcasecmp(xval.addr, "1") == 0);
101 if (XrmGetResource(xdb, "xnotify.alignment", "*", &type, &xval) == True) {
102 if (strcasecmp(xval.addr, "center") == 0)
103 config.alignment = CenterAlignment;
104 else if (strcasecmp(xval.addr, "left") == 0)
105 config.alignment = LeftAlignment;
106 else if (strcasecmp(xval.addr, "right") == 0)
107 config.alignment = RightAlignment;
108 }
109 }
110
111 /* get configuration from commmand-line */
112 static void
getoptions(int argc,char * argv[])113 getoptions(int argc, char *argv[])
114 {
115 unsigned long n;
116 int ch;
117
118 while ((ch = getopt(argc, argv, "G:b:g:m:os:")) != -1) {
119 switch (ch) {
120 case 'G':
121 config.gravityspec = optarg;
122 break;
123 case 'b':
124 if (*(optarg+1) != '\0')
125 break;
126 switch (*optarg) {
127 case '1':
128 config.actionbutton = Button1;
129 break;
130 case '2':
131 config.actionbutton = Button2;
132 break;
133 case '3':
134 config.actionbutton = Button3;
135 break;
136 case '4':
137 config.actionbutton = Button4;
138 break;
139 case '5':
140 config.actionbutton = Button5;
141 break;
142 default:
143 break;
144 }
145 break;
146 case 'g':
147 config.geometryspec = optarg;
148 break;
149 case 'm':
150 mon.num = atoi(optarg);
151 break;
152 case 'o':
153 oflag = 1;
154 break;
155 case 's':
156 if ((n = strtoul(optarg, NULL, 10)) < INT_MAX)
157 config.sec = n;
158 break;
159 default:
160 usage();
161 break;
162 }
163 }
164 argc -= optind;
165 argv += optind;
166
167 if (argc)
168 usage();
169 }
170
171 /* get XftColor *color from color string s */
172 static int
ealloccolor(const char * s,XftColor * color,int exitonerror)173 ealloccolor(const char *s, XftColor *color, int exitonerror)
174 {
175 if (!XftColorAllocName(dpy, visual, colormap, s, color)) {
176 if (exitonerror)
177 errx(1, "could not allocate color: %s", s);
178 warnx("could not allocate color: %s", s);
179 return -1;
180 }
181 return 0;
182 }
183
184 /* get number from *s into n, return 1 if error, update s to end of number */
185 static int
getnum(const char ** s,int * n)186 getnum(const char **s, int *n)
187 {
188 int retval;
189 long num;
190 char *endp;
191
192 num = strtol(*s, &endp, 10);
193 errno = 0;
194 retval = (errno == ERANGE || num > INT_MAX || num < 0 || endp == *s);
195 *s = endp;
196 *n = num;
197 return retval;
198 }
199
200 /* parse geometry specification and return geometry values */
201 static void
parsegeometryspec(int * x,int * y,int * w,int * h)202 parsegeometryspec(int *x, int *y, int *w, int *h)
203 {
204 int sign;
205 int n;
206 const char *s;
207
208 *x = *y = *w = *h = 0;
209 s = config.geometryspec;
210
211 if (*s != '+' && *s != '-') {
212 /* get *w */
213 if (getnum(&s, &n))
214 goto error;
215 if (*s == '%') {
216 if (n > 100)
217 goto error;
218 *w = (n * (mon.w - config.border_pixels * 2))/100;
219 s++;
220 } else {
221 *w = n;
222 }
223 if (*s++ != 'x')
224 goto error;
225
226 /* get *h */
227 if (getnum(&s, &n))
228 goto error;
229 if (*s == '%') {
230 if (n > 100)
231 goto error;
232 *h = (n * (mon.h - config.border_pixels * 2))/100;
233 s++;
234 } else {
235 *h = n;
236 }
237 }
238
239 if (*s == '+' || *s == '-') {
240 /* get *x */
241 sign = (*s++ == '-') ? -1 : 1;
242 if (getnum(&s, &n))
243 goto error;
244 *x = n * sign;
245 if (*s != '+' && *s != '-')
246 goto error;
247
248 /* get *y */
249 sign = (*s++ == '-') ? -1 : 1;
250 if (getnum(&s, &n))
251 goto error;
252 *y = n * sign;
253 }
254 if (*s != '\0')
255 goto error;
256
257 return;
258
259 error:
260 errx(1, "improper geometry specification %s", config.geometryspec);
261 }
262
263 /* parse gravity specification and return gravity value */
264 static void
parsegravityspec(int * gravity,int * direction)265 parsegravityspec(int *gravity, int *direction)
266 {
267 if (config.gravityspec == NULL || strcmp(config.gravityspec, "N") == 0) {
268 *gravity = NorthGravity;
269 *direction = DownWards;
270 } else if (strcmp(config.gravityspec, "NW") == 0) {
271 *gravity = NorthWestGravity;
272 *direction = DownWards;
273 } else if (strcmp(config.gravityspec, "NE") == 0) {
274 *gravity = NorthEastGravity;
275 *direction = DownWards;
276 } else if (strcmp(config.gravityspec, "W") == 0) {
277 *gravity = WestGravity;
278 *direction = DownWards;
279 } else if (strcmp(config.gravityspec, "C") == 0) {
280 *gravity = CenterGravity;
281 *direction = DownWards;
282 } else if (strcmp(config.gravityspec, "E") == 0) {
283 *gravity = EastGravity;
284 *direction = DownWards;
285 } else if (strcmp(config.gravityspec, "SW") == 0) {
286 *gravity = SouthWestGravity;
287 *direction = UpWards;
288 } else if (strcmp(config.gravityspec, "S") == 0) {
289 *gravity = SouthGravity;
290 *direction = UpWards;
291 } else if (strcmp(config.gravityspec, "SE") == 0) {
292 *gravity = SouthEastGravity;
293 *direction = UpWards;
294 } else {
295 errx(EXIT_FAILURE, "Unknown gravity %s", config.gravityspec);
296 }
297 }
298
299 /* parse font string */
300 static void
parsefonts(struct Fonts * fnt,const char * s)301 parsefonts(struct Fonts *fnt, const char *s)
302 {
303 const char *p;
304 char buf[1024];
305 size_t nfont = 0;
306
307 fnt->nfonts = 1;
308 for (p = s; *p; p++)
309 if (*p == ',')
310 fnt->nfonts++;
311
312 if ((fnt->fonts = calloc(fnt->nfonts, sizeof *fnt->fonts)) == NULL)
313 err(1, "calloc");
314
315 p = s;
316 while (*p != '\0') {
317 size_t i;
318
319 i = 0;
320 while (isspace(*p))
321 p++;
322 while (i < sizeof buf && *p != '\0' && *p != ',')
323 buf[i++] = *p++;
324 if (i >= sizeof buf)
325 errx(1, "font name too long");
326 if (*p == ',')
327 p++;
328 buf[i] = '\0';
329 if (nfont == 0)
330 if ((fnt->pattern = FcNameParse((FcChar8 *)buf)) == NULL)
331 errx(1, "the first font in the cache must be loaded from a font string");
332 if ((fnt->fonts[nfont++] = XftFontOpenName(dpy, screen, buf)) == NULL)
333 errx(1, "cannot load font");
334 }
335 fnt->texth = fnt->fonts[0]->height;
336 }
337
338 /* signal SIGUSR1 handler (close all notifications) */
339 static void
sigusr1handler(int sig)340 sigusr1handler(int sig)
341 {
342 (void)sig;
343 usrflag = 1;
344 }
345
346 /* signal SIGUSR2 handler (print cmd of first notification) */
347 static void
sigusr2handler(int sig)348 sigusr2handler(int sig)
349 {
350 (void)sig;
351 usrflag = 2;
352 }
353
354 /* init signal */
355 static void
initsignal(void)356 initsignal(void)
357 {
358 struct sigaction sa;
359
360 sa.sa_handler = sigusr1handler;
361 sa.sa_flags = 0;
362 sigemptyset(&sa.sa_mask);
363 if (sigaction(SIGUSR1, &sa, NULL) == -1)
364 err(1, "sigaction");
365
366 sa.sa_handler = sigusr2handler;
367 sa.sa_flags = 0;
368 sigemptyset(&sa.sa_mask);
369 if (sigaction(SIGUSR2, &sa, NULL) == -1)
370 err(1, "sigaction");
371 }
372
373 /* query monitor information */
374 static void
initmonitor(void)375 initmonitor(void)
376 {
377 XineramaScreenInfo *info = NULL;
378 int nmons;
379
380 mon.x = mon.y = 0;
381 mon.w = DisplayWidth(dpy, screen);
382 mon.h = DisplayHeight(dpy, screen);
383 if ((info = XineramaQueryScreens(dpy, &nmons)) != NULL) {
384 int selmon;
385
386 selmon = (mon.num >= 0 && mon.num < nmons) ? mon.num : 0;
387 mon.x = info[selmon].x_org;
388 mon.y = info[selmon].y_org;
389 mon.w = info[selmon].width;
390 mon.h = info[selmon].height;
391 XFree(info);
392 }
393 }
394
395 /* init draw context structure */
396 static void
initdc(void)397 initdc(void)
398 {
399 /* get colors */
400 ealloccolor(config.background_color, &dc.background, 1);
401 ealloccolor(config.foreground_color, &dc.foreground, 1);
402 ealloccolor(config.border_color, &dc.border, 1);
403
404 /* create common GC */
405 dc.gc = XCreateGC(dpy, root, 0, NULL);
406
407 /* try to get font */
408 parsefonts(&titlefnt, config.titlefont);
409 parsefonts(&bodyfnt, config.bodyfont);
410 }
411
412 /* Intern the used atoms */
413 static void
initatoms(void)414 initatoms(void)
415 {
416 utf8string = XInternAtom(dpy, "UTF8_STRING", False);
417 netatom[NetWMName] = XInternAtom(dpy, "_NET_WM_NAME", False);
418 netatom[NetWMWindowType] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False);
419 netatom[NetWMWindowTypeNotification] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False);
420 netatom[NetWMState] = XInternAtom(dpy, "_NET_WM_STATE", False);
421 netatom[NetWMStateAbove] = XInternAtom(dpy, "_NET_WM_STATE_ABOVE", False);
422 }
423
424 /* watch ConfigureNotify on root window so we're notified when monitors change */
425 static void
initstructurenotify(void)426 initstructurenotify(void)
427 {
428 XSelectInput(dpy, root, StructureNotifyMask);
429 }
430
431 /* allocate queue and set its members */
432 static struct Queue *
setqueue(void)433 setqueue(void)
434 {
435 struct Queue *queue;
436
437 if ((queue = malloc(sizeof *queue)) == NULL)
438 err(1, "malloc");
439
440 queue->head = NULL;
441 queue->tail = NULL;
442 queue->change = 0;
443
444 /* set geometry of notification queue */
445 parsegravityspec(&queue->gravity, &queue->direction);
446 parsegeometryspec(&queue->x, &queue->y, &queue->w, &queue->h);
447 if (queue->w == 0)
448 queue->w = DEFWIDTH;
449 if (queue->h == 0)
450 queue->h = titlefnt.texth + config.padding_pixels * 2;
451
452 if (config.image_pixels <= 0)
453 config.image_pixels = queue->h - config.padding_pixels;
454 if (config.image_pixels < 0)
455 config.image_pixels = 0;
456
457 return queue;
458 }
459
460 /* get item of given window */
461 static struct Item *
getitem(struct Queue * queue,Window win)462 getitem(struct Queue *queue, Window win)
463 {
464 struct Item *item;
465
466 for (item = queue->head; item; item = item->next)
467 if (item->win == win)
468 return item;
469 return NULL;
470 }
471
472 /* copy area from item's pixmap into item's window */
473 static void
copypixmap(struct Item * item)474 copypixmap(struct Item *item)
475 {
476 XCopyArea(dpy, item->pixmap, item->win, dc.gc, 0, 0, item->w, item->h, 0, 0);
477 }
478
479 /* load and scale image */
480 static Imlib_Image
loadimage(const char * file,int * width_ret,int * height_ret)481 loadimage(const char *file, int *width_ret, int *height_ret)
482 {
483 Imlib_Image image;
484 Imlib_Load_Error errcode;
485 const char *errstr;
486 int width, height;
487
488 image = imlib_load_image_with_error_return(file, &errcode);
489 if (*file == '\0') {
490 warnx("could not load image (file name is blank)");
491 return NULL;
492 } else if (image == NULL) {
493 switch (errcode) {
494 case IMLIB_LOAD_ERROR_FILE_DOES_NOT_EXIST:
495 errstr = "file does not exist";
496 break;
497 case IMLIB_LOAD_ERROR_FILE_IS_DIRECTORY:
498 errstr = "file is directory";
499 break;
500 case IMLIB_LOAD_ERROR_PERMISSION_DENIED_TO_READ:
501 case IMLIB_LOAD_ERROR_PERMISSION_DENIED_TO_WRITE:
502 errstr = "permission denied";
503 break;
504 case IMLIB_LOAD_ERROR_NO_LOADER_FOR_FILE_FORMAT:
505 errstr = "unknown file format";
506 break;
507 case IMLIB_LOAD_ERROR_PATH_TOO_LONG:
508 errstr = "path too long";
509 break;
510 case IMLIB_LOAD_ERROR_PATH_COMPONENT_NON_EXISTANT:
511 case IMLIB_LOAD_ERROR_PATH_COMPONENT_NOT_DIRECTORY:
512 case IMLIB_LOAD_ERROR_PATH_POINTS_OUTSIDE_ADDRESS_SPACE:
513 errstr = "improper path";
514 break;
515 case IMLIB_LOAD_ERROR_TOO_MANY_SYMBOLIC_LINKS:
516 errstr = "too many symbolic links";
517 break;
518 case IMLIB_LOAD_ERROR_OUT_OF_MEMORY:
519 errstr = "out of memory";
520 break;
521 case IMLIB_LOAD_ERROR_OUT_OF_FILE_DESCRIPTORS:
522 errstr = "out of file descriptors";
523 break;
524 default:
525 errstr = "unknown error";
526 break;
527 }
528 warnx("could not load image (%s): %s", errstr, file);
529 return NULL;
530 }
531
532 imlib_context_set_image(image);
533
534 width = imlib_image_get_width();
535 height = imlib_image_get_height();
536
537 if (width > height) {
538 *width_ret = config.image_pixels;
539 *height_ret = (height * config.image_pixels) / width;
540 } else {
541 *width_ret = (width * config.image_pixels) / height;
542 *height_ret = config.image_pixels;
543 }
544
545 image = imlib_create_cropped_scaled_image(0, 0, width, height, *width_ret, *height_ret);
546
547 return image;
548 }
549
550 /* create window for item */
551 static void
createwindow(struct Item * item)552 createwindow(struct Item *item)
553 {
554 XClassHint classhint = {"XNotify", "XNotify"};
555 XSetWindowAttributes swa;
556
557 swa.override_redirect = True;
558 swa.background_pixel = dc.background.pixel;
559 swa.border_pixel = dc.border.pixel;
560 swa.save_under = True; /* pop-up windows should save_under */
561 swa.event_mask = ExposureMask | ButtonPressMask | PointerMotionMask;
562
563 /* windows are created at 0,0 position for they'll be moved later */
564 item->win = XCreateWindow(dpy, root, 0, 0, item->w, item->h, config.border_pixels,
565 CopyFromParent, CopyFromParent, CopyFromParent,
566 CWOverrideRedirect | CWBackPixel | CWBorderPixel |
567 CWSaveUnder | CWEventMask, &swa);
568
569 XSetClassHint(dpy, item->win, &classhint);
570
571 XStoreName(dpy, item->win, "XNotify");
572 XChangeProperty(dpy, item->win, netatom[NetWMName], utf8string, 8, PropModeReplace,
573 (unsigned char *)"XNotify", strlen("XNotify"));
574 XChangeProperty(dpy, item->win, netatom[NetWMWindowType], XA_ATOM, 32, PropModeReplace,
575 (unsigned char *)&netatom[NetWMWindowTypeNotification], 1L);
576 XChangeProperty(dpy, item->win, netatom[NetWMState], XA_ATOM, 32, PropModeReplace,
577 (unsigned char *)&netatom[NetWMStateAbove], 1L);
578 }
579
580 /* get next utf8 char from s return its codepoint and set next_ret to pointer to end of character */
581 static FcChar32
getnextutf8char(const char * s,const char ** next_ret)582 getnextutf8char(const char *s, const char **next_ret)
583 {
584 static const unsigned char utfbyte[] = {0x80, 0x00, 0xC0, 0xE0, 0xF0};
585 static const unsigned char utfmask[] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
586 static const FcChar32 utfmin[] = {0, 0x00, 0x80, 0x800, 0x10000};
587 static const FcChar32 utfmax[] = {0, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
588 /* 0xFFFD is the replacement character, used to represent unknown characters */
589 static const FcChar32 unknown = 0xFFFD;
590 FcChar32 ucode; /* FcChar32 type holds 32 bits */
591 size_t usize = 0; /* n' of bytes of the utf8 character */
592 size_t i;
593
594 *next_ret = s+1;
595
596 /* get code of first byte of utf8 character */
597 for (i = 0; i < sizeof utfmask; i++) {
598 if (((unsigned char)*s & utfmask[i]) == utfbyte[i]) {
599 usize = i;
600 ucode = (unsigned char)*s & ~utfmask[i];
601 break;
602 }
603 }
604
605 /* if first byte is a continuation byte or is not allowed, return unknown */
606 if (i == sizeof utfmask || usize == 0)
607 return unknown;
608
609 /* check the other usize-1 bytes */
610 s++;
611 for (i = 1; i < usize; i++) {
612 *next_ret = s+1;
613 /* if byte is nul or is not a continuation byte, return unknown */
614 if (*s == '\0' || ((unsigned char)*s & utfmask[0]) != utfbyte[0])
615 return unknown;
616 /* 6 is the number of relevant bits in the continuation byte */
617 ucode = (ucode << 6) | ((unsigned char)*s & ~utfmask[0]);
618 s++;
619 }
620
621 /* check if ucode is invalid or in utf-16 surrogate halves */
622 if (!BETWEEN(ucode, utfmin[usize], utfmax[usize])
623 || BETWEEN (ucode, 0xD800, 0xDFFF))
624 return unknown;
625
626 return ucode;
627 }
628
629 /* get which font contains a given code point */
630 static XftFont *
getfontucode(struct Fonts * fnt,FcChar32 ucode)631 getfontucode(struct Fonts *fnt, FcChar32 ucode)
632 {
633 FcCharSet *fccharset = NULL;
634 FcPattern *fcpattern = NULL;
635 FcPattern *match = NULL;
636 XftFont *retfont = NULL;
637 XftResult result;
638 size_t i;
639
640 for (i = 0; i < fnt->nfonts; i++)
641 if (XftCharExists(dpy, fnt->fonts[i], ucode) == FcTrue)
642 return fnt->fonts[i];
643
644 /* create a charset containing our code point */
645 fccharset = FcCharSetCreate();
646 FcCharSetAddChar(fccharset, ucode);
647
648 /* create a pattern akin to the fnt->pattern but containing our charset */
649 if (fccharset) {
650 fcpattern = FcPatternDuplicate(fnt->pattern);
651 FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset);
652 }
653
654 /* find pattern matching fcpattern */
655 if (fcpattern) {
656 FcConfigSubstitute(NULL, fcpattern, FcMatchPattern);
657 FcDefaultSubstitute(fcpattern);
658 match = XftFontMatch(dpy, screen, fcpattern, &result);
659 }
660
661 /* if found a pattern, open its font */
662 if (match) {
663 retfont = XftFontOpenPattern(dpy, match);
664 if (retfont && XftCharExists(dpy, retfont, ucode) == FcTrue) {
665 if ((fnt->fonts = realloc(fnt->fonts, fnt->nfonts+1)) == NULL)
666 err(1, "realloc");
667 fnt->fonts[fnt->nfonts] = retfont;
668 return fnt->fonts[fnt->nfonts++];
669 } else {
670 XftFontClose(dpy, retfont);
671 }
672 }
673
674 /* in case no fount was found, return the first one */
675 return fnt->fonts[0];
676 }
677
678 /* draw text into draw (if draw != NULL); return width of text glyphs */
679 static int
drawtext(struct Fonts * fnt,XftDraw * draw,XftColor * color,int x,int y,const char * text)680 drawtext(struct Fonts *fnt, XftDraw *draw, XftColor *color, int x, int y, const char *text)
681 {
682 int textwidth = 0;
683
684 while (*text) {
685 XftFont *currfont;
686 XGlyphInfo ext;
687 FcChar32 ucode;
688 const char *next;
689 size_t len;
690
691 ucode = getnextutf8char(text, &next);
692 currfont = getfontucode(fnt, ucode);
693
694 len = next - text;
695 XftTextExtentsUtf8(dpy, currfont, (XftChar8 *)text, len, &ext);
696 textwidth += ext.xOff;
697
698 if (draw) {
699 int texty;
700
701 texty = y + (fnt->texth - (currfont->ascent + currfont->descent))/2 + currfont->ascent;
702 XftDrawStringUtf8(draw, color, currfont, x, texty, (XftChar8 *)text, len);
703 x += ext.xOff;
704 }
705
706 text = next;
707 }
708
709 return textwidth;
710 }
711
712 /* draw contents of notification item on item->pixmap */
713 static void
drawitem(struct Item * item)714 drawitem(struct Item *item)
715 {
716 XftDraw *draw;
717 int x, y;
718 int xaligned, oldx;
719
720 x = config.padding_pixels;
721 y = config.padding_pixels;
722 item->pixmap = XCreatePixmap(dpy, item->win, item->w, item->h, depth);
723 draw = XftDrawCreate(dpy, item->pixmap, visual, colormap);
724
725 /* draw background */
726 XSetForeground(dpy, dc.gc, item->background.pixel);
727 XFillRectangle(dpy, item->pixmap, dc.gc, 0, 0, item->w, item->h);
728
729 /* draw image */
730 if (item->image) {
731 imlib_context_set_image(item->image);
732 imlib_context_set_drawable(item->pixmap);
733 imlib_render_image_on_drawable((x + config.image_pixels - item->imgw) / 2,
734 (y + config.image_pixels - item->imgh) / 2);
735 imlib_free_image();
736 x += config.image_pixels;
737 }
738
739 /* draw text */
740 oldx = x;
741 switch (config.alignment) {
742 case LeftAlignment:
743 break;
744 case CenterAlignment:
745 xaligned = drawtext(&titlefnt, NULL, NULL, 0, 0, item->title);
746 xaligned = x + (item->w - x - titlefnt.texth - xaligned) / 2;
747 x = MAX(x, xaligned);
748 break;
749 case RightAlignment:
750 xaligned = drawtext(&titlefnt, NULL, NULL, 0, 0, item->title);
751 xaligned = x + (item->w - x - titlefnt.texth - xaligned);
752 x = MAX(x, xaligned);
753 break;
754 }
755 if (item->body)
756 y = (item->h - config.leading_pixels) / 2 - titlefnt.texth;
757 else
758 y = (item->h - titlefnt.texth) / 2;
759 drawtext(&titlefnt, draw, &item->foreground, x, y, item->title);
760
761 /* draw text body */
762 if (item->body) {
763 y += titlefnt.texth + config.leading_pixels;
764 x = oldx;
765 switch (config.alignment) {
766 case LeftAlignment:
767 break;
768 case CenterAlignment:
769 xaligned = drawtext(&bodyfnt, NULL, NULL, 0, 0, item->body);
770 xaligned = x + (item->w - x - bodyfnt.texth - xaligned) / 2;
771 x = MAX(x, xaligned);
772 break;
773 case RightAlignment:
774 xaligned = drawtext(&bodyfnt, NULL, NULL, 0, 0, item->body);
775 xaligned = x + (item->w - x - bodyfnt.texth - xaligned);
776 x = MAX(x, xaligned);
777 break;
778 }
779 drawtext(&bodyfnt, draw, &item->foreground, x, y, item->body);
780 }
781
782 /* change border color */
783 XSetWindowBorder(dpy, item->win, item->border.pixel);
784
785 XftDrawDestroy(draw);
786 }
787
788 /* reset time of item */
789 static void
resettime(struct Item * item)790 resettime(struct Item *item)
791 {
792 item->time = time(NULL);
793 }
794
795 /* add item notification item and set its window and contents */
796 static void
additem(struct Queue * queue,struct Itemspec * itemspec)797 additem(struct Queue *queue, struct Itemspec *itemspec)
798 {
799 struct Item *item;
800 int titlew, bodyw;
801 int w, h;
802
803 if ((item = malloc(sizeof *item)) == NULL)
804 err(1, "malloc");
805 item->next = NULL;
806 item->title = strdup(itemspec->title);
807 item->body = (itemspec->body) ? strdup(itemspec->body) : NULL;
808 item->image = (itemspec->file) ? loadimage(itemspec->file, &item->imgw, &item->imgh) : NULL;
809 item->tag = (itemspec->tag) ? strdup(itemspec->tag) : NULL;
810 item->cmd = (itemspec->cmd) ? strdup(itemspec->cmd) : NULL;
811 item->sec = itemspec->sec;
812 if (!queue->head)
813 queue->head = item;
814 else
815 queue->tail->next = item;
816 item->prev = queue->tail;
817 queue->tail = item;
818
819 /* allocate colors */
820 if (!itemspec->background || ealloccolor(itemspec->background, &item->background, 0) == -1)
821 item->background = dc.background;
822 if (!itemspec->foreground || ealloccolor(itemspec->foreground, &item->foreground, 0) == -1)
823 item->foreground = dc.foreground;
824 if (!itemspec->border || ealloccolor(itemspec->border, &item->border, 0) == -1)
825 item->border = dc.border;
826
827 /* compute notification height */
828 if (item->body)
829 h = titlefnt.texth + bodyfnt.texth + config.padding_pixels + config.leading_pixels;
830 else
831 h = titlefnt.texth + config.padding_pixels;
832
833 h = config.padding_pixels + ((item->image) ? MAX(h, config.image_pixels) : h);
834 item->h = MAX(h, queue->h);
835
836 /* compute notification width */
837 if (config.shrink) {
838 titlew = drawtext(&titlefnt, NULL, NULL, 0, 0, item->title);
839 bodyw = (item->body) ? drawtext(&bodyfnt, NULL, NULL, 0, 0, item->body) : 0;
840 w = MAX(titlew, bodyw);
841 if (item->image) {
842 w += config.image_pixels + config.padding_pixels * 2;
843 } else {
844 w += config.padding_pixels * 2;
845 }
846 item->w = MIN(w, queue->w);
847 } else {
848 item->w = queue->w;
849 }
850
851 /* call functions that set the item */
852 createwindow(item);
853 resettime(item);
854 drawitem(item);
855
856 /* a new item was added to the queue, so the queue changed */
857 queue->change = 1;
858 }
859
860 /* delete item */
861 static void
delitem(struct Queue * queue,struct Item * item)862 delitem(struct Queue *queue, struct Item *item)
863 {
864 free(item->title);
865 if (item->body)
866 free(item->body);
867 XFreePixmap(dpy, item->pixmap);
868 XDestroyWindow(dpy, item->win);
869 if (item->prev)
870 item->prev->next = item->next;
871 else
872 queue->head = item->next;
873 if (item->next)
874 item->next->prev = item->prev;
875 else
876 queue->tail = item->prev;
877 free(item);
878 queue->change = 1;
879 }
880
881 /* print item's command to stdout */
882 static void
cmditem(struct Item * item)883 cmditem(struct Item *item)
884 {
885 printf("%s\n", item->cmd);
886 }
887
888 /* check the type of option given to a notification item */
889 static enum ItemOption
optiontype(const char * s)890 optiontype(const char *s)
891 {
892 if (strncmp(s, "IMG:", 4) == 0)
893 return IMG;
894 if (strncmp(s, "BG:", 3) == 0)
895 return BG;
896 if (strncmp(s, "FG:", 3) == 0)
897 return FG;
898 if (strncmp(s, "BRD:", 4) == 0)
899 return BRD;
900 if (strncmp(s, "TAG:", 4) == 0)
901 return TAG;
902 if (strncmp(s, "CMD:", 4) == 0)
903 return CMD;
904 if (strncmp(s, "SEC:", 4) == 0)
905 return SEC;
906 return UNKNOWN;
907 }
908
909 /* parse notification line */
910 static struct Itemspec *
parseline(char * s)911 parseline(char *s)
912 {
913 enum ItemOption option;
914 struct Itemspec *itemspec;
915 const char *t;
916 int n;
917
918 if ((itemspec = malloc(sizeof *itemspec)) == NULL)
919 err(1, "malloc");
920
921 /* get the title */
922 itemspec->title = strtok(s, "\t\n");
923
924 /* get the filename */
925 itemspec->file = NULL;
926 itemspec->foreground = NULL;
927 itemspec->background = NULL;
928 itemspec->border = NULL;
929 itemspec->tag = NULL;
930 itemspec->cmd = NULL;
931 itemspec->sec = config.sec;
932 while (itemspec->title && (option = optiontype(itemspec->title)) != UNKNOWN) {
933 switch (option) {
934 case IMG:
935 itemspec->file = itemspec->title + 4;
936 itemspec->title = strtok(NULL, "\t\n");
937 break;
938 case BG:
939 itemspec->background = itemspec->title + 3;
940 itemspec->title = strtok(NULL, "\t\n");
941 break;
942 case FG:
943 itemspec->foreground = itemspec->title + 3;
944 itemspec->title = strtok(NULL, "\t\n");
945 break;
946 case BRD:
947 itemspec->border = itemspec->title + 4;
948 itemspec->title = strtok(NULL, "\t\n");
949 break;
950 case TAG:
951 itemspec->tag = itemspec->title + 4;
952 itemspec->title = strtok(NULL, "\t\n");
953 break;
954 case CMD:
955 itemspec->cmd = itemspec->title + 4;
956 itemspec->title = strtok(NULL, "\t\n");
957 break;
958 case SEC:
959 t = itemspec->title + 4;
960 if (!getnum(&t, &n))
961 itemspec->sec = n;
962 itemspec->title = strtok(NULL, "\t\n");
963 break;
964 default:
965 break;
966 }
967 }
968
969 /* get the body */
970 itemspec->body = strtok(NULL, "\n");
971 if (itemspec->body)
972 while (*itemspec->body == '\t')
973 itemspec->body++;
974
975 if (!itemspec->title)
976 return NULL;
977
978 return itemspec;
979 }
980
981 /* read x events */
982 static void
readevent(struct Queue * queue)983 readevent(struct Queue *queue)
984 {
985 struct Item *item;
986 XEvent ev;
987
988 while (XPending(dpy) && !XNextEvent(dpy, &ev)) {
989 switch (ev.type) {
990 case Expose:
991 if (ev.xexpose.count == 0 && (item = getitem(queue, ev.xexpose.window)) != NULL)
992 copypixmap(item);
993 break;
994 case ButtonPress:
995 if ((item = getitem(queue, ev.xbutton.window)) == NULL)
996 break;
997 if ((ev.xbutton.button == config.actionbutton) && item->cmd)
998 cmditem(item);
999 delitem(queue, item);
1000 break;
1001 case MotionNotify:
1002 if ((item = getitem(queue, ev.xmotion.window)) != NULL)
1003 resettime(item);
1004 break;
1005 case ConfigureNotify: /* monitor arrangement changed */
1006 if (ev.xproperty.window == root) {
1007 initmonitor();
1008 queue->change = 1;
1009 }
1010 break;
1011 }
1012 }
1013 }
1014
1015 /* check whether items have passed the time */
1016 static void
timeitems(struct Queue * queue)1017 timeitems(struct Queue *queue)
1018 {
1019 struct Item *item;
1020 struct Item *tmp;
1021
1022 item = queue->head;
1023 while (item) {
1024 tmp = item;
1025 item = item->next;
1026 if (tmp->sec && time(NULL) - tmp->time > tmp->sec) {
1027 delitem(queue, tmp);
1028 }
1029 }
1030 }
1031
1032 /* a notification was deleted or added, reorder the queue of notifications */
1033 static void
moveitems(struct Queue * queue)1034 moveitems(struct Queue *queue)
1035 {
1036 struct Item *item;
1037 int x, y;
1038 int h = 0;
1039
1040 for (item = queue->head; item; item = item->next) {
1041 x = queue->x + mon.x;
1042 y = queue->y + mon.y;
1043 switch (queue->gravity) {
1044 case NorthWestGravity:
1045 break;
1046 case NorthGravity:
1047 x += (mon.w - item->w) / 2 - config.border_pixels;
1048 break;
1049 case NorthEastGravity:
1050 x += mon.w - item->w - config.border_pixels * 2;
1051 break;
1052 case WestGravity:
1053 y += (mon.h - item->h) / 2 - config.border_pixels;
1054 break;
1055 case CenterGravity:
1056 x += (mon.w - item->w) / 2 - config.border_pixels;
1057 y += (mon.h - item->h) / 2 - config.border_pixels;
1058 break;
1059 case EastGravity:
1060 x += mon.w - item->w - config.border_pixels * 2;
1061 y += (mon.h - item->h) / 2 - config.border_pixels;
1062 break;
1063 case SouthWestGravity:
1064 y += mon.h - item->h - config.border_pixels * 2;
1065 break;
1066 case SouthGravity:
1067 x += (mon.w - item->w) / 2 - config.border_pixels;
1068 y += mon.h - item->h - config.border_pixels * 2;
1069 break;
1070 case SouthEastGravity:
1071 x += mon.w - item->w - config.border_pixels * 2;
1072 y += mon.h - item->h - config.border_pixels * 2;
1073 break;
1074 }
1075
1076 if (queue->direction == DownWards)
1077 y += h;
1078 else
1079 y -= h;
1080 h += item->h + config.gap_pixels + config.border_pixels * 2;
1081
1082 XMoveWindow(dpy, item->win, x, y);
1083 XMapWindow(dpy, item->win);
1084 copypixmap(item);
1085 }
1086
1087 queue->change = 0;
1088 }
1089
1090 /* destroy all notification items of the given tag, or all items if tag is NULL */
1091 static void
cleanitems(struct Queue * queue,const char * tag)1092 cleanitems(struct Queue *queue, const char *tag)
1093 {
1094 struct Item *item;
1095 struct Item *tmp;
1096
1097 item = queue->head;
1098 while (item) {
1099 tmp = item;
1100 item = item->next;
1101 if (tag == NULL || (tmp->tag && strcmp(tmp->tag, tag) == 0)) {
1102 delitem(queue, tmp);
1103 }
1104 }
1105 }
1106
1107 /* clean up dc elements */
1108 static void
cleandc(void)1109 cleandc(void)
1110 {
1111 XftColorFree(dpy, visual, colormap, &dc.background);
1112 XftColorFree(dpy, visual, colormap, &dc.foreground);
1113 XftColorFree(dpy, visual, colormap, &dc.border);
1114
1115 XFreeColormap(dpy, colormap);
1116
1117 XFreeGC(dpy, dc.gc);
1118 }
1119
1120 /* xnotify: show notifications from stdin */
1121 int
main(int argc,char * argv[])1122 main(int argc, char *argv[])
1123 {
1124 struct Itemspec *itemspec;
1125 struct Queue *queue; /* it contains the queue of notifications and their geometry */
1126 struct pollfd pfd[2]; /* [2] for stdin and xfd, see poll(2) */
1127 char buf[BUFSIZ]; /* buffer for stdin */
1128 int timeout = -1; /* maximum interval for poll(2) to complete */
1129 int flags; /* status flags for stdin */
1130 int done = 0; /* set to 1 when stdin reaches EOF */
1131
1132 /* open connection to server and set X variables */
1133 if ((dpy = XOpenDisplay(NULL)) == NULL)
1134 errx(1, "could not open display");
1135 screen = DefaultScreen(dpy);
1136 root = RootWindow(dpy, screen);
1137 visual = DefaultVisual(dpy, screen);
1138 depth = DefaultDepth(dpy, screen);
1139 colormap = DefaultColormap(dpy, screen);
1140 xfd = XConnectionNumber(dpy);
1141 XrmInitialize();
1142 if ((xrm = XResourceManagerString(dpy)) != NULL)
1143 xdb = XrmGetStringDatabase(xrm);
1144
1145 /* get configuration */
1146 getresources();
1147 getoptions(argc, argv);
1148
1149 /* imlib2 stuff */
1150 imlib_set_cache_size(2048 * 1024);
1151 imlib_context_set_dither(1);
1152 imlib_context_set_display(dpy);
1153 imlib_context_set_visual(visual);
1154 imlib_context_set_colormap(colormap);
1155
1156 /* init stuff */
1157 initsignal();
1158 initmonitor();
1159 initdc();
1160 initatoms();
1161 initstructurenotify();
1162
1163 /* set up queue of notifications */
1164 queue = setqueue();
1165
1166 /* Make stdin nonblocking */
1167 if ((flags = fcntl(STDIN_FILENO, F_GETFL)) == -1)
1168 err(1, "could not get status flags for stdin");
1169 if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1)
1170 err(1, "could not set status flags for stdin");
1171
1172 /* prepare the structure for poll(2) */
1173 pfd[0].fd = STDIN_FILENO;
1174 pfd[1].fd = xfd;
1175 pfd[0].events = pfd[1].events = POLLIN;
1176
1177 /* run main loop */
1178 for (;;) {
1179 if (poll(pfd, 2, timeout) > 0) {
1180 if (pfd[0].revents & POLLHUP) {
1181 pfd[0].fd = -1;
1182 done = 1;
1183 }
1184 if (pfd[0].revents & POLLIN) {
1185 if (fgets(buf, sizeof buf, stdin) == NULL)
1186 break;
1187 if ((itemspec = parseline(buf)) != NULL) {
1188 if (oflag) {
1189 cleanitems(queue, NULL);
1190 } else if (itemspec->tag) {
1191 cleanitems(queue, itemspec->tag);
1192 }
1193 additem(queue, itemspec);
1194 }
1195 }
1196 if (pfd[1].revents & POLLIN) {
1197 readevent(queue);
1198 }
1199 }
1200 if (usrflag) {
1201 if (usrflag > 1 && queue->head)
1202 cmditem(queue->head);
1203 cleanitems(queue, NULL);
1204 usrflag = 0;
1205 }
1206 timeitems(queue);
1207 if (queue->change)
1208 moveitems(queue);
1209 timeout = (queue->head) ? 1000 : -1;
1210 XFlush(dpy);
1211
1212 if (done && !queue->head)
1213 break;
1214 }
1215
1216 /* clean up stuff */
1217 cleanitems(queue, NULL);
1218 cleandc();
1219 free(queue);
1220
1221 /* close connection to server */
1222 XrmDestroyDatabase(xdb);
1223 XCloseDisplay(dpy);
1224
1225 return 0;
1226 }
1227