/* See LICENSE file for copyright and license details. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "arg.h" #include "util.h" #include "drw.h" char *argv0; /* macros */ #define LEN(a) (sizeof(a) / sizeof(a)[0]) #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) #define MAXFONTSTRLEN 128 typedef enum { NONE = 0, SCALED = 1, } imgstate; typedef struct { unsigned char *buf; unsigned int bufwidth, bufheight; imgstate state; XImage *ximg; int numpasses; } Image; typedef struct { char *regex; char *bin; } Filter; typedef struct { unsigned int linecount; char **lines; Image *img; char *embed; } Slide; /* Purely graphic info */ typedef struct { Display *dpy; Window win; Atom wmdeletewin, netwmname; Visual *vis; XSetWindowAttributes attrs; int scr; int w, h; int uw, uh; /* usable dimensions for drawing text and images */ } XWindow; typedef union { int i; unsigned int ui; float f; const void *v; } Arg; typedef struct { unsigned int b; void (*func)(const Arg *); const Arg arg; } Mousekey; typedef struct { KeySym keysym; void (*func)(const Arg *); const Arg arg; } Shortcut; static void fffree(Image *img); static void ffload(Slide *s); static void ffprepare(Image *img); static void ffscale(Image *img); static void ffdraw(Image *img); static void getfontsize(Slide *s, unsigned int *width, unsigned int *height); static void cleanup(int slidesonly); static void reload(const Arg *arg); static void load(FILE *fp); static void advance(const Arg *arg); static void quit(const Arg *arg); static void resize(int width, int height); static void run(); static void usage(); static void xdraw(); static void xhints(); static void xinit(); static void xloadfonts(); static void bpress(XEvent *); static void cmessage(XEvent *); static void expose(XEvent *); static void kpress(XEvent *); static void configure(XEvent *); /* config.h for applying patches and the configuration. */ #include "config.h" /* Globals */ static const char *fname = NULL; static Slide *slides = NULL; static int idx = 0; static int slidecount = 0; static XWindow xw; static Drw *d = NULL; static Clr *sc; static Fnt *fonts[NUMFONTSCALES]; static int running = 1; static void (*handler[LASTEvent])(XEvent *) = { [ButtonPress] = bpress, [ClientMessage] = cmessage, [ConfigureNotify] = configure, [Expose] = expose, [KeyPress] = kpress, }; int filter(int fd, const char *cmd) { int fds[2]; if (pipe(fds) < 0) die("sent: Unable to create pipe:"); switch (fork()) { case -1: die("sent: Unable to fork:"); case 0: dup2(fd, 0); dup2(fds[1], 1); close(fds[0]); close(fds[1]); execlp("sh", "sh", "-c", cmd, (char *)0); fprintf(stderr, "sent: execlp sh -c '%s': %s\n", cmd, strerror(errno)); _exit(1); } close(fds[1]); return fds[0]; } void fffree(Image *img) { free(img->buf); if (img->ximg) XDestroyImage(img->ximg); free(img); } void ffload(Slide *s) { uint32_t y, x; uint16_t *row; uint8_t opac, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b; size_t rowlen, off, nbytes, i; ssize_t count; unsigned char hdr[16]; char *bin = NULL; char *filename; regex_t regex; int fdin, fdout; if (s->img || !(filename = s->embed) || !s->embed[0]) return; /* already done */ for (i = 0; i < LEN(filters); i++) { if (regcomp(®ex, filters[i].regex, REG_NOSUB | REG_EXTENDED | REG_ICASE)) { fprintf(stderr, "sent: Invalid regex '%s'\n", filters[i].regex); continue; } if (!regexec(®ex, filename, 0, NULL, 0)) { bin = filters[i].bin; regfree(®ex); break; } regfree(®ex); } if (!bin) die("sent: Unable to find matching filter for '%s'", filename); if ((fdin = open(filename, O_RDONLY)) < 0) die("sent: Unable to open '%s':", filename); if ((fdout = filter(fdin, bin)) < 0) die("sent: Unable to filter '%s':", filename); close(fdin); if (read(fdout, hdr, 16) != 16) die("sent: Unable to read filtered file '%s':", filename); if (memcmp("farbfeld", hdr, 8)) die("sent: Filtered file '%s' has no valid farbfeld header", filename); s->img = ecalloc(1, sizeof(Image)); s->img->bufwidth = ntohl(*(uint32_t *)&hdr[8]); s->img->bufheight = ntohl(*(uint32_t *)&hdr[12]); if (s->img->buf) free(s->img->buf); /* internally the image is stored in 888 format */ s->img->buf = ecalloc(s->img->bufwidth * s->img->bufheight, strlen("888")); /* scratch buffer to read row by row */ rowlen = s->img->bufwidth * 2 * strlen("RGBA"); row = ecalloc(1, rowlen); /* extract window background color channels for transparency */ bg_r = (sc[ColBg].pixel >> 16) % 256; bg_g = (sc[ColBg].pixel >> 8) % 256; bg_b = (sc[ColBg].pixel >> 0) % 256; for (off = 0, y = 0; y < s->img->bufheight; y++) { nbytes = 0; while (nbytes < rowlen) { count = read(fdout, (char *)row + nbytes, rowlen - nbytes); if (count < 0) die("sent: Unable to read from pipe:"); nbytes += count; } for (x = 0; x < rowlen / 2; x += 4) { fg_r = ntohs(row[x + 0]) / 257; fg_g = ntohs(row[x + 1]) / 257; fg_b = ntohs(row[x + 2]) / 257; opac = ntohs(row[x + 3]) / 257; /* blend opaque part of image data with window background color to * emulate transparency */ s->img->buf[off++] = (fg_r * opac + bg_r * (255 - opac)) / 255; s->img->buf[off++] = (fg_g * opac + bg_g * (255 - opac)) / 255; s->img->buf[off++] = (fg_b * opac + bg_b * (255 - opac)) / 255; } } free(row); close(fdout); } void ffprepare(Image *img) { int depth = DefaultDepth(xw.dpy, xw.scr); int width = xw.uw; int height = xw.uh; if (xw.uw * img->bufheight > xw.uh * img->bufwidth) width = img->bufwidth * xw.uh / img->bufheight; else height = img->bufheight * xw.uw / img->bufwidth; if (depth < 24) die("sent: Display color depths < 24 not supported"); if (!(img->ximg = XCreateImage(xw.dpy, CopyFromParent, depth, ZPixmap, 0, NULL, width, height, 32, 0))) die("sent: Unable to create XImage"); img->ximg->data = ecalloc(height, img->ximg->bytes_per_line); if (!XInitImage(img->ximg)) die("sent: Unable to initiate XImage"); ffscale(img); img->state |= SCALED; } void ffscale(Image *img) { unsigned int x, y; unsigned int width = img->ximg->width; unsigned int height = img->ximg->height; char* newBuf = img->ximg->data; unsigned char* ibuf; unsigned int jdy = img->ximg->bytes_per_line / 4 - width; unsigned int dx = (img->bufwidth << 10) / width; for (y = 0; y < height; y++) { unsigned int bufx = img->bufwidth / width; ibuf = &img->buf[y * img->bufheight / height * img->bufwidth * 3]; for (x = 0; x < width; x++) { *newBuf++ = (ibuf[(bufx >> 10)*3+2]); *newBuf++ = (ibuf[(bufx >> 10)*3+1]); *newBuf++ = (ibuf[(bufx >> 10)*3+0]); newBuf++; bufx += dx; } newBuf += jdy; } } void ffdraw(Image *img) { int xoffset = (xw.w - img->ximg->width) / 2; int yoffset = (xw.h - img->ximg->height) / 2; XPutImage(xw.dpy, xw.win, d->gc, img->ximg, 0, 0, xoffset, yoffset, img->ximg->width, img->ximg->height); XFlush(xw.dpy); } void getfontsize(Slide *s, unsigned int *width, unsigned int *height) { int i, j; unsigned int curw, newmax; float lfac = linespacing * (s->linecount - 1) + 1; /* fit height */ for (j = NUMFONTSCALES - 1; j >= 0; j--) if (fonts[j]->h * lfac <= xw.uh) break; LIMIT(j, 0, NUMFONTSCALES - 1); drw_setfontset(d, fonts[j]); /* fit width */ *width = 0; for (i = 0; i < s->linecount; i++) { curw = drw_fontset_getwidth(d, s->lines[i]); newmax = (curw >= *width); while (j > 0 && curw > xw.uw) { drw_setfontset(d, fonts[--j]); curw = drw_fontset_getwidth(d, s->lines[i]); } if (newmax) *width = curw; } *height = fonts[j]->h * lfac; } void cleanup(int slidesonly) { unsigned int i, j; if (!slidesonly) { for (i = 0; i < NUMFONTSCALES; i++) drw_fontset_free(fonts[i]); free(sc); drw_free(d); XDestroyWindow(xw.dpy, xw.win); XSync(xw.dpy, False); XCloseDisplay(xw.dpy); } if (slides) { for (i = 0; i < slidecount; i++) { for (j = 0; j < slides[i].linecount; j++) free(slides[i].lines[j]); free(slides[i].lines); if (slides[i].img) fffree(slides[i].img); } if (!slidesonly) { free(slides); slides = NULL; } } } void reload(const Arg *arg) { FILE *fp = NULL; unsigned int i; if (!fname) { fprintf(stderr, "sent: Cannot reload from stdin. Use a file!\n"); return; } cleanup(1); slidecount = 0; if (!(fp = fopen(fname, "r"))) die("sent: Unable to open '%s' for reading:", fname); load(fp); fclose(fp); LIMIT(idx, 0, slidecount-1); for (i = 0; i < slidecount; i++) ffload(&slides[i]); xdraw(); } void load(FILE *fp) { static size_t size = 0; size_t blen, maxlines; char buf[BUFSIZ], *p; Slide *s; /* read each line from fp and add it to the item list */ while (1) { /* eat consecutive empty lines */ while ((p = fgets(buf, sizeof(buf), fp))) if (strcmp(buf, "\n") != 0 && buf[0] != '#') break; if (!p) break; if ((slidecount+1) * sizeof(*slides) >= size) if (!(slides = realloc(slides, (size += BUFSIZ)))) die("sent: Unable to reallocate %u bytes:", size); /* read one slide */ maxlines = 0; memset((s = &slides[slidecount]), 0, sizeof(Slide)); do { /* if there's a leading null, we can't do blen-1 */ if (buf[0] == '\0') continue; if (buf[0] == '#') continue; /* grow lines array */ if (s->linecount >= maxlines) { maxlines = 2 * s->linecount + 1; if (!(s->lines = realloc(s->lines, maxlines * sizeof(s->lines[0])))) die("sent: Unable to reallocate %u bytes:", maxlines * sizeof(s->lines[0])); } blen = strlen(buf); if (!(s->lines[s->linecount] = strdup(buf))) die("sent: Unable to strdup:"); if (s->lines[s->linecount][blen-1] == '\n') s->lines[s->linecount][blen-1] = '\0'; /* mark as image slide if first line of a slide starts with @ */ if (s->linecount == 0 && s->lines[0][0] == '@') s->embed = &s->lines[0][1]; if (s->lines[s->linecount][0] == '\\') memmove(s->lines[s->linecount], &s->lines[s->linecount][1], blen); s->linecount++; } while ((p = fgets(buf, sizeof(buf), fp)) && strcmp(buf, "\n") != 0); slidecount++; if (!p) break; } if (!slidecount) die("sent: No slides in file"); } void advance(const Arg *arg) { int new_idx = idx + arg->i; LIMIT(new_idx, 0, slidecount-1); if (new_idx != idx) { if (slides[idx].img) slides[idx].img->state &= ~SCALED; idx = new_idx; xdraw(); } } void quit(const Arg *arg) { running = 0; } void resize(int width, int height) { xw.w = width; xw.h = height; xw.uw = usablewidth * width; xw.uh = usableheight * height; drw_resize(d, width, height); } void run() { XEvent ev; /* Waiting for window mapping */ while (1) { XNextEvent(xw.dpy, &ev); if (ev.type == ConfigureNotify) { resize(ev.xconfigure.width, ev.xconfigure.height); } else if (ev.type == MapNotify) { break; } } while (running) { XNextEvent(xw.dpy, &ev); if (handler[ev.type]) (handler[ev.type])(&ev); } } void xdraw() { unsigned int height, width, i; Image *im = slides[idx].img; getfontsize(&slides[idx], &width, &height); XClearWindow(xw.dpy, xw.win); if (!im) { drw_rect(d, 0, 0, xw.w, xw.h, 1, 1); for (i = 0; i < slides[idx].linecount; i++) drw_text(d, (xw.w - width) / 2, (xw.h - height) / 2 + i * linespacing * d->fonts->h, width, d->fonts->h, 0, slides[idx].lines[i], 0); drw_map(d, xw.win, 0, 0, xw.w, xw.h); } else { if (!(im->state & SCALED)) ffprepare(im); ffdraw(im); } } void xhints() { XClassHint class = {.res_name = "sent", .res_class = "presenter"}; XWMHints wm = {.flags = InputHint, .input = True}; XSizeHints *sizeh = NULL; if (!(sizeh = XAllocSizeHints())) die("sent: Unable to allocate size hints"); sizeh->flags = PSize; sizeh->height = xw.h; sizeh->width = xw.w; XSetWMProperties(xw.dpy, xw.win, NULL, NULL, NULL, 0, sizeh, &wm, &class); XFree(sizeh); } void xinit() { XTextProperty prop; unsigned int i; if (!(xw.dpy = XOpenDisplay(NULL))) die("sent: Unable to open display"); xw.scr = XDefaultScreen(xw.dpy); xw.vis = XDefaultVisual(xw.dpy, xw.scr); resize(DisplayWidth(xw.dpy, xw.scr), DisplayHeight(xw.dpy, xw.scr)); xw.attrs.bit_gravity = CenterGravity; xw.attrs.event_mask = KeyPressMask | ExposureMask | StructureNotifyMask | ButtonMotionMask | ButtonPressMask; xw.win = XCreateWindow(xw.dpy, XRootWindow(xw.dpy, xw.scr), 0, 0, xw.w, xw.h, 0, XDefaultDepth(xw.dpy, xw.scr), InputOutput, xw.vis, CWBitGravity | CWEventMask, &xw.attrs); xw.wmdeletewin = XInternAtom(xw.dpy, "WM_DELETE_WINDOW", False); xw.netwmname = XInternAtom(xw.dpy, "_NET_WM_NAME", False); XSetWMProtocols(xw.dpy, xw.win, &xw.wmdeletewin, 1); if (!(d = drw_create(xw.dpy, xw.scr, xw.win, xw.w, xw.h))) die("sent: Unable to create drawing context"); sc = drw_scm_create(d, colors, 2); drw_setscheme(d, sc); XSetWindowBackground(xw.dpy, xw.win, sc[ColBg].pixel); xloadfonts(); for (i = 0; i < slidecount; i++) ffload(&slides[i]); XStringListToTextProperty(&argv0, 1, &prop); XSetWMName(xw.dpy, xw.win, &prop); XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmname); XFree(prop.value); XMapWindow(xw.dpy, xw.win); xhints(); XSync(xw.dpy, False); } void xloadfonts() { int i, j; char *fstrs[LEN(fontfallbacks)]; for (j = 0; j < LEN(fontfallbacks); j++) { fstrs[j] = ecalloc(1, MAXFONTSTRLEN); } for (i = 0; i < NUMFONTSCALES; i++) { for (j = 0; j < LEN(fontfallbacks); j++) { if (MAXFONTSTRLEN < snprintf(fstrs[j], MAXFONTSTRLEN, "%s:size=%d", fontfallbacks[j], FONTSZ(i))) die("sent: Font string too long"); } if (!(fonts[i] = drw_fontset_create(d, (const char**)fstrs, LEN(fstrs)))) die("sent: Unable to load any font for size %d", FONTSZ(i)); } for (j = 0; j < LEN(fontfallbacks); j++) if (fstrs[j]) free(fstrs[j]); } void bpress(XEvent *e) { unsigned int i; for (i = 0; i < LEN(mshortcuts); i++) if (e->xbutton.button == mshortcuts[i].b && mshortcuts[i].func) mshortcuts[i].func(&(mshortcuts[i].arg)); } void cmessage(XEvent *e) { if (e->xclient.data.l[0] == xw.wmdeletewin) running = 0; } void expose(XEvent *e) { if (0 == e->xexpose.count) xdraw(); } void kpress(XEvent *e) { unsigned int i; KeySym sym; sym = XkbKeycodeToKeysym(xw.dpy, (KeyCode)e->xkey.keycode, 0, 0); for (i = 0; i < LEN(shortcuts); i++) if (sym == shortcuts[i].keysym && shortcuts[i].func) shortcuts[i].func(&(shortcuts[i].arg)); } void configure(XEvent *e) { resize(e->xconfigure.width, e->xconfigure.height); if (slides[idx].img) slides[idx].img->state &= ~SCALED; xdraw(); } void usage() { die("usage: %s [-c fgcolor] [-b bgcolor] [-f font] [file]", argv0); } int main(int argc, char *argv[]) { FILE *fp = NULL; ARGBEGIN { case 'v': fprintf(stderr, "sent-"VERSION"\n"); return 0; case 'f': fontfallbacks[0] = EARGF(usage()); break; case 'c': colors[0] = EARGF(usage()); break; case 'b': colors[1] = EARGF(usage()); break; default: usage(); } ARGEND if (!argv[0] || !strcmp(argv[0], "-")) fp = stdin; else if (!(fp = fopen(fname = argv[0], "r"))) die("sent: Unable to open '%s' for reading:", fname); load(fp); fclose(fp); xinit(); run(); cleanup(0); return 0; }