Commit Diff


commit - /dev/null
commit + 3bacbae0d94df78a28282a8d62fcd65d0880efc0
blob - /dev/null
blob + e0be8cefcae5ec9d021eab34f49cc505dcb0998d (mode 644)
--- /dev/null
+++ LICENSE
@@ -0,0 +1,29 @@
+ISC-License
+
+(c) 2014-2017 Markus Teich <markus.teich@stusta.mhn.de>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+(c) 2016,2017 Laslo Hunhold <dev@frign.de>
+(c) 2016 ssd <ssd@mailless.org>
+(c) 2016 Hiltjo Posthuma <hiltjo@codemadness.org>
+(c) 2015 David Phillips <dbphillipsnz@gmail.com>
+(c) 2015 Grant Mathews <grant.m.mathews@gmail.com>
+(c) 2015 Dimitris Papastamos <sin@2f30.org>
+(c) 2015 Alexis <surryhill@gmail.com>
+(c) 2015 Quentin Rameau <quinq@fifth.space>
+(c) 2015 Ivan Tham <pickfire@riseup.net>
+(c) 2015 Jan Christoph Ebersbach <jceb@e-jc.de>
+(c) 2015 Tony Lainson <t.lainson@gmail.com>
+(c) 2015 Szabolcs Nagy <nsz@port70.net>
+(c) 2015 Jonas Jelten <jj@sft.mx>
blob - /dev/null
blob + 731f71b23cf70337a17391702ff5abee1bb505c6 (mode 644)
--- /dev/null
+++ Makefile
@@ -0,0 +1,52 @@
+# sent - plain text presentation tool
+# See LICENSE file for copyright and license details.
+
+include config.mk
+
+SRC = sent.c drw.c util.c
+OBJ = ${SRC:.c=.o}
+
+all: options sent
+
+options:
+	@echo sent build options:
+	@echo "CFLAGS   = ${CFLAGS}"
+	@echo "LDFLAGS  = ${LDFLAGS}"
+	@echo "CC       = ${CC}"
+
+config.h:
+	cp config.def.h config.h
+
+.c.o:
+	${CC} -c ${CFLAGS} $<
+
+${OBJ}: config.h config.mk
+
+sent: ${OBJ}
+	${CC} -o $@ ${OBJ} ${LDFLAGS}
+
+cscope: ${SRC} config.h
+	cscope -R -b || echo cScope not installed
+
+clean:
+	rm -f sent ${OBJ} sent-${VERSION}.tar.gz
+
+dist: clean
+	mkdir -p sent-${VERSION}
+	cp -R LICENSE Makefile config.mk config.def.h ${SRC} sent-${VERSION}
+	tar -cf sent-${VERSION}.tar sent-${VERSION}
+	gzip sent-${VERSION}.tar
+	rm -rf sent-${VERSION}
+
+install: all
+	mkdir -p ${DESTDIR}${PREFIX}/bin
+	cp -f sent ${DESTDIR}${PREFIX}/bin
+	chmod 755 ${DESTDIR}${PREFIX}/bin/sent
+	mkdir -p ${DESTDIR}${MANPREFIX}/man1
+	cp sent.1 ${DESTDIR}${MANPREFIX}/man1/sent.1
+	chmod 644 ${DESTDIR}${MANPREFIX}/man1/sent.1
+
+uninstall:
+	rm -f ${DESTDIR}${PREFIX}/bin/sent
+
+.PHONY: all options clean dist install uninstall cscope
blob - /dev/null
blob + c1e938576d122f53f84459d421fe00bca25e4690 (mode 644)
--- /dev/null
+++ README.md
@@ -0,0 +1,59 @@
+sent is a simple plaintext presentation tool.
+
+sent does not need latex, libreoffice or any other fancy file format, it uses
+plaintext files to describe the slides and can include images via farbfeld.
+Every paragraph represents a slide in the presentation.
+
+The presentation is displayed in a simple X11 window. The content of each slide
+is automatically scaled to fit the window and centered so you also don't have to
+worry about alignment. Instead you can really concentrate on the content.
+
+
+Dependencies
+
+You need Xlib and Xft to build sent and the farbfeld[0] tools installed to use
+images in your presentations.
+
+Demo
+
+To get a little demo, just type
+
+	make && ./sent example
+
+You can navigate with the arrow keys and quit with `q`.
+
+
+Usage
+
+	sent [FILE]
+
+If FILE is omitted or equals `-`, stdin will be read. Produce image slides by
+prepending a `@` in front of the filename as a single paragraph. Lines starting
+with `#` will be ignored. A `\` at the beginning of the line escapes `@` and
+`#`. A presentation file could look like this:
+
+	sent
+	
+	@nyan.png
+	
+	depends on
+	- Xlib
+	- Xft
+	- farbfeld
+	
+	sent FILENAME
+	one slide per paragraph
+	# This is a comment and will not be part of the presentation
+	\# This and the next line start with backslashes
+	
+	\@FILE.png
+	
+	thanks / questions?
+
+
+Development
+
+sent is developed at http://tools.suckless.org/sent
+
+
+0: http://tools.suckless.org/farbfeld/
blob - /dev/null
blob + 7f503ec13cba79d2d37cc20aaa7220ff7ddbff07 (mode 644)
--- /dev/null
+++ arg.h
@@ -0,0 +1,49 @@
+/*
+ * ISC-License
+ *
+ * Copyright 2017 Laslo Hunhold <dev@frign.de>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#ifndef ARG_H
+#define ARG_H
+
+extern char *argv0;
+
+/* int main(int argc, char *argv[]) */
+#define ARGBEGIN for (argv0 = *argv, *argv ? (argc--, argv++) : ((void *)0);      \
+                      *argv && (*argv)[0] == '-' && (*argv)[1]; argc--, argv++) { \
+                 	int i_, argused_;                                         \
+                 	if ((*argv)[1] == '-' && !(*argv)[2]) {                   \
+                 		argc--, argv++;                                   \
+                 		break;                                            \
+                 	}                                                         \
+                 	for (i_ = 1, argused_ = 0; (*argv)[i_]; i_++) {           \
+                 		switch((*argv)[i_])
+#define ARGEND   		if (argused_) {                                   \
+                 			if ((*argv)[i_ + 1]) {                    \
+                 				break;                            \
+                 			} else {                                  \
+                 				argc--, argv++;                   \
+                 				break;                            \
+                 			}                                         \
+                 		}                                                 \
+                 	}                                                         \
+                 }
+#define ARGC()   ((*argv)[i_])
+#define ARGF_(x) (((*argv)[i_ + 1]) ? (argused_ = 1, &((*argv)[i_ + 1])) :        \
+                  (*(argv + 1))     ? (argused_ = 1, *(argv + 1))        : (x))
+#define EARGF(x) ARGF_(((x), exit(1), (char *)0))
+#define ARGF()   ARGF_((char *)0)
+
+#endif
blob - /dev/null
blob + 60eb376def416f1386c83e0b956cca86ffaeb9e1 (mode 644)
--- /dev/null
+++ config.def.h
@@ -0,0 +1,56 @@
+/* See LICENSE file for copyright and license details. */
+
+static char *fontfallbacks[] = {
+	"dejavu sans",
+	"roboto",
+	"ubuntu",
+};
+#define NUMFONTSCALES 42
+#define FONTSZ(x) ((int)(10.0 * powf(1.1288, (x)))) /* x in [0, NUMFONTSCALES-1] */
+
+static const char *colors[] = {
+	"#000000", /* foreground color */
+	"#FFFFFF", /* background color */
+};
+
+static const float linespacing = 1.4;
+
+/* how much screen estate is to be used at max for the content */
+static const float usablewidth = 0.75;
+static const float usableheight = 0.75;
+
+static Mousekey mshortcuts[] = {
+	/* button         function        argument */
+	{ Button1,        advance,        {.i = +1} },
+	{ Button3,        advance,        {.i = -1} },
+	{ Button4,        advance,        {.i = -1} },
+	{ Button5,        advance,        {.i = +1} },
+};
+
+static Shortcut shortcuts[] = {
+	/* keysym         function        argument */
+	{ XK_Escape,      quit,           {0} },
+	{ XK_q,           quit,           {0} },
+	{ XK_Right,       advance,        {.i = +1} },
+	{ XK_Left,        advance,        {.i = -1} },
+	{ XK_Return,      advance,        {.i = +1} },
+	{ XK_space,       advance,        {.i = +1} },
+	{ XK_BackSpace,   advance,        {.i = -1} },
+	{ XK_l,           advance,        {.i = +1} },
+	{ XK_h,           advance,        {.i = -1} },
+	{ XK_j,           advance,        {.i = +1} },
+	{ XK_k,           advance,        {.i = -1} },
+	{ XK_Down,        advance,        {.i = +1} },
+	{ XK_Up,          advance,        {.i = -1} },
+	{ XK_Next,        advance,        {.i = +1} },
+	{ XK_Prior,       advance,        {.i = -1} },
+	{ XK_n,           advance,        {.i = +1} },
+	{ XK_p,           advance,        {.i = -1} },
+	{ XK_r,           reload,         {0} },
+};
+
+static Filter filters[] = {
+	{ "\\.ff$", "cat" },
+	{ "\\.ff.bz2$", "bunzip2" },
+	{ "\\.[a-z0-9]+$", "2ff" },
+};
blob - /dev/null
blob + d61c55437f46c8846b76db587860b5304f46a9c6 (mode 644)
--- /dev/null
+++ config.mk
@@ -0,0 +1,30 @@
+# sent version
+VERSION = 1
+
+# Customize below to fit your system
+
+# paths
+PREFIX = /usr/local
+MANPREFIX = ${PREFIX}/share/man
+
+X11INC = /usr/X11R6/include
+X11LIB = /usr/X11R6/lib
+
+# includes and libs
+INCS = -I. -I/usr/include -I/usr/include/freetype2 -I${X11INC}
+LIBS = -L/usr/lib -lc -lm -L${X11LIB} -lXft -lfontconfig -lX11
+# OpenBSD (uncomment)
+#INCS = -I. -I${X11INC} -I${X11INC}/freetype2
+# FreeBSD (uncomment)
+#INCS = -I. -I/usr/local/include -I/usr/local/include/freetype2 -I${X11INC}
+#LIBS = -L/usr/local/lib -lc -lm -L${X11LIB} -lXft -lfontconfig -lX11
+
+# flags
+CPPFLAGS = -DVERSION=\"${VERSION}\" -D_XOPEN_SOURCE=600
+CFLAGS += -g -std=c99 -pedantic -Wall ${INCS} ${CPPFLAGS}
+LDFLAGS += -g ${LIBS}
+#CFLAGS += -std=c99 -pedantic -Wall -Os ${INCS} ${CPPFLAGS}
+#LDFLAGS += ${LIBS}
+
+# compiler and linker
+CC ?= cc
blob - /dev/null
blob + c1582e746cc57b7a475c1de3fcc53507cdb50b37 (mode 644)
--- /dev/null
+++ drw.c
@@ -0,0 +1,421 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <X11/Xlib.h>
+#include <X11/Xft/Xft.h>
+
+#include "drw.h"
+#include "util.h"
+
+#define UTF_INVALID 0xFFFD
+#define UTF_SIZ     4
+
+static const unsigned char utfbyte[UTF_SIZ + 1] = {0x80,    0, 0xC0, 0xE0, 0xF0};
+static const unsigned char utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
+static const long utfmin[UTF_SIZ + 1] = {       0,    0,  0x80,  0x800,  0x10000};
+static const long utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
+
+static long
+utf8decodebyte(const char c, size_t *i)
+{
+	for (*i = 0; *i < (UTF_SIZ + 1); ++(*i))
+		if (((unsigned char)c & utfmask[*i]) == utfbyte[*i])
+			return (unsigned char)c & ~utfmask[*i];
+	return 0;
+}
+
+static size_t
+utf8validate(long *u, size_t i)
+{
+	if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF))
+		*u = UTF_INVALID;
+	for (i = 1; *u > utfmax[i]; ++i)
+		;
+	return i;
+}
+
+static size_t
+utf8decode(const char *c, long *u, size_t clen)
+{
+	size_t i, j, len, type;
+	long udecoded;
+
+	*u = UTF_INVALID;
+	if (!clen)
+		return 0;
+	udecoded = utf8decodebyte(c[0], &len);
+	if (!BETWEEN(len, 1, UTF_SIZ))
+		return 1;
+	for (i = 1, j = 1; i < clen && j < len; ++i, ++j) {
+		udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type);
+		if (type)
+			return j;
+	}
+	if (j < len)
+		return 0;
+	*u = udecoded;
+	utf8validate(u, len);
+
+	return len;
+}
+
+Drw *
+drw_create(Display *dpy, int screen, Window root, unsigned int w, unsigned int h)
+{
+	Drw *drw = ecalloc(1, sizeof(Drw));
+
+	drw->dpy = dpy;
+	drw->screen = screen;
+	drw->root = root;
+	drw->w = w;
+	drw->h = h;
+	drw->drawable = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, screen));
+	drw->gc = XCreateGC(dpy, root, 0, NULL);
+	XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter);
+
+	return drw;
+}
+
+void
+drw_resize(Drw *drw, unsigned int w, unsigned int h)
+{
+	if (!drw)
+		return;
+
+	drw->w = w;
+	drw->h = h;
+	if (drw->drawable)
+		XFreePixmap(drw->dpy, drw->drawable);
+	drw->drawable = XCreatePixmap(drw->dpy, drw->root, w, h, DefaultDepth(drw->dpy, drw->screen));
+}
+
+void
+drw_free(Drw *drw)
+{
+	XFreePixmap(drw->dpy, drw->drawable);
+	XFreeGC(drw->dpy, drw->gc);
+	free(drw);
+}
+
+/* This function is an implementation detail. Library users should use
+ * drw_fontset_create instead.
+ */
+static Fnt *
+xfont_create(Drw *drw, const char *fontname, FcPattern *fontpattern)
+{
+	Fnt *font;
+	XftFont *xfont = NULL;
+	FcPattern *pattern = NULL;
+
+	if (fontname) {
+		/* Using the pattern found at font->xfont->pattern does not yield the
+		 * same substitution results as using the pattern returned by
+		 * FcNameParse; using the latter results in the desired fallback
+		 * behaviour whereas the former just results in missing-character
+		 * rectangles being drawn, at least with some fonts. */
+		if (!(xfont = XftFontOpenName(drw->dpy, drw->screen, fontname))) {
+			fprintf(stderr, "error, cannot load font from name: '%s'\n", fontname);
+			return NULL;
+		}
+		if (!(pattern = FcNameParse((FcChar8 *) fontname))) {
+			fprintf(stderr, "error, cannot parse font name to pattern: '%s'\n", fontname);
+			XftFontClose(drw->dpy, xfont);
+			return NULL;
+		}
+	} else if (fontpattern) {
+		if (!(xfont = XftFontOpenPattern(drw->dpy, fontpattern))) {
+			fprintf(stderr, "error, cannot load font from pattern.\n");
+			return NULL;
+		}
+	} else {
+		die("no font specified.");
+	}
+
+	font = ecalloc(1, sizeof(Fnt));
+	font->xfont = xfont;
+	font->pattern = pattern;
+	font->h = xfont->ascent + xfont->descent;
+	font->dpy = drw->dpy;
+
+	return font;
+}
+
+static void
+xfont_free(Fnt *font)
+{
+	if (!font)
+		return;
+	if (font->pattern)
+		FcPatternDestroy(font->pattern);
+	XftFontClose(font->dpy, font->xfont);
+	free(font);
+}
+
+Fnt*
+drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount)
+{
+	Fnt *cur, *ret = NULL;
+	size_t i;
+
+	if (!drw || !fonts)
+		return NULL;
+
+	for (i = 1; i <= fontcount; i++) {
+		if ((cur = xfont_create(drw, fonts[fontcount - i], NULL))) {
+			cur->next = ret;
+			ret = cur;
+		}
+	}
+	return (drw->fonts = ret);
+}
+
+void
+drw_fontset_free(Fnt *font)
+{
+	if (font) {
+		drw_fontset_free(font->next);
+		xfont_free(font);
+	}
+}
+
+void
+drw_clr_create(Drw *drw, Clr *dest, const char *clrname)
+{
+	if (!drw || !dest || !clrname)
+		return;
+
+	if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen),
+	                       DefaultColormap(drw->dpy, drw->screen),
+	                       clrname, dest))
+		die("error, cannot allocate color '%s'", clrname);
+}
+
+/* Wrapper to create color schemes. The caller has to call free(3) on the
+ * returned color scheme when done using it. */
+Clr *
+drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount)
+{
+	size_t i;
+	Clr *ret;
+
+	/* need at least two colors for a scheme */
+	if (!drw || !clrnames || clrcount < 2 || !(ret = ecalloc(clrcount, sizeof(XftColor))))
+		return NULL;
+
+	for (i = 0; i < clrcount; i++)
+		drw_clr_create(drw, &ret[i], clrnames[i]);
+	return ret;
+}
+
+void
+drw_setfontset(Drw *drw, Fnt *set)
+{
+	if (drw)
+		drw->fonts = set;
+}
+
+void
+drw_setscheme(Drw *drw, Clr *scm)
+{
+	if (drw)
+		drw->scheme = scm;
+}
+
+void
+drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert)
+{
+	if (!drw || !drw->scheme)
+		return;
+	XSetForeground(drw->dpy, drw->gc, invert ? drw->scheme[ColBg].pixel : drw->scheme[ColFg].pixel);
+	if (filled)
+		XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
+	else
+		XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1);
+}
+
+int
+drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert)
+{
+	char buf[1024];
+	int ty;
+	unsigned int ew;
+	XftDraw *d = NULL;
+	Fnt *usedfont, *curfont, *nextfont;
+	size_t i, len;
+	int utf8strlen, utf8charlen, render = x || y || w || h;
+	long utf8codepoint = 0;
+	const char *utf8str;
+	FcCharSet *fccharset;
+	FcPattern *fcpattern;
+	FcPattern *match;
+	XftResult result;
+	int charexists = 0;
+
+	if (!drw || (render && !drw->scheme) || !text || !drw->fonts)
+		return 0;
+
+	if (!render) {
+		w = ~w;
+	} else {
+		XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel);
+		XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
+		d = XftDrawCreate(drw->dpy, drw->drawable,
+		                  DefaultVisual(drw->dpy, drw->screen),
+		                  DefaultColormap(drw->dpy, drw->screen));
+		x += lpad;
+		w -= lpad;
+	}
+
+	usedfont = drw->fonts;
+	while (1) {
+		utf8strlen = 0;
+		utf8str = text;
+		nextfont = NULL;
+		while (*text) {
+			utf8charlen = utf8decode(text, &utf8codepoint, UTF_SIZ);
+			for (curfont = drw->fonts; curfont; curfont = curfont->next) {
+				charexists = charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint);
+				if (charexists) {
+					if (curfont == usedfont) {
+						utf8strlen += utf8charlen;
+						text += utf8charlen;
+					} else {
+						nextfont = curfont;
+					}
+					break;
+				}
+			}
+
+			if (!charexists || nextfont)
+				break;
+			else
+				charexists = 0;
+		}
+
+		if (utf8strlen) {
+			drw_font_getexts(usedfont, utf8str, utf8strlen, &ew, NULL);
+			/* shorten text if necessary */
+			for (len = MIN(utf8strlen, sizeof(buf) - 1); len && ew > w; len--)
+				drw_font_getexts(usedfont, utf8str, len, &ew, NULL);
+
+			if (len) {
+				memcpy(buf, utf8str, len);
+				buf[len] = '\0';
+				if (len < utf8strlen)
+					for (i = len; i && i > len - 3; buf[--i] = '.')
+						; /* NOP */
+
+				if (render) {
+					ty = y + (h - usedfont->h) / 2 + usedfont->xfont->ascent;
+					XftDrawStringUtf8(d, &drw->scheme[invert ? ColBg : ColFg],
+					                  usedfont->xfont, x, ty, (XftChar8 *)buf, len);
+				}
+				x += ew;
+				w -= ew;
+			}
+		}
+
+		if (!*text) {
+			break;
+		} else if (nextfont) {
+			charexists = 0;
+			usedfont = nextfont;
+		} else {
+			/* Regardless of whether or not a fallback font is found, the
+			 * character must be drawn. */
+			charexists = 1;
+
+			fccharset = FcCharSetCreate();
+			FcCharSetAddChar(fccharset, utf8codepoint);
+
+			if (!drw->fonts->pattern) {
+				/* Refer to the comment in xfont_create for more information. */
+				die("the first font in the cache must be loaded from a font string.");
+			}
+
+			fcpattern = FcPatternDuplicate(drw->fonts->pattern);
+			FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset);
+			FcPatternAddBool(fcpattern, FC_SCALABLE, FcTrue);
+
+			FcConfigSubstitute(NULL, fcpattern, FcMatchPattern);
+			FcDefaultSubstitute(fcpattern);
+			match = XftFontMatch(drw->dpy, drw->screen, fcpattern, &result);
+
+			FcCharSetDestroy(fccharset);
+			FcPatternDestroy(fcpattern);
+
+			if (match) {
+				usedfont = xfont_create(drw, NULL, match);
+				if (usedfont && XftCharExists(drw->dpy, usedfont->xfont, utf8codepoint)) {
+					for (curfont = drw->fonts; curfont->next; curfont = curfont->next)
+						; /* NOP */
+					curfont->next = usedfont;
+				} else {
+					xfont_free(usedfont);
+					usedfont = drw->fonts;
+				}
+			}
+		}
+	}
+	if (d)
+		XftDrawDestroy(d);
+
+	return x + (render ? w : 0);
+}
+
+void
+drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h)
+{
+	if (!drw)
+		return;
+
+	XCopyArea(drw->dpy, drw->drawable, win, drw->gc, x, y, w, h, x, y);
+	XSync(drw->dpy, False);
+}
+
+unsigned int
+drw_fontset_getwidth(Drw *drw, const char *text)
+{
+	if (!drw || !drw->fonts || !text)
+		return 0;
+	return drw_text(drw, 0, 0, 0, 0, 0, text, 0);
+}
+
+void
+drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h)
+{
+	XGlyphInfo ext;
+
+	if (!font || !text)
+		return;
+
+	XftTextExtentsUtf8(font->dpy, font->xfont, (XftChar8 *)text, len, &ext);
+	if (w)
+		*w = ext.xOff;
+	if (h)
+		*h = font->h;
+}
+
+Cur *
+drw_cur_create(Drw *drw, int shape)
+{
+	Cur *cur;
+
+	if (!drw || !(cur = ecalloc(1, sizeof(Cur))))
+		return NULL;
+
+	cur->cursor = XCreateFontCursor(drw->dpy, shape);
+
+	return cur;
+}
+
+void
+drw_cur_free(Drw *drw, Cur *cursor)
+{
+	if (!cursor)
+		return;
+
+	XFreeCursor(drw->dpy, cursor->cursor);
+	free(cursor);
+}
blob - /dev/null
blob + 4c67419a98dd6d87e0b15e6f14094f24aa44c143 (mode 644)
--- /dev/null
+++ drw.h
@@ -0,0 +1,57 @@
+/* See LICENSE file for copyright and license details. */
+
+typedef struct {
+	Cursor cursor;
+} Cur;
+
+typedef struct Fnt {
+	Display *dpy;
+	unsigned int h;
+	XftFont *xfont;
+	FcPattern *pattern;
+	struct Fnt *next;
+} Fnt;
+
+enum { ColFg, ColBg }; /* Clr scheme index */
+typedef XftColor Clr;
+
+typedef struct {
+	unsigned int w, h;
+	Display *dpy;
+	int screen;
+	Window root;
+	Drawable drawable;
+	GC gc;
+	Clr *scheme;
+	Fnt *fonts;
+} Drw;
+
+/* Drawable abstraction */
+Drw *drw_create(Display *dpy, int screen, Window win, unsigned int w, unsigned int h);
+void drw_resize(Drw *drw, unsigned int w, unsigned int h);
+void drw_free(Drw *drw);
+
+/* Fnt abstraction */
+Fnt *drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount);
+void drw_fontset_free(Fnt* set);
+unsigned int drw_fontset_getwidth(Drw *drw, const char *text);
+void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h);
+
+/* Colorscheme abstraction */
+void drw_clr_create(Drw *drw, Clr *dest, const char *clrname);
+Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount);
+
+/* Cursor abstraction */
+Cur *drw_cur_create(Drw *drw, int shape);
+void drw_cur_free(Drw *drw, Cur *cursor);
+
+/* Drawing context manipulation */
+void drw_setfontset(Drw *drw, Fnt *set);
+void drw_setscheme(Drw *drw, Clr *scm);
+
+/* Drawing functions */
+void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert);
+int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert);
+
+/* Map functions */
+void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h);
blob - /dev/null
blob + 300577a47225a4b8841e6dd6d4685daa259f075c (mode 644)
--- /dev/null
+++ example
@@ -0,0 +1,69 @@
+sent
+
+Origin:
+     Takahashi
+
+Why?
+• PPTX sucks
+• LATEX sucks
+• PDF sucks
+
+also:
+terminal presentations
+don't support images…
+
+@nyan.png
+this text will not be displayed, since the @ at the start of the first line
+makes this paragraph an image slide.
+
+easy to use
+
+depends on
+♽ Xlib
+☢ Xft
+☃ farbfeld
+
+~1000 lines of code
+
+usage:
+$ sent FILE1 [FILE2 …]
+
+▸ one slide per paragraph
+▸ lines starting with # are ignored
+▸ image slide: paragraph containing @FILENAME
+▸ empty slide: just use a \ as a paragraph
+
+# This is a comment and will not be part of the presentation
+
+# multiple empty lines between paragraphs are also ignored
+
+
+# The following lines should produce
+# one empty slide
+
+
+
+\
+\
+
+\@this_line_actually_started_with_a_\.png
+\#This line as well
+⇒ Prepend a backslash to kill behaviour of special characters
+
+Images are handled in the
+http://tools.suckless.org/farbfeld/
+format internally.
+
+sent also supports transparent images.
+Try changing the background in config.h
+and rebuild.
+
+@transparent_test.ff
+
+😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏
+😐😑😒😓😔😕😖😗😘😙😚😛😜😝😞😟
+😠😡😢😣😥😦😧😨😩😪😫😭😮😯😰😱
+😲😳😴😵😶😷😸😹😺😻😼😽😾😿🙀☠
+
+thanks.
+questions?
blob - /dev/null
blob + 377b9d0843dac6e6ef1318612973a780a79f01a8 (mode 644)
Binary files /dev/null and nyan.png differ
blob - /dev/null
blob + 752eb910d65830a6a8fed2b523f6de2fb579feae (mode 644)
--- /dev/null
+++ sent-caption-20260224-1.diff
@@ -0,0 +1,78 @@
+--- a/config.def.h
++++ b/config.def.h
+@@ -8,6 +8,8 @@ static char *fontfallbacks[] = {
+ #define NUMFONTSCALES 42
+ #define FONTSZ(x) ((int)(10.0 * powf(1.1288, (x)))) /* x in [0, NUMFONTSCALES-1] */
+ 
++/* index into the fonts[] array used for image captions (0 = smallest) */
++#define CAPTIONFONTSCALE 8
++
+ static const char *colors[] = {
+ 	"#000000", /* foreground color */
+ 	"#FFFFFF", /* background color */
+--- a/sent.c
++++ b/sent.c
+@@ -53,6 +53,7 @@ typedef struct {
+ 	char **lines;
+ 	Image *img;
+ 	char *embed;
++	char *caption;	/* optional caption for image slides */
+ } Slide;
+ 
+ /* Purely graphic info */
+@@ -430,12 +430,14 @@ load(FILE *fp)
+ 		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]))))
+@@ -449,11 +451,17 @@ load(FILE *fp)
+ 			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);
++
++		/* mark second line of image slide as caption */
++		if (s->embed && s->linecount >= 2)
++			s->caption = s->lines[1];
+ 
+ 		slidecount++;
+ 		if (!p)
+@@ -544,6 +552,21 @@ xdraw(void)
+ 		if (!(im->state & SCALED))
+ 			ffprepare(im);
+ 		ffdraw(im);
++		if (slides[idx].caption) {
++			unsigned int capw;
++			int imgy_bottom = (xw.h + im->ximg->height) / 2 + 4;
++			drw_setfontset(d, fonts[CAPTIONFONTSCALE]);
++			capw = drw_fontset_getwidth(d, slides[idx].caption);
++			drw_rect(d, 0, imgy_bottom,
++			         xw.w, fonts[CAPTIONFONTSCALE]->h + 4, 1, 1);
++			drw_text(d, (xw.w - capw) / 2,
++			         imgy_bottom + 2,
++			         capw,
++			         fonts[CAPTIONFONTSCALE]->h,
++			         0, slides[idx].caption, 0);
++			drw_map(d, xw.win, 0, imgy_bottom,
++			        xw.w, fonts[CAPTIONFONTSCALE]->h + 4);
++		}
+ 	}
+ }
+ 
blob - /dev/null
blob + e5a7d25c95536d57494c482b4c1f73e4f09438f5 (mode 644)
--- /dev/null
+++ sent.1
@@ -0,0 +1,72 @@
+.Dd August 12, 2016
+.Dt SENT 1
+.Os
+.Sh NAME
+.Nm sent
+.Nd simple plaintext presentation tool
+.Sh SYNOPSIS
+.Nm
+.Op Fl v
+.Op Ar file
+.Sh DESCRIPTION
+.Nm
+is a simple plain text presentation tool for X. sent does not need LaTeX,
+LibreOffice or any other fancy file format.
+Instead, sent reads plain text describing the slides. sent can also draw
+images.
+.Pp
+Every paragraph represents a slide in the presentation.
+Especially for presentations using the Takahashi method this is very nice and
+allows you to write the presentation for a quick lightning talk within a few
+minutes.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl v
+Print version information to stdout and exit.
+.El
+.Sh USAGE
+.Bl -tag -width Ds
+.It Em Mouse commands
+.Bl -tag -width Ds
+.It Sy Button1 | Button5
+Go to next slide, if existent.
+.It Sy Button3 | Button4
+Go to previous slide, if existent.
+.El
+.It Em Keyboard commands
+.Bl -tag -width Ds
+.It Sy Escape | q
+Quit.
+.It Sy r
+Reload the slides.
+Only works on file input.
+.It Sy Right | Return | Space | l | j | Down | Next | n
+Go to next slide, if existent.
+.It Sy Left | Backspace | h | k | Up | Prior | p
+Go to previous slide, if existent.
+.El
+.El
+.Sh FORMAT
+The presentation file is made up of at least one paragraph, with an
+empty line separating two slides.
+Each input line is interpreted literally, except from control characters
+at the beginning of lines described as follows:
+.Bl -tag -width Ds
+.It Sy @
+Create individual slide containing the image pointed to by the filename
+following the
+.Sy @ .
+.It Sy #
+Ignore this input line.
+.It Sy \e
+Create input line using the characters following the
+.Sy \e
+without interpreting them.
+.El
+.Sh CUSTOMIZATION
+.Nm
+can be customized by creating a custom config.h and (re)compiling the
+source code.
+This keeps it fast, secure and simple.
+.Sh SEE ALSO
+.Xr 2ff 1
blob - /dev/null
blob + dfadd3a3b22cce23ccbbecf92d476c1bd26ec72e (mode 644)
--- /dev/null
+++ sent.c
@@ -0,0 +1,711 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/types.h>
+#include <arpa/inet.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <math.h>
+#include <regex.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <X11/keysym.h>
+#include <X11/XKBlib.h>
+#include <X11/Xatom.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xft/Xft.h>
+
+#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(void);
+static void usage(void);
+static void xdraw(void);
+static void xhints(void);
+static void xinit(void);
+static void xloadfonts(void);
+
+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(&regex, filters[i].regex,
+		            REG_NOSUB | REG_EXTENDED | REG_ICASE)) {
+			fprintf(stderr, "sent: Invalid regex '%s'\n", filters[i].regex);
+			continue;
+		}
+		if (!regexec(&regex, filename, 0, NULL, 0)) {
+			bin = filters[i].bin;
+			regfree(&regex);
+			break;
+		}
+		regfree(&regex);
+	}
+	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]);
+
+	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)
+		XDestroyImage(img->ximg);
+
+	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(void)
+{
+	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(void)
+{
+	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(void)
+{
+	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(void)
+{
+	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(void)
+{
+	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++)
+		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(void)
+{
+	die("usage: %s [file]", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+	FILE *fp = NULL;
+
+	ARGBEGIN {
+	case 'v':
+		fprintf(stderr, "sent-"VERSION"\n");
+		return 0;
+	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;
+}
blob - /dev/null
blob + 505acaa36ca0058c016817256a4618af6268a798 (mode 644)
Binary files /dev/null and transparent_test.ff differ
blob - /dev/null
blob + fe044fc7b7978d1f3c23768e315aae415d90b11a (mode 644)
--- /dev/null
+++ util.c
@@ -0,0 +1,35 @@
+/* See LICENSE file for copyright and license details. */
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+void *
+ecalloc(size_t nmemb, size_t size)
+{
+	void *p;
+
+	if (!(p = calloc(nmemb, size)))
+		die("calloc:");
+	return p;
+}
+
+void
+die(const char *fmt, ...) {
+	va_list ap;
+
+	va_start(ap, fmt);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
+
+	if (fmt[0] && fmt[strlen(fmt)-1] == ':') {
+		fputc(' ', stderr);
+		perror(NULL);
+	} else {
+		fputc('\n', stderr);
+	}
+
+	exit(1);
+}
blob - /dev/null
blob + f633b5173ad2b5058d48574d5a18fe3f135c4193 (mode 644)
--- /dev/null
+++ util.h
@@ -0,0 +1,8 @@
+/* See LICENSE file for copyright and license details. */
+
+#define MAX(A, B)               ((A) > (B) ? (A) : (B))
+#define MIN(A, B)               ((A) < (B) ? (A) : (B))
+#define BETWEEN(X, A, B)        ((A) <= (X) && (X) <= (B))
+
+void die(const char *fmt, ...);
+void *ecalloc(size_t nmemb, size_t size);