Index: uspace/lib/vt/include/vt/vt100.h
===================================================================
--- uspace/lib/vt/include/vt/vt100.h	(revision d31c3ea70b393289736cdac0e91fa5c5eba06c4c)
+++ uspace/lib/vt/include/vt/vt100.h	(revision 6907f26c4f1d524ca40bdb6622b8dc0254203a1f)
@@ -36,4 +36,5 @@
 
 #include <io/charfield.h>
+#include <io/keycode.h>
 #include <ipc/common.h>
 #include <uchar.h>
@@ -75,5 +76,48 @@
 	void (*control_puts)(void *, const char *);
 	void (*flush)(void *);
+	void (*key)(void *, keymod_t, keycode_t, char);
 } vt100_cb_t;
+
+/** VT100 decoding state */
+typedef enum {
+	/** Base state */
+	vts_base,
+	/** Prefix 1b */
+	vts_1b,
+	/** Prefix 1b 4f */
+	vts_1b4f,
+	/** Prefix 1b 5b */
+	vts_1b5b,
+	/** Prefix 1b 5b 31 */
+	vts_1b5b31,
+	/** Prefix 1b 5b 31 35 */
+	vts_1b5b3135,
+	/** Prefix 1b 5b 31 37 */
+	vts_1b5b3137,
+	/** Prefix 1b 5b 31 38 */
+	vts_1b5b3138,
+	/** Prefix 1b 5b 31 39 */
+	vts_1b5b3139,
+	/** Prefix 1b 5b 32 */
+	vts_1b5b32,
+	/** Prefix 1b 5b 32 30 */
+	vts_1b5b3230,
+	/** Prefix 1b 5b 32 31 */
+	vts_1b5b3231,
+	/** Prefix 1b 5b 32 33 */
+	vts_1b5b3233,
+	/** Prefix 1b 5b 32 34 */
+	vts_1b5b3234,
+	/** Prefix 1b 5b 32 35 */
+	vts_1b5b3235,
+	/** Prefix 1b 5b 32 38 */
+	vts_1b5b3238,
+	/** Prefix 1b 5b 35 */
+	vts_1b5b35,
+	/** Prefix 1b 5b 33 */
+	vts_1b5b33,
+	/** Prefix 1b 5b 36 */
+	vts_1b5b36
+} vt100_state_t;
 
 /** VT100 instance */
@@ -98,4 +142,7 @@
 	/** Argument to callback functions */
 	void *arg;
+
+	/** Input decoding state */
+	vt100_state_t state;
 } vt100_t;
 
@@ -119,4 +166,6 @@
 extern void vt100_flush(vt100_t *);
 
+extern void vt100_rcvd_char(vt100_t *, char);
+
 #endif
 
Index: uspace/lib/vt/meson.build
===================================================================
--- uspace/lib/vt/meson.build	(revision d31c3ea70b393289736cdac0e91fa5c5eba06c4c)
+++ uspace/lib/vt/meson.build	(revision 6907f26c4f1d524ca40bdb6622b8dc0254203a1f)
@@ -27,5 +27,5 @@
 #
 
