summary refs log tree commit diff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rw-r--r--bin/shotty.c648
-rw-r--r--bin/shotty.l554
2 files changed, 554 insertions, 648 deletions
diff --git a/bin/shotty.c b/bin/shotty.c
deleted file mode 100644
index de7fc8ac..00000000
--- a/bin/shotty.c
+++ /dev/null
@@ -1,648 +0,0 @@
-/* Copyright (C) 2019  June McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <assert.h>
-#include <err.h>
-#include <locale.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/ioctl.h>
-#include <sysexits.h>
-#include <unistd.h>
-#include <wchar.h>
-
-#define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit
-
-typedef unsigned uint;
-
-enum {
-	NUL, SOH, STX, ETX, EOT, ENQ, ACK, BEL,
-	BS, HT, NL, VT, NP, CR, SO, SI,
-	DLE, DC1, DC2, DC3, DC4, NAK, SYN, ETB,
-	CAN, EM, SUB, ESC, FS, GS, RS, US,
-	DEL = 0x7F,
-};
-
-enum Attr {
-	BIT(Bold),
-	BIT(Dim),
-	BIT(Italic),
-	BIT(Underline),
-	BIT(Blink),
-	BIT(Reverse),
-};
-
-struct Style {
-	enum Attr attr;
-	int bg, fg;
-};
-
-struct Cell {
-	struct Style style;
-	wchar_t ch;
-};
-
-static uint rows = 24, cols = 80;
-static struct Cell *cells;
-
-static struct Cell *cell(uint y, uint x) {
-	assert(y <= rows);
-	assert(x <= cols);
-	assert(y * cols + x <= rows * cols);
-	return &cells[y * cols + x];
-}
-
-static uint y, x;
-static struct Style style = { .bg = -1, .fg = -1 };
-
-static struct {
-	uint y, x;
-} save;
-
-enum { ParamCap = 16 };
-static struct {
-	uint s[ParamCap];
-	uint n, i;
-} param;
-
-static uint p(uint i, uint z) {
-	return (i < param.n ? param.s[i] : z);
-}
-
-static uint min(uint a, uint b) {
-	return (a < b ? a : b);
-}
-
-#define _ch ch __attribute__((unused))
-typedef void Action(wchar_t ch);
-
-static void nop(wchar_t _ch) {
-}
-
-static void csi(wchar_t _ch) {
-	memset(&param, 0, sizeof(param));
-}
-
-static void csiSep(wchar_t _ch) {
-	if (param.n == ParamCap) return;
-	if (!param.n) param.n++;
-	param.n++;
-	param.i++;
-}
-
-static void csiDigit(wchar_t ch) {
-	param.s[param.i] *= 10;
-	param.s[param.i] += ch - L'0';
-	if (!param.n) param.n++;
-}
-
-static void bs(wchar_t _ch)  { if (x) x--; }
-static void ht(wchar_t _ch)  { x = min(x - x % 8 + 8, cols - 1); }
-static void cr(wchar_t _ch)  { x = 0; }
-static void cuu(wchar_t _ch) { y -= min(p(0, 1), y); }
-static void cud(wchar_t _ch) { y  = min(y + p(0, 1), rows - 1); }
-static void cuf(wchar_t _ch) { x  = min(x + p(0, 1), cols - 1); }
-static void cub(wchar_t _ch) { x -= min(p(0, 1), x); }
-static void cnl(wchar_t _ch) { x = 0; cud(0); }
-static void cpl(wchar_t _ch) { x = 0; cuu(0); }
-static void cha(wchar_t _ch) { x = min(p(0, 1) - 1, cols - 1); }
-static void vpa(wchar_t _ch) { y = min(p(0, 1) - 1, rows - 1); }
-static void cup(wchar_t _ch) {
-	y = min(p(0, 1) - 1, rows - 1);
-	x = min(p(1, 1) - 1, cols - 1);
-}
-static void decsc(wchar_t _ch) {
-	save.y = y;
-	save.x = x;
-}
-static void decrc(wchar_t _ch) {
-	y = save.y;
-	x = save.x;
-}
-
-static void move(struct Cell *dst, struct Cell *src, size_t len) {
-	memmove(dst, src, sizeof(*dst) * len);
-}
-
-static void erase(struct Cell *at, struct Cell *to) {
-	for (; at < to; ++at) {
-		at->style = style;
-		at->ch = L' ';
-	}
-}
-
-static void ed(wchar_t _ch) {
-	erase(
-		(p(0, 0) == 0 ? cell(y, x) : cell(0, 0)),
-		(p(0, 0) == 1 ? cell(y, x) : cell(rows - 1, cols))
-	);
-}
-static void el(wchar_t _ch) {
-	erase(
-		(p(0, 0) == 0 ? cell(y, x) : cell(y, 0)),
-		(p(0, 0) == 1 ? cell(y, x) : cell(y, cols))
-	);
-}
-static void ech(wchar_t _ch) {
-	erase(cell(y, x), cell(y, min(x + p(0, 1), cols)));
-}
-
-static void dch(wchar_t _ch) {
-	uint n = min(p(0, 1), cols - x);
-	move(cell(y, x), cell(y, x + n), cols - x - n);
-	erase(cell(y, cols - n), cell(y, cols));
-}
-static void ich(wchar_t _ch) {
-	uint n = min(p(0, 1), cols - x);
-	move(cell(y, x + n), cell(y, x), cols - x - n);
-	erase(cell(y, x), cell(y, x + n));
-}
-
-static struct {
-	uint top, bot;
-} scroll;
-
-static void scrollUp(uint top, uint n) {
-	n = min(n, scroll.bot - top);
-	move(cell(top, 0), cell(top + n, 0), cols * (scroll.bot - top - n));
-	erase(cell(scroll.bot - n, 0), cell(scroll.bot, 0));
-}
-
-static void scrollDown(uint top, uint n) {
-	n = min(n, scroll.bot - top);
-	move(cell(top + n, 0), cell(top, 0), cols * (scroll.bot - top - n));
-	erase(cell(top, 0), cell(top + n, 0));
-}
-
-static void decstbm(wchar_t _ch) {
-	scroll.bot = min(p(1, rows), rows);
-	scroll.top = min(p(0, 1) - 1, scroll.bot);
-}
-
-static void su(wchar_t _ch) { scrollUp(scroll.top, p(0, 1)); }
-static void sd(wchar_t _ch) { scrollDown(scroll.top, p(0, 1)); }
-static void dl(wchar_t _ch) { scrollUp(min(y, scroll.bot), p(0, 1)); }
-static void il(wchar_t _ch) { scrollDown(min(y, scroll.bot), p(0, 1)); }
-
-static void nl(wchar_t _ch) {
-	if (y + 1 == scroll.bot) {
-		scrollUp(scroll.top, 1);
-	} else {
-		y = min(y + 1, rows - 1);
-	}
-}
-static void ri(wchar_t _ch) {
-	if (y == scroll.top) {
-		scrollDown(scroll.top, 1);
-	} else {
-		if (y) y--;
-	}
-}
-
-static enum Mode {
-	BIT(Insert),
-	BIT(Wrap),
-	BIT(Cursor),
-} mode = Wrap | Cursor;
-
-static enum Mode paramMode(void) {
-	enum Mode mode = 0;
-	for (uint i = 0; i < param.n; ++i) {
-		switch (param.s[i]) {
-			break; case 4: mode |= Insert;
-			break; default: warnx("unhandled SM/RM %u", param.s[i]);
-		}
-	}
-	return mode;
-}
-
-static enum Mode paramDECMode(void) {
-	enum Mode mode = 0;
-	for (uint i = 0; i < param.n; ++i) {
-		switch (param.s[i]) {
-			break; case 1: // DECCKM
-			break; case 7: mode |= Wrap;
-			break; case 12: // "Start Blinking Cursor"
-			break; case 25: mode |= Cursor;
-			break; default: {
-				if (param.s[i] < 1000) {
-					warnx("unhandled DECSET/DECRST %u", param.s[i]);
-				}
-			}
-		}
-	}
-	return mode;
-}
-
-static void sm(wchar_t _ch) { mode |= paramMode(); }
-static void rm(wchar_t _ch) { mode &= ~paramMode(); }
-static void decset(wchar_t _ch) { mode |= paramDECMode(); }
-static void decrst(wchar_t _ch) { mode &= ~paramDECMode(); }
-
-enum {
-	Reset,
-	SetBold,
-	SetDim,
-	SetItalic,
-	SetUnderline,
-	SetBlink,
-	SetReverse = 7,
-
-	UnsetBoldDim = 22,
-	UnsetItalic,
-	UnsetUnderline,
-	UnsetBlink,
-	UnsetReverse = 27,
-
-	SetFg0 = 30,
-	SetFg7 = 37,
-	SetFg,
-	ResetFg,
-	SetBg0 = 40,
-	SetBg7 = 47,
-	SetBg,
-	ResetBg,
-
-	SetFg8 = 90,
-	SetFgF = 97,
-	SetBg8 = 100,
-	SetBgF = 107,
-
-	Color256 = 5,
-};
-
-static void sgr(wchar_t _ch) {
-	uint n = param.i + 1;
-	for (uint i = 0; i < n; ++i) {
-		switch (param.s[i]) {
-			break; case Reset: style = (struct Style) { .bg = -1, .fg = -1 };
-
-			break; case SetBold:      style.attr |= Bold; style.attr &= ~Dim;
-			break; case SetDim:       style.attr |= Dim; style.attr &= ~Bold;
-			break; case SetItalic:    style.attr |= Italic;
-			break; case SetUnderline: style.attr |= Underline;
-			break; case SetBlink:     style.attr |= Blink;
-			break; case SetReverse:   style.attr |= Reverse;
-
-			break; case UnsetBoldDim:   style.attr &= ~(Bold | Dim);
-			break; case UnsetItalic:    style.attr &= ~Italic;
-			break; case UnsetUnderline: style.attr &= ~Underline;
-			break; case UnsetBlink:     style.attr &= ~Blink;
-			break; case UnsetReverse:   style.attr &= ~Reverse;
-
-			break; case SetFg: {
-				if (++i < n && param.s[i] == Color256) {
-					if (++i < n) style.fg = param.s[i];
-				}
-			}
-			break; case SetBg: {
-				if (++i < n && param.s[i] == Color256) {
-					if (++i < n) style.bg = param.s[i];
-				}
-			}
-
-			break; case ResetFg: style.fg = -1;
-			break; case ResetBg: style.bg = -1;
-
-			break; default: {
-				uint p = param.s[i];
-				if (p >= SetFg0 && p <= SetFg7) {
-					style.fg = p - SetFg0;
-				} else if (p >= SetBg0 && p <= SetBg7) {
-					style.bg = p - SetBg0;
-				} else if (p >= SetFg8 && p <= SetFgF) {
-					style.fg = 8 + p - SetFg8;
-				} else if (p >= SetBg8 && p <= SetBgF) {
-					style.bg = 8 + p - SetBg8;
-				} else {
-					warnx("unhandled SGR %u", p);
-				}
-			}
-		}
-	}
-}
-
-static enum {
-	USASCII,
-	DECSpecial,
-} charset;
-
-static void usascii(wchar_t _ch) { charset = USASCII; }
-static void decSpecial(wchar_t _ch) { charset = DECSpecial; }
-
-static const wchar_t AltCharset[128] = {
-	['`'] = L'◆', ['a'] = L'▒', ['f'] = L'°', ['g'] = L'±', ['i'] = L'␋',
-	['j'] = L'┘', ['k'] = L'┐', ['l'] = L'┌', ['m'] = L'└', ['n'] = L'┼',
-	['o'] = L'⎺', ['p'] = L'⎻', ['q'] = L'─', ['r'] = L'⎼', ['s'] = L'⎽',
-	['t'] = L'├', ['u'] = L'┤', ['v'] = L'┴', ['w'] = L'┬', ['x'] = L'│',
-	['y'] = L'≤', ['z'] = L'≥', ['{'] = L'π', ['|'] = L'≠', ['}'] = L'£',
-	['~'] = L'·',
-};
-
-static void add(wchar_t ch) {
-	if (charset == DECSpecial && ch < 128 && AltCharset[ch]) {
-		ch = AltCharset[ch];
-	}
-
-	int width = wcwidth(ch);
-	if (width < 0) {
-		warnx("unhandled \\u%02X", ch);
-		return;
-	}
-
-	if (mode & Insert) {
-		uint n = min(width, cols - x);
-		move(cell(y, x + n), cell(y, x), cols - x - n);
-	}
-	if (mode & Wrap && x + width > cols) {
-		cr(0);
-		nl(0);
-	}
-
-	cell(y, x)->style = style;
-	cell(y, x)->ch = ch;
-	for (int i = 1; i < width && x + i < cols; ++i) {
-		cell(y, x + i)->style = style;
-		cell(y, x + i)->ch = L'\0';
-	}
-	x = min(x + width, (mode & Wrap ? cols : cols - 1));
-}
-
-static void html(void);
-static void mc(wchar_t _ch) {
-	if (p(0, 0) == 10) {
-		html();
-	} else {
-		warnx("unhandled CSI %u MC", p(0, 0));
-	}
-}
-
-static enum {
-	Data,
-	Esc,
-	G0,
-	CSI,
-	CSILt,
-	CSIEq,
-	CSIGt,
-	CSIQm,
-	CSIInter,
-	OSC,
-	OSCEsc,
-} state;
-
-static void escDefault(wchar_t ch) {
-	warnx("unhandled ESC %lc", ch);
-}
-
-static void g0Default(wchar_t ch) {
-	warnx("unhandled G0 %lc", ch);
-	charset = USASCII;
-}
-
-static void csiInter(wchar_t ch) {
-	switch (state) {
-		break; case CSI: warnx("unhandled CSI %lc ...", ch);
-		break; case CSILt: warnx("unhandled CSI < %lc ...", ch);
-		break; case CSIEq: warnx("unhandled CSI = %lc ...", ch);
-		break; case CSIGt: warnx("unhandled CSI > %lc ...", ch);
-		break; case CSIQm: warnx("unhandled CSI ? %lc ...", ch);
-		break; default: abort();
-	}
-}
-
-static void csiFinal(wchar_t ch) {
-	switch (state) {
-		break; case CSI: warnx("unhandled CSI %lc", ch);
-		break; case CSILt: warnx("unhandled CSI < %lc", ch);
-		break; case CSIEq: warnx("unhandled CSI = %lc", ch);
-		break; case CSIGt: warnx("unhandled CSI > %lc", ch);
-		break; case CSIQm: warnx("unhandled CSI ? %lc", ch);
-		break; case CSIInter: warnx("unhandled CSI ... %lc", ch);
-		break; default: abort();
-	}
-}
-
-#define S(s) break; case s: switch (ch)
-#define A(c, a, s) break; case c: a(ch); state = s
-#define D(a, s) break; default: a(ch); state = s
-static void update(wchar_t ch) {
-	switch (state) {
-		default: abort();
-
-		S(Data) {
-			A(BEL, nop, Data);
-			A(BS,  bs,  Data);
-			A(HT,  ht,  Data);
-			A(NL,  nl,  Data);
-			A(CR,  cr,  Data);
-			A(ESC, nop, Esc);
-			D(add, Data);
-		}
-
-		S(Esc) {
-			A('(', nop, G0);
-			A('7', decsc, Data);
-			A('8', decrc, Data);
-			A('=', nop, Data);
-			A('>', nop, Data);
-			A('M', ri,  Data);
-			A('[', csi, CSI);
-			A(']', nop, OSC);
-			D(escDefault, Data);
-		}
-		S(G0) {
-			A('0', decSpecial, Data);
-			A('B', usascii, Data);
-			D(g0Default, Data);
-		}
-
-		S(CSI) {
-			A(' ' ... '/', csiInter, CSIInter);
-			A('0' ... '9', csiDigit, CSI);
-			A(':', nop, CSI);
-			A(';', csiSep, CSI);
-			A('<', nop, CSILt);
-			A('=', nop, CSIEq);
-			A('>', nop, CSIGt);
-			A('?', nop, CSIQm);
-			A('@', ich, Data);
-			A('A', cuu, Data);
-			A('B', cud, Data);
-			A('C', cuf, Data);
-			A('D', cub, Data);
-			A('E', cnl, Data);
-			A('F', cpl, Data);
-			A('G', cha, Data);
-			A('H', cup, Data);
-			A('J', ed,  Data);
-			A('K', el,  Data);
-			A('L', il,  Data);
-			A('M', dl,  Data);
-			A('P', dch, Data);
-			A('S', su,  Data);
-			A('T', sd,  Data);
-			A('X', ech, Data);
-			A('d', vpa, Data);
-			A('h', sm,  Data);
-			A('i', mc,  Data);
-			A('l', rm,  Data);
-			A('m', sgr, Data);
-			A('r', decstbm, Data);
-			A('t', nop, Data);
-			D(csiFinal, Data);
-		}
-
-		S(CSILt ... CSIGt) {
-			A(' ' ... '/', csiInter, CSIInter);
-			A('0' ... '9', csiDigit, state);
-			A(':', nop, state);
-			A(';', csiSep, state);
-			D(csiFinal, Data);
-		}
-
-		S(CSIQm) {
-			A(' ' ... '/', csiInter, CSIInter);
-			A('0' ... '9', csiDigit, CSIQm);
-			A(':', nop, CSIQm);
-			A(';', csiSep, CSIQm);
-			A('h', decset, Data);
-			A('l', decrst, Data);
-			D(csiFinal, Data);
-		}
-
-		S(CSIInter) {
-			D(csiFinal, Data);
-		}
-
-		S(OSC) {
-			A(BEL, nop, Data);
-			A(ESC, nop, OSCEsc);
-			D(nop, OSC);
-		}
-		S(OSCEsc) {
-			A('\\', nop, Data);
-			D(nop, OSC);
-		}
-	}
-}
-
-static bool bright;
-static int defaultBg = 0;
-static int defaultFg = 7;
-
-static void span(const struct Style *prev, const struct Cell *cell) {
-	struct Style style = cell->style;
-	if (!prev || memcmp(prev, &style, sizeof(*prev))) {
-		if (prev) printf("</span>");
-		if (style.bg < 0) style.bg = defaultBg;
-		if (style.fg < 0) style.fg = defaultFg;
-		if (bright && style.attr & Bold) {
-			if (style.fg < 8) style.fg += 8;
-			style.attr ^= Bold;
-		}
-		printf(
-			"<span style=\"%s%s%s\" class=\"bg%u fg%u\">",
-			(style.attr & Bold ? "font-weight:bold;" : ""),
-			(style.attr & Italic ? "font-style:italic;" : ""),
-			(style.attr & Underline ? "text-decoration:underline;" : ""),
-			(style.attr & Reverse ? style.fg : style.bg),
-			(style.attr & Reverse ? style.bg : style.fg)
-		);
-	}
-	switch (cell->ch) {
-		break; case '&': printf("&amp;");
-		break; case '<': printf("&lt;");
-		break; case '>': printf("&gt;");
-		break; default:  printf("%lc", (wint_t)cell->ch);
-	}
-}
-
-static bool mediaCopy;
-static void html(void) {
-	mediaCopy = true;
-	if (mode & Cursor) cell(y, x)->style.attr ^= Reverse;
-	printf(
-		"<pre style=\"width: %uch;\" class=\"bg%u fg%u\">",
-		cols, defaultBg, defaultFg
-	);
-	for (uint y = 0; y < rows; ++y) {
-		for (uint x = 0; x < cols; ++x) {
-			if (!cell(y, x)->ch) continue;
-			span(x ? &cell(y, x - 1)->style : NULL, cell(y, x));
-		}
-		printf("</span>\n");
-	}
-	printf("</pre>\n");
-	if (mode & Cursor) cell(y, x)->style.attr ^= Reverse;
-}
-
-int main(int argc, char *argv[]) {
-	setlocale(LC_CTYPE, "");
-
-	bool debug = false;
-	bool size = false;
-	bool hide = false;
-
-	int opt;
-	while (0 < (opt = getopt(argc, argv, "Bb:df:h:nsw:"))) {
-		switch (opt) {
-			break; case 'B': bright = true;
-			break; case 'b': defaultBg = strtol(optarg, NULL, 0);
-			break; case 'd': debug = true;
-			break; case 'f': defaultFg = strtol(optarg, NULL, 0);
-			break; case 'h': rows = strtoul(optarg, NULL, 0);
-			break; case 'n': hide = true;
-			break; case 's': size = true;
-			break; case 'w': cols = strtoul(optarg, NULL, 0);
-			break; default:  return EX_USAGE;
-		}
-	}
-
-	FILE *file = stdin;
-	if (optind < argc) {
-		file = fopen(argv[optind], "r");
-		if (!file) err(EX_NOINPUT, "%s", argv[optind]);
-	}
-
-	if (size) {
-		struct winsize window;
-		int error = ioctl(STDERR_FILENO, TIOCGWINSZ, &window);
-		if (error) err(EX_IOERR, "ioctl");
-		rows = window.ws_row;
-		cols = window.ws_col;
-	}
-	scroll.bot = rows;
-
-	cells = calloc(rows * cols, sizeof(*cells));
-	if (!cells) err(EX_OSERR, "calloc");
-	erase(cell(0, 0), cell(rows - 1, cols));
-
-	wint_t ch;
-	while (WEOF != (ch = getwc(file))) {
-		uint prev = state;
-		update(ch);
-		if (debug && state != prev && state == Data) html();
-	}
-	if (ferror(file)) err(EX_IOERR, "getwc");
-
-	if (!mediaCopy) {
-		if (hide) mode &= ~Cursor;
-		html();
-	}
-}
diff --git a/bin/shotty.l b/bin/shotty.l
new file mode 100644
index 00000000..b8c04f52
--- /dev/null
+++ b/bin/shotty.l
@@ -0,0 +1,554 @@
+/* Copyright (C) 2019, 2021  June McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+%option noyywrap
+
+%{
+
+#include <assert.h>
+#include <err.h>
+#include <locale.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sysexits.h>
+#include <unistd.h>
+#include <wchar.h>
+
+#define Q(...) #__VA_ARGS__
+#define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit
+
+#define ENUM_CODE \
+	X(BS) \
+	X(CHA) \
+	X(CNL) \
+	X(CPL) \
+	X(CR) \
+	X(CUB) \
+	X(CUD) \
+	X(CUF) \
+	X(CUP) \
+	X(CUU) \
+	X(DCH) \
+	X(DECKPAM) \
+	X(DECKPNM) \
+	X(DECRC) \
+	X(DECRST) \
+	X(DECSC) \
+	X(DECSET) \
+	X(DECSTBM) \
+	X(DL) \
+	X(ECH) \
+	X(ED) \
+	X(EL) \
+	X(HT) \
+	X(ICH) \
+	X(IL) \
+	X(MC) \
+	X(NL) \
+	X(RI) \
+	X(RM) \
+	X(SD) \
+	X(SGR) \
+	X(SM) \
+	X(SU) \
+	X(VPA)
+
+enum Code {
+	Data = 1,
+#define X(code) code,
+	ENUM_CODE
+#undef X
+};
+
+static enum {
+	USASCII,
+	DECSpecial,
+} g0;
+
+static const wchar_t AltCharset[128] = {
+	['`'] = L'\u25C6', ['a'] = L'\u2592', ['f'] = L'\u00B0', ['g'] = L'\u00B1',
+	['i'] = L'\u240B', ['j'] = L'\u2518', ['k'] = L'\u2510', ['l'] = L'\u250C',
+	['m'] = L'\u2514', ['n'] = L'\u253C', ['o'] = L'\u23BA', ['p'] = L'\u23BB',
+	['q'] = L'\u2500', ['r'] = L'\u23BC', ['s'] = L'\u23BD', ['t'] = L'\u251C',
+	['u'] = L'\u2524', ['v'] = L'\u2534', ['w'] = L'\u252C', ['x'] = L'\u2502',
+	['y'] = L'\u2264', ['z'] = L'\u2265', ['{'] = L'\u03C0', ['|'] = L'\u2260',
+	['}'] = L'\u00A3', ['~'] = L'\u00B7',
+};
+
+static int pn;
+static int ps[16];
+static wchar_t ch;
+
+%}
+
+ESC \x1B
+
+%x CSI CSI_LT CSI_EQ CSI_GT CSI_QM
+%x OSC
+
+%%
+	(void)input;
+	(void)yyunput;
+	pn = 0;
+
+{ESC}"["	BEGIN(CSI);
+{ESC}"[<"	BEGIN(CSI_LT);
+{ESC}"[="	BEGIN(CSI_EQ);
+{ESC}"[>"	BEGIN(CSI_GT);
+{ESC}"[?"	BEGIN(CSI_QM);
+{ESC}"]"	BEGIN(OSC);
+
+<CSI,CSI_LT,CSI_EQ,CSI_GT,CSI_QM>{
+	[0-9]+;?	if (pn < 16) ps[pn++] = atoi(yytext);
+	;			if (pn < 16) ps[pn++] = 0;
+}
+
+<OSC>{
+	\x07	BEGIN(0);
+	{ESC}\\	BEGIN(0);
+	.|\n	;
+}
+
+\b	return BS;
+\t	return HT;
+\n	return NL;
+\r	return CR;
+
+{ESC}7	return DECSC;
+{ESC}8	return DECRC;
+{ESC}=	return DECKPAM;
+{ESC}>	return DECKPNM;
+{ESC}M	return RI;
+
+{ESC}"(0"	g0 = DECSpecial;
+{ESC}"(B"	g0 = USASCII;
+
+<CSI>@	BEGIN(0); return ICH;
+<CSI>A	BEGIN(0); return CUU;
+<CSI>B	BEGIN(0); return CUD;
+<CSI>C	BEGIN(0); return CUF;
+<CSI>D	BEGIN(0); return CUB;
+<CSI>E	BEGIN(0); return CNL;
+<CSI>F	BEGIN(0); return CPL;
+<CSI>G	BEGIN(0); return CHA;
+<CSI>H	BEGIN(0); return CUP;
+<CSI>J	BEGIN(0); return ED;
+<CSI>K	BEGIN(0); return EL;
+<CSI>L	BEGIN(0); return IL;
+<CSI>M	BEGIN(0); return DL;
+<CSI>P	BEGIN(0); return DCH;
+<CSI>S	BEGIN(0); return SU;
+<CSI>T	BEGIN(0); return SD;
+<CSI>X	BEGIN(0); return ECH;
+<CSI>d	BEGIN(0); return VPA;
+<CSI>h	BEGIN(0); return SM;
+<CSI>i	BEGIN(0); return MC;
+<CSI>l	BEGIN(0); return RM;
+<CSI>m	BEGIN(0); return SGR;
+<CSI>r	BEGIN(0); return DECSTBM;
+
+<CSI_QM>h	BEGIN(0); return DECSET;
+<CSI_QM>l	BEGIN(0); return DECRST;
+
+<CSI>[ -/]*.	BEGIN(0); warnx("unhandled CSI %s", yytext);
+<CSI_LT>[ -/]*.	BEGIN(0); warnx("unhandled CSI < %s", yytext);
+<CSI_EQ>[ -/]*.	BEGIN(0); warnx("unhandled CSI = %s", yytext);
+<CSI_GT>[ -/]*.	BEGIN(0); warnx("unhandled CSI > %s", yytext);
+<CSI_QM>[ -/]*.	BEGIN(0); warnx("unhandled CSI ? %s", yytext);
+
+[\x00-\x7F] {
+	ch = yytext[0];
+	if (g0 == DECSpecial && AltCharset[ch]) {
+		ch = AltCharset[ch];
+	}
+	return Data;
+}
+[\xC0-\xDF][\x80-\xBF] {
+	ch = (wchar_t)(yytext[0] & 0x1F) << 6
+		| (wchar_t)(yytext[1] & 0x3F);
+	return Data;
+}
+[\xE0-\xEF][\x80-\xBF]{2} {
+	ch = (wchar_t)(yytext[0] & 0x0F) << 12
+		| (wchar_t)(yytext[1] & 0x3F) << 6
+		| (wchar_t)(yytext[2] & 0x3F);
+	return Data;
+}
+[\xF0-\xF7][\x80-\xBF]{3} {
+	ch = (wchar_t)(yytext[0] & 0x07) << 18
+		| (wchar_t)(yytext[1] & 0x3F) << 12
+		| (wchar_t)(yytext[2] & 0x3F) << 6
+		| (wchar_t)(yytext[3] & 0x3F);
+	return Data;
+}
+
+.	ch = yytext[0]; return Data;
+
+%%
+
+static int rows = 24;
+static int cols = 80;
+
+static struct Cell {
+	enum {
+		BIT(Bold),
+		BIT(Italic),
+		BIT(Underline),
+		BIT(Reverse),
+	} attr;
+	int bg, fg;
+	wchar_t ch;
+} *cells;
+
+static int y, x;
+static struct {
+	int y, x;
+} sc;
+static struct {
+	int top, bot;
+} scr;
+
+static enum Mode {
+	BIT(Insert),
+	BIT(Wrap),
+	BIT(Cursor),
+} mode = Wrap | Cursor;
+
+static struct Cell sgr = {
+	.bg = -1,
+	.fg = -1,
+	.ch = L' ',
+};
+
+static struct Cell *cell(int y, int x) {
+	assert(y <= rows);
+	assert(x <= cols);
+	assert(y * cols + x <= rows * cols);
+	return &cells[y * cols + x];
+}
+
+static int p(int i, int d) {
+	return (i < pn ? ps[i] : d);
+}
+
+static int bound(int a, int x, int b) {
+	if (x < a) return a;
+	if (x > b) return b;
+	return x;
+}
+
+static void move(struct Cell *dst, struct Cell *src, size_t len) {
+	memmove(dst, src, sizeof(*dst) * len);
+}
+static void erase(struct Cell *at, struct Cell *to) {
+	for (; at < to; ++at) {
+		*at = sgr;
+	}
+}
+
+static void scrup(int top, int n) {
+	n = bound(0, n, scr.bot - top);
+	move(cell(top, 0), cell(top+n, 0), cols * (scr.bot-top-n));
+	erase(cell(scr.bot-n, 0), cell(scr.bot, 0));
+}
+static void scrdn(int top, int n) {
+	n = bound(0, n, scr.bot - top);
+	move(cell(top+n, 0), cell(top, 0), cols * (scr.bot-top-n));
+	erase(cell(top, 0), cell(top+n, 0));
+}
+
+static enum Mode pmode(void) {
+	enum Mode mode = 0;
+	for (int i = 0; i < pn; ++i) {
+		switch (ps[i]) {
+			break; case 4: mode |= Insert;
+			break; default: warnx("unhandled SM/RM %d", ps[i]);
+		}
+	}
+	return mode;
+}
+static enum Mode pdmode(void) {
+	enum Mode mode = 0;
+	for (int i = 0; i < pn; ++i) {
+		switch (ps[i]) {
+			break; case 1: // DECCKM
+			break; case 7: mode |= Wrap;
+			break; case 12: // "Start Blinking Cursor"
+			break; case 25: mode |= Cursor;
+			break; default: {
+				if (ps[i] < 1000) warnx("unhandled DECSET/DECRST %d", ps[i]);
+			}
+		}
+	}
+	return mode;
+}
+
+static void update(enum Code cc) {
+	switch (cc) {
+		break; case BS: x--;
+		break; case HT: x = x - x % 8 + 8;
+		break; case CR: x = 0;
+		break; case CUU: y -= p(0, 1);
+		break; case CUD: y += p(0, 1);
+		break; case CUF: x += p(0, 1);
+		break; case CUB: x -= p(0, 1);
+		break; case CNL: x = 0; y += p(0, 1);
+		break; case CPL: x = 0; y -= p(0, 1);
+		break; case CHA: x = p(0, 1) - 1;
+		break; case VPA: y = p(0, 1) - 1;
+		break; case CUP: y = p(0, 1) - 1; x = p(1, 1) - 1;
+		break; case DECSC: sc.y = y; sc.x = x;
+		break; case DECRC: y = sc.y; x = sc.x;
+
+		break; case ED: erase(
+			(p(0, 0) == 0 ? cell(y, x) : cell(0, 0)),
+			(p(0, 0) == 1 ? cell(y, x) : cell(rows-1, cols))
+		);
+		break; case EL: erase(
+			(p(0, 0) == 0 ? cell(y, x) : cell(y, 0)),
+			(p(0, 0) == 1 ? cell(y, x) : cell(y, cols))
+		);
+		break; case ECH: erase(
+			cell(y, x), cell(y, bound(0, x + p(0, 1), cols))
+		);
+
+		break; case DCH: {
+			int n = bound(0, p(0, 1), cols-x);
+			move(cell(y, x), cell(y, x+n), cols-x-n);
+			erase(cell(y, cols-n), cell(y, cols));
+		}
+		break; case ICH: {
+			int n = bound(0, p(0, 1), cols-x);
+			move(cell(y, x+n), cell(y, x), cols-x-n);
+			erase(cell(y, x), cell(y, x+n));
+		}
+
+		break; case DECSTBM: {
+			scr.bot = bound(0, p(1, rows), rows);
+			scr.top = bound(0, p(0, 1) - 1, scr.bot);
+		}
+		break; case SU: scrup(scr.top, p(0, 1));
+		break; case SD: scrdn(scr.top, p(0, 1));
+		break; case DL: scrup(bound(0, y, scr.bot), p(0, 1));
+		break; case IL: scrdn(bound(0, y, scr.bot), p(0, 1));
+
+		break; case NL: {
+			if (y+1 == scr.bot) {
+				scrup(scr.top, 1);
+			} else {
+				y++;
+			}
+		}
+		break; case RI: {
+			if (y == scr.top) {
+				scrdn(scr.top, 1);
+			} else {
+				y--;
+			}
+		}
+
+		break; case SM: mode |= pmode();
+		break; case RM: mode &= ~pmode();
+		break; case DECSET: mode |= pdmode();
+		break; case DECRST: mode &= ~pdmode();
+
+		break; case SGR: {
+			if (!pn) ps[pn++] = 0;
+			for (int i = 0; i < pn; ++i) {
+				switch (ps[i]) {
+					break; case 0: sgr.attr = 0; sgr.bg = -1; sgr.fg = -1;
+					break; case 1: sgr.attr |= Bold;
+					break; case 3: sgr.attr |= Italic;
+					break; case 4: sgr.attr |= Underline;
+					break; case 7: sgr.attr |= Reverse;
+					break; case 22: sgr.attr &= ~Bold;
+					break; case 23: sgr.attr &= ~Italic;
+					break; case 24: sgr.attr &= ~Underline;
+					break; case 27: sgr.attr &= ~Reverse;
+					break; case 30 ... 37: sgr.fg = ps[i] - 30;
+					break; case 38: {
+						if (++i < pn && ps[i] == 5) {
+							if (++i < pn) sgr.fg = ps[i];
+						}
+					}
+					break; case 39: sgr.fg = -1;
+					break; case 40 ... 47: sgr.bg = ps[i] - 40;
+					break; case 48: {
+						if (++i < pn && ps[i] == 5) {
+							if (++i < pn) sgr.bg = ps[i];
+						}
+					}
+					break; case 49: sgr.bg = -1;
+					break; case 90 ... 97: sgr.fg = 8 + ps[i] - 90;
+					break; case 100 ... 107: sgr.bg = 8 + ps[i] - 100;
+					break; default: warnx("unhandled SGR %d", ps[i]);
+				}
+			}
+		}
+
+		break; case Data: {
+			int w = wcwidth(ch);
+			if (w < 0) {
+				warnx("unhandled \\u%04X", ch);
+				return;
+			}
+			if (mode & Insert) {
+				int n = bound(0, w, cols-x);
+				move(cell(y, x+n), cell(y, x), cols-x-n);
+			}
+			if (mode & Wrap && x+w > cols) {
+				update(CR);
+				update(NL);
+			}
+			*cell(y, x) = sgr;
+			cell(y, x)->ch = ch;
+			for (int i = 1; i < w && x+i < cols; ++i) {
+				*cell(y, x+i) = sgr;
+				cell(y, x+i)->ch = L'\0';
+			}
+			x = bound(0, x+w, (mode & Wrap ? cols : cols-1));
+			return;
+		}
+
+		break; case MC:;
+		break; case DECKPAM:;
+		break; case DECKPNM:;
+	}
+
+	x = bound(0, x, cols-1);
+	y = bound(0, y, rows-1);
+}
+
+static bool bright;
+static int defaultBg = 0;
+static int defaultFg = 7;
+
+static void span(const struct Cell *prev, const struct Cell *cell) {
+	if (
+		!prev ||
+		cell->attr != prev->attr ||
+		cell->bg != prev->bg ||
+		cell->fg != prev->fg
+	) {
+		if (prev) printf("</span>");
+		int attr = cell->attr;
+		int bg = (cell->bg < 0 ? defaultBg : cell->bg);
+		int fg = (cell->fg < 0 ? defaultFg : cell->fg);
+		if (bright && cell->attr & Bold) {
+			if (fg < 8) fg += 8;
+			attr &= ~Bold;
+		}
+		printf(
+			Q(<span style="%s%s%s" class="bg%d fg%d">),
+			(attr & Bold ? "font-weight:bold;" : ""),
+			(attr & Italic ? "font-style:italic;" : ""),
+			(attr & Underline ? "text-decoration:underline;" : ""),
+			(attr & Reverse ? fg : bg), (attr & Reverse ? bg : fg)
+		);
+	}
+	switch (cell->ch) {
+		break; case L'&': printf("&amp;");
+		break; case L'<': printf("&lt;");
+		break; case L'>': printf("&gt;");
+		break; case L'"': printf("&quot;");
+		break; default: printf("%lc", (wint_t)cell->ch);
+	}
+}
+
+static void html(void) {
+	if (mode & Cursor) cell(y, x)->attr ^= Reverse;
+	printf(
+		Q(<pre style="width: %dch;" class="bg%d fg%d">),
+		cols, defaultBg, defaultFg
+	);
+	for (int y = 0; y < rows; ++y) {
+		for (int x = 0; x < cols; ++x) {
+			if (!cell(y, x)->ch) continue;
+			span((x ? cell(y, x-1) : NULL), cell(y, x));
+		}
+		printf("</span>\n");
+	}
+	printf("</pre>\n");
+	if (mode & Cursor) cell(y, x)->attr ^= Reverse;
+}
+
+static const char *Debug[] = {
+#define X(code) [code] = #code,
+	ENUM_CODE
+#undef X
+};
+
+int main(int argc, char *argv[]) {
+	setlocale(LC_CTYPE, "");
+
+	bool debug = false;
+	bool size = false;
+	bool hide = false;
+
+	for (int opt; 0 < (opt = getopt(argc, argv, "Bb:df:h:nsw:"));) {
+		switch (opt) {
+			break; case 'B': bright = true;
+			break; case 'b': defaultBg = atoi(optarg);
+			break; case 'd': debug = true;
+			break; case 'f': defaultFg = atoi(optarg);
+			break; case 'h': rows = atoi(optarg);
+			break; case 'n': hide = true;
+			break; case 's': size = true;
+			break; case 'w': cols = atoi(optarg);
+			break; default:  return EX_USAGE;
+		}
+	}
+	if (optind < argc) {
+		yyin = fopen(argv[optind], "r");
+		if (!yyin) err(EX_NOINPUT, "%s", argv[optind]);
+	}
+
+	if (size) {
+		struct winsize win;
+		int error = ioctl(STDERR_FILENO, TIOCGWINSZ, &win);
+		if (error) err(EX_IOERR, "ioctl");
+		cols = win.ws_col;
+		rows = win.ws_row;
+	}
+	scr.bot = rows;
+
+	cells = calloc(cols * rows, sizeof(*cells));
+	if (!cells) err(EX_OSERR, "calloc");
+	erase(cell(0, 0), cell(rows-1, cols));
+
+	bool mc = false;
+	for (int cc; (cc = yylex());) {
+		if (cc == MC) {
+			mc = true;
+			html();
+		} else {
+			update(cc);
+		}
+		if (debug && cc != Data) {
+			printf("%s", Debug[cc]);
+			for (int i = 0; i < pn; ++i) {
+				printf("%s%d", (i ? ", " : " "), ps[i]);
+			}
+			printf("\n");
+			html();
+		}
+	}
+	if (hide) mode &= ~Cursor;
+	if (!mc) html();
+}