sent

Simple plaintext presentation tool
git clone https://git.sinitax.com/suckless/sent
Log | Files | Refs | README | LICENSE | sfeed.txt

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(&regex, 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(&regex, filename, 0, NULL, 0)) {
    194			bin = filters[i].bin;
    195			regfree(&regex);
    196			break;
    197		}
    198		regfree(&regex);
    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}