-deps = [ 'output' ]
+deps = [ 'input', 'output' ]
 src = files(
 	'src/vt100.c'
Index: uspace/lib/vt/src/vt100.c
===================================================================
--- uspace/lib/vt/src/vt100.c	(revision d31c3ea70b393289736cdac0e91fa5c5eba06c4c)
+++ uspace/lib/vt/src/vt100.c	(revision 6907f26c4f1d524ca40bdb6622b8dc0254203a1f)
@@ -34,8 +34,10 @@
 #include <errno.h>
 #include <io/color.h>
+#include <io/keycode.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <vt/vt100.h>
 
+/** Map console colors to VT100 color indices */
 sgr_color_index_t color_map[] = {
 	[COLOR_BLACK]   = CI_BLACK,
@@ -49,20 +51,36 @@
 };
 
-void vt100_cls(vt100_t *state)
-{
-	state->cb->control_puts(state->arg, "\033[2J");
-}
-
-/** ECMA-48 Set Graphics Rendition. */
-static void vt100_sgr(vt100_t *state, unsigned int mode)
+/** Clear screen.
+ *
+ * @param vt VT instance
+ */
+void vt100_cls(vt100_t *vt)
+{
+	vt->cb->control_puts(vt->arg, "\033[2J");
+}
+
+/** ECMA-48 Set Graphics Rendition.
+ *
+ * @param vt VT instance
+ * @param mode SGR mode number
+ */
+static void vt100_sgr(vt100_t *vt, unsigned int mode)
 {
 	char control[MAX_CONTROL];
 
 	snprintf(control, MAX_CONTROL, "\033[%um", mode);
-	state->cb->control_puts(state->arg, control);
-}
-
-/** Set Graphics Rendition with 5 arguments. */
-static void vt100_sgr5(vt100_t *state, unsigned a1, unsigned a2,
+	vt->cb->control_puts(vt->arg, control);
+}
+
+/** Set Graphics Rendition with 5 arguments.
+ *
+ * @param vt VT instance
+ * @param a1 First argument
+ * @param a2 Second argument
+ * @param a3 Third argument
+ * @param a4 Fourth argument
+ * @param a5 Fifth argument
+ */
+static void vt100_sgr5(vt100_t *vt, unsigned a1, unsigned a2,
     unsigned a3, unsigned a4, unsigned a5)
 {
@@ -71,8 +89,14 @@
 	snprintf(control, MAX_CONTROL, "\033[%u;%u;%u;%u;%um",
 	    a1, a2, a3, a4, a5);
-	state->cb->control_puts(state->arg, control);
-}
-
-void vt100_set_pos(vt100_t *state, sysarg_t col, sysarg_t row)
+	vt->cb->control_puts(vt->arg, control);
+}
+
+/** Set cussor position.
+ *
+ * @param vt VT instance
+ * @param col Column (starting from 0)
+ * @param row Row (starting from 0)
+ */
+void vt100_set_pos(vt100_t *vt, sysarg_t col, sysarg_t row)
 {
 	char control[MAX_CONTROL];
@@ -80,8 +104,13 @@
 	snprintf(control, MAX_CONTROL, "\033[%" PRIun ";%" PRIun "f",
 	    row + 1, col + 1);
-	state->cb->control_puts(state->arg, control);
-}
-
-void vt100_set_sgr(vt100_t *state, char_attrs_t attrs)
+	vt->cb->control_puts(vt->arg, control);
+}
+
+/** Set graphics rendition based on attributes,
+ *
+ * @param vt VT instance
+ * @param attrs Character attributes
+ */
+void vt100_set_sgr(vt100_t *vt, char_attrs_t attrs)
 {
 	unsigned color;
@@ -91,55 +120,55 @@
 		switch (attrs.val.style) {
 		case STYLE_NORMAL:
-			vt100_sgr(state, SGR_RESET);
-			vt100_sgr(state, SGR_BGCOLOR + CI_WHITE);
-			vt100_sgr(state, SGR_FGCOLOR + CI_BLACK);
+			vt100_sgr(vt, SGR_RESET);
+			vt100_sgr(vt, SGR_BGCOLOR + CI_WHITE);
+			vt100_sgr(vt, SGR_FGCOLOR + CI_BLACK);
 			break;
 		case STYLE_EMPHASIS:
-			vt100_sgr(state, SGR_RESET);
-			vt100_sgr(state, SGR_BGCOLOR + CI_WHITE);
-			vt100_sgr(state, SGR_FGCOLOR + CI_RED);
-			vt100_sgr(state, SGR_BOLD);
+			vt100_sgr(vt, SGR_RESET);
+			vt100_sgr(vt, SGR_BGCOLOR + CI_WHITE);
+			vt100_sgr(vt, SGR_FGCOLOR + CI_RED);
+			vt100_sgr(vt, SGR_BOLD);
 			break;
 		case STYLE_INVERTED:
-			vt100_sgr(state, SGR_RESET);
-			vt100_sgr(state, SGR_BGCOLOR + CI_BLACK);
-			vt100_sgr(state, SGR_FGCOLOR + CI_WHITE);
+			vt100_sgr(vt, SGR_RESET);
+			vt100_sgr(vt, SGR_BGCOLOR + CI_BLACK);
+			vt100_sgr(vt, SGR_FGCOLOR + CI_WHITE);
 			break;
 		case STYLE_SELECTED:
-			vt100_sgr(state, SGR_RESET);
-			vt100_sgr(state, SGR_BGCOLOR + CI_RED);
-			vt100_sgr(state, SGR_FGCOLOR + CI_WHITE);
+			vt100_sgr(vt, SGR_RESET);
+			vt100_sgr(vt, SGR_BGCOLOR + CI_RED);
+			vt100_sgr(vt, SGR_FGCOLOR + CI_WHITE);
 			break;
 		}
 		break;
 	case CHAR_ATTR_INDEX:
-		vt100_sgr(state, SGR_RESET);
-		vt100_sgr(state, SGR_BGCOLOR + color_map[attrs.val.index.bgcolor & 7]);
-		vt100_sgr(state, SGR_FGCOLOR + color_map[attrs.val.index.fgcolor & 7]);
+		vt100_sgr(vt, SGR_RESET);
+		vt100_sgr(vt, SGR_BGCOLOR + color_map[attrs.val.index.bgcolor & 7]);
+		vt100_sgr(vt, SGR_FGCOLOR + color_map[attrs.val.index.fgcolor & 7]);
 
 		if (attrs.val.index.attr & CATTR_BRIGHT)
-			vt100_sgr(state, SGR_BOLD);
+			vt100_sgr(vt, SGR_BOLD);
 
 		break;
 	case CHAR_ATTR_RGB:
-		if (state->enable_rgb == true) {
-			vt100_sgr5(state, 48, 2, RED(attrs.val.rgb.bgcolor),
+		if (vt->enable_rgb == true) {
+			vt100_sgr5(vt, 48, 2, RED(attrs.val.rgb.bgcolor),
 			    GREEN(attrs.val.rgb.bgcolor),
 			    BLUE(attrs.val.rgb.bgcolor));
-			vt100_sgr5(state, 38, 2, RED(attrs.val.rgb.fgcolor),
+			vt100_sgr5(vt, 38, 2, RED(attrs.val.rgb.fgcolor),
 			    GREEN(attrs.val.rgb.fgcolor),
 			    BLUE(attrs.val.rgb.fgcolor));
 		} else {
-			vt100_sgr(state, SGR_RESET);
+			vt100_sgr(vt, SGR_RESET);
 			color =
 			    ((RED(attrs.val.rgb.fgcolor) >= 0x80) ? COLOR_RED : 0) |
 			    ((GREEN(attrs.val.rgb.fgcolor) >= 0x80) ? COLOR_GREEN : 0) |
 			    ((BLUE(attrs.val.rgb.fgcolor) >= 0x80) ? COLOR_BLUE : 0);
-			vt100_sgr(state, SGR_FGCOLOR + color_map[color]);
+			vt100_sgr(vt, SGR_FGCOLOR + color_map[color]);
 			color =
 			    ((RED(attrs.val.rgb.bgcolor) >= 0x80) ? COLOR_RED : 0) |
 			    ((GREEN(attrs.val.rgb.bgcolor) >= 0x80) ? COLOR_GREEN : 0) |
 			    ((BLUE(attrs.val.rgb.bgcolor) >= 0x80) ? COLOR_BLUE : 0);
-			vt100_sgr(state, SGR_BGCOLOR + color_map[color]);
+			vt100_sgr(vt, SGR_BGCOLOR + color_map[color]);
 		}
 		break;
@@ -147,89 +176,1169 @@
 }
 
+/** Create VT instance.
+ *
+ * @param arg Argument passed to callback functions
+ * @param cols Number of columns
+ * @param rows Number of rows
+ * @param cb Callback functions
+ *
+ * @return Pointer to new VT instance on success, NULL on failure.
+ */
 vt100_t *vt100_create(void *arg, sysarg_t cols, sysarg_t rows, vt100_cb_t *cb)
 {
-	vt100_t *state = malloc(sizeof(vt100_t));
-	if (state == NULL)
+	vt100_t *vt = malloc(sizeof(vt100_t));
+	if (vt == NULL)
 		return NULL;
 
-	state->cb = cb;
-	state->arg = arg;
-
-	state->cols = cols;
-	state->rows = rows;
-
-	state->cur_col = (sysarg_t) -1;
-	state->cur_row = (sysarg_t) -1;
-
-	state->cur_attrs.type = CHAR_ATTR_STYLE;
-	state->cur_attrs.val.style = STYLE_NORMAL;
-
-	return state;
-}
-
-void vt100_destroy(vt100_t *state)
-{
-	free(state);
-}
-
-void vt100_get_dimensions(vt100_t *state, sysarg_t *cols,
+	vt->cb = cb;
+	vt->arg = arg;
+
+	vt->cols = cols;
+	vt->rows = rows;
+
+	vt->cur_col = (sysarg_t) -1;
+	vt->cur_row = (sysarg_t) -1;
+
+	vt->cur_attrs.type = CHAR_ATTR_STYLE;
+	vt->cur_attrs.val.style = STYLE_NORMAL;
+
+	return vt;
+}
+
+/** Destroy VT instance.
+ *
+ * @param vt VT instance
+ */
+void vt100_destroy(vt100_t *vt)
+{
+	free(vt);
+}
+
+/** Get VT size.
+ *
+ * @param vt VT instance
+ * @param cols Place to store number of columns
+ * @param rows Place to store number of rows
+ */
+void vt100_get_dimensions(vt100_t *vt, sysarg_t *cols,
     sysarg_t *rows)
 {
-	*cols = state->cols;
-	*rows = state->rows;
-}
-
-errno_t vt100_yield(vt100_t *state)
+	*cols = vt->cols;
+	*rows = vt->rows;
+}
+
+/** Temporarily yield VT to other users.
+ *
+ * @param vt VT instance
+ * @return EOK on success or an error code
+ */
+errno_t vt100_yield(vt100_t *vt)
 {
 	return EOK;
 }
 
-errno_t vt100_claim(vt100_t *state)
+/** Reclaim VT.
+ *
+ * @param vt VT instance
+ * @return EOK on success or an error code
+ */
+errno_t vt100_claim(vt100_t *vt)
 {
 	return EOK;
 }
 
-void vt100_goto(vt100_t *state, sysarg_t col, sysarg_t row)
-{
-	if ((col >= state->cols) || (row >= state->rows))
+/** Go to specified position, if needed.
+ *
+ * @param vt VT instance
+ * @param col Column (starting from 0)
+ * @param row Row (starting from 0)
+ */
+void vt100_goto(vt100_t *vt, sysarg_t col, sysarg_t row)
+{
+	if ((col >= vt->cols) || (row >= vt->rows))
 		return;
 
-	if ((col != state->cur_col) || (row != state->cur_row)) {
-		vt100_set_pos(state, col, row);
-		state->cur_col = col;
-		state->cur_row = row;
-	}
-}
-
-void vt100_set_attr(vt100_t *state, char_attrs_t attrs)
-{
-	if (!attrs_same(state->cur_attrs, attrs)) {
-		vt100_set_sgr(state, attrs);
-		state->cur_attrs = attrs;
-	}
-}
-
-void vt100_cursor_visibility(vt100_t *state, bool visible)
+	if ((col != vt->cur_col) || (row != vt->cur_row)) {
+		vt100_set_pos(vt, col, row);
+		vt->cur_col = col;
+		vt->cur_row = row;
+	}
+}
+
+/** Set character attributes, if needed.
+ *
+ * @param vt VT instance
+ * @param attrs Attributes
+ */
+void vt100_set_attr(vt100_t *vt, char_attrs_t attrs)
+{
+	if (!attrs_same(vt->cur_attrs, attrs)) {
+		vt100_set_sgr(vt, attrs);
+		vt->cur_attrs = attrs;
+	}
+}
+
+/** Set cursor visibility.
+ *
+ * @param vt VT instance
+ * @param @c true to make cursor visible, @c false to make it invisible
+ */
+void vt100_cursor_visibility(vt100_t *vt, bool visible)
 {
 	if (visible)
-		state->cb->control_puts(state->arg, "\033[?25h");
+		vt->cb->control_puts(vt->arg, "\033[?25h");
 	else
-		state->cb->control_puts(state->arg, "\033[?25l");
-}
-
-void vt100_putuchar(vt100_t *state, char32_t ch)
-{
-	state->cb->putuchar(state->arg, ch == 0 ? ' ' : ch);
-	state->cur_col++;
-
-	if (state->cur_col >= state->cols) {
-		state->cur_row += state->cur_col / state->cols;
-		state->cur_col %= state->cols;
-	}
-}
-
-void vt100_flush(vt100_t *state)
-{
-	state->cb->flush(state->arg);
+		vt->cb->control_puts(vt->arg, "\033[?25l");
+}
+
+/** Print Unicode character.
+ *
+ * @param vt VT instance
+ * @parma ch Unicode character
+ */
+void vt100_putuchar(vt100_t *vt, char32_t ch)
+{
+	vt->cb->putuchar(vt->arg, ch == 0 ? ' ' : ch);
+	vt->cur_col++;
+
+	if (vt->cur_col >= vt->cols) {
+		vt->cur_row += vt->cur_col / vt->cols;
+		vt->cur_col %= vt->cols;
+	}
+}
+
+/** Flush VT.
+ *
+ * @param vt VT instance
+ */
+void vt100_flush(vt100_t *vt)
+{
+	vt->cb->flush(vt->arg);
+}
+
+/** Recognize a key.
+ *
+ * Generate a key callback and reset decoder state.
+ *
+ * @param vt VT instance
+ * @param mods Key modifiers
+ * @param key Key code
+ * @param c Character
+ */
+static void vt100_key(vt100_t *vt, keymod_t mods, keycode_t key, char c)
+{
+	vt->cb->key(vt->arg, mods, key, c);
+	vt->state = vts_base;
+}
+
+/** Process input character with prefix 1b.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x1b:
+		vt100_key(vt, 0, KC_ESCAPE, c);
+		break;
+	case 0x60:
+		vt100_key(vt, KM_ALT, KC_BACKTICK, 0);
+		break;
+
+	case 0x31:
+		vt100_key(vt, KM_ALT, KC_1, 0);
+		break;
+	case 0x32:
+		vt100_key(vt, KM_ALT, KC_2, 0);
+		break;
+	case 0x33:
+		vt100_key(vt, KM_ALT, KC_3, 0);
+		break;
+	case 0x34:
+		vt100_key(vt, KM_ALT, KC_4, 0);
+		break;
+	case 0x35:
+		vt100_key(vt, KM_ALT, KC_5, 0);
+		break;
+	case 0x36:
+		vt100_key(vt, KM_ALT, KC_6, 0);
+		break;
+	case 0x37:
+		vt100_key(vt, KM_ALT, KC_7, 0);
+		break;
+	case 0x38:
+		vt100_key(vt, KM_ALT, KC_8, 0);
+		break;
+	case 0x39:
+		vt100_key(vt, KM_ALT, KC_9, 0);
+		break;
+	case 0x30:
+		vt100_key(vt, KM_ALT, KC_0, 0);
+		break;
+
+	case 0x2d:
+		vt100_key(vt, KM_ALT, KC_MINUS, 0);
+		break;
+	case 0x3d:
+		vt100_key(vt, KM_ALT, KC_EQUALS, 0);
+		break;
+
+	case 0x71:
+		vt100_key(vt, KM_ALT, KC_Q, 0);
+		break;
+	case 0x77:
+		vt100_key(vt, KM_ALT, KC_W, 0);
+		break;
+	case 0x65:
+		vt100_key(vt, KM_ALT, KC_E, 0);
+		break;
+	case 0x72:
+		vt100_key(vt, KM_ALT, KC_R, 0);
+		break;
+	case 0x74:
+		vt100_key(vt, KM_ALT, KC_T, 0);
+		break;
+	case 0x79:
+		vt100_key(vt, KM_ALT, KC_Y, 0);
+		break;
+	case 0x75:
+		vt100_key(vt, KM_ALT, KC_U, 0);
+		break;
+	case 0x69:
+		vt100_key(vt, KM_ALT, KC_I, 0);
+		break;
+	case 0x6f:
+		vt100_key(vt, KM_ALT, KC_O, 0);
+		break;
+	case 0x70:
+		vt100_key(vt, KM_ALT, KC_P, 0);
+		break;
+
+		/* 0x1b, 0x5b is used by other keys/sequences */
+
+	case 0x5d:
+		vt100_key(vt, KM_ALT, KC_RBRACKET, 0);
+		break;
+
+	case 0x61:
+		vt100_key(vt, KM_ALT, KC_A, 0);
+		break;
+	case 0x73:
+		vt100_key(vt, KM_ALT, KC_S, 0);
+		break;
+	case 0x64:
+		vt100_key(vt, KM_ALT, KC_D, 0);
+		break;
+	case 0x66:
+		vt100_key(vt, KM_ALT, KC_F, 0);
+		break;
+	case 0x67:
+		vt100_key(vt, KM_ALT, KC_G, 0);
+		break;
+	case 0x68:
+		vt100_key(vt, KM_ALT, KC_H, 0);
+		break;
+	case 0x6a:
+		vt100_key(vt, KM_ALT, KC_J, 0);
+		break;
+	case 0x6b:
+		vt100_key(vt, KM_ALT, KC_K, 0);
+		break;
+	case 0x6c:
+		vt100_key(vt, KM_ALT, KC_L, 0);
+		break;
+
+	case 0x3b:
+		vt100_key(vt, KM_ALT, KC_SEMICOLON, 0);
+		break;
+	case 0x27:
+		vt100_key(vt, KM_ALT, KC_QUOTE, 0);
+		break;
+	case 0x5c:
+		vt100_key(vt, KM_ALT, KC_BACKSLASH, 0);
+		break;
+
+	case 0x7a:
+		vt100_key(vt, KM_ALT, KC_Z, 0);
+		break;
+	case 0x78:
+		vt100_key(vt, KM_ALT, KC_X, 0);
+		break;
+	case 0x63:
+		vt100_key(vt, KM_ALT, KC_C, 0);
+		break;
+	case 0x76:
+		vt100_key(vt, KM_ALT, KC_V, 0);
+		break;
+	case 0x62:
+		vt100_key(vt, KM_ALT, KC_B, 0);
+		break;
+	case 0x6e:
+		vt100_key(vt, KM_ALT, KC_N, 0);
+		break;
+	case 0x6d:
+		vt100_key(vt, KM_ALT, KC_M, 0);
+		break;
+
+	case 0x2c:
+		vt100_key(vt, KM_ALT, KC_COMMA, 0);
+		break;
+	case 0x2e:
+		vt100_key(vt, KM_ALT, KC_PERIOD, 0);
+		break;
+	case 0x2f:
+		vt100_key(vt, KM_ALT, KC_SLASH, 0);
+		break;
+
+	case 0x4f:
+		vt->state = vts_1b4f;
+		break;
+	case 0x5b:
+		vt->state = vts_1b5b;
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 4f.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b4f(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x50:
+		vt100_key(vt, 0, KC_F1, 0);
+		break;
+	case 0x51:
+		vt100_key(vt, 0, KC_F2, 0);
+		break;
+	case 0x52:
+		vt100_key(vt, 0, KC_F3, 0);
+		break;
+	case 0x53:
+		vt100_key(vt, 0, KC_F4, 0);
+		break;
+	case 0x48:
+		vt100_key(vt, 0, KC_HOME, 0);
+		break;
+	case 0x46:
+		vt100_key(vt, 0, KC_END, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x31:
+		vt->state = vts_1b5b31;
+		break;
+	case 0x32:
+		vt->state = vts_1b5b32;
+		break;
+	case 0x35:
+		vt->state = vts_1b5b35;
+		break;
+	case 0x33:
+		vt->state = vts_1b5b33;
+		break;
+	case 0x36:
+		vt->state = vts_1b5b36;
+		break;
+	case 0x41:
+		vt100_key(vt, 0, KC_UP, 0);
+		break;
+	case 0x44:
+		vt100_key(vt, 0, KC_LEFT, 0);
+		break;
+	case 0x42:
+		vt100_key(vt, 0, KC_DOWN, 0);
+		break;
+	case 0x43:
+		vt100_key(vt, 0, KC_RIGHT, 0);
+		break;
+	case 0x48:
+		vt100_key(vt, 0, KC_HOME, 0);
+		break;
+	case 0x46:
+		vt100_key(vt, 0, KC_END, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 31.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b31(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x35:
+		vt->state = vts_1b5b3135;
+		break;
+	case 0x37:
+		vt->state = vts_1b5b3137;
+		break;
+	case 0x38:
+		vt->state = vts_1b5b3138;
+		break;
+	case 0x39:
+		vt->state = vts_1b5b3139;
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 31 35.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3135(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_F5, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 31 37.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3137(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_F6, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 31 38.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3138(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_F7, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 31 39.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3139(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_F8, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 32.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b32(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x30:
+		vt->state = vts_1b5b3230;
+		break;
+	case 0x31:
+		vt->state = vts_1b5b3231;
+		break;
+	case 0x33:
+		vt->state = vts_1b5b3233;
+		break;
+	case 0x34:
+		vt->state = vts_1b5b3234;
+		break;
+	case 0x35:
+		vt->state = vts_1b5b3235;
+		break;
+	case 0x38:
+		vt->state = vts_1b5b3238;
+		break;
+	case 0x7e:
+		vt100_key(vt, 0, KC_INSERT, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 32 30.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3230(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_F9, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 32 31.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3231(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_F10, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 32 33.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3233(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_F11, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 32 34.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3234(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_F12, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 32 35.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3235(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_PRTSCR, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 32 38.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b3238(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_PAUSE, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 35.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b35(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_PAGE_UP, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 33.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b33(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_DELETE, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character with prefix 1b 5b 36.
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_1b5b36(vt100_t *vt, char c)
+{
+	switch (c) {
+	case 0x7e:
+		vt100_key(vt, 0, KC_PAGE_DOWN, 0);
+		break;
+	default:
+		vt->state = vts_base;
+		break;
+	}
+}
+
+/** Process input character (base state, no prefix).
+ *
+ * @param vt VT instance
+ * @param c Input character
+ */
+static void vt100_rcvd_base(vt100_t *vt, char c)
+{
+	switch (c) {
+		/*
+		 * Not shifted
+		 */
+	case 0x60:
+		vt100_key(vt, 0, KC_BACKTICK, c);
+		break;
+
+	case 0x31:
+		vt100_key(vt, 0, KC_1, c);
+		break;
+	case 0x32:
+		vt100_key(vt, 0, KC_2, c);
+		break;
+	case 0x33:
+		vt100_key(vt, 0, KC_3, c);
+		break;
+	case 0x34:
+		vt100_key(vt, 0, KC_4, c);
+		break;
+	case 0x35:
+		vt100_key(vt, 0, KC_5, c);
+		break;
+	case 0x36:
+		vt100_key(vt, 0, KC_6, c);
+		break;
+	case 0x37:
+		vt100_key(vt, 0, KC_7, c);
+		break;
+	case 0x38:
+		vt100_key(vt, 0, KC_8, c);
+		break;
+	case 0x39:
+		vt100_key(vt, 0, KC_9, c);
+		break;
+	case 0x30:
+		vt100_key(vt, 0, KC_0, c);
+		break;
+	case 0x2d:
+		vt100_key(vt, 0, KC_MINUS, c);
+		break;
+	case 0x3d:
+		vt100_key(vt, 0, KC_EQUALS, c);
+		break;
+
+	case 0x08:
+		vt100_key(vt, 0, KC_BACKSPACE, c);
+		break;
+
+	case 0x09:
+		vt100_key(vt, 0, KC_TAB, c);
+		break;
+
+	case 0x71:
+		vt100_key(vt, 0, KC_Q, c);
+		break;
+	case 0x77:
+		vt100_key(vt, 0, KC_W, c);
+		break;
+	case 0x65:
+		vt100_key(vt, 0, KC_E, c);
+		break;
+	case 0x72:
+		vt100_key(vt, 0, KC_R, c);
+		break;
+	case 0x74:
+		vt100_key(vt, 0, KC_T, c);
+		break;
+	case 0x79:
+		vt100_key(vt, 0, KC_Y, c);
+		break;
+	case 0x75:
+		vt100_key(vt, 0, KC_U, c);
+		break;
+	case 0x69:
+		vt100_key(vt, 0, KC_I, c);
+		break;
+	case 0x6f:
+		vt100_key(vt, 0, KC_O, c);
+		break;
+	case 0x70:
+		vt100_key(vt, 0, KC_P, c);
+		break;
+
+	case 0x5b:
+		vt100_key(vt, 0, KC_LBRACKET, c);
+		break;
+	case 0x5d:
+		vt100_key(vt, 0, KC_RBRACKET, c);
+		break;
+
+	case 0x61:
+		vt100_key(vt, 0, KC_A, c);
+		break;
+	case 0x73:
+		vt100_key(vt, 0, KC_S, c);
+		break;
+	case 0x64:
+		vt100_key(vt, 0, KC_D, c);
+		break;
+	case 0x66:
+		vt100_key(vt, 0, KC_F, c);
+		break;
+	case 0x67:
+		vt100_key(vt, 0, KC_G, c);
+		break;
+	case 0x68:
+		vt100_key(vt, 0, KC_H, c);
+		break;
+	case 0x6a:
+		vt100_key(vt, 0, KC_J, c);
+		break;
+	case 0x6b:
+		vt100_key(vt, 0, KC_K, c);
+		break;
+	case 0x6c:
+		vt100_key(vt, 0, KC_L, c);
+		break;
+
+	case 0x3b:
+		vt100_key(vt, 0, KC_SEMICOLON, c);
+		break;
+	case 0x27:
+		vt100_key(vt, 0, KC_QUOTE, c);
+		break;
+	case 0x5c:
+		vt100_key(vt, 0, KC_BACKSLASH, c);
+		break;
+
+	case 0x7a:
+		vt100_key(vt, 0, KC_Z, c);
+		break;
+	case 0x78:
+		vt100_key(vt, 0, KC_X, c);
+		break;
+	case 0x63:
+		vt100_key(vt, 0, KC_C, c);
+		break;
+	case 0x76:
+		vt100_key(vt, 0, KC_V, c);
+		break;
+	case 0x62:
+		vt100_key(vt, 0, KC_B, c);
+		break;
+	case 0x6e:
+		vt100_key(vt, 0, KC_N, c);
+		break;
+	case 0x6d:
+		vt100_key(vt, 0, KC_M, c);
+		break;
+
+	case 0x2c:
+		vt100_key(vt, 0, KC_COMMA, c);
+		break;
+	case 0x2e:
+		vt100_key(vt, 0, KC_PERIOD, c);
+		break;
+	case 0x2f:
+		vt100_key(vt, 0, KC_SLASH, c);
+		break;
+
+		/*
+		 * Shifted
+		 */
+	case 0x7e:
+		vt100_key(vt, KM_SHIFT, KC_BACKTICK, c);
+		break;
+
+	case 0x21:
+		vt100_key(vt, KM_SHIFT, KC_1, c);
+		break;
+	case 0x40:
+		vt100_key(vt, KM_SHIFT, KC_2, c);
+		break;
+	case 0x23:
+		vt100_key(vt, KM_SHIFT, KC_3, c);
+		break;
+	case 0x24:
+		vt100_key(vt, KM_SHIFT, KC_4, c);
+		break;
+	case 0x25:
+		vt100_key(vt, KM_SHIFT, KC_5, c);
+		break;
+	case 0x5e:
+		vt100_key(vt, KM_SHIFT, KC_6, c);
+		break;
+	case 0x26:
+		vt100_key(vt, KM_SHIFT, KC_7, c);
+		break;
+	case 0x2a:
+		vt100_key(vt, KM_SHIFT, KC_8, c);
+		break;
+	case 0x28:
+		vt100_key(vt, KM_SHIFT, KC_9, c);
+		break;
+	case 0x29:
+		vt100_key(vt, KM_SHIFT, KC_0, c);
+		break;
+	case 0x5f:
+		vt100_key(vt, KM_SHIFT, KC_MINUS, c);
+		break;
+	case 0x2b:
+		vt100_key(vt, KM_SHIFT, KC_EQUALS, c);
+		break;
+
+	case 0x51:
+		vt100_key(vt, KM_SHIFT, KC_Q, c);
+		break;
+	case 0x57:
+		vt100_key(vt, KM_SHIFT, KC_W, c);
+		break;
+	case 0x45:
+		vt100_key(vt, KM_SHIFT, KC_E, c);
+		break;
+	case 0x52:
+		vt100_key(vt, KM_SHIFT, KC_R, c);
+		break;
+	case 0x54:
+		vt100_key(vt, KM_SHIFT, KC_T, c);
+		break;
+	case 0x59:
+		vt100_key(vt, KM_SHIFT, KC_Y, c);
+		break;
+	case 0x55:
+		vt100_key(vt, KM_SHIFT, KC_U, c);
+		break;
+	case 0x49:
+		vt100_key(vt, KM_SHIFT, KC_I, c);
+		break;
+	case 0x4f:
+		vt100_key(vt, KM_SHIFT, KC_O, c);
+		break;
+	case 0x50:
+		vt100_key(vt, KM_SHIFT, KC_P, c);
+		break;
+
+	case 0x7b:
+		vt100_key(vt, KM_SHIFT, KC_LBRACKET, c);
+		break;
+	case 0x7d:
+		vt100_key(vt, KM_SHIFT, KC_RBRACKET, c);
+		break;
+
+	case 0x41:
+		vt100_key(vt, KM_SHIFT, KC_A, c);
+		break;
+	case 0x53:
+		vt100_key(vt, KM_SHIFT, KC_S, c);
+		break;
+	case 0x44:
+		vt100_key(vt, KM_SHIFT, KC_D, c);
+		break;
+	case 0x46:
+		vt100_key(vt, KM_SHIFT, KC_F, c);
+		break;
+	case 0x47:
+		vt100_key(vt, KM_SHIFT, KC_G, c);
+		break;
+	case 0x48:
+		vt100_key(vt, KM_SHIFT, KC_H, c);
+		break;
+	case 0x4a:
+		vt100_key(vt, KM_SHIFT, KC_J, c);
+		break;
+	case 0x4b:
+		vt100_key(vt, KM_SHIFT, KC_K, c);
+		break;
+	case 0x4c:
+		vt100_key(vt, KM_SHIFT, KC_L, c);
+		break;
+
+	case 0x3a:
+		vt100_key(vt, KM_SHIFT, KC_SEMICOLON, c);
+		break;
+	case 0x22:
+		vt100_key(vt, KM_SHIFT, KC_QUOTE, c);
+		break;
+	case 0x7c:
+		vt100_key(vt, KM_SHIFT, KC_BACKSLASH, c);
+		break;
+
+	case 0x5a:
+		vt100_key(vt, KM_SHIFT, KC_Z, c);
+		break;
+	case 0x58:
+		vt100_key(vt, KM_SHIFT, KC_X, c);
+		break;
+	case 0x43:
+		vt100_key(vt, KM_SHIFT, KC_C, c);
+		break;
+	case 0x56:
+		vt100_key(vt, KM_SHIFT, KC_V, c);
+		break;
+	case 0x42:
+		vt100_key(vt, KM_SHIFT, KC_B, c);
+		break;
+	case 0x4e:
+		vt100_key(vt, KM_SHIFT, KC_N, c);
+		break;
+	case 0x4d:
+		vt100_key(vt, KM_SHIFT, KC_M, c);
+		break;
+
+	case 0x3c:
+		vt100_key(vt, KM_SHIFT, KC_COMMA, c);
+		break;
+	case 0x3e:
+		vt100_key(vt, KM_SHIFT, KC_PERIOD, c);
+		break;
+	case 0x3f:
+		vt100_key(vt, KM_SHIFT, KC_SLASH, c);
+		break;
+
+		/*
+		 * ...
+		 */
+	case 0x20:
+		vt100_key(vt, 0, KC_SPACE, c);
+		break;
+	case 0x0a:
+		vt100_key(vt, 0, KC_ENTER, '\n');
+		break;
+	case 0x0d:
+		vt100_key(vt, 0, KC_ENTER, '\n');
+		break;
+
+		/*
+		 * Ctrl + key
+		 */
+	case 0x11:
+		vt100_key(vt, KM_CTRL, KC_Q, c);
+		break;
+	case 0x17:
+		vt100_key(vt, KM_CTRL, KC_W, c);
+		break;
+	case 0x05:
+		vt100_key(vt, KM_CTRL, KC_E, c);
+		break;
+	case 0x12:
+		vt100_key(vt, KM_CTRL, KC_R, c);
+		break;
+	case 0x14:
+		vt100_key(vt, KM_CTRL, KC_T, c);
+		break;
+	case 0x19:
+		vt100_key(vt, KM_CTRL, KC_Y, c);
+		break;
+	case 0x15:
+		vt100_key(vt, KM_CTRL, KC_U, c);
+		break;
+	case 0x0f:
+		vt100_key(vt, KM_CTRL, KC_O, c);
+		break;
+	case 0x10:
+		vt100_key(vt, KM_CTRL, KC_P, c);
+		break;
+
+	case 0x01:
+		vt100_key(vt, KM_CTRL, KC_A, c);
+		break;
+	case 0x13:
+		vt100_key(vt, KM_CTRL, KC_S, c);
+		break;
+	case 0x04:
+		vt100_key(vt, KM_CTRL, KC_D, c);
+		break;
+	case 0x06:
+		vt100_key(vt, KM_CTRL, KC_F, c);
+		break;
+	case 0x07:
+		vt100_key(vt, KM_CTRL, KC_G, c);
+		break;
+	case 0x0b:
+		vt100_key(vt, KM_CTRL, KC_K, c);
+		break;
+	case 0x0c:
+		vt100_key(vt, KM_CTRL, KC_L, c);
+		break;
+
+	case 0x1a:
+		vt100_key(vt, KM_CTRL, KC_Z, c);
+		break;
+	case 0x18:
+		vt100_key(vt, KM_CTRL, KC_X, c);
+		break;
+	case 0x03:
+		vt100_key(vt, KM_CTRL, KC_C, c);
+		break;
+	case 0x16:
+		vt100_key(vt, KM_CTRL, KC_V, c);
+		break;
+	case 0x02:
+		vt100_key(vt, KM_CTRL, KC_B, c);
+		break;
+	case 0x0e:
+		vt100_key(vt, KM_CTRL, KC_N, c);
+		break;
+
+	case 0x7f:
+		vt100_key(vt, 0, KC_BACKSPACE, '\b');
+		break;
+
+	case 0x1b:
+		vt->state = vts_1b;
+		break;
+	}
+}
+
+void vt100_rcvd_char(vt100_t *vt, char c)
+{
+	switch (vt->state) {
+	case vts_base:
+		vt100_rcvd_base(vt, c);
+		break;
+	case vts_1b:
+		vt100_rcvd_1b(vt, c);
+		break;
+	case vts_1b4f:
+		vt100_rcvd_1b4f(vt, c);
+		break;
+	case vts_1b5b:
+		vt100_rcvd_1b5b(vt, c);
+		break;
+	case vts_1b5b31:
+		vt100_rcvd_1b5b31(vt, c);
+		break;
+	case vts_1b5b3135:
+		vt100_rcvd_1b5b3135(vt, c);
+		break;
+	case vts_1b5b3137:
+		vt100_rcvd_1b5b3137(vt, c);
+		break;
+	case vts_1b5b3138:
+		vt100_rcvd_1b5b3138(vt, c);
+		break;
+	case vts_1b5b3139:
+		vt100_rcvd_1b5b3139(vt, c);
+		break;
+	case vts_1b5b32:
+		vt100_rcvd_1b5b32(vt, c);
+		break;
+	case vts_1b5b3230:
+		vt100_rcvd_1b5b3230(vt, c);
+		break;
+	case vts_1b5b3231:
+		vt100_rcvd_1b5b3231(vt, c);
+		break;
+	case vts_1b5b3233:
+		vt100_rcvd_1b5b3233(vt, c);
+		break;
+	case vts_1b5b3234:
+		vt100_rcvd_1b5b3234(vt, c);
+		break;
+	case vts_1b5b3235:
+		vt100_rcvd_1b5b3235(vt, c);
+		break;
+	case vts_1b5b3238:
+		vt100_rcvd_1b5b3238(vt, c);
+		break;
+	case vts_1b5b35:
+		vt100_rcvd_1b5b35(vt, c);
+		break;
+	case vts_1b5b33:
+		vt100_rcvd_1b5b33(vt, c);
+		break;
+	case vts_1b5b36:
+		vt100_rcvd_1b5b36(vt, c);
+		break;
+	}
 }
 
Index: uspace/srv/hid/remcons/remcons.c
===================================================================
--- uspace/srv/hid/remcons/remcons.c	(revision d31c3ea70b393289736cdac0e91fa5c5eba06c4c)
+++ uspace/srv/hid/remcons/remcons.c	(revision 6907f26c4f1d524ca40bdb6622b8dc0254203a1f)
@@ -34,4 +34,5 @@
  */
 
+#include <adt/prodcons.h>
 #include <as.h>
 #include <async.h>
@@ -124,9 +125,11 @@
 static void remcons_vt_cputs(void *, const char *);
 static void remcons_vt_flush(void *);
+static void remcons_vt_key(void *, keymod_t, keycode_t, char);
 
 static vt100_cb_t remcons_vt_cb = {
 	.putuchar = remcons_vt_putchar,
 	.control_puts = remcons_vt_cputs,
-	.flush = remcons_vt_flush
+	.flush = remcons_vt_flush,
+	.key = remcons_vt_key
 };
 
@@ -340,19 +343,48 @@
 }
 
+/** Creates new keyboard event from given char.
+ *
+ * @param type Event type (press / release).
+ * @param c Pressed character.
+ */
+static kbd_event_t *new_kbd_event(kbd_event_type_t type, keymod_t mods,
+    keycode_t key, char c)
+{
+	kbd_event_t *event = malloc(sizeof(kbd_event_t));
+	assert(event);
+
+	link_initialize(&event->link);
+	event->type = type;
+	event->mods = mods;
+	event->key = key;
+	event->c = c;
+
+	return event;
+}
+
 static errno_t remcons_get_event(con_srv_t *srv, cons_event_t *event)
 {
+	remcons_t *remcons = srv_to_remcons(srv);
 	telnet_user_t *user = srv_to_user(srv);
-	kbd_event_t kevent;
-	errno_t rc;
-
-	rc = telnet_user_get_next_keyboard_event(user, &kevent);
-	if (rc != EOK) {
-		/* XXX What? */
-		memset(event, 0, sizeof(*event));
-		return EOK;
-	}
+	size_t nread;
+
+	while (list_empty(&remcons->in_events.list)) {
+		char next_byte = 0;
+
+		errno_t rc = telnet_user_recv(user, &next_byte, 1,
+		    &nread);
+		if (rc != EOK)
+			return rc;
+
+		vt100_rcvd_char(remcons->vt, next_byte);
+	}
+
+	link_t *link = prodcons_consume(&remcons->in_events);
+	kbd_event_t *tmp = list_get_instance(link, kbd_event_t, link);
 
 	event->type = CEV_KEY;
-	event->ev.key = kevent;
+	event->ev.key = *tmp;
+
+	free(tmp);
 
 	return EOK;
@@ -551,4 +583,16 @@
 }
 
+static void remcons_vt_key(void *arg, keymod_t mods, keycode_t key, char c)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+
+	kbd_event_t *down = new_kbd_event(KEY_PRESS, mods, key, c);
+	kbd_event_t *up = new_kbd_event(KEY_RELEASE, mods, key, c);
+	assert(down);
+	assert(up);
+	prodcons_produce(&remcons->in_events, &down->link);
+	prodcons_produce(&remcons->in_events, &up->link);
+}
+
 /** Handle network connection.
  *
@@ -567,4 +611,5 @@
 	remcons->enable_rgb = !no_ctl && !no_rgb;
 	remcons->user = user;
+	prodcons_initialize(&remcons->in_events);
 
 	if (remcons->enable_ctl) {
Index: uspace/srv/hid/remcons/remcons.h
===================================================================
--- uspace/srv/hid/remcons/remcons.h	(revision d31c3ea70b393289736cdac0e91fa5c5eba06c4c)
+++ uspace/srv/hid/remcons/remcons.h	(revision 6907f26c4f1d524ca40bdb6622b8dc0254203a1f)
@@ -37,4 +37,5 @@
 #define REMCONS_H_
 
+#include <adt/prodcons.h>
 #include <stdbool.h>
 #include <vt/vt100.h>
@@ -54,4 +55,7 @@
 	charfield_t *ubuf;	/**< user buffer */
 	bool curs_visible;	/**< cursor is visible */
+
+	/** Producer-consumer of kbd_event_t. */
+	prodcons_t in_events;
 } remcons_t;
 
Index: uspace/srv/hid/remcons/user.c
===================================================================
--- uspace/srv/hid/remcons/user.c	(revision d31c3ea70b393289736cdac0e91fa5c5eba06c4c)
+++ uspace/srv/hid/remcons/user.c	(revision 6907f26c4f1d524ca40bdb6622b8dc0254203a1f)
@@ -83,5 +83,4 @@
 	user->conn = conn;
 	user->service_id = (service_id_t) -1;
-	prodcons_initialize(&user->in_events);
 	link_initialize(&user->link);
 	user->socket_buffer_len = 0;
@@ -211,7 +210,9 @@
 }
 
-/** Receive next byte from a socket (use buffering.
- * We need to return the value via extra argument because the read byte
- * might be negative.
+/** Receive next byte from a socket (use buffering).
+ *
+ * @param user Telnet user
+ * @param byte Place to store the received byte
+ * @return EOK on success or an error code
  */
 static errno_t telnet_user_recv_next_byte_locked(telnet_user_t *user, char *byte)
@@ -230,57 +231,12 @@
 }
 
-errno_t telnet_user_recv(telnet_user_t *user, void *data, size_t size,
-    size_t *nread)
-{
-	errno_t rc;
-	size_t nb;
-
-	/* No more buffered data? */
-	if (user->socket_buffer_len <= user->socket_buffer_pos) {
-		rc = telnet_user_fill_recv_buf(user);
-		if (rc != EOK)
-			return rc;
-	}
-
-	nb = user->socket_buffer_len - user->socket_buffer_pos;
-	memcpy(data, user->socket_buffer + user->socket_buffer_pos, nb);
-	user->socket_buffer_pos += nb;
-	*nread = nb;
-	return EOK;
-}
-
-/** Creates new keyboard event from given char.
- *
- * @param type Event type (press / release).
- * @param c Pressed character.
- */
-static kbd_event_t *new_kbd_event(kbd_event_type_t type, char32_t c)
-{
-	kbd_event_t *event = malloc(sizeof(kbd_event_t));
-	assert(event);
-
-	link_initialize(&event->link);
-	event->type = type;
-	event->c = c;
-	event->mods = 0;
-
-	switch (c) {
-	case '\n':
-		event->key = KC_ENTER;
-		break;
-	case '\t':
-		event->key = KC_TAB;
-		break;
-	case '\b':
-	case 127: /* This is what Linux telnet sends. */
-		event->key = KC_BACKSPACE;
-		event->c = '\b';
-		break;
-	default:
-		event->key = KC_A;
-		break;
-	}
-
-	return event;
+/** Determine if a received byte is available without waiting.
+ *
+ * @param user Telnet user
+ * @return @c true iff a byte is currently available
+ */
+static bool telnet_user_byte_avail(telnet_user_t *user)
+{
+	return user->socket_buffer_len > user->socket_buffer_pos;
 }
 
@@ -303,14 +259,22 @@
 }
 
-/** Get next keyboard event.
+/** Receive data from telnet connection.
  *
  * @param user Telnet user.
- * @param event Where to store the keyboard event.
- * @return Error code.
- */
-errno_t telnet_user_get_next_keyboard_event(telnet_user_t *user, kbd_event_t *event)
-{
-	fibril_mutex_lock(&user->guard);
-	if (list_empty(&user->in_events.list)) {
+ * @param buf Destination buffer
+ * @param size Buffer size
+ * @param nread Place to store number of bytes read (>0 on success)
+ * @return EOK on success or an error code
+ */
+errno_t telnet_user_recv(telnet_user_t *user, void *buf, size_t size,
+    size_t *nread)
+{
+	uint8_t *bp = (uint8_t *)buf;
+	fibril_mutex_lock(&user->guard);
+
+	assert(size > 0);
+	*nread = 0;
+
+	do {
 		char next_byte = 0;
 		bool inside_telnet_command = false;
@@ -319,13 +283,12 @@
 
 		/* Skip zeros, bail-out on error. */
-		while (next_byte == 0) {
-			fibril_mutex_unlock(&user->guard);
-
-			errno_t rc = telnet_user_recv_next_byte_locked(user, &next_byte);
-			if (rc != EOK)
+		do {
+			errno_t rc = telnet_user_recv_next_byte_locked(user,
+			    &next_byte);
+			if (rc != EOK) {
+				fibril_mutex_unlock(&user->guard);
 				return rc;
-
+			}
 			uint8_t byte = (uint8_t) next_byte;
-			fibril_mutex_lock(&user->guard);
 
 			/* Skip telnet commands. */
@@ -345,5 +308,5 @@
 				next_byte = 0;
 			}
-		}
+		} while (next_byte == 0 && telnet_user_byte_avail(user));
 
 		/* CR-LF conversions. */
@@ -352,21 +315,12 @@
 		}
 
-		kbd_event_t *down = new_kbd_event(KEY_PRESS, next_byte);
-		kbd_event_t *up = new_kbd_event(KEY_RELEASE, next_byte);
-		assert(down);
-		assert(up);
-		prodcons_produce(&user->in_events, &down->link);
-		prodcons_produce(&user->in_events, &up->link);
-	}
-
-	link_t *link = prodcons_consume(&user->in_events);
-	kbd_event_t *tmp = list_get_instance(link, kbd_event_t, link);
-
-	fibril_mutex_unlock(&user->guard);
-
-	*event = *tmp;
-
-	free(tmp);
-
+		if (next_byte != 0) {
+			*bp++ = next_byte;
+			++*nread;
+			--size;
+		}
+	} while (size > 0 && (telnet_user_byte_avail(user) || *nread == 0));
+
+	fibril_mutex_unlock(&user->guard);
 	return EOK;
 }
Index: uspace/srv/hid/remcons/user.h
===================================================================
--- uspace/srv/hid/remcons/user.h	(revision d31c3ea70b393289736cdac0e91fa5c5eba06c4c)
+++ uspace/srv/hid/remcons/user.h	(revision 6907f26c4f1d524ca40bdb6622b8dc0254203a1f)
@@ -37,5 +37,4 @@
 #define TELNET_USER_H_
 
-#include <adt/prodcons.h>
 #include <fibril_synch.h>
 #include <inet/tcp.h>
@@ -62,6 +61,4 @@
 	con_srvs_t srvs;
 
-	/** Producer-consumer of kbd_event_t. */
-	prodcons_t in_events;
 	link_t link;
 	char socket_buffer[BUFFER_SIZE];
