summary refs log tree commit diff homepage
diff options
context:
space:
mode:
-rw-r--r--Makefile3
-rw-r--r--client.c614
-rw-r--r--server.c2
-rw-r--r--torus.h28
4 files changed, 246 insertions, 401 deletions
diff --git a/Makefile b/Makefile
index 4091276..f2b2ca0 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ CHROOT_USER = torus
 CHROOT_GROUP = $(CHROOT_USER)
 
 CFLAGS += -Wall -Wextra -Wpedantic
-LDLIBS = -lm -lcurses
+LDLIBS = -lm -lcursesw
 BINS = server client help meta merge
 OBJS = $(BINS:%=%.o)
 
@@ -32,6 +32,7 @@ chroot.tar: server client help
 	    /lib/libncurses.so.8 \
 	    /lib/libncursesw.so.8 \
 	    root/lib
+	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 help root/bin
diff --git a/client.c b/client.c
index cb0107e..8086d3f 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,14 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include <ctype.h>
+#define _XOPEN_SOURCE_EXTENDED
+
 #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,43 +29,42 @@
 #include <sys/un.h>
 #include <sysexits.h>
 #include <unistd.h>
+#include <wchar.h>
+#include <wctype.h>
 
 #include "torus.h"
 
+#define err(...) do { endwin(); err(__VA_ARGS__); } while(0)
+#define errx(...) do { endwin(); errx(__VA_ARGS__); } while (0)
+
+#define CTRL(ch) ((ch) ^ 0x40)
 enum {
 	ESC = 0x1B,
 	DEL = 0x7F,
 };
 
-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 void curse(void) {
+	setlocale(LC_CTYPE, "");
 
-static void clientMap(void) {
-	struct ClientMessage msg = { .type = CLIENT_MAP };
-	clientMessage(msg);
-}
+	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);
+	}
 
-static void colorPairs(void) {
 	assume_default_colors(0, 0);
 	if (COLORS >= 16) {
 		for (short pair = 1; pair < 0x80; ++pair) {
@@ -74,421 +75,243 @@ static void colorPairs(void) {
 			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);
-}
+	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);
 
-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);
+	cbreak();
+	noecho();
+	keypad(stdscr, true);
+	set_escdelay(100);
 }
 
