diff --git a/sixel.c b/sixel.c
new file mode 100644
index 0000000..69bf42b
--- /dev/null
+++ b/sixel.c
@@ -0,0 +1,164 @@
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct sixel_reg {
+	uint8_t colors[3];
+};
+
+static uint8_t *sixel_buf, sixel_mode;
+static size_t sixel_bufsiz, sixel_offset, sixel_len;
+static XImage *sixel_img;
+static int sixel_drawheight;
+
+#define nextchar() ((sixel_offset >= sixel_len) ? EOF : sixel_buf[sixel_offset++])
+
+/* TODO: rewrite parser: ugly as balls */
+int
+parsesixels2img(void)
+{
+#define SIXEL_MAXREGS 255
+	struct sixel_reg regs[SIXEL_MAXREGS + 1];
+	uint8_t colors[3], v;
+	int reg = -1, rep = 1, mode, i, j, charmode = 0, yy, t, c;
+	int x = 0, y = 0, w = sixel_img->width, h = sixel_img->height;
+
+	sixel_drawheight = 0;
+	memset(&regs, 0, sizeof(regs));
+	for (c = nextchar(); c != EOF; c = nextchar()) {
+loop:
+		switch (c) {
+		case '#': /* register */
+			c = nextchar();
+			reg = -1; /* register number */
+			if (c >= '0' && c <= '9')
+				for (i = 0, reg = 0; i < 3 && (c >= '0' && c <= '9'); i++, c = nextchar())
+					reg = (reg * 10) + (c - '0');
+			if (reg < 0 || reg > SIXEL_MAXREGS)
+				return 1;
+			switch (c) {
+			case ';': /* set register */
+				c = nextchar();
+				mode = -1;
+				if (c >= '0' && c <= '9')
+					for (i = 0, mode = 0; i < 3 && (c >= '0' && c <= '9'); i++, c = nextchar())
+						mode = (mode * 10) + (c - '0');
+				if (mode != 2)
+					return 1;
+				if (c != ';')
+					goto loop;
+				c = nextchar();
+				/* color values */
+				for (j = 0; j < 3 && c != '#'; j++) {
+					t = -1;
+					if (c >= '0' && c <= '9')
+						for (i = 0, t = 0; i < 3 && (c >= '0' && c <= '9'); i++, c = nextchar())
+							t = (t * 10) + (c - '0');
+					if (t < 0)
+						return 1;
+					if (c == ';')
+						c = nextchar();
+					colors[j] = t;
+				}
+				/* not all colors parsed */
+				if (j != 3)
+					return 1;
+				/* check and convert RGB: 0-100, to 0-255 range, round */
+				for (j = 0; j < 3; j++) {
+					if (colors[j] > 100)
+						return 1;
+					regs[reg].colors[j] = (int)(((float)colors[j] * 25.5f+.5f) / 10);
+				}
+				break;
+			}
+			goto loop;
+		case '!': /* RLE: draw repeat */
+			c = nextchar();
+			t = -1;
+			if (c >= '0' && c <= '9')
+				for (i = 0, t = 0; i < 3 && (c >= '0' && c <= '9'); i++, c = nextchar())
+					t = (t * 10) + (c - '0');
+			if (t < 0)
+				return 1;
+			rep = t;
+			goto loop;
+		case '-': y += 6; /* FALLTHROUGH */
+		case '$': x = 0;  break;
+		default: /* draw */
+			/* not in draw mode */
+			if (charmode)
+				break;
+			if (c < 63 || c > 126) {
+				rep = 1;
+				continue;
+			}
+			v = c - 63;
+			/* no register or out-of-bounds or nothing to draw */
+			if (!v || reg < 0 || x >= w || y + 6 >= h) {
+				x += rep;
+				rep = 0;
+			}
+			for (; rep; rep--) {
+				/* TODO: faster check of bit patterns */
+				for (yy = 0; yy < 6; yy++) {
+					if (!((v >> yy) & 1))
+						continue;
+					sixel_img->data[((y + yy) * w + x) * 4]     = regs[reg].colors[2]; /* B */
+					sixel_img->data[((y + yy) * w + x) * 4 + 1] = regs[reg].colors[1]; /* G */
+					sixel_img->data[((y + yy) * w + x) * 4 + 2] = regs[reg].colors[0]; /* R */
+					sixel_img->data[((y + yy) * w + x) * 4 + 3] = ~0; /* A */
+					if (y + yy > sixel_drawheight)
+						sixel_drawheight = y + yy;
+				}
+				x++;
+			}
+		}
+		rep = 1;
+	}
+	return 0;
+}
+
+void
+tputsixelc(Rune u)
+{
+
+
+	if (!((!sixel_len || sixel_buf[sixel_len - 1] == '\x1b') && u == '\\')) {
+		/* expand and store sixel instructions into buffer to parse later */
+		if (sixel_len >= sixel_bufsiz) {
+			if (!sixel_bufsiz)
+				sixel_bufsiz = BUFSIZ;
+			sixel_bufsiz *= 2; /* greedy allocation */
+			sixel_buf = xrealloc(sixel_buf, sixel_bufsiz);
+		}
+		sixel_buf[sixel_len++] = (int)(u & 0xff);
+		return;
+	}
+	sixel_mode = 0;
+
+	/* TODO: (re)create XImage in xresize() ? */
+	/* TODO: also clear buffer, but not while in an animation */
+	if (!sixel_img) {
+		if (!(sixel_img = XCreateImage(xw.dpy, CopyFromParent,
+			DefaultDepth(xw.dpy, xw.scr), ZPixmap, 0,
+			NULL, xw.w, xw.h, 32, 0)))
+			die("XCreateImage: %s\n", strerror(errno));
+		if (!(sixel_img->data = calloc(1, sixel_img->bytes_per_line * sixel_img->height)))
+			die("calloc: %s\n", strerror(errno));
+		if (!XInitImage(sixel_img))
+			die("XInitImage() for sixel image failed\n");
+	}
+	parsesixels2img();
+	/* clear sixel read buffer, but keep buffer in memory */
+	sixel_len = sixel_offset = 0;
+
+	/* TODO: only draw if dirty, use backbuffer to prevent flicker (xw.buf) */
+	XPutImage(xw.dpy, xw.buf, dc.gc, sixel_img, 0, 0,
+		term.c.x * xw.cw, term.c.y * xw.ch, sixel_img->width, sixel_img->height);
+
+	/* TODO: proper seek to end of image height */
+	/* TODO: detect "animation mode" ? don't forward cursor while in animation */
+/*	tmoveto(term.c.x, term.c.y + MAX(1, drawheight / xw.ch));*/
+}
diff --git a/st.c b/st.c
index 2594c65..ad95779 100644
--- a/st.c
+++ b/st.c
@@ -560,6 +560,8 @@ typedef struct {
 static Fontcache frc[16];
 static int frclen = 0;
 
+#include "sixel.c"
+
 ssize_t
 xwrite(int fd, const char *s, size_t len)
 {
@@ -1482,7 +1484,10 @@ ttyread(void)
 	buflen += ret;
 	ptr = buf;
 	while ((charsize = utf8decode(ptr, &unicodep, buflen))) {
-		tputc(unicodep);
+		if (sixel_mode)
+			tputsixelc(unicodep);
+		else
+			tputc(unicodep);
 		ptr += charsize;
 		buflen -= charsize;
 	}
@@ -2852,6 +2857,7 @@ eschandle(uchar ascii)
 		term.esc |= ESC_TEST;
 		return 0;
 	case 'P': /* DCS -- Device Control String */
+		sixel_mode = 1;
 	case '_': /* APC -- Application Program Command */
 	case '^': /* PM -- Privacy Message */
 	case ']': /* OSC -- Operating System Command */
