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 }