sent.c - sent - simple plaintext presentation tool
(HTM) git clone git://git.suckless.org/sent
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
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
26 char *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
33 typedef enum {
34 NONE = 0,
35 SCALED = 1,
36 } imgstate;
37
38 typedef struct {
39 unsigned char *buf;
40 unsigned int bufwidth, bufheight;
41 imgstate state;
42 XImage *ximg;
43 int numpasses;
44 } Image;
45
46 typedef struct {
47 char *regex;
48 char *bin;
49 } Filter;
50
51 typedef struct {
52 unsigned int linecount;
53 char **lines;
54 Image *img;
55 char *embed;
56 } Slide;
57
58 /* Purely graphic info */
59 typedef 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
70 typedef union {
71 int i;
72 unsigned int ui;
73 float f;
74 const void *v;
75 } Arg;
76
77 typedef struct {
78 unsigned int b;
79 void (*func)(const Arg *);
80 const Arg arg;
81 } Mousekey;
82
83 typedef struct {
84 KeySym keysym;
85 void (*func)(const Arg *);
86 const Arg arg;
87 } Shortcut;
88
89 static void fffree(Image *img);
90 static void ffload(Slide *s);
91 static void ffprepare(Image *img);
92 static void ffscale(Image *img);
93 static void ffdraw(Image *img);
94
95 static void getfontsize(Slide *s, unsigned int *width, unsigned int *height);
96 static void cleanup(int slidesonly);
97 static void reload(const Arg *arg);
98 static void load(FILE *fp);
99 static void advance(const Arg *arg);
100 static void quit(const Arg *arg);
101 static void resize(int width, int height);
102 static void run(void);
103 static void usage(void);
104 static void xdraw(void);
105 static void xhints(void);
106 static void xinit(void);
107 static void xloadfonts(void);
108
109 static void bpress(XEvent *);
110 static void cmessage(XEvent *);
111 static void expose(XEvent *);
112 static void kpress(XEvent *);
113 static void configure(XEvent *);
114
115 /* config.h for applying patches and the configuration. */
116 #include "config.h"
117
118 /* Globals */
119 static const char *fname = NULL;
120 static Slide *slides = NULL;
121 static int idx = 0;
122 static int slidecount = 0;
123 static XWindow xw;
124 static Drw *d = NULL;
125 static Clr *sc;
126 static Fnt *fonts[NUMFONTSCALES];
127 static int running = 1;
128
129 static void (*handler[LASTEvent])(XEvent *) = {
130 [ButtonPress] = bpress,
131 [ClientMessage] = cmessage,
132 [ConfigureNotify] = configure,
133 [Expose] = expose,
134 [KeyPress] = kpress,
135 };
136
137 int
138 filter(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
161 void
162 fffree(Image *img)
163 {
164 free(img->buf);
165 if (img->ximg)
166 XDestroyImage(img->ximg);
167 free(img);
168 }
169
170 void
171 ffload(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
257 void
258 ffprepare(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
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(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
522 void
523 xdraw(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
550 void
551 xhints(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
568 void
569 xinit(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
612 void
613 xloadfonts(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
635 void
636 bpress(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
645 void
646 cmessage(XEvent *e)
647 {
648 if (e->xclient.data.l[0] == xw.wmdeletewin)
649 running = 0;
650 }
651
652 void
653 expose(XEvent *e)
654 {
655 if (0 == e->xexpose.count)
656 xdraw();
657 }
658
659 void
660 kpress(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
671 void
672 configure(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
680 void
681 usage(void)
682 {
683 die("usage: %s [file]", argv0);
684 }
685
686 int
687 main(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 }