about summary refs log tree commit diff homepage
diff options
context:
space:
mode:
authorJune McEnroe <june@causal.agency>2018-08-27 17:23:33 -0400
committerJune McEnroe <june@causal.agency>2018-08-27 17:23:33 -0400
commit0da835b5d6e38df0dc19c9d35e1f255b4e620216 (patch)
treedca2ea4320a8ad2bb33cf8c47139fb07f0bd3ef7
parentFix color pairs once and for all (diff)
parentDump HELP_DATA with client -h (diff)
downloadtorus-0da835b5d6e38df0dc19c9d35e1f255b4e620216.tar.gz
torus-0da835b5d6e38df0dc19c9d35e1f255b4e620216.zip
Merge branch 'ansi'
Diffstat (limited to '')
-rw-r--r--.gitignore2
-rw-r--r--Makefile49
-rw-r--r--client.c888
-rw-r--r--help.c187
-rw-r--r--help.h376
-rw-r--r--index.html12
-rw-r--r--merge.c72
-rw-r--r--meta.c10
-rw-r--r--server.c111
-rw-r--r--torus.h87
10 files changed, 1113 insertions, 681 deletions
diff --git a/.gitignore b/.gitignore
index 2f400d8..230472d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
+*.o
 chroot.tar
 client
-help
 merge
 meta
 root
diff --git a/Makefile b/Makefile
index 8df5425..9199a7b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,31 @@
-USER = torus
-BINS = server client help meta merge
+CHROOT_USER = torus
+CHROOT_GROUP = $(CHROOT_USER)
+
 CFLAGS += -Wall -Wextra -Wpedantic
-LDLIBS = -lm -lcurses
+LDLIBS = -lcursesw
+BINS = server client meta merge
+OBJS = $(BINS:%=%.o)
 
 all: tags $(BINS)
 
-$(BINS): torus.h
+.o:
+	$(CC) $(LDFLAGS) $< $(LDLIBS) -o $@
+
+$(OBJS): torus.h
+
+client.o: help.h
 
-# Only necessary so GNU make doesn't try to use torus.h as a source.
-.c:
-	$(CC) $(CFLAGS) $(LDFLAGS) $< $(LDLIBS) -o $@
+help.h:
+	head -c 4096 torus.dat \
+		| file2c -s -x 'static const uint8_t HELP_DATA[] = {' '};' \
+		> help.h
+	echo 'static const struct Tile *HELP = (const struct Tile *)HELP_DATA;' \
+		>> help.h
 
-chroot.tar: server client help
+tags: *.h *.c
+	ctags -w *.h *.c
+
+chroot.tar: server client
 	mkdir -p root
 	install -d -o root -g wheel \
 	    root/bin \
@@ -21,22 +35,19 @@ chroot.tar: server client help
 	    root/usr \
 	    root/usr/share \
 	    root/usr/share/misc
-	install -d -o $(USER) -g $(USER) root/home/$(USER)
-	install -o root -g wheel -m 555 /libexec/ld-elf.so.1 root/libexec
-	install -o root -g wheel -m 444 \
+	install -d -o $(CHROOT_USER) -g $(CHROOT_GROUP) root/home/$(CHROOT_USER)
+	cp -p -f /libexec/ld-elf.so.1 root/libexec
+	cp -p -f \
 	    /lib/libc.so.7 \
-		/lib/libm.so.5 \
 	    /lib/libedit.so.7 \
 	    /lib/libncurses.so.8 \
 	    /lib/libncursesw.so.8 \
 	    root/lib
-	install -o root -g wheel -m 444 /usr/share/misc/termcap.db root/usr/share/misc
-	install -o root -g wheel -m 555 /bin/sh root/bin
-	install -o root -g wheel -m 555 server client help root/bin
+	cp -a -f /usr/share/locale root/usr/share
+	cp -p -f /usr/share/misc/termcap.db root/usr/share/misc
+	cp -p -f /bin/sh root/bin
+	install -o root -g wheel -m 555 server client root/bin
 	tar -c -f chroot.tar -C root bin home lib libexec usr
 
-tags: *.h *.c
-	ctags -w *.h *.c
-
 clean:
