sxalert

Simple notification system for X
git clone git://mcdim.xyz/sxalert.git
Log | Files | Refs | README | LICENSE

sxalert.c (7087B)


      1 /* sxalert - simple notification program for X
      2  *
      3  * Copyright (C) 2023, 2026 Michail Konstantinos Dimopoulos
      4  * License: GPL-3.0-or-later (see LICENSE)
      5  */
      6 
      7 #include <stdlib.h>
      8 #include <stdio.h>
      9 #include <unistd.h>
     10 #include <string.h>
     11 #include <stdbool.h>
     12 #include <getopt.h>
     13 #include <time.h>
     14 #include <poll.h>
     15 #include <X11/Xft/Xft.h>
     16 #include <X11/extensions/Xrandr.h>
     17 #include <sys/file.h>
     18 
     19 #include "config.h"
     20 
     21 #define VERSION "v1.0"
     22 
     23 static inline void
     24 die(const char *msg, int code)
     25 {
     26 	fprintf(stderr, "%s\n", msg);
     27 	exit(code);
     28 }
     29 
     30 static inline void
     31 help(char *bin)
     32 {
     33 	printf("sxalert %s - simple alert utility for X\n\n"
     34 	       "Options:\n"
     35 	       " -t color       Set text color (hexadecimal, e.g., FFFFFF)\n"
     36 	       " -g color       Set background hex color\n"
     37 	       " -r color       Set border color\n"
     38 	       " -b width       Set border width in pixels\n"
     39 	       " -d duration    Set duration in milliseconds\n"
     40 	       " -s		Read from standard input\n"
     41 	       " -v             Print version and exit\n"
     42 	       " -h             Print help and exit\n\n"
     43 	       "Usage: %s [-s] [-t color] [-g color] [-r color]\n"
     44 	       "       %*s [-b width] [-d duration] [line ...]\n",
     45 	       VERSION, bin, (int)strlen(bin), "");
     46 	exit(0);
     47 }
     48 
     49 static unsigned int
     50 hex2int(char *str)
     51 {
     52 	unsigned int num;
     53 	sscanf(str, "%x", &num);
     54 	return num;
     55 }
     56 
     57 static int
     58 get_max(int arr[], int len)
     59 {
     60 	int max = -1;
     61 	for (int i=0; i<len; i++) {
     62 		if (arr[i] > max)
     63 			max = arr[i];
     64 	}
     65 
     66 	return max;
     67 }
     68 
     69 static int
     70 get_width(Display *dpy, XftFont *font, struct alert *a)
     71 {
     72 	XGlyphInfo ext;
     73 	int width = 0;
     74 	if (a->lines_len > 0) {
     75 		int width_lines[a->lines_len];
     76 		for (int i = 0; i < a->lines_len; i++) {
     77 			XftTextExtentsUtf8(dpy, font,
     78 				           (XftChar8*)a->lines[i],
     79 					   (int)strlen(a->lines[i]),
     80 					   &ext);
     81     			width_lines[i] = ext.xOff;
     82 		}
     83 
     84 		int text_width = get_max(width_lines, a->lines_len);
     85 		width = (a->text_x_padding * 2) + text_width;
     86 	}
     87 
     88 	if (width < a->min_width)
     89 		return a->min_width;
     90 	else if (width > a->max_width)
     91 		return a->max_width;
     92 	return width;
     93 }
     94 
     95 static int
     96 find_window(struct xwin *windows, int count, Window id)
     97 {
     98 	for (int i = 0; i < count; ++i) {
     99 		if (windows[i].win == id)
    100 			return i;
    101 	}
    102 
    103 	return -1;
    104 }
    105 
    106 static void
    107 write_text(Display *dpy, struct xwin *windows, int count,
    108            XftColor color, XftFont *font, struct alert *a)
    109 {
    110 	while (1) {
    111 		struct pollfd pfd = {
    112 		        .fd = ConnectionNumber(dpy),
    113 		        .events = POLLIN,
    114 		};
    115 		
    116 		int pending = XPending(dpy) > 0 ||
    117 			      poll(&pfd, 1, a->duration) > 0;
    118 
    119 		if (!pending)
    120 		        break;
    121 		
    122 		XEvent ev;
    123 		XNextEvent(dpy, &ev);
    124 	
    125 		int i = find_window(windows, count, ev.xany.window);
    126 		
    127 		if (i < 0)
    128 		        continue;
    129 		
    130 		if (ev.type == Expose) {
    131 			int spacing = a->text_height * 2;
    132 			
    133 			for (int line = 0; line < a->lines_len; ++line) {
    134 				XftDrawStringUtf8(windows[i].draw, &color,
    135 					font, a->text_x_padding, spacing,
    136 				        (XftChar8 *)a->lines[line],
    137 				        (int)strlen(a->lines[line])
    138 				);
    139 				
    140 				spacing += a->text_height * 2;
    141 			}
    142 		} else if (ev.type == ButtonPress) {
    143 			switch (ev.xbutton.button) {
    144 			case Button1: /* left click */
    145 				exit(0);
    146 				break;
    147 			case Button2: /* middle click */
    148 				exit(1);
    149 				break;
    150 			case Button3: /* right click */
    151 				exit(2);
    152 				break;
    153 			}
    154 		}
    155 	}
    156 }
    157 
    158 
    159 static void
    160 init_render(Display *dpy, struct xrender *r, struct alert *a)
    161 {
    162 	char text_color_pnd[8];
    163 	snprintf(text_color_pnd, 8, "#%s", a->text_color);
    164 
    165 	r->scr = DefaultScreen(dpy);
    166 	r->visual = DefaultVisual(dpy, r->scr);
    167 	r->cmap = DefaultColormap(dpy, r->scr);
    168 
    169 	r->font = XftFontOpenName(dpy, r->scr, a->font);
    170 	if (!r->font)
    171 		die("Cannot load font\n", -1);
    172 	if (!XftColorAllocName(dpy, r->visual, r->cmap,
    173 			       text_color_pnd, &(r->color)))
    174 		die("Cannot allocate Xft color\n", -1);
    175 }
    176 
    177 static void
    178 init_window(Display *dpy, struct xrender *r, struct xwin *w,
    179 	    struct alert *a, XRRMonitorInfo * monitor)
    180 {
    181 	w->width = get_width(dpy, r->font, a);
    182 	w->height = a->lines_len * (a->text_height * 2) + a->text_height;
    183 
    184 	w->x = monitor->x + monitor->width - w->width - a->x_offset;
    185 	w->y = monitor->y + a->y_offset;
    186 
    187 	w->win = XCreateSimpleWindow(dpy, RootWindow(dpy, r->scr), w->x,
    188 				     a->y_offset, (unsigned int)w->width,
    189 				     (unsigned int)w->height,
    190 				     (unsigned int)a->border_width,
    191 				     hex2int(a->border_color),
    192 				     hex2int(a->bg_color));
    193 	XSetWindowAttributes attr = {
    194         	.override_redirect = True
    195 	};
    196 	XChangeWindowAttributes(dpy, w->win, CWOverrideRedirect, &attr);
    197 	XSelectInput(dpy, w->win, ExposureMask | KeyPressMask);
    198 	XMapWindow(dpy, w->win);
    199 
    200 	w->draw = XftDrawCreate(dpy, w->win, r->visual, r->cmap);
    201 
    202 	XSelectInput(dpy, w->win, ExposureMask | ButtonPressMask);
    203 }
    204 
    205 static void
    206 draw(struct alert a)
    207 {
    208 	Display *dpy = XOpenDisplay(NULL);
    209 	if (!dpy)
    210 		die("Cannot open X11 display\n", -1);
    211 
    212 	struct xrender r;
    213 	init_render(dpy, &r, &a);
    214 
    215 	struct mon m;
    216 	m.monitor_count = 1;
    217 	m.monitors = XRRGetMonitors(dpy, RootWindow(dpy, r.scr),
    218 				    True, &m.monitor_count);
    219 	
    220 	struct xwin *w = calloc((size_t)m.monitor_count,
    221                                 sizeof(struct xwin));
    222 	if (!w)
    223 		die("Cannot allocate window array\n", -1);
    224 
    225 	for (int i = 0; i < m.monitor_count; i++)
    226 		init_window(dpy, &r, &w[i], &a, &m.monitors[i]);
    227 
    228 	write_text(dpy, w, m.monitor_count, r.color, r.font, &a);
    229 }
    230 
    231 static void
    232 read_stdin(struct alert *a, FILE *f)
    233 {
    234 	size_t buffer = 264;
    235 	char *p = malloc(buffer);
    236 
    237 	int i = 0;
    238 	while (fgets(p, (int)buffer, f) != NULL) {
    239 		for (int j=0; j<264; j++) {
    240 			if (p[j] == '\n')
    241 				p[j] = '\0';
    242 		}
    243 
    244 		a->lines = realloc(a->lines,
    245 				   (size_t)(i + 1)*sizeof(*a->lines));
    246 		if (a->lines == NULL) {
    247 			die("Cannot allocate memory\n", -1);
    248 		}
    249 		a->lines[i] = calloc(buffer, 1);
    250 		strncpy(a->lines[i], p, buffer);
    251 		i++;
    252 	}
    253 	a->lines_len = i;
    254 	free(p);
    255 }
    256 
    257 
    258 int
    259 main(int argc, char **argv)
    260 {
    261 	int c = 3;
    262 	extern char *optarg;
    263 
    264 	struct alert a = default_alert;
    265 
    266 	int s = 0;
    267 	while ((c = getopt(argc, argv, "d:b:t:g:r:svh")) != -1 ) {
    268 		switch (c) {
    269 		case 'v':
    270 			die(VERSION, EXIT_SUCCESS);
    271 			break;
    272 		case 'h':
    273 			help(argv[0]);
    274 			break;
    275 		case 'd':
    276 			a.duration=atoi(optarg);
    277 			if (a.duration == 0)
    278 				a.duration = -1;
    279 			break;
    280 		case 'b':
    281 			a.border_width=atoi(optarg);
    282 			break;
    283 		case 't':
    284 			strncpy(a.text_color, optarg, 7);
    285 			break;
    286 		case 'g':
    287 			strncpy(a.bg_color, optarg, 7);
    288 			break;
    289 		case 'r':
    290 			strncpy(a.border_color, optarg, 7);
    291 			break;
    292 		case 's':
    293 			s = 1;
    294 			break;
    295 		}
    296 	}
    297 
    298 	int fd = open("/tmp/sxalert.lock", O_CREAT | O_RDWR, 0600);
    299 	if (fd == -1)
    300 		return 1;
    301 	if (flock(fd, LOCK_EX) == -1)
    302 		return 1;
    303 
    304 	int cmd_lines_len = argc - optind;
    305 	char **lines = argv + optind;
    306 
    307 	if (!s) {
    308 		a.lines = lines;
    309 		a.lines_len = cmd_lines_len;
    310 		draw(a);
    311 		return 3;
    312 	}
    313 
    314 	FILE *f = tmpfile();
    315 	int cc;
    316 	
    317 	while ((cc = getchar()) != EOF)
    318 	    fputc(cc, f);
    319 	
    320 	for (int i=0; i<cmd_lines_len; i++)
    321 		fprintf(f, "%s\n", lines[i]);
    322 		//fputs(lines[i], f);
    323 	
    324 	a.lines = NULL;
    325 	a.lines_len = 0;
    326 	rewind(f);
    327 	read_stdin(&a, f);
    328 
    329 	draw(a);
    330 
    331 	return 4;
    332 }