-static struct {
-	int8_t speed;
-	uint8_t color;
-	enum {
-		MODE_NORMAL,
-		MODE_MAP,
-		MODE_INSERT,
-		MODE_REPLACE,
-		MODE_PUT,
-		MODE_DRAW,
-	} mode;
-	int8_t dx;
-	int8_t dy;
-	uint8_t len;
-	char draw;
-} input = {
-	.speed = 1,
-	.color = COLOR_WHITE,
-	.dx = 1,
-};
-
-static void colorFg(uint8_t fg) {
-	input.color = (input.color & 0xF8) | fg;
+static attr_t colorAttr(uint8_t color) {
+	if (COLORS >= 16) return A_NORMAL;
+	return (color & COLOR_BRIGHT) ? A_BOLD : A_NORMAL;
 }
-
-static void colorBg(uint8_t bg) {
-	input.color = (input.color & 0x0F) | (bg << 4);
+static short colorPair(uint8_t color) {
+	if (COLORS >= 16) return color;
+	return (color & 0x70) >> 1 | (color & 0x07);
 }
 
-static void colorInvert(void) {
-	input.color =
-		(input.color & 0x08) |
-		((input.color & 0x07) << 4) |
-		((input.color & 0x70) >> 4);
+static void put(uint8_t cellX, uint8_t cellY, uint8_t color, uint8_t cell) {
+	cchar_t cch;
+	wchar_t wch[] = { CP437[cell], L'\0' };
+	setcchar(&cch, wch, colorAttr(color), colorPair(color), NULL);
+	mvadd_wch(cellY, cellX, &cch);
 }
 
-static void insertMode(int8_t dx, int8_t dy) {
-	input.mode = MODE_INSERT;
-	input.dx = dx;
-	input.dy = dy;
-	input.len = 0;
-}
+static int client;
 
-static void swapCell(int8_t dx, int8_t dy) {
-	uint8_t aColor = attrColor(inch());
-	char aCell = inch() & A_CHARTEXT;
+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, "truncated tile");
 
-	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);
+	for (uint8_t cellY = 0; cellY < CELL_ROWS; ++cellY) {
+		for (uint8_t cellX = 0; cellX < CELL_COLS; ++cellX) {
+			put(cellX, cellY, tile.colors[cellY][cellX], tile.cells[cellY][cellX]);
+		}
+	}
+}
 
-	clientPut(bColor, bCell);
-	clientMove(dx, dy);
-	clientPut(aColor, aCell);
+static void serverPut(struct ServerMessage msg) {
+	put(msg.put.cellX, msg.put.cellY, msg.put.color, msg.put.cell);
 }
 
-static void inputNormal(int c) {
-	switch (c) {
-		break; case ESC: input.mode = MODE_NORMAL;
+static void serverCursor(struct ServerMessage msg) {
+}
 
-		break; case 'q': endwin(); exit(EX_OK);
-		break; case 'm': clientMap();
+static void serverMap(void) {
+}
 
-		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()), ' ');
-
-		break; case '~': {
-			clientPut(input.color, inch() & A_CHARTEXT);
-			clientMove(input.dx, input.dy);
-		}
+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");
 
-		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 '0': colorFg(COLOR_BLACK);
-		break; case '1': colorFg(COLOR_RED);
-		break; case '2': colorFg(COLOR_GREEN);
-		break; case '3': colorFg(COLOR_YELLOW);
-		break; case '4': colorFg(COLOR_BLUE);
-		break; case '5': colorFg(COLOR_MAGENTA);
-		break; case '6': colorFg(COLOR_CYAN);
-		break; case '7': colorFg(COLOR_WHITE);
-
-		break; case ')': colorBg(COLOR_BLACK);
-		break; case '!': colorBg(COLOR_RED);
-		break; case '@': colorBg(COLOR_GREEN);
-		break; case '#': colorBg(COLOR_YELLOW);
-		break; case '$': colorBg(COLOR_BLUE);
-		break; case '%': colorBg(COLOR_MAGENTA);
-		break; case '^': colorBg(COLOR_CYAN);
-		break; case '&': colorBg(COLOR_WHITE);
-
-		break; case '*': case '8': input.color ^= COLOR_BRIGHT;
-
-		break; case '(': case '9': colorInvert();
-
-		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);
+	int y, x;
+	getyx(stdscr, y, x);
+	switch (msg.type) {
+		break; case SERVER_TILE:   serverTile();
+		break; case SERVER_MOVE:   y = msg.move.cellY; x = msg.move.cellX;
+		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(y, x);
 }
 
-static void inputMap(void) {
-	input.mode = MODE_NORMAL;
-	curs_set(1);
-	touchwin(stdscr);
+static void clientMessage(struct ClientMessage msg) {
+	ssize_t size = send(client, &msg, sizeof(msg), 0);
+	if (size < 0) err(EX_IOERR, "send");
 }
 
-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);
-		}
-	} 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 clientMove(int8_t dx, int8_t dy) {
+	struct ClientMessage msg = {
+		.type = CLIENT_MOVE,
+		.move = { .dx = dx, .dy = dy },
+	};
+	clientMessage(msg);
 }
 
-static void inputReplace(int c) {
-	if (isprint(c)) clientPut(attrColor(inch()), c);
-	input.mode = MODE_NORMAL;
+static void clientPut(uint8_t color, uint8_t cell) {
+	struct ClientMessage msg = {
+		.type = CLIENT_PUT,
+		.put = { .color = color, .cell = cell },
+	};
+	clientMessage(msg);
 }
 
-static void inputPut(int c) {
-	if (isprint(c)) clientPut(input.color, c);
-	input.mode = MODE_NORMAL;
+static void clientMap(void) {
+	struct ClientMessage msg = { .type = CLIENT_MAP };
+	clientMessage(msg);
 }
 
-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;
-	}
-}
+static struct {
+	enum {
+		MODE_NORMAL,
+		MODE_INSERT,
+	} mode;
+	uint8_t color;
+	uint8_t shift;
+} input;
 