-	rm -f tags $(BINS) chroot.tar
+	rm -f tags $(OBJS) $(BINS) chroot.tar
diff --git a/client.c b/client.c
index 7af6950..66e6d08 100644
--- a/client.c
+++ b/client.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2017  June McEnroe <june@causal.agency>
+/* Copyright (C) 2018  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
@@ -14,12 +14,15 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <ctype.h>
+#define _XOPEN_SOURCE_EXTENDED
+
+#include <assert.h>
 #include <curses.h>
 #include <err.h>
 #include <errno.h>
-#include <math.h>
+#include <locale.h>
 #include <poll.h>
+#include <stdbool.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -27,16 +30,215 @@
 #include <sys/un.h>
 #include <sysexits.h>
 #include <unistd.h>
+#include <wchar.h>
 
 #include "torus.h"
+#include "help.h"
+
+#define err(...) do { endwin(); err(__VA_ARGS__); } while(0)
+#define errx(...) do { endwin(); errx(__VA_ARGS__); } while (0)
 
+#define DIV_ROUND(a, b) (((a) + (b) / 2) / (b))
+
+#define CTRL(ch) ((ch) ^ 0x40)
 enum {
 	ESC = 0x1B,
 	DEL = 0x7F,
 };
 
+static uint32_t log2(uint32_t n) {
+	assert(n > 0);
+	return 32 - __builtin_clz(n) - 1;
+}
+
+static void curse(void) {
+	setlocale(LC_CTYPE, "");
+
+	initscr();
+	start_color();
+	if (!has_colors() || COLOR_PAIRS < 64) {
+		endwin();
+		fprintf(stderr, "Sorry, your terminal doesn't support colors!\n");
+		fprintf(stderr, "If you think it should, check TERM.\n");
+		exit(EX_CONFIG);
+	}
+	if (LINES < CELL_ROWS || COLS < CELL_COLS) {
+		endwin();
+		fprintf(
+			stderr,
+			"Sorry, your terminal is too small!\n"
+			"It must be at least %ux%u characters.\n",
+			CELL_COLS, CELL_ROWS
+		);
+		exit(EX_CONFIG);
+	}
+
+	assume_default_colors(0, 0);
+	if (COLORS >= 16) {
+		for (short pair = 1; pair < 0x80; ++pair) {
+			init_pair(pair, pair & 0x0F, (pair & 0xF0) >> 4);
+		}
+	} else {
+		for (short pair = 1; pair < 0100; ++pair) {
+			init_pair(pair, pair & 007, (pair & 070) >> 3);
+		}
+	}
+
+	color_set(COLOR_WHITE, NULL);
+	bool hline = (LINES > CELL_ROWS);
+	bool vline = (COLS > CELL_COLS);
+	if (hline) mvhline(CELL_ROWS, 0, 0, CELL_COLS);
+	if (vline) mvvline(0, CELL_COLS, 0, CELL_ROWS);
+	if (hline && vline) mvaddch(CELL_ROWS, CELL_COLS, ACS_LRCORNER);
+	color_set(0, NULL);
+
+	cbreak();
+	noecho();
+	keypad(stdscr, true);
+	set_escdelay(100);
+}
+
+static attr_t colorAttr(uint8_t color) {
+	if (COLORS >= 16) return A_NORMAL;
+	return (color & COLOR_BRIGHT) ? A_BOLD : A_NORMAL;
+}
+static short colorPair(uint8_t color) {
+	if (COLORS >= 16) return color;
+	return (color & 0x70) >> 1 | (color & 0x07);
+}
+
+static void drawCell(
+	const struct Tile *tile, uint8_t cellX, uint8_t cellY, attr_t attr
+) {
+	uint8_t color = tile->colors[cellY][cellX];
+	uint8_t cell = tile->cells[cellY][cellX];
+
+	cchar_t cch;
+	wchar_t wch[] = { CP437[cell], L'\0' };
+	setcchar(&cch, wch, attr | colorAttr(color), colorPair(color), NULL);
+	mvadd_wch(cellY, cellX, &cch);
+}
+
+static void drawTile(const struct Tile *tile) {
+	for (uint8_t cellY = 0; cellY < CELL_ROWS; ++cellY) {
+		for (uint8_t cellX = 0; cellX < CELL_COLS; ++cellX) {
+			drawCell(tile, cellX, cellY, A_NORMAL);
+		}
+	}
+}
+
 static int client;
 
+static uint8_t cellX;
+static uint8_t cellY;
+static struct Tile tile;
+
+static void serverTile(void) {
+	ssize_t size = recv(client, &tile, sizeof(tile), 0);
+	if (size < 0) err(EX_IOERR, "recv");
+	if ((size_t)size < sizeof(tile)) errx(EX_PROTOCOL, "truncated tile");
+	drawTile(&tile);
+}
+
+static void serverMove(struct ServerMessage msg) {
+	cellX = msg.move.cellX;
+	cellY = msg.move.cellY;
+}
+
+static void serverPut(struct ServerMessage msg) {
+	tile.colors[msg.put.cellY][msg.put.cellX] = msg.put.color;
+	tile.cells[msg.put.cellY][msg.put.cellX] = msg.put.cell;
+	drawCell(&tile, msg.put.cellX, msg.put.cellY, A_NORMAL);
+}
+
+static void serverCursor(struct ServerMessage msg) {
+	if (msg.cursor.oldCellX != CURSOR_NONE) {
+		drawCell(&tile, msg.cursor.oldCellX, msg.cursor.oldCellY, A_NORMAL);
+	}
+	if (msg.cursor.newCellX != CURSOR_NONE) {
+		drawCell(&tile, msg.cursor.newCellX, msg.cursor.newCellY, A_REVERSE);
+	}
+}
+
+static const uint8_t MAP_X = (CELL_COLS / 2) - (3 * MAP_COLS / 2);
+static const uint8_t MAP_Y = (CELL_ROWS / 2) - (MAP_ROWS / 2);
+
+static const wchar_t MAP_CELLS[5] = L" ░▒▓█";
+static const uint8_t MAP_COLORS[] = {
+	COLOR_BLUE, COLOR_CYAN, COLOR_GREEN, COLOR_YELLOW, COLOR_RED,
+};
+
+static void serverMap(void) {
+	int t = MAP_Y - 1;
+	int l = MAP_X - 1;
+	int b = MAP_Y + MAP_ROWS;
+	int r = MAP_X + 3 * MAP_COLS;
+	color_set(colorPair(COLOR_WHITE), NULL);
+	mvhline(t, MAP_X, ACS_HLINE, 3 * MAP_COLS);
+	mvhline(b, MAP_X, ACS_HLINE, 3 * MAP_COLS);
+	mvvline(MAP_Y, l, ACS_VLINE, MAP_ROWS);
+	mvvline(MAP_Y, r, ACS_VLINE, MAP_ROWS);
+	mvaddch(t, l, ACS_ULCORNER);
+	mvaddch(t, r, ACS_URCORNER);
+	mvaddch(b, l, ACS_LLCORNER);
+	mvaddch(b, r, ACS_LRCORNER);
+	color_set(0, NULL);
+
+	struct Map map;
+	ssize_t size = recv(client, &map, sizeof(map), 0);
+	if (size < 0) err(EX_IOERR, "recv");
+	if ((size_t)size < sizeof(map)) errx(EX_PROTOCOL, "truncated map");
+
+	if (0 == map.max.modifyCount) return;
+	if (0 == map.now - map.min.createTime) return;
+
+	for (uint8_t y = 0; y < MAP_ROWS; ++y) {
+		for (uint8_t x = 0; x < MAP_COLS; ++x) {
+			struct Meta meta = map.meta[y][x];
+
+			uint32_t count = 0;
+			if (meta.modifyCount && log2(map.max.modifyCount)) {
+				count = DIV_ROUND(
+					(ARRAY_LEN(MAP_CELLS) - 1) * log2(meta.modifyCount),
+					log2(map.max.modifyCount)
+				);
+			}
+			uint32_t time = 0;
+			if (meta.modifyTime) {
+				uint32_t modify = meta.modifyTime - map.min.createTime;
+				time = DIV_ROUND(
+					(ARRAY_LEN(MAP_COLORS) - 1) * modify,
+					map.now - map.min.createTime
+				);
+			}
+
+			wchar_t cell = MAP_CELLS[count];
+			uint8_t color = MAP_COLORS[time];
+			wchar_t tile[] = { cell, cell, cell, L'\0' };
+			attr_set(colorAttr(color), colorPair(color), NULL);
+			mvaddwstr(MAP_Y + y, MAP_X + 3 * x, tile);
+		}
+	}
+	attr_set(A_NORMAL, 0, NULL);
+}
+
+static void readMessage(void) {
+	struct ServerMessage msg;
+	ssize_t size = recv(client, &msg, sizeof(msg), 0);
+	if (size < 0) err(EX_IOERR, "recv");
+	if ((size_t)size < sizeof(msg)) errx(EX_PROTOCOL, "truncated message");
+
+	switch (msg.type) {
+		break; case SERVER_TILE:   serverTile();
+		break; case SERVER_MOVE:   serverMove(msg);
+		break; case SERVER_PUT:    serverPut(msg);
+		break; case SERVER_CURSOR: serverCursor(msg);
+		break; case SERVER_MAP:    serverMap();
+		break; default: errx(EX_PROTOCOL, "unknown message type %d", msg.type);
+	}
+	move(cellY, cellX);
+}
+
 static void clientMessage(struct ClientMessage msg) {
 	ssize_t size = send(client, &msg, sizeof(msg), 0);
 	if (size < 0) err(EX_IOERR, "send");
@@ -50,18 +252,15 @@ static void clientMove(int8_t dx, int8_t dy) {
 	clientMessage(msg);
 }
 
-static void clientPut(uint8_t color, char cell) {
-	struct ClientMessage msg = {
-		.type = CLIENT_PUT,
-		.put = { .color = color, .cell = cell },
-	};
+static void clientFlip(void) {
+	struct ClientMessage msg = { .type = CLIENT_FLIP };
 	clientMessage(msg);
 }
 
-static void clientSpawn(uint8_t spawn) {
+static void clientPut(uint8_t color, uint8_t cell) {
 	struct ClientMessage msg = {
-		.type = CLIENT_SPAWN,
-		.spawn = spawn,
+		.type = CLIENT_PUT,
+		.put = { .color = color, .cell = cell },
 	};
 	clientMessage(msg);
 }
@@ -71,141 +270,144 @@ static void clientMap(void) {
 	clientMessage(msg);
 }
 
-static void colorPairs(void) {
-	assume_default_colors(0, 0);
-	if (COLORS >= 16) {
-		for (short pair = 1; pair < 0x80; ++pair) {
-			init_pair(pair, pair & 0x0F, (pair & 0xF0) >> 4);
-		}
-	} else {
-		for (short pair = 1; pair < 0100; ++pair) {
-			init_pair(pair, pair & 007, (pair & 070) >> 3);
-		}
-	}
-}
-
-static chtype colorAttr(uint8_t color) {
-	if (COLORS >= 16) return COLOR_PAIR(color);
-	chtype bold = (color & COLOR_BRIGHT) ? A_BOLD : A_NORMAL;
-	short pair = (color & 0x70) >> 1 | (color & 0x07);
-	return bold | COLOR_PAIR(pair);
-}
-
-static uint8_t attrColor(chtype attr) {
-	if (COLORS >= 16) return PAIR_NUMBER(attr);
-	uint8_t bright = (attr & A_BOLD) ? COLOR_BRIGHT : 0;
-	short pair = PAIR_NUMBER(attr);
-	return (pair & 070) << 1 | bright | (pair & 007);
-}
-
 static struct {
-	int8_t speed;
-	uint8_t color;
 	enum {
 		MODE_NORMAL,
+		MODE_HELP,
 		MODE_MAP,
+		MODE_DIRECTION,
 		MODE_INSERT,
 		MODE_REPLACE,
-		MODE_PUT,
 		MODE_DRAW,
+		MODE_LINE,
 	} mode;
-	int8_t dx;
-	int8_t dy;
-	uint8_t len;
-	char draw;
+	uint8_t color;
+	uint8_t shift;
+	uint8_t draw;
 } input = {
-	.speed = 1,
 	.color = COLOR_WHITE,
-	.dx = 1,
 };
 
-static void colorFg(uint8_t fg) {
-	input.color = (input.color & 0xF8) | fg;
+static struct {
+	uint8_t color;
+	uint8_t cell;
+} copy;
+
+static struct {
+	int8_t dx;
+	int8_t dy;
+	uint8_t len;
+} insert;
+
+static void modeNormal(void) {
+	curs_set(1);
+	move(cellY, cellX);
+	input.mode = MODE_NORMAL;
+}
+static void modeHelp(void) {
+	curs_set(0);
+	drawTile(HELP);
+	input.mode = MODE_HELP;
+}
+static void modeMap(void) {
+	curs_set(0);
+	clientMap();
+	input.mode = MODE_MAP;
+}
+static void modeDirection(void) {
+	input.mode = MODE_DIRECTION;
+}
+static void modeInsert(int8_t dx, int8_t dy) {
+	insert.dx = dx;
+	insert.dy = dy;
+	insert.len = 0;
+	input.mode = MODE_INSERT;
+}
+static void modeReplace(void) {
+	input.mode = MODE_REPLACE;
+}
+static void modeDraw(void) {
+	input.draw = 0;
+	input.mode = MODE_DRAW;
+}
+static void modeLine(void) {
+	input.mode = MODE_LINE;
 }
 
+static void colorFg(uint8_t fg) {
+	input.color = (input.color & 0x78) | (fg & 0x07);
+}
 static void colorBg(uint8_t bg) {
-	input.color = (input.color & 0x0F) | (bg << 4);
+	input.color = (input.color & 0x0F) | (bg & 0x07) << 4;
 }
 
-static void colorInvert(void) {
-	input.color =
-		(input.color & 0x08) |
-		((input.color & 0x07) << 4) |
-		((input.color & 0x70) >> 4);
+static uint8_t colorInvert(uint8_t color) {
+	return (color & 0x08)
+		| (color & 0x70) >> 4
+		| (color & 0x07) << 4;
 }
 
-static void insertMode(int8_t dx, int8_t dy) {
-	input.mode = MODE_INSERT;
-	input.dx = dx;
-	input.dy = dy;
-	input.len = 0;
+static void cellCopy(void) {
+	copy.color = tile.colors[cellY][cellX];
+	copy.cell = tile.cells[cellY][cellX];
 }
 
-static void swapCell(int8_t dx, int8_t dy) {
-	uint8_t aColor = attrColor(inch());
-	char aCell = inch() & A_CHARTEXT;
+static void cellSwap(int8_t dx, int8_t dy) {
+	if ((uint8_t)(cellX + dx) >= CELL_COLS) return;
+	if ((uint8_t)(cellY + dy) >= CELL_ROWS) return;
 
-	int sy, sx;
-	getyx(stdscr, sy, sx);
-	move(sy + dy, sx + dx);
-	uint8_t bColor = attrColor(inch());
-	char bCell = inch() & A_CHARTEXT;
-	move(sy, sx);
+	uint8_t aColor = tile.colors[cellY][cellX];
+	uint8_t aCell = tile.cells[cellY][cellX];
+
+	uint8_t bColor = tile.colors[cellY + dy][cellX + dx];
+	uint8_t bCell = tile.cells[cellY + dy][cellX + dx];
 
 	clientPut(bColor, bCell);
 	clientMove(dx, dy);
 	clientPut(aColor, aCell);
 }
 
-static void inputNormal(int c) {
-	switch (c) {
-		break; case ESC: input.mode = MODE_NORMAL;
+static uint8_t inputCell(wchar_t ch) {
+	if (ch == ' ') return ' ';
+	if (ch < 0x80) return (uint8_t)ch + input.shift;
+	for (size_t i = 0; i < ARRAY_LEN(CP437); ++i) {
+		if (ch == CP437[i]) return i;
+	}
+	return 0;
+}
 
-		break; case 'q': endwin(); exit(EX_OK);
-		break; case 'Q': {
-			if ((input.color & 0x07) < ARRAY_LEN(SPAWNS)) {
-				clientSpawn(input.color & 0x07);
-			} else {
-				clientSpawn(0);
-			}
+static void inputNormal(bool keyCode, wchar_t ch) {
+	if (keyCode) {
+		switch (ch) {
+			break; case KEY_LEFT:  clientMove(-1,  0);
+			break; case KEY_RIGHT: clientMove( 1,  0);
+			break; case KEY_UP:    clientMove( 0, -1);
+			break; case KEY_DOWN:  clientMove( 0,  1);
+
+			break; case KEY_F(1): input.shift = 0x00;
+			break; case KEY_F(2): input.shift = 0xC0;
+			break; case KEY_F(3): input.shift = 0xA0;
+			break; case KEY_F(4): input.shift = 0x70;
+			break; case KEY_F(5): input.shift = 0x40;
 		}
-		break; case 'm': clientMap();
+		return;
+	}
 
-		break; case 'i': insertMode(1, 0);
-		break; case 'a': clientMove(1, 0); insertMode(1, 0);
-		break; case 'I': insertMode(0, 0);
-		break; case 'r': input.mode = MODE_REPLACE;
-		break; case 'p': input.mode = MODE_PUT;
-		break; case 'R': input.mode = MODE_DRAW; input.draw = 0;
-		break; case 'x': clientPut(attrColor(inch()), ' ');
+	switch (ch) {
+		break; case CTRL('L'): clearok(curscr, true);
 
-		break; case '~': {
-			clientPut(input.color, inch() & A_CHARTEXT);
-			clientMove(input.dx, input.dy);
-		}
+		break; case ESC: modeNormal(); input.shift = 0;
+		break; case 'q': endwin(); exit(EX_OK);
 
-		break; case '[': if (input.speed > 1) input.speed--;
-		break; case ']': if (input.speed < 4) input.speed++;
-
-		break; case 'h': clientMove(-input.speed,            0);
-		break; case 'j': clientMove(           0,  input.speed);
-		break; case 'k': clientMove(           0, -input.speed);
-		break; case 'l': clientMove( input.speed,            0);
-		break; case 'y': clientMove(-input.speed, -input.speed);
-		break; case 'u': clientMove( input.speed, -input.speed);
-		break; case 'b': clientMove(-input.speed,  input.speed);
-		break; case 'n': clientMove( input.speed,  input.speed);
-
-		break; case 'H': swapCell(-1,  0);
-		break; case 'J': swapCell( 0,  1);
-		break; case 'K': swapCell( 0, -1);
-		break; case 'L': swapCell( 1,  0);
-		break; case 'Y': swapCell(-1, -1);
-		break; case 'U': swapCell( 1, -1);
-		break; case 'B': swapCell(-1,  1);
-		break; case 'N': swapCell( 1,  1);
-
-		break; case '`': input.color = attrColor(inch());
+		break; case 'g': clientFlip();
+		break; case 'h': clientMove(-1,  0);
+		break; case 'l': clientMove( 1,  0);
+		break; case 'k': clientMove( 0, -1);
+		break; case 'j': clientMove( 0,  1);
+		break; case 'y': clientMove(-1, -1);
+		break; case 'u': clientMove( 1, -1);
+		break; case 'b': clientMove(-1,  1);
+		break; case 'n': clientMove( 1,  1);
 
 		break; case '0': colorFg(COLOR_BLACK);
 		break; case '1': colorFg(COLOR_RED);
@@ -225,288 +427,261 @@ static void inputNormal(int c) {
 		break; case '^': colorBg(COLOR_CYAN);
 		break; case '&': colorBg(COLOR_WHITE);
 
-		break; case '*': case '8': input.color ^= COLOR_BRIGHT;
+		break; case '8': input.color ^= COLOR_BRIGHT;
+		break; case '9': input.color = colorInvert(input.color);
+		break; case '`': input.color = tile.colors[cellY][cellX];
 
-		break; case '(': case '9': colorInvert();
+		break; case 'H': cellSwap(-1,  0);
+		break; case 'L': cellSwap( 1,  0);
+		break; case 'K': cellSwap( 0, -1);
+		break; case 'J': cellSwap( 0,  1);
+		break; case 'Y': cellSwap(-1, -1);
+		break; case 'U': cellSwap( 1, -1);
+		break; case 'B': cellSwap(-1,  1);
+		break; case 'N': cellSwap( 1,  1);
 
-		break; case KEY_LEFT:  clientMove(-1,  0);
-		break; case KEY_DOWN:  clientMove( 0,  1);
-		break; case KEY_UP:    clientMove( 0, -1);
-		break; case KEY_RIGHT: clientMove( 1,  0);
-	}
-}
+		break; case 's': cellCopy();
+		break; case 'x': cellCopy(); clientPut(copy.color, ' ');
+		break; case 'p': clientPut(copy.color, copy.cell);
 
-static void inputMap(void) {
-	input.mode = MODE_NORMAL;
-	curs_set(1);
-	touchwin(stdscr);
-}
-
-static void inputInsert(int c) {
-	if (c == ESC) {
-		input.mode = MODE_NORMAL;
-		clientMove(-input.dx, -input.dy);
-	} else if (!input.dx && !input.dy) {
-		switch (c) {
-			break; case 'h': insertMode(-1,  0);
-			break; case 'j': insertMode( 0,  1);
-			break; case 'k': insertMode( 0, -1);
-			break; case 'l': insertMode( 1,  0);
-			break; case 'y': insertMode(-1, -1);
-			break; case 'u': insertMode( 1, -1);
-			break; case 'b': insertMode(-1,  1);
-			break; case 'n': insertMode( 1,  1);
+		break; case '~': {
+			cellCopy();
+			clientPut(input.color, tile.cells[cellY][cellX]);
+			clientMove(1, 0);
+		}
+		break; case '*': {
+			clientPut(
+				tile.colors[cellY][cellX] ^ COLOR_BRIGHT,
+				tile.cells[cellY][cellX]
+			);
+			clientMove(1, 0);
+		}
+		break; case '(': {
+			clientPut(
+				colorInvert(tile.colors[cellY][cellX]),
+				tile.cells[cellY][cellX]
+			);
+			clientMove(1, 0);
 		}
-	} else if (c == '\b' || c == DEL) {
-		clientMove(-input.dx, -input.dy);
-		clientPut(input.color, ' ');
-		input.len--;
-	} else if (c == '\n') {
-		clientMove(input.dy, input.dx);
-		clientMove(-input.dx * input.len, -input.dy * input.len);
-		input.len = 0;
-	} else if (isprint(c)) {
-		clientPut(input.color, c);
-		clientMove(input.dx, input.dy);
-		input.len++;
-	}
-}
-
-static void inputReplace(int c) {
-	if (isprint(c)) clientPut(attrColor(inch()), c);
-	input.mode = MODE_NORMAL;
-}
 
-static void inputPut(int c) {
-	if (isprint(c)) clientPut(input.color, c);
-	input.mode = MODE_NORMAL;
-}
+		break; case CTRL('A'): {
+			clientPut(tile.colors[cellY][cellX], tile.cells[cellY][cellX] + 1);
+		}
+		break; case CTRL('X'): {
+			clientPut(tile.colors[cellY][cellX], tile.cells[cellY][cellX] - 1);
+		}
 
-static void inputDraw(int c) {
-	if (input.draw) {
-		inputNormal(c);
-		clientPut(input.color, input.draw);
-	} else if (isprint(c)) {
-		input.draw = c;
-		clientPut(input.color, c);
-	} else if (c == ESC) {
-		input.mode = MODE_NORMAL;
+		break; case '?': modeHelp();
+		break; case 'm': modeMap();
+		break; case 'I': modeDirection();
+		break; case 'i': modeInsert(1, 0);
+		break; case 'a': modeInsert(1, 0); clientMove(1, 0);
+		break; case 'r': modeReplace(); cellCopy();
+		break; case 'R': modeDraw();
+		break; case '.': modeLine();
 	}
 }
 
-static void readInput(void) {
-	int c = getch();
-	switch (input.mode) {
-		break; case MODE_NORMAL:  inputNormal(c);
-		break; case MODE_MAP:     inputMap();
-		break; case MODE_INSERT:  inputInsert(c);
-		break; case MODE_REPLACE: inputReplace(c);
-		break; case MODE_PUT:     inputPut(c);
-		break; case MODE_DRAW:    inputDraw(c);
-	}
+static void inputHelp(bool keyCode, wchar_t ch) {
+	(void)keyCode;
+	(void)ch;
+	if (tile.meta.createTime) drawTile(&tile);
+	modeNormal();
 }
 
-static void serverPut(uint8_t x, uint8_t y, uint8_t color, char cell) {
-	mvaddch(y, x, colorAttr(color) | cell);
+static void inputMap(bool keyCode, wchar_t ch) {
+	(void)keyCode;
+	(void)ch;
+	drawTile(&tile);
+	modeNormal();
 }
 
-static void serverTile(void) {
-	struct Tile tile;
-	ssize_t size = recv(client, &tile, sizeof(tile), 0);
-	if (size < 0) err(EX_IOERR, "recv");
-	if ((size_t)size < sizeof(tile)) {
-		errx(EX_PROTOCOL, "This tile isn't big enough...");
+static void inputDirection(bool keyCode, wchar_t ch) {
+	if (keyCode) return;
+	switch (ch) {
+		break; case ESC: modeNormal();
+		break; case 'h': modeInsert(-1,  0);
+		break; case 'l': modeInsert( 1,  0);
+		break; case 'k': modeInsert( 0, -1);
+		break; case 'j': modeInsert( 0,  1);
+		break; case 'y': modeInsert(-1, -1);
+		break; case 'u': modeInsert( 1, -1);
+		break; case 'b': modeInsert(-1,  1);
+		break; case 'n': modeInsert( 1,  1);
 	}
+}
 
-	for (int y = 0; y < CELL_ROWS; ++y) {
-		for (int x = 0; x < CELL_COLS; ++x) {
-			serverPut(x, y, tile.colors[y][x], tile.cells[y][x]);
+static void inputInsert(bool keyCode, wchar_t ch) {
+	if (keyCode) {
+		inputNormal(keyCode, ch);
+		return;
+	}
+	switch (ch) {
+		break; case ESC: {
+			clientMove(-insert.dx, -insert.dy);
+			modeNormal();
+		}
+		break; case '\b': case DEL: {
+			clientMove(-insert.dx, -insert.dy);
+			clientPut(input.color, ' ');
+			insert.len--;
+		}
+		break; case '\n': {
+			clientMove(insert.dy, insert.dx);
+			clientMove(insert.len * -insert.dx, insert.len * -insert.dy);
+			insert.len = 0;
+		}
+		break; default: {
+			uint8_t cell = inputCell(ch);
+			if (!cell) break;
+			clientPut(input.color, cell);
+			clientMove(insert.dx, insert.dy);
+			insert.len++;
 		}
 	}
 }
 
-static void serverCursor(uint8_t oldX, uint8_t oldY, uint8_t newX, uint8_t newY) {
-	if (oldX != CURSOR_NONE) {
-		move(oldY, oldX);
-		addch(inch() & ~A_REVERSE);
+static void inputReplace(bool keyCode, wchar_t ch) {
+	if (keyCode) {
+		inputNormal(keyCode, ch);
+		return;
 	}
-	if (newX != CURSOR_NONE) {
-		move(newY, newX);
-		addch(inch() | A_REVERSE);
+	if (ch != ESC) {
+		uint8_t cell = inputCell(ch);
+		if (!cell) return;
+		clientPut(tile.colors[cellY][cellX], cell);
 	}
+	modeNormal();
 }
 
-static WINDOW *mapFrame;
-static WINDOW *mapWindow;
-
-static const char MAP_CELLS[] = " -~=+:$%#";
-static const uint8_t MAP_COLORS[] = {
-	COLOR_BLUE,    COLOR_BRIGHT | COLOR_BLUE,
-	COLOR_CYAN,    COLOR_BRIGHT | COLOR_CYAN,
-	COLOR_GREEN,   COLOR_BRIGHT | COLOR_GREEN,
-	COLOR_YELLOW,  COLOR_BRIGHT | COLOR_YELLOW,
-	COLOR_RED,     COLOR_BRIGHT | COLOR_RED,
-	COLOR_MAGENTA, COLOR_BRIGHT | COLOR_MAGENTA,
-	COLOR_WHITE,   COLOR_BRIGHT | COLOR_WHITE,
-};
-
-static void serverMap(void) {
-	struct Map map;
-	ssize_t size = recv(client, &map, sizeof(map), 0);
-	if (size < 0) err(EX_IOERR, "recv");
-	if ((size_t)size < sizeof(map)) errx(EX_PROTOCOL, "This map is incomplete...");
-
-	uint32_t countMax = 0;
-	time_t timeNow = time(NULL);
-	time_t timeMin = timeNow;
-	for (int y = 0; y < MAP_ROWS; ++y) {
-		for (int x = 0; x < MAP_COLS; ++x) {
-			struct MapTile tile = map.tiles[y][x];
-			if (countMax < tile.modifyCount) countMax = tile.modifyCount;
-			if (tile.modifyTime && timeMin > tile.modifyTime) {
-				timeMin = tile.modifyTime;
-			}
+static void inputDraw(bool keyCode, wchar_t ch) {
+	if (!keyCode && ch == ESC) {
+		modeNormal();
+		return;
+	}
+	if (input.draw) {
+		inputNormal(keyCode, ch);
+	} else {
+		if (keyCode) {
+			inputNormal(keyCode, ch);
+			return;
 		}
+		input.draw = inputCell(ch);
 	}
+	clientPut(input.color, input.draw);
+}
 
-	for (int y = 0; y < MAP_ROWS; ++y) {
-		for (int x = 0; x < MAP_COLS; ++x) {
-			struct MapTile tile = map.tiles[y][x];
-
-			double count = (tile.modifyCount && countMax > 1)
-				? log(tile.modifyCount) / log(countMax)
-				: 0.0;
-			double time = (tile.modifyTime && timeNow - timeMin)
-				? (double)(tile.modifyTime - timeMin) / (double)(timeNow - timeMin)
-				: 0.0;
-			count *= ARRAY_LEN(MAP_CELLS) - 2;
-			time *= ARRAY_LEN(MAP_COLORS) - 1;
-
-			char cell = MAP_CELLS[(int)round(count)];
-			chtype attr = colorAttr(MAP_COLORS[(int)round(time)]);
-			if (y == MAP_ROWS / 2 && x == MAP_COLS / 2) {
-				attr |= A_REVERSE;
-			}
-
-			wmove(mapWindow, y, 3 * x);
-			waddch(mapWindow, attr | cell);
-			waddch(mapWindow, attr | cell);
-			waddch(mapWindow, attr | cell);
+static uint8_t lineCell(uint8_t cell, int8_t dx, int8_t dy) {
+	if (dx < 0) {
+		switch (CP437[cell]) {
+			default:   return inputCell(L'→');
+			case L'←': return inputCell(L'─'); case L'─': return 0;
+			case L'↑': return inputCell(L'┐'); case L'┐': return 0;
+			case L'↓': return inputCell(L'┘'); case L'┘': return 0;
+			case L'│': return inputCell(L'┤'); case L'┤': return 0;
+			case L'└': return inputCell(L'┴'); case L'┴': return 0;
+			case L'┌': return inputCell(L'┬'); case L'┬': return 0;
+			case L'├': return inputCell(L'┼'); case L'┼': return 0;
+		}
+	} else if (dx > 0) {
+		switch (CP437[cell]) {
+			default:   return inputCell(L'←');
+			case L'→': return inputCell(L'─'); case L'─': return 0;
+			case L'↑': return inputCell(L'┌'); case L'┌': return 0;
+			case L'↓': return inputCell(L'└'); case L'└': return 0;
+			case L'│': return inputCell(L'├'); case L'├': return 0;
+			case L'┘': return inputCell(L'┴'); case L'┴': return 0;
+			case L'┐': return inputCell(L'┬'); case L'┬': return 0;
+			case L'┤': return inputCell(L'┼'); case L'┼': return 0;
+		}
+	} else if (dy < 0) {
+		switch (CP437[cell]) {
+			default:   return inputCell(L'↓');
+			case L'↑': return inputCell(L'│'); case L'│': return 0;
+			case L'←': return inputCell(L'└'); case L'└': return 0;
+			case L'→': return inputCell(L'┘'); case L'┘': return 0;
+			case L'─': return inputCell(L'┴'); case L'┴': return 0;
+			case L'┌': return inputCell(L'├'); case L'├': return 0;
+			case L'┐': return inputCell(L'┤'); case L'┤': return 0;
+			case L'┬': return inputCell(L'┼'); case L'┼': return 0;
+		}
+	} else if (dy > 0) {
+		switch (CP437[cell]) {
+			default:   return inputCell(L'↑');
+			case L'↓': return inputCell(L'│'); case L'│': return 0;
+			case L'←': return inputCell(L'┌'); case L'┌': return 0;
+			case L'→': return inputCell(L'┐'); case L'┐': return 0;
+			case L'─': return inputCell(L'┬'); case L'┬': return 0;
+			case L'└': return inputCell(L'├'); case L'├': return 0;
+			case L'┘': return inputCell(L'┤'); case L'┤': return 0;
+			case L'┴': return inputCell(L'┼'); case L'┼': return 0;
 		}
 	}
-
-	input.mode = MODE_MAP;
-	curs_set(0);
+	return 0;
 }
 
-static void readMessage(void) {
-	struct ServerMessage msg;
-	ssize_t size = recv(client, &msg, sizeof(msg), 0);
-	if (size < 0) err(EX_IOERR, "recv");
-	if ((size_t)size < sizeof(msg)) errx(EX_PROTOCOL, "A message was cut short.");
-
-	int sy, sx;
-	getyx(stdscr, sy, sx);
-	switch (msg.type) {
-		break; case SERVER_TILE: serverTile();
-		break; case SERVER_MOVE: move(msg.move.cellY, msg.move.cellX); return;
-		break; case SERVER_PUT: {
-			serverPut(
-				msg.put.cellX,
-				msg.put.cellY,
-				msg.put.color,
-				msg.put.cell
-			);
+static void inputLine(bool keyCode, wchar_t ch) {
+	int8_t dx = 0;
+	int8_t dy = 0;
+	if (keyCode) {
+		switch (ch) {
+			break; case KEY_LEFT:  dx = -1;
+			break; case KEY_RIGHT: dx =  1;
+			break; case KEY_UP:    dy = -1;
+			break; case KEY_DOWN:  dy =  1;
+			break; default: return;
 		}
-		break; case SERVER_CURSOR: {
-			serverCursor(
-				msg.cursor.oldCellX,
-				msg.cursor.oldCellY,
-				msg.cursor.newCellX,
-				msg.cursor.newCellY
-			);
+	} else {
+		switch (ch) {
+			break; case ESC: case '.': modeNormal(); return;
+			break; case 'h': dx = -1;
+			break; case 'l': dx =  1;
+			break; case 'k': dy = -1;
+			break; case 'j': dy =  1;
+			break; default: return;
 		}
-		break; case SERVER_MAP: serverMap();
-		break; default: errx(EX_PROTOCOL, "I don't know what %d means!", msg.type);
 	}
-	move(sy, sx);
+	if ((uint8_t)(cellX + dx) >= CELL_COLS) return;
+	if ((uint8_t)(cellY + dy) >= CELL_ROWS) return;
+
+	uint8_t leave = lineCell(tile.cells[cellY][cellX], dx, dy);
+	uint8_t enter = lineCell(tile.cells[cellY + dy][cellX + dx], -dx, -dy);
+
+	if (leave) clientPut(input.color, leave);
+	clientMove(dx, dy);
+	if (enter) clientPut(input.color, enter);
 }
 
-static void draw(void) {
-	wnoutrefresh(stdscr);
-	if (input.mode == MODE_MAP) {
-		touchwin(mapFrame);
-		touchwin(mapWindow);
-		wnoutrefresh(mapFrame);
-		wnoutrefresh(mapWindow);
+static void readInput(void) {
+	wint_t ch;
+	bool keyCode = (KEY_CODE_YES == get_wch(&ch));
+	switch (input.mode) {
+		break; case MODE_NORMAL:    inputNormal(keyCode, ch);
+		break; case MODE_HELP:      inputHelp(keyCode, ch);
+		break; case MODE_MAP:       inputMap(keyCode, ch);
+		break; case MODE_DIRECTION: inputDirection(keyCode, ch);
+		break; case MODE_INSERT:    inputInsert(keyCode, ch);
+		break; case MODE_REPLACE:   inputReplace(keyCode, ch);
+		break; case MODE_DRAW:      inputDraw(keyCode, ch);
+		break; case MODE_LINE:      inputLine(keyCode, ch);
 	}
-	doupdate();
 }
 
-static void curse(void) {
-	initscr();
-	cbreak();
-	noecho();
-	keypad(stdscr, true);
-	set_escdelay(100);
-
-	if (!has_colors()) {
-		endwin();
-		fprintf(
-			stderr,
-			"Sorry, your terminal doesn't support colors!\n"
-			"If you think it does, check TERM.\n"
-		);
-		exit(EX_CONFIG);
-	}
-	start_color();
-	if (COLOR_PAIRS < 64) {
-		endwin();
-		fprintf(
-			stderr,
-			"Sorry, your terminal doesn't support enough color pairs!\n"
-			"If you think it does, check TERM.\n"
-		);
-		exit(EX_CONFIG);
+int main(int argc, char *argv[]) {
+	int opt;
+	while (0 < (opt = getopt(argc, argv, "h"))) {
+		if (opt == 'h') {
+			fwrite(HELP_DATA, sizeof(HELP_DATA), 1, stdout);
+			return EX_OK;
+		} else {
+			return EX_USAGE;
+		}
 	}
-	colorPairs();
 
-	if (LINES < CELL_ROWS || COLS < CELL_COLS) {
-		endwin();
-		fprintf(stderr, "Sorry, your terminal is too small!\n");
-		fprintf(stderr, "It needs to be at least 80x25 characters.\n");
-		exit(EX_CONFIG);
-	}
+	curse();
+	modeHelp();
+	readInput();
 
-	attrset(colorAttr(COLOR_WHITE));
-	if (LINES > CELL_ROWS) {
-		mvhline(CELL_ROWS, 0, 0, CELL_COLS);
-	}
-	if (COLS > CELL_COLS) {
-		mvvline(0, CELL_COLS, 0, CELL_ROWS);
-	}
-	if (LINES > CELL_ROWS && COLS > CELL_COLS) {
-		mvaddch(CELL_ROWS, CELL_COLS, ACS_LRCORNER);
-	}
-	attrset(A_NORMAL);
-
-	mapFrame = newwin(
-		MAP_ROWS + 2,
-		3 * MAP_COLS + 2,
-		CELL_INIT_Y - MAP_ROWS / 2 - 1,
-		CELL_INIT_X - 3 * MAP_COLS / 2 - 1
-	);
-	mapWindow = newwin(
-		MAP_ROWS,
-		3 * MAP_COLS,
-		CELL_INIT_Y - MAP_ROWS / 2,
-		CELL_INIT_X - 3 * MAP_COLS / 2
-	);
-	wattrset(mapFrame, colorAttr(COLOR_WHITE));
-	box(mapFrame, 0, 0);
-}
-
-int main() {
 	client = socket(PF_LOCAL, SOCK_STREAM, 0);
 	if (client < 0) err(EX_OSERR, "socket");
 
@@ -517,8 +692,6 @@ int main() {
 	int error = connect(client, (struct sockaddr *)&addr, sizeof(addr));
 	if (error) err(EX_NOINPUT, "torus.sock");
 
-	curse();
-
 	struct pollfd fds[2] = {
 		{ .fd = STDIN_FILENO, .events = POLLIN },
 		{ .fd = client, .events = POLLIN },
@@ -530,6 +703,7 @@ int main() {
 
 		if (fds[0].revents) readInput();
 		if (fds[1].revents) readMessage();
-		draw();
+
+		refresh();
 	}
 }
diff --git a/help.c b/help.c
deleted file mode 100644
index dd13c4f..0000000
--- a/help.c
+++ /dev/null
@@ -1,187 +0,0 @@
-/* Copyright (C) 2017  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 <err.h>
-#include <stdint.h>
-#include <sys/socket.h>
-#include <sys/un.h>
-#include <sysexits.h>
-#include <unistd.h>
-
-#include "torus.h"
-
-static int client;
-
-static void clientMessage(struct ClientMessage msg) {
-	ssize_t size = send(client, &msg, sizeof(msg), 0);
-	if (size < 0) err(EX_IOERR, "send");
-}
-
-static void clientMove(int8_t dx, int8_t dy) {
-	struct ClientMessage msg = {
-		.type = CLIENT_MOVE,
-		.move = { .dx = dx, .dy = dy },
-	};
-	clientMessage(msg);
-}
-
-static void clientPut(uint8_t color, char cell) {
-	struct ClientMessage msg = {
-		.type = CLIENT_PUT,
-		.put = { .color = color, .cell = cell },
-	};
-	clientMessage(msg);
-}
-
-static const useconds_t DELAY = 50000;
-
-enum {
-	K = COLOR_BLACK,
-	R = COLOR_RED,
-	G = COLOR_GREEN,
-	Y = COLOR_YELLOW,
-	B = COLOR_BLUE,
-	M = COLOR_MAGENTA,
-	C = COLOR_CYAN,
-	W = COLOR_WHITE,
-	I = COLOR_BRIGHT | COLOR_WHITE,
-};
-
-static void h(void) { clientMove(-1,  0); usleep(DELAY); }
-static void j(void) { clientMove( 0,  1); usleep(DELAY); }
-static void k(void) { clientMove( 0, -1); usleep(DELAY); }
-static void l(void) { clientMove( 1,  0); usleep(DELAY); }
-static void y(void) { clientMove(-1, -1); usleep(DELAY); }
-static void u(void) { clientMove( 1, -1); usleep(DELAY); }
-static void b(void) { clientMove(-1,  1); usleep(DELAY); }
-static void n(void) { clientMove( 1,  1); usleep(DELAY); }
-
-static void p(uint8_t color, char cell) {
-	clientPut(color, cell);
-	usleep(DELAY);
-}
-
-static uint8_t len;
-
-static void s(uint8_t color, const char *str) {
-	for (; *str; ++len, ++str) {
-		clientPut(color, *str);
-		clientMove(1, 0);
-		usleep(DELAY);
-	}
-}
-
-static void r(void) {
-	clientMove(-len, 1);
-	usleep(DELAY);
-	len = 0;
-}
-
-int main() {
-	client = socket(PF_LOCAL, SOCK_STREAM, 0);
-	if (client < 0) err(EX_OSERR, "socket");
-
-	struct sockaddr_un addr = {
-		.sun_family = AF_LOCAL,
-		.sun_path = "torus.sock",
-	};
-	int error = connect(client, (struct sockaddr *)&addr, sizeof(addr));
-	if (error) err(EX_NOINPUT, "torus.sock");
-
-	pid_t pid = fork();
-	if (pid < 0) err(EX_OSERR, "fork");
-
-	if (!pid) {
-		for (;;) {
-			char buf[4096];
-			ssize_t size = recv(client, buf, sizeof(buf), 0);
-			if (size < 0) err(EX_IOERR, "recv");
-			if (!size) return EX_OK;
-		}
-	}
-
-	clientMove(-CELL_INIT_X, -CELL_INIT_Y);
-	clientMove(28, 0);
-
-	for (;;) {
-		for (int i = 0; i < 10; ++i) {
-			clientMove(0, 1);
-			usleep(DELAY / 5);
-		}
-		for (int i = 0; i < 11; ++i) {
-			for (int j = 0; j < 28; ++j) {
-				clientPut(W, ' ');
-				if (i % 2) clientMove(1, 0);
-				else clientMove(-1, 0);
-				usleep(DELAY / 5);
-			}
-			clientPut(W, ' ');
-			if (i != 10) clientMove(0, -1);
-			usleep(DELAY / 5);
-		}
-
-		j(); l(); l();
-		s(W, "Welcome to "); s(I, "ascii.town"); s(W, "!"); r();
-		r(); r();
-
-		n(); n(); s(W, "o-"); s(I, "l");
-		h(); b(); p(W, '\\'); n(); p(I, 'n');
-		y(); h(); p(W, '|'); j(); p(I, 'j');
-		y(); p(W, '/'); b(); p(I, 'b');
-		k(); u(); p(W, '-'); h(); p(I, 'h');
-		u(); p(W, '\\'); y(); p(I, 'y');
-		n(); l(); p(W, '|'); k(); p(I, 'k');
-		n(); p(W, '/');  u(); p(I, 'u');
-
-		u(); s(W, "    "); len = 0;
-
-		s(I, "q "); s(W, "quit");    r();
-		s(I, "i "); s(W, "insert");  r();
-		s(I, "r "); s(W, "replace"); r();
-		s(I, "R "); s(W, "draw");    r();
-		s(I, "~ "); s(W, "color");   r();
-		s(I, "` "); s(W, "pipette"); r();
-		s(I, "* "); s(W, "bright");
-
-		s(W, "     "); len = 0;
-
-		clientPut(W, '7'); k();
-		clientPut(C, '6'); k();
-		clientPut(M, '5'); k();
-		clientPut(B, '4'); k();
-		clientPut(Y, '3'); k();
-		clientPut(G, '2'); k();
-		clientPut(R, '1'); k();
-		clientPut(K, '0');
-
-		l(); l();
-
-		clientPut(K << 4, ')'); j();
-		clientPut(R << 4, '!'); j();
-		clientPut(G << 4, '@'); j();
-		clientPut(Y << 4, '#'); j();
-		clientPut(B << 4, '$'); j();
-		clientPut(M << 4, '%'); j();
-		clientPut(C << 4, '^'); j();
-		clientPut(W << 4, '&'); j();
-
-		h(); k(); k(); k(); k(); k(); k(); k(); k(); k(); h();
-
-		sleep(30);
-
-		u(); l(); l(); l();
-	}
-}
diff --git a/help.h b/help.h
new file mode 100644
index 0000000..8c092ed
--- /dev/null
+++ b/help.h
@@ -0,0 +1,376 @@
+static const uint8_t HELP_DATA[] = {
+	0xda, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xbf, 0xb3, 0x20, 0x57, 0x65, 0x6c, 0x63, 0x6f, 0x6d,
+	0x65, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x73, 0x63, 0x69, 0x69, 0x2e,
+	0x74, 0x6f, 0x77, 0x6e, 0x21, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x71, 0x20, 0x71, 0x75, 0x69, 0x74, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x20, 0x6d, 0x69, 0x6e,
+	0x69, 0x2d, 0x6d, 0x61, 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x3f, 0x20, 0x68, 0x65, 0x6c, 0x70, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20,
+	0x20, 0x20, 0x20, 0x6b, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x30, 0x20, 0x31, 0x20, 0x32, 0x20, 0x33, 0x20, 0x34, 0x20, 0x35,
+	0x20, 0x36, 0x20, 0x37, 0x20, 0x20, 0x20, 0x20, 0x65, 0x73, 0x63,
+	0x20, 0x6e, 0x61, 0x76, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+	0x20, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x20, 0x20, 0x20, 0x20, 0x73,
+	0x20, 0x63, 0x6f, 0x70, 0x79, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0xb3, 0xb3, 0x20, 0x20, 0x79, 0x20, 0x18, 0x20, 0x75, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x69, 0x20, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x20,
+	0x6d, 0x6f, 0x64, 0x65, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x70, 0x20, 0x70, 0x61, 0x73, 0x74, 0x65, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x68, 0x20, 0x1b, 0xf9, 0x1a,
+	0x20, 0x6c, 0x20, 0x20, 0x20, 0x20, 0x29, 0x20, 0x21, 0x20, 0x40,
+	0x20, 0x23, 0x20, 0x24, 0x20, 0x25, 0x20, 0x5e, 0x20, 0x26, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x20, 0x69, 0x6e, 0x73, 0x65,
+	0x72, 0x74, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x7e, 0x20, 0x70, 0x61, 0x69, 0x6e,
+	0x74, 0x20, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x20, 0x62,
+	0x20, 0x19, 0x20, 0x6e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x49, 0x20, 0x69,
+	0x6e, 0x73, 0x65, 0x72, 0x74, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63,
+	0x74, 0x69, 0x6f, 0x6e, 0x20, 0x20, 0x20, 0x20, 0x2a, 0x20, 0x70,
+	0x61, 0x69, 0x6e, 0x74, 0x20, 0x62, 0x72, 0x69, 0x67, 0x68, 0x74,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3,
+	0x20, 0x20, 0x20, 0x20, 0x6a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x38, 0x20, 0x62, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x20, 0x39,
+	0x20, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x20, 0x20, 0x20, 0x20,
+	0x52, 0x20, 0x64, 0x72, 0x61, 0x77, 0x20, 0x6d, 0x6f, 0x64, 0x65,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x28, 0x20, 0x70, 0x61, 0x69, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x76,
+	0x65, 0x72, 0x74, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0xb3, 0xb3, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x60, 0x20, 0x70, 0x69,
+	0x70, 0x65, 0x74, 0x74, 0x65, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x2e, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x6d,
+	0x6f, 0x64, 0x65, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x43, 0x2d, 0x61, 0x20, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d,
+	0x65, 0x6e, 0x74, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x67, 0x20, 0x74, 0x75,
+	0x6e, 0x6e, 0x65, 0x6c, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x72, 0x20, 0x72, 0x65, 0x70,
+	0x6c, 0x61, 0x63, 0x65, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x43, 0x2d, 0x78, 0x20, 0x64, 0x65, 0x63,
+	0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x78, 0x20,
+	0x65, 0x72, 0x61, 0x73, 0x65, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x48, 0x4a, 0x4b, 0x4c, 0x59, 0x55, 0x42, 0x4e, 0x20,
+	0x73, 0x77, 0x61, 0x70, 0x20, 0x63, 0x65, 0x6c, 0x6c, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3,
+	0xb3, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0xb3, 0xb3, 0x20, 0x46, 0x31, 0x20, 0x40, 0x41, 0x42,
+	0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d,
+	0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
+	0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63,
+	0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e,
+	0x6f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x46, 0x32, 0x20,
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+	0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
+	0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20,
+	0x46, 0x33, 0x20, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7,
+	0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2,
+	0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd,
+	0xfe, 0xff, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0xb3, 0xb3, 0x20, 0x46, 0x34, 0x20, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4,
+	0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
+	0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca,
+	0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5,
+	0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x46, 0x35, 0x20, 0x80, 0x81,
+	0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c,
+	0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
+	0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2,
+	0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad,
+	0xae, 0xaf, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0xb3, 0xb3, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73,
+	0x20, 0x41, 0x47, 0x50, 0x4c, 0x76, 0x33, 0x20, 0x46, 0x72, 0x65,
+	0x65, 0x20, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x21,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x54, 0x68, 0x65, 0x20,
+	0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x20, 0x69, 0x73, 0x20, 0x61,
+	0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x61, 0x74,
+	0x20, 0x3c, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x63,
+	0x6f, 0x64, 0x65, 0x2e, 0x63, 0x61, 0x75, 0x73, 0x61, 0x6c, 0x2e,
+	0x61, 0x67, 0x65, 0x6e, 0x63, 0x79, 0x2f, 0x6a, 0x75, 0x6e, 0x65,
+	0x2f, 0x74, 0x6f, 0x72, 0x75, 0x73, 0x3e, 0x2e, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3, 0xb3, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xb3,
+	0xb3, 0x20, 0x50, 0x72, 0x65, 0x73, 0x73, 0x20, 0x3f, 0x20, 0x74,
+	0x6f, 0x20, 0x6f, 0x70, 0x65, 0x6e, 0x20, 0x74, 0x68, 0x69, 0x73,
+	0x20, 0x68, 0x65, 0x6c, 0x70, 0x20, 0x61, 0x67, 0x61, 0x69, 0x6e,
+	0x2e, 0x20, 0x50, 0x72, 0x65, 0x73, 0x73, 0x20, 0x61, 0x6e, 0x79,
+	0x20, 0x6b, 0x65, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x63, 0x6f, 0x6e,
+	0x74, 0x69, 0x6e, 0x75, 0x65, 0x2e, 0x2e, 0x2e, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+	0x20, 0x20, 0xb3, 0xc0, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xc4,
+	0xc4, 0xc4, 0xc4, 0xc4, 0xc4, 0xd9, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x0c, 0x0e, 0x0a, 0x0b, 0x09, 0x0d, 0x0c, 0x0e, 0x0a, 0x0b, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x03, 0x03, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x08, 0x0f, 0x01, 0x0f, 0x02,
+	0x0f, 0x03, 0x0f, 0x04, 0x0f, 0x05, 0x0f, 0x06, 0x0f, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x0f, 0x0f, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x07, 0x0f,
+	0x0f, 0x07, 0x07, 0x0f, 0x0f, 0x07, 0x0f, 0x07, 0x0f, 0x07, 0x0f,
+	0x07, 0x0f, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03,
+	0x07, 0x0f, 0x08, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07,
+	0x0f, 0x08, 0x07, 0x10, 0x07, 0x20, 0x07, 0x30, 0x07, 0x40, 0x07,
+	0x50, 0x07, 0x60, 0x07, 0x70, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x03, 0x03, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x0f, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x07, 0x07, 0x07, 0x0f,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x0f, 0x0f, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03,
+	0x03, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f,
+	0x0f, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x03, 0x03, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x0f, 0x0f,
+	0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x07,
+	0x0f, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x03, 0x03, 0x07, 0x0f, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x0f, 0x0f, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x0f, 0x0f,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03,
+	0x07, 0x0f, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x03, 0x03, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03,
+	0x03, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x06, 0x06, 0x06, 0x06,
+	0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+	0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+	0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
+	0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x03, 0x03, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x0f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
+	0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0xa9, 0x60, 0x80, 0x5b, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x60,
+	0x83, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x99, 0x60, 0x83, 0x5b, 0x00,
+	0x00, 0x00, 0x00, 0xb4, 0x2e, 0x00, 0x00, 0x87, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00
+};
+static const struct Tile *HELP = (const struct Tile *)HELP_DATA;
diff --git a/index.html b/index.html
index a934f1c..b3d71a6 100644
--- a/index.html
+++ b/index.html
@@ -3,18 +3,12 @@
 <p>
 Hey there, friend. This should get you there.
 <p>
-<code>
-	ssh torus@ascii.town
-</code>
+<a href="ssh://torus@ascii.town"><code>ssh torus@ascii.town</code></a>
 <p>
 Or you can just come and chat.
 <p>
-<code>
-	ssh chat@ascii.town
-</code>
+<a href="ssh://chat@ascii.town"><code>ssh chat@ascii.town</code></a>
 <p>
 You can also take a detour and play NetHack.
 <p>
-<code>
-	ssh nethack@ascii.town
-</code>
+<a href="ssh://nethack@ascii.town"><code>ssh nethack@ascii.town</code></a>
diff --git a/merge.c b/merge.c
index 70c5d4e..22b0eac 100644
--- a/merge.c
+++ b/merge.c
@@ -14,15 +14,24 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#define _XOPEN_SOURCE_EXTENDED
+
 #include <curses.h>
 #include <err.h>
 #include <fcntl.h>
+#include <locale.h>
 #include <stdio.h>
 #include <sysexits.h>
+#include <wchar.h>
 
 #include "torus.h"
 
-static void colorPairs(void) {
+static void curse(void) {
+	setlocale(LC_CTYPE, "");
+
+	initscr();
+	start_color();
+
 	assume_default_colors(0, 0);
 	if (COLORS >= 16) {
 		for (short pair = 1; pair < 0x80; ++pair) {
@@ -33,22 +42,40 @@ static void colorPairs(void) {
 			init_pair(pair, pair & 007, (pair & 070) >> 3);
 		}
 	}
+
+	color_set(COLOR_WHITE, NULL);
+	mvhline(CELL_ROWS, 0, 0, CELL_COLS);
+	mvhline(CELL_ROWS * 2 + 1, 0, 0, CELL_COLS);
+	mvvline(0, CELL_COLS, 0, CELL_ROWS * 2 + 1);
+	mvaddch(CELL_ROWS, CELL_COLS, ACS_RTEE);
+	mvaddch(CELL_ROWS * 2 + 1, CELL_COLS, ACS_LRCORNER);
+	color_set(0, NULL);
+
+	cbreak();
+	noecho();
+	keypad(stdscr, true);
+	set_escdelay(100);
 }
 
-static chtype colorAttr(uint8_t color) {
-	if (COLORS >= 16) return COLOR_PAIR(color);
-	chtype bold = (color & COLOR_BRIGHT) ? A_BOLD : A_NORMAL;
-	short pair = (color & 0x70) >> 1 | (color & 0x07);
-	return bold | COLOR_PAIR(pair);
+static attr_t colorAttr(uint8_t color) {
+	if (COLORS >= 16) return A_NORMAL;
+	return (color & COLOR_BRIGHT) ? A_BOLD : A_NORMAL;
+}
+static short colorPair(uint8_t color) {
+	if (COLORS >= 16) return color;
+	return (color & 0x70) >> 1 | (color & 0x07);
 }
 
 static void drawTile(int offsetY, const struct Tile *tile) {
-	for (uint8_t y = 0; y < CELL_ROWS; ++y) {
-		for (uint8_t x = 0; x < CELL_COLS; ++x) {
-			uint8_t color = tile->colors[y][x];
-			char cell = tile->cells[y][x];
-
-			mvaddch(offsetY + y, x, colorAttr(color) | cell);
+	for (uint8_t cellY = 0; cellY < CELL_ROWS; ++cellY) {
+		for (uint8_t cellX = 0; cellX < CELL_COLS; ++cellX) {
+			uint8_t color = tile->colors[cellY][cellX];
+			uint8_t cell = tile->cells[cellY][cellX];
+
+			cchar_t cch;
+			wchar_t wch[] = { CP437[cell], L'\0' };
+			setcchar(&cch, wch, colorAttr(color), colorPair(color), NULL);
+			mvadd_wch(offsetY + cellY, cellX, &cch);
 		}
 	}
 }
@@ -65,22 +92,7 @@ int main(int argc, char *argv[]) {
 	FILE *fileC = fopen(argv[3], "w");
 	if (!fileC) err(EX_CANTCREAT, "%s", argv[3]);
 
-	initscr();
-	cbreak();
-	noecho();
-	keypad(stdscr, true);
-	set_escdelay(100);
-
-	start_color();
-	colorPairs();
-
-	attrset(colorAttr(COLOR_WHITE));
-	mvhline(CELL_ROWS, 0, 0, CELL_COLS);
-	mvhline(CELL_ROWS * 2 + 1, 0, 0, CELL_COLS);
-	mvvline(0, CELL_COLS, 0, CELL_ROWS * 2 + 1);
-	mvaddch(CELL_ROWS, CELL_COLS, ACS_RTEE);
-	mvaddch(CELL_ROWS * 2 + 1, CELL_COLS, ACS_LRCORNER);
-	attrset(A_NORMAL);
+	curse();
 
 	struct Tile tileA, tileB;
 	for (;;) {
@@ -93,11 +105,11 @@ int main(int argc, char *argv[]) {
 		if (!countA && !countB) break;
 		if (!countA || !countB) errx(EX_DATAERR, "different size inputs");
 
-		const struct Tile *tileC = (tileA.accessTime > tileB.accessTime)
+		const struct Tile *tileC = (tileA.meta.accessTime > tileB.meta.accessTime)
 			? &tileA
 			: &tileB;
 
-		if (tileA.modifyTime != tileB.modifyTime) {
+		if (tileA.meta.modifyTime != tileB.meta.modifyTime) {
 			drawTile(0, &tileA);
 			drawTile(CELL_ROWS + 1, &tileB);
 			move(CELL_ROWS * 2 + 2, 0);
diff --git a/meta.c b/meta.c
index c913cf7..990928a 100644
--- a/meta.c
+++ b/meta.c
@@ -32,11 +32,11 @@ int main() {
 			"%d,%d,%jd,%u,%jd,%u,%jd\n",
 			i % TILE_COLS,
 			i / TILE_COLS,
-			tile.createTime,
-			tile.modifyCount,
-			tile.modifyTime,
-			tile.accessCount,
-			tile.accessTime
+			tile.meta.createTime,
+			tile.meta.modifyCount,
+			tile.meta.modifyTime,
+			tile.meta.accessCount,
+			tile.meta.accessTime
 		);
 	}
 }
diff --git a/server.c b/server.c
index 9cae5dd..b8cf76a 100644
--- a/server.c
+++ b/server.c
@@ -58,25 +58,25 @@ static void tilesMap(void) {
 
 static struct Tile *tileGet(uint32_t tileX, uint32_t tileY) {
 	struct Tile *tile = &tiles[tileY * TILE_ROWS + tileX];
-	if (!tile->createTime) {
+	if (!tile->meta.createTime) {
 		memset(tile->cells, ' ', CELLS_SIZE);
 		memset(tile->colors, COLOR_WHITE, CELLS_SIZE);
-		tile->createTime = time(NULL);
+		tile->meta.createTime = time(NULL);
 	}
 	return tile;
 }
 
 static struct Tile *tileAccess(uint32_t tileX, uint32_t tileY) {
 	struct Tile *tile = tileGet(tileX, tileY);
-	tile->accessTime = time(NULL);
-	tile->accessCount++;
+	tile->meta.accessTime = time(NULL);
+	tile->meta.accessCount++;
 	return tile;
 }
 
 static struct Tile *tileModify(uint32_t tileX, uint32_t tileY) {
 	struct Tile *tile = tileGet(tileX, tileY);
-	tile->modifyTime = time(NULL);
-	tile->modifyCount++;
+	tile->meta.modifyTime = time(NULL);
+	tile->meta.modifyCount++;
 	return tile;
 }
 
@@ -97,8 +97,8 @@ static struct Client *clientAdd(int fd) {
 	if (!client) err(EX_OSERR, "malloc");
 
 	client->fd = fd;
-	client->tileX = TILE_VOID_X;
-	client->tileY = TILE_VOID_Y;
+	client->tileX = TILE_INIT_X;
+	client->tileY = TILE_INIT_Y;
 	client->cellX = CELL_INIT_X;
 	client->cellY = CELL_INIT_Y;
 
@@ -172,7 +172,7 @@ static bool clientCursors(const struct Client *client) {
 	return true;
 }
 
-static bool clientUpdate(const struct Client *client, const struct Client *old) {
+static bool clientUpdate(struct Client *client, const struct Client *old) {
 	struct ServerMessage msg = {
 		.type = SERVER_MOVE,
 		.move = { .cellX = client->cellX, .cellY = client->cellY },
@@ -217,16 +217,6 @@ static bool clientUpdate(const struct Client *client, const struct Client *old)
 	return true;
 }
 
-static bool clientSpawn(struct Client *client, uint8_t spawn) {
-	if (spawn >= ARRAY_LEN(SPAWNS)) return false;
-	struct Client old = *client;
-	client->tileX = SPAWNS[spawn].tileX;
-	client->tileY = SPAWNS[spawn].tileY;
-	client->cellX = CELL_INIT_X;
-	client->cellY = CELL_INIT_Y;
-	return clientUpdate(client, &old);
-}
-
 static bool clientMove(struct Client *client, int8_t dx, int8_t dy) {
 	struct Client old = *client;
 
@@ -268,7 +258,14 @@ static bool clientMove(struct Client *client, int8_t dx, int8_t dy) {
 	return clientUpdate(client, &old);
 }
 
-static bool clientPut(const struct Client *client, uint8_t color, char cell) {
+static bool clientFlip(struct Client *client) {
+	struct Client old = *client;
+	client->tileX = (client->tileX + TILE_COLS / 2) % TILE_COLS;
+	client->tileY = (client->tileY + TILE_ROWS / 2) % TILE_ROWS;
+	return clientUpdate(client, &old);
+}
+
+static bool clientPut(const struct Client *client, uint8_t color, uint8_t cell) {
 	struct Tile *tile = tileModify(client->tileX, client->tileY);
 	tile->colors[client->cellY][client->cellX] = color;
 	tile->cells[client->cellY][client->cellX] = cell;
@@ -291,20 +288,62 @@ static bool clientMap(const struct Client *client) {
 	int32_t mapY = (int32_t)client->tileY - MAP_ROWS / 2;
 	int32_t mapX = (int32_t)client->tileX - MAP_COLS / 2;
 
-	struct Map map;
+	time_t now = time(NULL);
+	struct Map map = {
+		.now = now,
+		.min = {
+			.createTime = now,
+			.modifyTime = now,
+			.accessTime = now,
+			.modifyCount = UINT32_MAX,
+			.accessCount = UINT32_MAX,
+		},
+	};
+
 	for (int32_t y = 0; y < MAP_ROWS; ++y) {
 		for (int32_t x = 0; x < MAP_COLS; ++x) {
 			uint32_t tileY = ((mapY + y) % TILE_ROWS + TILE_ROWS) % TILE_ROWS;
 			uint32_t tileX = ((mapX + x) % TILE_COLS + TILE_COLS) % TILE_COLS;
+			struct Meta meta = tiles[tileY * TILE_ROWS + tileX].meta;
+
+			if (meta.createTime) {
+				if (meta.createTime < map.min.createTime) {
+					map.min.createTime = meta.createTime;
+				}
+				if (meta.createTime > map.max.createTime) {
+					map.max.createTime = meta.createTime;
+				}
+			}
+			if (meta.modifyTime) {
+				if (meta.modifyTime < map.min.modifyTime) {
+					map.min.modifyTime = meta.modifyTime;
+				}
+				if (meta.modifyTime > map.max.modifyTime) {
+					map.max.modifyTime = meta.modifyTime;
+				}
+			}
+			if (meta.accessTime) {
+				if (meta.accessTime < map.min.accessTime) {
+					map.min.accessTime = meta.accessTime;
+				}
+				if (meta.accessTime > map.max.accessTime) {
+					map.max.accessTime = meta.accessTime;
+				}
+			}
+			if (meta.modifyCount < map.min.modifyCount) {
+				map.min.modifyCount = meta.modifyCount;
+			}
+			if (meta.modifyCount > map.max.modifyCount) {
+				map.max.modifyCount = meta.modifyCount;
+			}
+			if (meta.accessCount < map.min.accessCount) {
+				map.min.accessCount = meta.accessCount;
+			}
+			if (meta.accessCount > map.max.accessCount) {
+				map.max.accessCount = meta.accessCount;
+			}
 
-			const struct Tile *tile = &tiles[tileY * TILE_ROWS + tileX];
-			map.tiles[y][x] = (struct MapTile) {
-				.createTime = tile->createTime,
-				.modifyTime = tile->modifyTime,
-				.accessTime = tile->accessTime,
-				.modifyCount = tile->modifyCount,
-				.accessCount = tile->accessCount,
-			};
+			map.meta[y][x] = meta;
 		}
 	}
 
@@ -366,7 +405,11 @@ int main() {
 			nevents = kevent(kq, &event, 1, NULL, 0, NULL);
 			if (nevents < 0) err(EX_IOERR, "kevent");
 
-			if (!clientSpawn(client, 0)) clientRemove(client);
+			struct ServerMessage msg = { .type = SERVER_TILE };
+			bool success = clientSend(client, msg)
+				&& clientMove(client, 0, 0)
+				&& clientCursors(client);
+			if (!success) clientRemove(client);
 
 			continue;
 		}
@@ -389,11 +432,15 @@ int main() {
 			break; case CLIENT_MOVE: {
 				success = clientMove(client, msg.move.dx, msg.move.dy);
 			}
+			break; case CLIENT_FLIP: {
+				success = clientFlip(client);
+			}
 			break; case CLIENT_PUT: {
 				success = clientPut(client, msg.put.color, msg.put.cell);
 			}
-			break; case CLIENT_SPAWN: success = clientSpawn(client, msg.spawn);
-			break; case CLIENT_MAP: success = clientMap(client);
+			break; case CLIENT_MAP: {
+				success = clientMap(client);
+			}
 		}
 		if (!success) clientRemove(client);
 	}
diff --git a/torus.h b/torus.h
index b5a0235..3b4d133 100644
--- a/torus.h
+++ b/torus.h
@@ -15,14 +15,13 @@
  */
 
 #include <assert.h>
