sent.c (16317B)
1 /* See LICENSE file for copyright and license details. */ 2 #include <sys/types.h> 3 #include <arpa/inet.h> 4 5 #include <errno.h> 6 #include <fcntl.h> 7 #include <math.h> 8 #include <regex.h> 9 #include <stdarg.h> 10 #include <stdio.h> 11 #include <stdint.h> 12 #include <stdlib.h> 13 #include <string.h> 14 #include <unistd.h> 15 #include <X11/keysym.h> 16 #include <X11/XKBlib.h> 17 #include <X11/Xatom.h> 18 #include <X11/Xlib.h> 19 #include <X11/Xutil.h> 20 #include <X11/Xft/Xft.h> 21 22 #include "arg.h" 23 #include "util.h" 24 #include "drw.h" 25 26 char *argv0; 27 int use_inverted_colors = 0; 28 29 /* macros */ 30 #define LEN(a) (sizeof(a) / sizeof(a)[0]) 31 #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) 32 #define MAXFONTSTRLEN 128 33 34 typedef enum { 35 NONE = 0, 36 SCALED = 1, 37 } imgstate; 38 39 typedef struct { 40 unsigned char *buf; 41 unsigned int bufwidth, bufheight; 42 imgstate state; 43 XImage *ximg; 44 int numpasses; 45 } Image; 46 47 typedef struct { 48 char *regex; 49 char *bin; 50 } Filter; 51 52 typedef struct { 53 unsigned int linecount; 54 char **lines; 55 Image *img; 56 char *embed; 57 } Slide; 58 59 /* Purely graphic info */ 60 typedef struct { 61 Display *dpy; 62 Window win; 63 Atom wmdeletewin, netwmname; 64 Visual *vis; 65 XSetWindowAttributes attrs; 66 int scr; 67 int w, h; 68 int uw, uh; /* usable dimensions for drawing text and images */ 69 } XWindow; 70 71 typedef union { 72 int i; 73 unsigned int ui; 74 float f; 75 const void *v; 76 } Arg; 77 78 typedef struct { 79 unsigned int b; 80 void (*func)(const Arg *); 81 const Arg arg; 82 } Mousekey; 83 84 typedef struct { 85 KeySym keysym; 86 void (*func)(const Arg *); 87 const Arg arg; 88 } Shortcut; 89 90 static void fffree(Image *img); 91 static void ffload(Slide *s); 92 static void ffprepare(Image *img); 93 static void ffscale(Image *img); 94 static void ffdraw(Image *img); 95 96 static void getfontsize(Slide *s, unsigned int *width, unsigned int *height); 97 static void cleanup(int slidesonly); 98 static void reload(const Arg *arg); 99 static void load(FILE *fp); 100 static void advance(const Arg *arg); 101 static void quit(const Arg *arg); 102 static void resize(int width, int height); 103 static void run(); 104 static void usage(); 105 static void xdraw(); 106 static void xhints(); 107 static void xinit(); 108 static void xloadfonts(); 109 static void togglescm(); 110 111 static void bpress(XEvent *); 112 static void cmessage(XEvent *); 113 static void expose(XEvent *); 114 static void kpress(XEvent *); 115 static void configure(XEvent *); 116 117 /* config.h for applying patches and the configuration. */ 118 #include "config.h" 119 120 /* Globals */ 121 static const char *fname = NULL; 122 static Slide *slides = NULL; 123 static int idx = 0; 124 static int slidecount = 0; 125 static XWindow xw; 126 static Drw *d = NULL; 127 static Clr *sc; 128 static Fnt *fonts[NUMFONTSCALES]; 129 static int running = 1; 130 131 static void (*handler[LASTEvent])(XEvent *) = { 132 [ButtonPress] = bpress, 133 [ClientMessage] = cmessage, 134 [ConfigureNotify] = configure, 135 [Expose] = expose, 136 [KeyPress] = kpress, 137 }; 138 139 int 140 filter(int fd, const char *cmd) 141 { 142 int fds[2]; 143 144 if (pipe(fds) < 0) 145 die("sent: Unable to create pipe:"); 146 147 switch (fork()) { 148 case -1: 149 die("sent: Unable to fork:"); 150 case 0: 151 dup2(fd, 0); 152 dup2(fds[1], 1); 153 close(fds[0]); 154 close(fds[1]); 155 execlp("sh", "sh", "-c", cmd, (char *)0); 156 fprintf(stderr, "sent: execlp sh -c '%s': %s\n", cmd, strerror(errno)); 157 _exit(1); 158 } 159 close(fds[1]); 160 return fds[0]; 161 } 162 163 void 164 fffree(Image *img) 165 { 166 free(img->buf); 167 if (img->ximg) 168 XDestroyImage(img->ximg); 169 free(img); 170 } 171 172 void 173 ffload(Slide *s) 174 { 175 uint32_t y, x; 176 uint16_t *row; 177 uint8_t opac, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b; 178 size_t rowlen, off, nbytes, i; 179 ssize_t count; 180 unsigned char hdr[16]; 181 char *bin = NULL; 182 char *filename; 183 regex_t regex; 184 int fdin, fdout; 185 186 if (s->img || !(filename = s->embed) || !s->embed[0]) 187 return; /* already done */ 188 189 for (i = 0; i < LEN(filters); i++) { 190 if (regcomp(®ex, filters[i].regex, 191 REG_NOSUB | REG_EXTENDED | REG_ICASE)) { 192 fprintf(stderr, "sent: Invalid regex '%s'\n", filters[i].regex); 193 continue; 194 } 195 if (!regexec(®ex, filename, 0, NULL, 0)) { 196 bin = filters[i].bin; 197 regfree(®ex); 198 break; 199 } 200 regfree(®ex); 201 } 202 if (!bin) 203 die("sent: Unable to find matching filter for '%s'", filename); 204 205 if ((fdin = open(filename, O_RDONLY)) < 0) 206 die("sent: Unable to open '%s':", filename); 207 208 if ((fdout = filter(fdin, bin)) < 0) 209 die("sent: Unable to filter '%s':", filename); 210 close(fdin); 211 212 if (read(fdout, hdr, 16) != 16) 213 die("sent: Unable to read filtered file '%s':", filename); 214 if (memcmp("farbfeld", hdr, 8)) 215 die("sent: Filtered file '%s' has no valid farbfeld header", filename); 216 217 s->img = ecalloc(1, sizeof(Image)); 218 s->img->bufwidth = ntohl(*(uint32_t *)&hdr[8]); 219 s->img->bufheight = ntohl(*(uint32_t *)&hdr[12]); 220 221 if (s->img->buf) 222 free(s->img->buf); 223 /* internally the image is stored in 888 format */ 224 s->img->buf = ecalloc(s->img->bufwidth * s->img->bufheight, strlen("888")); 225 226 /* scratch buffer to read row by row */ 227 rowlen = s->img->bufwidth * 2 * strlen("RGBA"); 228 row = ecalloc(1, rowlen); 229 230 /* extract window background color channels for transparency */ 231 bg_r = (sc[ColBg].pixel >> 16) % 256; 232 bg_g = (sc[ColBg].pixel >> 8) % 256; 233 bg_b = (sc[ColBg].pixel >> 0) % 256; 234 235 for (off = 0, y = 0; y < s->img->bufheight; y++) { 236 nbytes = 0; 237 while (nbytes < rowlen) { 238 count = read(fdout, (char *)row + nbytes, rowlen - nbytes); 239 if (count < 0) 240 die("sent: Unable to read from pipe:"); 241 nbytes += count; 242 } 243 for (x = 0; x < rowlen / 2; x += 4) { 244 fg_r = ntohs(row[x + 0]) / 257; 245 fg_g = ntohs(row[x + 1]) / 257; 246 fg_b = ntohs(row[x + 2]) / 257; 247 opac = ntohs(row[x + 3]) / 257; 248 /* blend opaque part of image data with window background color to 249 * emulate transparency */ 250 s->img->buf[off++] = (fg_r * opac + bg_r * (255 - opac)) / 255; 251 s->img->buf[off++] = (fg_g * opac + bg_g * (255 - opac)) / 255; 252 s->img->buf[off++] = (fg_b * opac + bg_b * (255 - opac)) / 255; 253 } 254 } 255 256 free(row); 257 close(fdout); 258 } 259 260 void 261 ffprepare(Image *img) 262 { 263 int depth = DefaultDepth(xw.dpy, xw.scr); 264 int width = xw.uw; 265 int height = xw.uh; 266 267 if (xw.uw * img->bufheight > xw.uh * img->bufwidth) 268 width = img->bufwidth * xw.uh / img->bufheight; 269 else 270 height = img->bufheight * xw.uw / img->bufwidth; 271 272 if (depth < 24) 273 die("sent: Display color depths < 24 not supported"); 274 275 if (!(img->ximg = XCreateImage(xw.dpy, CopyFromParent, depth, ZPixmap, 0, 276 NULL, width, height, 32, 0))) 277 die("sent: Unable to create XImage"); 278 279 img->ximg->data = ecalloc(height, img->ximg->bytes_per_line); 280 if (!XInitImage(img->ximg)) 281 die("sent: Unable to initiate XImage"); 282 283 ffscale(img); 284 img->state |= SCALED; 285 } 286 287 void 288 ffscale(Image *img) 289 { 290 unsigned int x, y; 291 unsigned int width = img->ximg->width; 292 unsigned int height = img->ximg->height; 293 char* newBuf = img->ximg->data; 294 unsigned char* ibuf; 295 unsigned int jdy = img->ximg->bytes_per_line / 4 - width; 296 unsigned int dx = (img->bufwidth << 10) / width; 297 298 for (y = 0; y < height; y++) { 299 unsigned int bufx = img->bufwidth / width; 300 ibuf = &img->buf[y * img->bufheight / height * img->bufwidth * 3]; 301 302 for (x = 0; x < width; x++) { 303 *newBuf++ = (ibuf[(bufx >> 10)*3+2]); 304 *newBuf++ = (ibuf[(bufx >> 10)*3+1]); 305 *newBuf++ = (ibuf[(bufx >> 10)*3+0]); 306 newBuf++; 307 bufx += dx; 308 } 309 newBuf += jdy; 310 } 311 } 312 313 void 314 ffdraw(Image *img) 315 { 316 int xoffset = (xw.w - img->ximg->width) / 2; 317 int yoffset = (xw.h - img->ximg->height) / 2; 318 XPutImage(xw.dpy, xw.win, d->gc, img->ximg, 0, 0, 319 xoffset, yoffset, img->ximg->width, img->ximg->height); 320 XFlush(xw.dpy); 321 } 322 323 void 324 getfontsize(Slide *s, unsigned int *width, unsigned int *height) 325 { 326 int i, j; 327 unsigned int curw, newmax; 328 float lfac = linespacing * (s->linecount - 1) + 1; 329 330 /* fit height */ 331 for (j = NUMFONTSCALES - 1; j >= 0; j--) 332 if (fonts[j]->h * lfac <= xw.uh) 333 break; 334 LIMIT(j, 0, NUMFONTSCALES - 1); 335 drw_setfontset(d, fonts[j]); 336 337 /* fit width */ 338 *width = 0; 339 for (i = 0; i < s->linecount; i++) { 340 curw = drw_fontset_getwidth(d, s->lines[i]); 341 newmax = (curw >= *width); 342 while (j > 0 && curw > xw.uw) { 343 drw_setfontset(d, fonts[--j]); 344 curw = drw_fontset_getwidth(d, s->lines[i]); 345 } 346 if (newmax) 347 *width = curw; 348 } 349 *height = fonts[j]->h * lfac; 350 } 351 352 void 353 cleanup(int slidesonly) 354 { 355 unsigned int i, j; 356 357 if (!slidesonly) { 358 for (i = 0; i < NUMFONTSCALES; i++) 359 drw_fontset_free(fonts[i]); 360 free(sc); 361 drw_free(d); 362 363 XDestroyWindow(xw.dpy, xw.win); 364 XSync(xw.dpy, False); 365 XCloseDisplay(xw.dpy); 366 } 367 368 if (slides) { 369 for (i = 0; i < slidecount; i++) { 370 for (j = 0; j < slides[i].linecount; j++) 371 free(slides[i].lines[j]); 372 free(slides[i].lines); 373 if (slides[i].img) 374 fffree(slides[i].img); 375 } 376 if (!slidesonly) { 377 free(slides); 378 slides = NULL; 379 } 380 } 381 } 382 383 void 384 reload(const Arg *arg) 385 { 386 FILE *fp = NULL; 387 unsigned int i; 388 389 if (!fname) { 390 fprintf(stderr, "sent: Cannot reload from stdin. Use a file!\n"); 391 return; 392 } 393 394 cleanup(1); 395 slidecount = 0; 396 397 if (!(fp = fopen(fname, "r"))) 398 die("sent: Unable to open '%s' for reading:", fname); 399 load(fp); 400 fclose(fp); 401 402 LIMIT(idx, 0, slidecount-1); 403 for (i = 0; i < slidecount; i++) 404 ffload(&slides[i]); 405 xdraw(); 406 } 407 408 void 409 load(FILE *fp) 410 { 411 static size_t size = 0; 412 size_t blen, maxlines; 413 char buf[BUFSIZ], *p; 414 Slide *s; 415 416 /* read each line from fp and add it to the item list */ 417 while (1) { 418 /* eat consecutive empty lines */ 419 while ((p = fgets(buf, sizeof(buf), fp))) 420 if (strcmp(buf, "\n") != 0 && buf[0] != '#') 421 break; 422 if (!p) 423 break; 424 425 if ((slidecount+1) * sizeof(*slides) >= size) 426 if (!(slides = realloc(slides, (size += BUFSIZ)))) 427 die("sent: Unable to reallocate %u bytes:", size); 428 429 /* read one slide */ 430 maxlines = 0; 431 memset((s = &slides[slidecount]), 0, sizeof(Slide)); 432 do { 433 /* if there's a leading null, we can't do blen-1 */ 434 if (buf[0] == '\0') 435 continue; 436 437 if (buf[0] == '#') 438 continue; 439 440 /* grow lines array */ 441 if (s->linecount >= maxlines) { 442 maxlines = 2 * s->linecount + 1; 443 if (!(s->lines = realloc(s->lines, maxlines * sizeof(s->lines[0])))) 444 die("sent: Unable to reallocate %u bytes:", maxlines * sizeof(s->lines[0])); 445 } 446 447 blen = strlen(buf); 448 if (!(s->lines[s->linecount] = strdup(buf))) 449 die("sent: Unable to strdup:"); 450 if (s->lines[s->linecount][blen-1] == '\n') 451 s->lines[s->linecount][blen-1] = '\0'; 452 453 /* mark as image slide if first line of a slide starts with @ */ 454 if (s->linecount == 0 && s->lines[0][0] == '@') 455 s->embed = &s->lines[0][1]; 456 457 if (s->lines[s->linecount][0] == '\\') 458 memmove(s->lines[s->linecount], &s->lines[s->linecount][1], blen); 459 s->linecount++; 460 } while ((p = fgets(buf, sizeof(buf), fp)) && strcmp(buf, "\n") != 0); 461 462 slidecount++; 463 if (!p) 464 break; 465 } 466 467 if (!slidecount) 468 die("sent: No slides in file"); 469 } 470 471 void 472 advance(const Arg *arg) 473 { 474 int new_idx = idx + arg->i; 475 LIMIT(new_idx, 0, slidecount-1); 476 if (new_idx != idx) { 477 if (slides[idx].img) 478 slides[idx].img->state &= ~SCALED; 479 idx = new_idx; 480 xdraw(); 481 } 482 } 483 484 void 485 quit(const Arg *arg) 486 { 487 running = 0; 488 } 489 490 void 491 resize(int width, int height) 492 { 493 xw.w = width; 494 xw.h = height; 495 xw.uw = usablewidth * width; 496 xw.uh = usableheight * height; 497 drw_resize(d, width, height); 498 } 499 500 void 501 run() 502 { 503 XEvent ev; 504 505 /* Waiting for window mapping */ 506 while (1) { 507 XNextEvent(xw.dpy, &ev); 508 if (ev.type == ConfigureNotify) { 509 resize(ev.xconfigure.width, ev.xconfigure.height); 510 } else if (ev.type == MapNotify) { 511 break; 512 } 513 } 514 515 while (running) { 516 XNextEvent(xw.dpy, &ev); 517 if (handler[ev.type]) 518 (handler[ev.type])(&ev); 519 } 520 } 521 522 void 523 xdraw() 524 { 525 unsigned int height, width, i; 526 Image *im = slides[idx].img; 527 528 getfontsize(&slides[idx], &width, &height); 529 XClearWindow(xw.dpy, xw.win); 530 531 if (!im) { 532 drw_rect(d, 0, 0, xw.w, xw.h, 1, 1); 533 for (i = 0; i < slides[idx].linecount; i++) 534 drw_text(d, 535 (xw.w - width) / 2, 536 (xw.h - height) / 2 + i * linespacing * d->fonts->h, 537 width, 538 d->fonts->h, 539 0, 540 slides[idx].lines[i], 541 0); 542 if (idx != 0 && progressheight != 0) { 543 drw_rect(d, 544 0, xw.h - progressheight, 545 (xw.w * idx)/(slidecount - 1), progressheight, 546 1, 0); 547 } 548 drw_map(d, xw.win, 0, 0, xw.w, xw.h); 549 } else { 550 if (!(im->state & SCALED)) 551 ffprepare(im); 552 ffdraw(im); 553 } 554 } 555 556 void 557 xhints() 558 { 559 XClassHint class = {.res_name = "sent", .res_class = "presenter"}; 560 XWMHints wm = {.flags = InputHint, .input = True}; 561 XSizeHints *sizeh = NULL; 562 563 if (!(sizeh = XAllocSizeHints())) 564 die("sent: Unable to allocate size hints"); 565 566 sizeh->flags = PSize; 567 sizeh->height = xw.h; 568 sizeh->width = xw.w; 569 570 XSetWMProperties(xw.dpy, xw.win, NULL, NULL, NULL, 0, sizeh, &wm, &class); 571 XFree(sizeh); 572 } 573 574 void 575 xinit() 576 { 577 XTextProperty prop; 578 unsigned int i; 579 580 if (!(xw.dpy = XOpenDisplay(NULL))) 581 die("sent: Unable to open display"); 582 xw.scr = XDefaultScreen(xw.dpy); 583 xw.vis = XDefaultVisual(xw.dpy, xw.scr); 584 resize(DisplayWidth(xw.dpy, xw.scr), DisplayHeight(xw.dpy, xw.scr)); 585 586 xw.attrs.bit_gravity = CenterGravity; 587 xw.attrs.event_mask = KeyPressMask | ExposureMask | StructureNotifyMask | 588 ButtonMotionMask | ButtonPressMask; 589 590 xw.win = XCreateWindow(xw.dpy, XRootWindow(xw.dpy, xw.scr), 0, 0, 591 xw.w, xw.h, 0, XDefaultDepth(xw.dpy, xw.scr), 592 InputOutput, xw.vis, CWBitGravity | CWEventMask, 593 &xw.attrs); 594 595 xw.wmdeletewin = XInternAtom(xw.dpy, "WM_DELETE_WINDOW", False); 596 xw.netwmname = XInternAtom(xw.dpy, "_NET_WM_NAME", False); 597 XSetWMProtocols(xw.dpy, xw.win, &xw.wmdeletewin, 1); 598 599 if (!(d = drw_create(xw.dpy, xw.scr, xw.win, xw.w, xw.h))) 600 die("sent: Unable to create drawing context"); 601 //sc = drw_scm_create(d, colors, 2); 602 if (use_inverted_colors) { 603 sc = drw_scm_create(d, inverted_colors, 2); 604 } else { 605 sc = drw_scm_create(d, colors, 2); 606 } 607 drw_setscheme(d, sc); 608 XSetWindowBackground(xw.dpy, xw.win, sc[ColBg].pixel); 609 610 xloadfonts(); 611 for (i = 0; i < slidecount; i++) 612 ffload(&slides[i]); 613 614 XStringListToTextProperty(&argv0, 1, &prop); 615 XSetWMName(xw.dpy, xw.win, &prop); 616 XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmname); 617 XFree(prop.value); 618 XMapWindow(xw.dpy, xw.win); 619 xhints(); 620 XSync(xw.dpy, False); 621 } 622 623 void 624 togglescm() 625 { 626 if (use_inverted_colors) { 627 free(sc); 628 sc = drw_scm_create(d, colors, 2); 629 use_inverted_colors = 0; 630 } else { 631 sc = drw_scm_create(d, inverted_colors, 2); 632 use_inverted_colors = 1; 633 } 634 drw_setscheme(d, sc); 635 XSetWindowBackground(xw.dpy, xw.win, sc[ColBg].pixel); 636 xdraw(); 637 } 638 639 void 640 xloadfonts() 641 { 642 int i, j; 643 char *fstrs[LEN(fontfallbacks)]; 644 645 for (j = 0; j < LEN(fontfallbacks); j++) { 646 fstrs[j] = ecalloc(1, MAXFONTSTRLEN); 647 } 648 649 for (i = 0; i < NUMFONTSCALES; i++) { 650 for (j = 0; j < LEN(fontfallbacks); j++) { 651 if (MAXFONTSTRLEN < snprintf(fstrs[j], MAXFONTSTRLEN, "%s:size=%d", fontfallbacks[j], FONTSZ(i))) 652 die("sent: Font string too long"); 653 } 654 if (!(fonts[i] = drw_fontset_create(d, (const char**)fstrs, LEN(fstrs)))) 655 die("sent: Unable to load any font for size %d", FONTSZ(i)); 656 } 657 658 for (j = 0; j < LEN(fontfallbacks); j++) 659 if (fstrs[j]) 660 free(fstrs[j]); 661 } 662 663 void 664 bpress(XEvent *e) 665 { 666 unsigned int i; 667 668 for (i = 0; i < LEN(mshortcuts); i++) 669 if (e->xbutton.button == mshortcuts[i].b && mshortcuts[i].func) 670 mshortcuts[i].func(&(mshortcuts[i].arg)); 671 } 672 673 void 674 cmessage(XEvent *e) 675 { 676 if (e->xclient.data.l[0] == xw.wmdeletewin) 677 running = 0; 678 } 679 680 void 681 expose(XEvent *e) 682 { 683 if (0 == e->xexpose.count) 684 xdraw(); 685 } 686 687 void 688 kpress(XEvent *e) 689 { 690 unsigned int i; 691 KeySym sym; 692 693 sym = XkbKeycodeToKeysym(xw.dpy, (KeyCode)e->xkey.keycode, 0, 0); 694 for (i = 0; i < LEN(shortcuts); i++) 695 if (sym == shortcuts[i].keysym && shortcuts[i].func) 696 shortcuts[i].func(&(shortcuts[i].arg)); 697 } 698 699 void 700 configure(XEvent *e) 701 { 702 resize(e->xconfigure.width, e->xconfigure.height); 703 if (slides[idx].img) 704 slides[idx].img->state &= ~SCALED; 705 xdraw(); 706 } 707 708 void 709 usage() 710 { 711 die("usage: %s [file]", argv0); 712 } 713 714 int 715 main(int argc, char *argv[]) 716 { 717 FILE *fp = NULL; 718 719 ARGBEGIN { 720 case 'v': 721 fprintf(stderr, "sent-"VERSION"\n"); 722 return 0; 723 case 'i': 724 use_inverted_colors = 1; 725 break; 726 default: 727 usage(); 728 } ARGEND 729 730 if (!argv[0] || !strcmp(argv[0], "-")) 731 fp = stdin; 732 else if (!(fp = fopen(fname = argv[0], "r"))) 733 die("sent: Unable to open '%s' for reading:", fname); 734 load(fp); 735 fclose(fp); 736 737 xinit(); 738 run(); 739 740 cleanup(0); 741 return 0; 742 }