about summary refs log tree commit diff homepage
path: root/client.c
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 /client.c
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 'client.c')
-rw-r--r--client.c888
1 files changed, 531 insertions, 357 deletions
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();
 	}
 }