sent.c (15670B)
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 26char *argv0; 27 28/* macros */ 29#define LEN(a) (sizeof(a) / sizeof(a)[0]) 30#define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) 31#define MAXFONTSTRLEN 128 32 33typedef enum { 34 NONE = 0, 35 SCALED = 1, 36} imgstate; 37 38typedef struct { 39 unsigned char *buf; 40 unsigned int bufwidth, bufheight; 41 imgstate state; 42 XImage *ximg; 43 int numpasses; 44} Image; 45 46typedef struct { 47 char *regex; 48 char *bin; 49} Filter; 50 51typedef struct { 52 unsigned int linecount; 53 char **lines; 54 Image *img; 55 char *embed; 56} Slide; 57 58/* Purely graphic info */ 59typedef struct { 60 Display *dpy; 61 Window win; 62 Atom wmdeletewin, netwmname; 63 Visual *vis; 64 XSetWindowAttributes attrs; 65 int scr; 66 int w, h; 67 int uw, uh; /* usable dimensions for drawing text and images */ 68} XWindow; 69 70typedef union { 71 int i; 72 unsigned int ui; 73 float f; 74 const void *v; 75} Arg; 76 77typedef struct { 78 unsigned int b; 79 void (*func)(const Arg *); 80 const Arg arg; 81} Mousekey; 82 83typedef struct { 84 KeySym keysym; 85 void (*func)(const Arg *); 86 const Arg arg; 87} Shortcut; 88 89static void fffree(Image *img); 90static void ffload(Slide *s); 91static void ffprepare(Image *img); 92static void ffscale(Image *img); 93static void ffdraw(Image *img); 94 95static void getfontsize(Slide *s, unsigned int *width, unsigned int *height); 96static void cleanup(int slidesonly); 97static void reload(const Arg *arg); 98static void load(FILE *fp); 99static void advance(const Arg *arg); 100static void quit(const Arg *arg); 101static void resize(int width, int height); 102static void run(void); 103static void usage(void); 104static void xdraw(void); 105static void xhints(void); 106static void xinit(void); 107static void xloadfonts(void); 108 109static void bpress(XEvent *); 110static void cmessage(XEvent *); 111static void expose(XEvent *); 112static void kpress(XEvent *); 113static void configure(XEvent *); 114 115/* config.h for applying patches and the configuration. */ 116#include "config.h" 117 118/* Globals */ 119static const char *fname = NULL; 120static Slide *slides = NULL; 121static int idx = 0; 122static int slidecount = 0; 123static XWindow xw; 124static Drw *d = NULL; 125static Clr *sc; 126static Fnt *fonts[NUMFONTSCALES]; 127static int running = 1; 128 129static void (*handler[LASTEvent])(XEvent *) = { 130 [ButtonPress] = bpress, 131 [ClientMessage] = cmessage, 132 [ConfigureNotify] = configure, 133 [Expose] = expose, 134 [KeyPress] = kpress, 135}; 136 137int 138filter(int fd, const char *cmd) 139{ 140 int fds[2]; 141 142 if (pipe(fds) < 0) 143 die("sent: Unable to create pipe:"); 144 145 switch (fork()) { 146 case -1: 147 die("sent: Unable to fork:"); 148 case 0: 149 dup2(fd, 0); 150 dup2(fds[1], 1); 151 close(fds[0]); 152 close(fds[1]); 153 execlp("sh", "sh", "-c", cmd, (char *)0); 154 fprintf(stderr, "sent: execlp sh -c '%s': %s\n", cmd, strerror(errno)); 155 _exit(1); 156 } 157 close(fds[1]); 158 return fds[0]; 159} 160 161void 162fffree(Image *img) 163{ 164 free(img->buf); 165 if (img->ximg) 166 XDestroyImage(img->ximg); 167 free(img); 168} 169 170void 171ffload(Slide *s) 172{ 173 uint32_t y, x; 174 uint16_t *row; 175 uint8_t opac, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b; 176 size_t rowlen, off, nbytes, i; 177 ssize_t count; 178 unsigned char hdr[16]; 179 char *bin = NULL; 180 char *filename; 181 regex_t regex; 182 int fdin, fdout; 183 184 if (s->img || !(filename = s->embed) || !s->embed[0]) 185 return; /* already done */ 186 187 for (i = 0; i < LEN(filters); i++) { 188 if (regcomp(®ex, filters[i].regex, 189 REG_NOSUB | REG_EXTENDED | REG_ICASE)) { 190 fprintf(stderr, "sent: Invalid regex '%s'\n", filters[i].regex); 191 continue; 192 } 193 if (!regexec(®ex, filename, 0, NULL, 0)) { 194 bin = filters[i].bin; 195 regfree(®ex); 196 break; 197 } 198 regfree(®ex); 199 } 200 if (!bin) 201 die("sent: Unable to find matching filter for '%s'", filename); 202 203 if ((fdin = open(filename, O_RDONLY)) < 0) 204 die("sent: Unable to open '%s':", filename); 205 206 if ((fdout = filter(fdin, bin)) < 0) 207 die("sent: Unable to filter '%s':", filename); 208 close(fdin); 209 210 if (read(fdout, hdr, 16) != 16) 211 die("sent: Unable to read filtered file '%s':", filename); 212 if (memcmp("farbfeld", hdr, 8)) 213 die("sent: Filtered file '%s' has no valid farbfeld header", filename); 214 215 s->img = ecalloc(1, sizeof(Image)); 216 s->img->bufwidth = ntohl(*(uint32_t *)&hdr[8]); 217 s->img->bufheight = ntohl(*(uint32_t *)&hdr[12]); 218 219 free(s->img->buf); 220 /* internally the image is stored in 888 format */ 221 s->img->buf = ecalloc(s->img->bufwidth * s->img->bufheight, strlen("888")); 222 223 /* scratch buffer to read row by row */ 224 rowlen = s->img->bufwidth * 2 * strlen("RGBA"); 225 row = ecalloc(1, rowlen); 226 227 /* extract window background color channels for transparency */ 228 bg_r = (sc[ColBg].pixel >> 16) % 256; 229 bg_g = (sc[ColBg].pixel >> 8) % 256; 230 bg_b = (sc[ColBg].pixel >> 0) % 256; 231 232 for (off = 0, y = 0; y < s->img->bufheight; y++) { 233 nbytes = 0; 234 while (nbytes < rowlen) { 235 count = read(fdout, (char *)row + nbytes, rowlen - nbytes); 236 if (count < 0) 237 die("sent: Unable to read from pipe:"); 238 nbytes += count; 239 } 240 for (x = 0; x < rowlen / 2; x += 4) { 241 fg_r = ntohs(row[x + 0]) / 257; 242 fg_g = ntohs(row[x + 1]) / 257; 243 fg_b = ntohs(row[x + 2]) / 257; 244 opac = ntohs(row[x + 3]) / 257; 245 /* blend opaque part of image data with window background color to 246 * emulate transparency */ 247 s->img->buf[off++] = (fg_r * opac + bg_r * (255 - opac)) / 255; 248 s->img->buf[off++] = (fg_g * opac + bg_g * (255 - opac)) / 255; 249 s->img->buf[off++] = (fg_b * opac + bg_b * (255 - opac)) / 255; 250 } 251 } 252 253 free(row); 254 close(fdout); 255} 256 257void 258ffprepare(Image *img) 259{ 260 int depth = DefaultDepth(xw.dpy, xw.scr); 261 int width = xw.uw; 262 int height = xw.uh; 263 264 if (xw.uw * img->bufheight > xw.uh * img->bufwidth) 265 width = img->bufwidth * xw.uh / img->bufheight; 266 else 267 height = img->bufheight * xw.uw / img->bufwidth; 268 269 if (depth < 24) 270 die("sent: Display color depths < 24 not supported"); 271 272 if (img->ximg) 273 XDestroyImage(img->ximg); 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 287void 288ffscale(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 313void 314ffdraw(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 323void 324getfontsize(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 352void 353cleanup(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 383void 384reload(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 408void 409load(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 471void 472advance(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 484void 485quit(const Arg *arg) 486{ 487 running = 0; 488} 489 490void 491resize(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 500void 501run(void) 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 522void 523xdraw(void) 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 drw_map(d, xw.win, 0, 0, xw.w, xw.h); 543 } else { 544 if (!(im->state & SCALED)) 545 ffprepare(im); 546 ffdraw(im); 547 } 548} 549 550void 551xhints(void) 552{ 553 XClassHint class = {.res_name = "sent", .res_class = "presenter"}; 554 XWMHints wm = {.flags = InputHint, .input = True}; 555 XSizeHints *sizeh = NULL; 556 557 if (!(sizeh = XAllocSizeHints())) 558 die("sent: Unable to allocate size hints"); 559 560 sizeh->flags = PSize; 561 sizeh->height = xw.h; 562 sizeh->width = xw.w; 563 564 XSetWMProperties(xw.dpy, xw.win, NULL, NULL, NULL, 0, sizeh, &wm, &class); 565 XFree(sizeh); 566} 567 568void 569xinit(void) 570{ 571 XTextProperty prop; 572 unsigned int i; 573 574 if (!(xw.dpy = XOpenDisplay(NULL))) 575 die("sent: Unable to open display"); 576 xw.scr = XDefaultScreen(xw.dpy); 577 xw.vis = XDefaultVisual(xw.dpy, xw.scr); 578 resize(DisplayWidth(xw.dpy, xw.scr), DisplayHeight(xw.dpy, xw.scr)); 579 580 xw.attrs.bit_gravity = CenterGravity; 581 xw.attrs.event_mask = KeyPressMask | ExposureMask | StructureNotifyMask | 582 ButtonMotionMask | ButtonPressMask; 583 584 xw.win = XCreateWindow(xw.dpy, XRootWindow(xw.dpy, xw.scr), 0, 0, 585 xw.w, xw.h, 0, XDefaultDepth(xw.dpy, xw.scr), 586 InputOutput, xw.vis, CWBitGravity | CWEventMask, 587 &xw.attrs); 588 589 xw.wmdeletewin = XInternAtom(xw.dpy, "WM_DELETE_WINDOW", False); 590 xw.netwmname = XInternAtom(xw.dpy, "_NET_WM_NAME", False); 591 XSetWMProtocols(xw.dpy, xw.win, &xw.wmdeletewin, 1); 592 593 if (!(d = drw_create(xw.dpy, xw.scr, xw.win, xw.w, xw.h))) 594 die("sent: Unable to create drawing context"); 595 sc = drw_scm_create(d, colors, 2); 596 drw_setscheme(d, sc); 597 XSetWindowBackground(xw.dpy, xw.win, sc[ColBg].pixel); 598 599 xloadfonts(); 600 for (i = 0; i < slidecount; i++) 601 ffload(&slides[i]); 602 603 XStringListToTextProperty(&argv0, 1, &prop); 604 XSetWMName(xw.dpy, xw.win, &prop); 605 XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmname); 606 XFree(prop.value); 607 XMapWindow(xw.dpy, xw.win); 608 xhints(); 609 XSync(xw.dpy, False); 610} 611 612void 613xloadfonts(void) 614{ 615 int i, j; 616 char *fstrs[LEN(fontfallbacks)]; 617 618 for (j = 0; j < LEN(fontfallbacks); j++) { 619 fstrs[j] = ecalloc(1, MAXFONTSTRLEN); 620 } 621 622 for (i = 0; i < NUMFONTSCALES; i++) { 623 for (j = 0; j < LEN(fontfallbacks); j++) { 624 if (MAXFONTSTRLEN < snprintf(fstrs[j], MAXFONTSTRLEN, "%s:size=%d", fontfallbacks[j], FONTSZ(i))) 625 die("sent: Font string too long"); 626 } 627 if (!(fonts[i] = drw_fontset_create(d, (const char**)fstrs, LEN(fstrs)))) 628 die("sent: Unable to load any font for size %d", FONTSZ(i)); 629 } 630 631 for (j = 0; j < LEN(fontfallbacks); j++) 632 free(fstrs[j]); 633} 634 635void 636bpress(XEvent *e) 637{ 638 unsigned int i; 639 640 for (i = 0; i < LEN(mshortcuts); i++) 641 if (e->xbutton.button == mshortcuts[i].b && mshortcuts[i].func) 642 mshortcuts[i].func(&(mshortcuts[i].arg)); 643} 644 645void 646cmessage(XEvent *e) 647{ 648 if (e->xclient.data.l[0] == xw.wmdeletewin) 649 running = 0; 650} 651 652void 653expose(XEvent *e) 654{ 655 if (0 == e->xexpose.count) 656 xdraw(); 657} 658 659void 660kpress(XEvent *e) 661{ 662 unsigned int i; 663 KeySym sym; 664 665 sym = XkbKeycodeToKeysym(xw.dpy, (KeyCode)e->xkey.keycode, 0, 0); 666 for (i = 0; i < LEN(shortcuts); i++) 667 if (sym == shortcuts[i].keysym && shortcuts[i].func) 668 shortcuts[i].func(&(shortcuts[i].arg)); 669} 670 671void 672configure(XEvent *e) 673{ 674 resize(e->xconfigure.width, e->xconfigure.height); 675 if (slides[idx].img) 676 slides[idx].img->state &= ~SCALED; 677 xdraw(); 678} 679 680void 681usage(void) 682{ 683 die("usage: %s [file]", argv0); 684} 685 686int 687main(int argc, char *argv[]) 688{ 689 FILE *fp = NULL; 690 691 ARGBEGIN { 692 case 'v': 693 fprintf(stderr, "sent-"VERSION"\n"); 694 return 0; 695 default: 696 usage(); 697 } ARGEND 698 699 if (!argv[0] || !strcmp(argv[0], "-")) 700 fp = stdin; 701 else if (!(fp = fopen(fname = argv[0], "r"))) 702 die("sent: Unable to open '%s' for reading:", fname); 703 load(fp); 704 fclose(fp); 705 706 xinit(); 707 run(); 708 709 cleanup(0); 710 return 0; 711}