-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 struct {
+	int8_t dx;
+	int8_t dy;
+	uint8_t len;
+} insert;
 
-static void serverPut(uint8_t x, uint8_t y, uint8_t color, char cell) {
-	mvaddch(y, x, colorAttr(color) | cell);
+static void insertMode(int8_t dx, int8_t dy) {
+	input.mode = MODE_INSERT;
+	insert.dx = dx;
+	insert.dy = dy;
+	insert.len = 0;
 }
 
-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...");
-	}
-
-	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 inputFg(uint8_t fg) {
+	input.color = (input.color & 0x78) | (fg & 0x07);
+}
+static void inputBg(uint8_t bg) {
+	input.color = (input.color & 0x0F) | (bg & 0x07) << 4;
+}
+static void inputInvert(void) {
+	input.color = (input.color & 0x08)
+		| (input.color & 0x70) >> 4
+		| (input.color & 0x07) << 4;
 }
 
-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);
-	}
-	if (newX != CURSOR_NONE) {
-		move(newY, newX);
-		addch(inch() | A_REVERSE);
+static uint8_t inputCell(wchar_t ch) {
+	if (ch < 0x7F) return (uint8_t)ch + input.shift;
+	for (size_t i = 0; i < ARRAY_LEN(CP437); ++i) {
+		if (ch == CP437[i]) return i;
 	}
+	return 0;
 }
 
-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 Meta meta = map.meta[y][x];
-			if (countMax < meta.modifyCount) countMax = meta.modifyCount;
-			if (meta.modifyTime && timeMin > meta.modifyTime) {
-				timeMin = meta.modifyTime;
-			}
+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);
 		}
+		return;
 	}
 