+#include <stdalign.h>
 #include <stdbool.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include <time.h>
-
-#define PACKED __attribute__((packed))
-#define ALIGNED(x) __attribute__((aligned(x)))
+#include <wchar.h>
 
 #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
 
@@ -47,65 +46,72 @@ enum {
 	COLOR_BRIGHT,
 };
 
+static const wchar_t CP437[256] = (
+	L" ☺☻♥♦♣♠•◘○◙♂♀♪♫☼"
+	L"►◄↕‼¶§▬↨↑↓→←∟↔▲▼"
+	L" !\"#$%&'()*+,-./"
+	L"0123456789:;<=>?"
+	L"@ABCDEFGHIJKLMNO"
+	L"PQRSTUVWXYZ[\\]^_"
+	L"`abcdefghijklmno"
+	L"pqrstuvwxyz{|}~⌂"
+	L"ÇüéâäàåçêëèïîìÄÅ"
+	L"ÉæÆôöòûùÿÖÜ¢£¥₧ƒ"
+	L"áíóúñѪº¿⌐¬½¼¡«»"
+	L"░▒▓│┤╡╢╖╕╣║╗╝╜╛┐"
+	L"└┴┬├─┼╞╟╚╔╩╦╠═╬╧"
+	L"╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀"
+	L"αßΓπΣσµτΦΘΩδ∞φε∩"
+	L"≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ "
+);
+
 enum {
-	CELL_ROWS = 25,
+	CELL_ROWS = 24,
 	CELL_COLS = 80,
 };
