Index: emu/ser.c
===================================================================
--- emu/ser.c	(revision 4c71d39d7a49a544bcfc00821d3634af9b073eca)
+++ emu/ser.c	(revision bb4fd0c90d58bb5c8ecd61934d580a4bbdb5e842)
@@ -22,9 +22,212 @@
 #define ver3(...) _ver(ser_verbose, 2, __VA_ARGS__)
 
+#define CON_W 80
+#define CON_H 25
+
+#define CON_BGR 0x00000000
+#define CON_CUR 0x00e87000
+#define CON_FGR ((SDL_Color){ .r = 255, .b = 255, .g = 255, .a = 255 })
+
+#define CON_FONT "ttf/vera-sans-mono.ttf"
+
 int32_t ser_verbose = 0;
 
+static uint8_t mem[CON_H][CON_W + 1];
+
+static TTF_Font *fon;
+static int32_t fon_w, fon_h;
+
+static int32_t sur_w, sur_h;
+static SDL_Surface *sur;
+
+static int32_t cur_x = 0, cur_y = 0;
+
+static void update(void)
+{
+	if (SDL_FillRect(sur, NULL, CON_BGR) < 0) {
+		fail("SDL_FillRect() failed: %s", SDL_GetError());
+	}
+
+	if (SDL_FillRect(sur, &(SDL_Rect){
+		.x = cur_x * fon_w,
+		.y = cur_y * fon_h,
+		.w = fon_w,
+		.h = fon_h
+	}, CON_CUR) < 0) {
+		fail("SDL_FillRect() failed: %s", SDL_GetError());
+	}
+
+	for (int32_t y = 0; y < CON_H; ++y) {
+		SDL_Surface *lin = TTF_RenderText_Blended(fon, (char *)mem[y], CON_FGR);
+
+		if (lin == NULL) {
+			fail("TTF_RenderText_Blended() failed: %s", TTF_GetError());
+		}
+
+		if (SDL_BlitSurface(lin, NULL, sur, &(SDL_Rect){
+			.x = 0,
+			.y = y * fon_h,
+			.w = CON_W * fon_w,
+			.h = fon_h
+		})) {
+			fail("SDL_BlitSurface() failed: %s", SDL_GetError());
+		}
+
+		SDL_FreeSurface(lin);
+	}
+
+	SDL_Texture *tex = SDL_CreateTextureFromSurface(sdl_ren, sur);
+
+	if (tex == NULL) {
+		fail("SDL_CreateTextureFromSurface() failed: %s", SDL_GetError());
+	}
+
+	if (SDL_RenderCopy(sdl_ren, tex, NULL, NULL) < 0) {
+		fail("SDL_RenderCopy() failed: %s", SDL_GetError());
+	}
+
+	SDL_DestroyTexture(tex);
+	SDL_RenderPresent(sdl_ren);
+}
+
+static void scroll(void)
+{
+	memmove(mem, mem + 1, (CON_H - 1) * (CON_W + 1));
+	memset(mem + (CON_H - 1), ' ', CON_W);
+}
+
+static void forw(void)
+{
+	if (cur_x < CON_W - 1) {
+		++cur_x;
+		return;
+	}
+
+	if (cur_y == CON_H - 1) {
+		cur_x = 0;
+		scroll();
+		return;
+	}
+
+	cur_x = 0;
+	++cur_y;
+}
+
+static void back(void)
+{
+	if (cur_x > 0) {
+		--cur_x;
+		return;
+	}
+
+	if (cur_y == 0) {
+		return;
+	}
+
+	cur_x = CON_W - 1;
+	--cur_y;
+}
+
+static void down(void)
+{
+	if (cur_y < CON_H - 1) {
+		++cur_y;
+		return;
+	}
+
+	scroll();
+}
+
+static void echo(uint8_t c)
+{
+	switch (c) {
+	case '\r':
+		cur_x = 0;
+		break;
+
+	case '\n':
+		down();
+		break;
+
+	case '\b':
+		back();
+		break;
+
+	default:
+		if (c < 32) {
+			echo('^');
+			echo((uint8_t)(c + '@'));
+			return;
+		}
+
+		mem[cur_y][cur_x] = c;
+		forw();
+		break;
+	}
+
+	update();
+}
+
+void ser_key(SDL_KeyboardEvent *ev)
+{
+	switch (ev->keysym.sym) {
+	case SDLK_BACKSPACE:
+		echo('\b');
+		break;
+
+	case SDLK_RETURN:
+		echo('\r');
+		echo('\n');
+		break;
+	}
+}
+
+void ser_text(SDL_TextInputEvent *ev)
+{
+	for (int32_t i = 0; ev->text[i] != 0; ++i) {
+		echo((uint8_t)ev->text[i]);
+	}
+}
+
 void ser_init(void)
 {
 	ver("ser init");
+
+	SDL_RWops *ops = SDL_RWFromFile(CON_FONT, "rb");
+
+	if (ops == NULL) {
+		fail("error while opening font file " CON_FONT ": %s", SDL_GetError());
+	}
+
+	fon = TTF_OpenFontRW(ops, 1, 32);
+
+	if (fon == NULL) {
+		fail("error while loading font file " CON_FONT ": %s", TTF_GetError());
+	}
+
+	fon_h = TTF_FontLineSkip(fon);
+
+	if (TTF_GlyphMetrics(fon, 'X', NULL, NULL, NULL, NULL, &fon_w) < 0) {
+		fail("error while measuring font width: %s", TTF_GetError());
+	}
+
+	sur_w = CON_W * fon_w;
+	sur_h = CON_H * fon_h;
+
+	sur = SDL_CreateRGBSurface(0, sur_w, sur_h, 32, 0, 0, 0, 0);
+
+	if (sur == NULL) {
+		fail("SDL_CreateRGBSurface() failed: %s", SDL_GetError());
+	}
+
+	for (int32_t y = 0; y < CON_H; ++y) {
+		for (int32_t x = 0; x < CON_W; ++x) {
+			mem[y][x] = ' ';
+		}
+
+		mem[y][CON_W] = 0;
+	}
+
+	update();
 }
 
@@ -32,4 +235,7 @@
 {
 	ver("ser quit");
+
+	SDL_FreeSurface(sur);
+	TTF_CloseFont(fon);
 }
 