-	for (int y = 0; y < MAP_ROWS; ++y) {
-		for (int x = 0; x < MAP_COLS; ++x) {
-			struct Meta meta = map.meta[y][x];
-
-			double count = (meta.modifyCount && countMax > 1)
-				? log(meta.modifyCount) / log(countMax)
-				: 0.0;
-			double time = (meta.modifyTime && timeNow - timeMin)
-				? (double)(meta.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;
-			}
+	switch (ch) {
+		break; case ESC: input.mode = MODE_NORMAL;
+		break; case 'q': endwin(); exit(EX_OK);
 
-			wmove(mapWindow, y, 3 * x);
-			waddch(mapWindow, attr | cell);
-			waddch(mapWindow, attr | cell);
-			waddch(mapWindow, attr | cell);
-		}
-	}
+		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': inputFg(COLOR_BLACK);
+		break; case '1': inputFg(COLOR_RED);
+		break; case '2': inputFg(COLOR_GREEN);
+		break; case '3': inputFg(COLOR_YELLOW);
+		break; case '4': inputFg(COLOR_BLUE);
+		break; case '5': inputFg(COLOR_MAGENTA);
+		break; case '6': inputFg(COLOR_CYAN);
+		break; case '7': inputFg(COLOR_WHITE);
+
+		break; case ')': inputBg(COLOR_BLACK);
+		break; case '!': inputBg(COLOR_RED);
+		break; case '@': inputBg(COLOR_GREEN);
+		break; case '#': inputBg(COLOR_YELLOW);
+		break; case '$': inputBg(COLOR_BLUE);
+		break; case '%': inputBg(COLOR_MAGENTA);
+		break; case '^': inputBg(COLOR_CYAN);
+		break; case '&': inputBg(COLOR_WHITE);
+
+		break; case '8': case '*': input.color ^= COLOR_BRIGHT;
+		break; case '9': case '(': inputInvert();
+
+		break; case CTRL('P'): input.shift -= 0x20;
+		break; case CTRL('N'): input.shift += 0x20;
 
-	input.mode = MODE_MAP;
-	curs_set(0);
+		break; case 'i': insertMode(1, 0);
+		break; case 'a': clientMove(1, 0); insertMode(1, 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 inputInsert(bool keyCode, wchar_t ch) {
+	if (keyCode) {
+		inputNormal(keyCode, ch);
+		return;
+	}
+	switch (ch) {
+		break; case ESC: {
+			input.mode = MODE_NORMAL;
+			clientMove(-insert.dx, -insert.dy);
 		}
-		break; case SERVER_CURSOR: {
-			serverCursor(
-				msg.cursor.oldCellX,
-				msg.cursor.oldCellY,
-				msg.cursor.newCellX,
-				msg.cursor.newCellY
-			);
+		break; case '\b': case DEL: {
+			clientMove(-insert.dx, -insert.dy);
+			clientPut(input.color, ' ');
+			insert.len--;
 		}
-		break; case SERVER_MAP: serverMap();
-		break; default: errx(EX_PROTOCOL, "I don't know what %d means!", msg.type);
-	}
-	move(sy, sx);
-}
+		break; case '\n': {
+			clientMove(insert.dy, insert.dx);
+			clientMove(insert.len * -insert.dx, insert.len * -insert.dy);
+			insert.len = 0;
+		}
+		break; default: {
+			if (iswcntrl(ch)) {
+				inputNormal(false, ch);
+				break;
+			}
 
-static void draw(void) {
-	wnoutrefresh(stdscr);
-	if (input.mode == MODE_MAP) {
-		touchwin(mapFrame);
-		touchwin(mapWindow);
-		wnoutrefresh(mapFrame);
-		wnoutrefresh(mapWindow);
+			uint8_t cell = inputCell(ch);
+			if (!cell) break;
+			clientPut(input.color, cell);
+			clientMove(insert.dx, insert.dy);
+			insert.len++;
+		}
 	}
-	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);
-	}
-	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);
-	}
-
-	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);
+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_INSERT: inputInsert(keyCode, ch);
 	}
-	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() {
@@ -515,6 +338,7 @@ int main() {
 
 		if (fds[0].revents) readInput();
 		if (fds[1].revents) readMessage();
-		draw();
+
+		refresh();
 	}
 }
diff --git a/server.c b/server.c
index ff13af3..3dedad8 100644
--- a/server.c
+++ b/server.c
@@ -254,7 +254,7 @@ static bool clientMove(struct Client *client, int8_t dx, int8_t dy) {
 	return true;
 }
 
-static bool clientPut(const struct Client *client, uint8_t color, char cell) {
+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;
diff --git a/torus.h b/torus.h
index 9e8697c..1bbb43a 100644
--- a/torus.h
+++ b/torus.h
@@ -21,6 +21,7 @@
 #include <stdint.h>
 #include <stdlib.h>
 #include <time.h>
+#include <wchar.h>
 
 #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
 
@@ -45,11 +46,30 @@ 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 = 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;
@@ -63,7 +83,7 @@ struct Meta {
 };
 
 struct Tile {
-	alignas(4096) char cells[CELL_ROWS][CELL_COLS];
+	alignas(4096) uint8_t cells[CELL_ROWS][CELL_COLS];
 	uint8_t colors[CELL_ROWS][CELL_COLS];
 	struct Meta meta;
 };
@@ -104,7 +124,7 @@ struct ServerMessage {
 			uint8_t cellX;
 			uint8_t cellY;
 			uint8_t color;
-			char cell;
+			uint8_t cell;
 		} put;
 		struct {
 			uint8_t oldCellX;
@@ -130,7 +150,7 @@ struct ClientMessage {
 		} move;
 		struct {
 			uint8_t color;
-			char cell;
+			uint8_t cell;
 		} put;
 	};
 };