-static const size_t CELLS_SIZE = sizeof(char[CELL_ROWS][CELL_COLS]);
+static const size_t CELLS_SIZE = sizeof(uint8_t[CELL_ROWS][CELL_COLS]);
 
 static const uint8_t CELL_INIT_X = CELL_COLS / 2;
 static const uint8_t CELL_INIT_Y = CELL_ROWS / 2;
 
-struct ALIGNED(4096) Tile {
+struct Meta {
 	time_t createTime;
 	time_t modifyTime;
-	char ALIGNED(16) cells[CELL_ROWS][CELL_COLS];
-	uint8_t ALIGNED(16) colors[CELL_ROWS][CELL_COLS];
+	time_t accessTime;
 	uint32_t modifyCount;
 	uint32_t accessCount;
-	time_t accessTime;
+};
+
+struct Tile {
+	alignas(4096) uint8_t cells[CELL_ROWS][CELL_COLS];
+	uint8_t colors[CELL_ROWS][CELL_COLS];
+	struct Meta meta;
 };
 static_assert(4096 == sizeof(struct Tile), "struct Tile is page-sized");
-static_assert(16 == offsetof(struct Tile, cells), "stable cells offset");
-static_assert(2016 == offsetof(struct Tile, colors), "stable colors offset");
 
 enum {
-	TILE_ROWS = 512,
-	TILE_COLS = 512,
+	TILE_ROWS = 64,
+	TILE_COLS = 64,
 };
 static const size_t TILES_SIZE = sizeof(struct Tile[TILE_ROWS][TILE_COLS]);
 
-static const uint32_t TILE_VOID_X = UINT32_MAX;
-static const uint32_t TILE_VOID_Y = UINT32_MAX;
-
-static const struct {
-	uint32_t tileX;
-	uint32_t tileY;
-} SPAWNS[] = {
-	{ 0, 0 },
-	{ TILE_COLS * 3 / 4, TILE_ROWS * 3 / 4 }, // NW
-	{ TILE_COLS * 1 / 4, TILE_ROWS * 3 / 4 }, // NE
-	{ TILE_COLS * 1 / 4, TILE_ROWS * 1 / 4 }, // SE
-	{ TILE_COLS * 3 / 4, TILE_ROWS * 1 / 4 }, // SW
-};
+static const uint32_t TILE_INIT_X = TILE_COLS / 2;
+static const uint32_t TILE_INIT_Y = TILE_ROWS / 2;
 
 enum {
-	MAP_ROWS = 11,
-	MAP_COLS = 11,
+	MAP_ROWS = 7,
+	MAP_COLS = 7,
 };
 
 struct Map {
-	struct MapTile {
-		time_t createTime;
-		time_t modifyTime;
-		time_t accessTime;
-		uint32_t modifyCount;
-		uint32_t accessCount;
-	} tiles[MAP_ROWS][MAP_COLS];
+	time_t now;
+	struct Meta min;
+	struct Meta max;
+	struct Meta meta[MAP_ROWS][MAP_COLS];
 };
 
 struct ServerMessage {
-	enum PACKED {
+	enum {
 		SERVER_TILE,
 		SERVER_MOVE,
 		SERVER_PUT,
@@ -121,7 +127,7 @@ struct ServerMessage {
 			uint8_t cellX;
 			uint8_t cellY;
 			uint8_t color;
-			char cell;
+			uint8_t cell;
 		} put;
 		struct {
 			uint8_t oldCellX;
@@ -135,10 +141,10 @@ struct ServerMessage {
 static const uint8_t CURSOR_NONE = UINT8_MAX;
 
 struct ClientMessage {
-	enum PACKED {
+	enum {
 		CLIENT_MOVE,
+		CLIENT_FLIP,
 		CLIENT_PUT,
-		CLIENT_SPAWN,
 		CLIENT_MAP,
 	} type;
 	union {
@@ -148,8 +154,7 @@ struct ClientMessage {
 		} move;
 		struct {
 			uint8_t color;
-			char cell;
+			uint8_t cell;
 		} put;
-		uint8_t spawn;
 	};
 };