Index: uspace/lib/meson.build
===================================================================
--- uspace/lib/meson.build	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/lib/meson.build	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -121,4 +121,5 @@
 
 	'ui',
+	'vt',
 ]
 
Index: uspace/lib/vt/include/vt/vt100.h
===================================================================
--- uspace/lib/vt/include/vt/vt100.h	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
+++ uspace/lib/vt/include/vt/vt100.h	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2024 Jiri Svoboda
+ * Copyright (c) 2011 Martin Decky
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libvt
+ * @{
+ */
+
+#ifndef LIBVT_VT100_H_
+#define LIBVT_VT100_H_
+
+#include <io/charfield.h>
+#include <ipc/common.h>
+#include <uchar.h>
+
+/** Buffer size when creating actual VT100 commands.
+ *
+ * This is absurdly large but since we accept numbers via sysarg_t,
+ * we make it big enough for the largest value to be on the safe side
+ * (and to silence compiler too).
+ *
+ * TODO: find out if VT100 has some hard limits or perhaps simply cut-out
+ * values larger than 16 bits or something.
+ */
+#define MAX_CONTROL 64
+
+typedef enum {
+	CI_BLACK   = 0,
+	CI_RED     = 1,
+	CI_GREEN   = 2,
+	CI_BROWN   = 3,
+	CI_BLUE    = 4,
+	CI_MAGENTA = 5,
+	CI_CYAN    = 6,
+	CI_WHITE   = 7
+} sgr_color_index_t;
+
+typedef enum {
+	SGR_RESET       = 0,
+	SGR_BOLD        = 1,
+	SGR_UNDERLINE   = 4,
+	SGR_BLINK       = 5,
+	SGR_REVERSE     = 7,
+	SGR_FGCOLOR     = 30,
+	SGR_BGCOLOR     = 40
+} sgr_command_t;
+
+typedef void (*vt100_putuchar_t)(void *, char32_t ch);
+typedef void (*vt100_control_puts_t)(void *, const char *str);
+typedef void (*vt100_flush_t)(void *);
+
+typedef struct {
+	sysarg_t cols;
+	sysarg_t rows;
+
+	sysarg_t cur_col;
+	sysarg_t cur_row;
+	char_attrs_t cur_attrs;
+
+	bool enable_rgb;
+
+	void *arg;
+	vt100_putuchar_t putuchar;
+	vt100_control_puts_t control_puts;
+	vt100_flush_t flush;
+} vt100_state_t;
+
+extern sgr_color_index_t color_map[];
+
+extern vt100_state_t *vt100_state_create(void *, sysarg_t, sysarg_t,
+    vt100_putuchar_t, vt100_control_puts_t, vt100_flush_t);
+extern void vt100_state_destroy(vt100_state_t *);
+
+extern errno_t vt100_yield(vt100_state_t *);
+extern errno_t vt100_claim(vt100_state_t *);
+extern void vt100_get_dimensions(vt100_state_t *, sysarg_t *, sysarg_t *);
+
+extern void vt100_cls(vt100_state_t *);
+extern void vt100_set_pos(vt100_state_t *, sysarg_t, sysarg_t);
+extern void vt100_goto(vt100_state_t *, sysarg_t, sysarg_t);
+extern void vt100_set_sgr(vt100_state_t *, char_attrs_t);
+extern void vt100_set_attr(vt100_state_t *, char_attrs_t);
+extern void vt100_cursor_visibility(vt100_state_t *, bool);
+extern void vt100_putuchar(vt100_state_t *, char32_t);
+extern void vt100_flush(vt100_state_t *);
+
+#endif
+
+/** @}
+ */
Index: uspace/lib/vt/meson.build
===================================================================
--- uspace/lib/vt/meson.build	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
+++ uspace/lib/vt/meson.build	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -0,0 +1,32 @@
+#
+# Copyright (c) 2024 Jiri Svoboda
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# - Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in the
+#   documentation and/or other materials provided with the distribution.
+# - The name of the author may not be used to endorse or promote products
+#   derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+deps = [ 'output' ]
+src = files(
+	'src/vt100.c'
+)
Index: uspace/lib/vt/src/vt100.c
===================================================================
--- uspace/lib/vt/src/vt100.c	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
+++ uspace/lib/vt/src/vt100.c	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2024 Jiri Svoboda
+ * Copyright (c) 2011 Martin Decky
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup libvt
+ * @{
+ */
+
+#include <errno.h>
+#include <io/color.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <vt/vt100.h>
+
+sgr_color_index_t color_map[] = {
+	[COLOR_BLACK]   = CI_BLACK,
+	[COLOR_BLUE]    = CI_BLUE,
+	[COLOR_GREEN]   = CI_GREEN,
+	[COLOR_CYAN]    = CI_CYAN,
+	[COLOR_RED]     = CI_RED,
+	[COLOR_MAGENTA] = CI_MAGENTA,
+	[COLOR_YELLOW]  = CI_BROWN,
+	[COLOR_WHITE]   = CI_WHITE
+};
+
+void vt100_cls(vt100_state_t *state)
+{
+	state->control_puts(state->arg, "\033[2J");
+}
+
+/** ECMA-48 Set Graphics Rendition. */
+static void vt100_sgr(vt100_state_t *state, unsigned int mode)
+{
+	char control[MAX_CONTROL];
+
+	snprintf(control, MAX_CONTROL, "\033[%um", mode);
+	state->control_puts(state->arg, control);
+}
+
+/** Set Graphics Rendition with 5 arguments. */
+static void vt100_sgr5(vt100_state_t *state, unsigned a1, unsigned a2,
+    unsigned a3, unsigned a4, unsigned a5)
+{
+	char control[MAX_CONTROL];
+
+	snprintf(control, MAX_CONTROL, "\033[%u;%u;%u;%u;%um",
+	    a1, a2, a3, a4, a5);
+	state->control_puts(state->arg, control);
+}
+
+void vt100_set_pos(vt100_state_t *state, sysarg_t col, sysarg_t row)
+{
+	char control[MAX_CONTROL];
+
+	snprintf(control, MAX_CONTROL, "\033[%" PRIun ";%" PRIun "f",
+	    row + 1, col + 1);
+	state->control_puts(state->arg, control);
+}
+
+void vt100_set_sgr(vt100_state_t *state, char_attrs_t attrs)
+{
+	unsigned color;
+
+	switch (attrs.type) {
+	case CHAR_ATTR_STYLE:
+		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);
+			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);
+			break;
+		case STYLE_INVERTED:
+			vt100_sgr(state, SGR_RESET);
+			vt100_sgr(state, SGR_BGCOLOR + CI_BLACK);
+			vt100_sgr(state, 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);
+			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]);
+
+		if (attrs.val.index.attr & CATTR_BRIGHT)
+			vt100_sgr(state, SGR_BOLD);
+
+		break;
+	case CHAR_ATTR_RGB:
+		if (state->enable_rgb == true) {
+			vt100_sgr5(state, 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),
+			    GREEN(attrs.val.rgb.fgcolor),
+			    BLUE(attrs.val.rgb.fgcolor));
+		} else {
+			vt100_sgr(state, 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]);
+			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]);
+		}
+		break;
+	}
+}
+
+vt100_state_t *vt100_state_create(void *arg, sysarg_t cols, sysarg_t rows,
+    vt100_putuchar_t putuchar_fn, vt100_control_puts_t control_puts_fn,
+    vt100_flush_t flush_fn)
+{
+	vt100_state_t *state = malloc(sizeof(vt100_state_t));
+	if (state == NULL)
+		return NULL;
+
+	state->arg = arg;
+	state->putuchar = putuchar_fn;
+	state->control_puts = control_puts_fn;
+	state->flush = flush_fn;
+
+	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_state_destroy(vt100_state_t *state)
+{
+	free(state);
+}
+
+void vt100_get_dimensions(vt100_state_t *state, sysarg_t *cols,
+    sysarg_t *rows)
+{
+	*cols = state->cols;
+	*rows = state->rows;
+}
+
+errno_t vt100_yield(vt100_state_t *state)
+{
+	return EOK;
+}
+
+errno_t vt100_claim(vt100_state_t *state)
+{
+	return EOK;
+}
+
+void vt100_goto(vt100_state_t *state, sysarg_t col, sysarg_t row)
+{
+	if ((col >= state->cols) || (row >= state->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_state_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_state_t *state, bool visible)
+{
+	if (visible)
+		state->control_puts(state->arg, "\033[?25h");
+	else
+		state->control_puts(state->arg, "\033[?25l");
+}
+
+void vt100_putuchar(vt100_state_t *state, char32_t ch)
+{
+	state->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_state_t *state)
+{
+	state->flush(state->arg);
+}
+
+/** @}
+ */
Index: uspace/srv/hid/output/ctl/serial.c
===================================================================
--- uspace/srv/hid/output/ctl/serial.c	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/output/ctl/serial.c	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2006 Ondrej Palkovsky
  * Copyright (c) 2008 Martin Decky
@@ -37,6 +38,6 @@
 #include <errno.h>
 #include <io/chargrid.h>
+#include <vt/vt100.h>
 #include "../output.h"
-#include "../proto/vt100.h"
 #include "serial.h"
 
@@ -84,5 +85,5 @@
 static console_caps_t serial_get_caps(outdev_t *dev)
 {
-	return (CONSOLE_CAP_STYLE | CONSOLE_CAP_INDEXED);
+	return (CONSOLE_CAP_STYLE | CONSOLE_CAP_INDEXED | CONSOLE_CAP_RGB);
 }
 
@@ -125,9 +126,17 @@
     vt100_control_puts_t control_puts_fn, vt100_flush_t flush_fn)
 {
+	char_attrs_t attrs;
 	vt100_state_t *state =
-	    vt100_state_create(SERIAL_COLS, SERIAL_ROWS, putuchar_fn,
+	    vt100_state_create(NULL, SERIAL_COLS, SERIAL_ROWS, putuchar_fn,
 	    control_puts_fn, flush_fn);
 	if (state == NULL)
 		return ENOMEM;
+	state->enable_rgb = true;
+
+	vt100_cursor_visibility(state, false);
+	attrs.type = CHAR_ATTR_STYLE;
+	attrs.val.style = STYLE_NORMAL;
+	vt100_set_attr(state, attrs);
+	vt100_cls(state);
 
 	outdev_t *dev = outdev_register(&serial_ops, state);
Index: uspace/srv/hid/output/ctl/serial.h
===================================================================
--- uspace/srv/hid/output/ctl/serial.h	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/output/ctl/serial.h	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2006 Ondrej Palkovsky
  * Copyright (c) 2008 Martin Decky
@@ -35,5 +36,5 @@
 #define OUTPUT_CTL_SERIAL_H_
 
-#include "../proto/vt100.h"
+#include <vt/vt100.h>
 
 extern errno_t serial_init(vt100_putuchar_t, vt100_control_puts_t, vt100_flush_t);
Index: uspace/srv/hid/output/meson.build
===================================================================
--- uspace/srv/hid/output/meson.build	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/output/meson.build	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,3 +1,4 @@
 #
+# Copyright (c) 2024 Jiri Svoboda
 # Copyright (c) 2005 Martin Decky
 # Copyright (c) 2007 Jakub Jermar
@@ -28,5 +29,6 @@
 #
 
-deps = [ 'codepage', 'console', 'drv', 'fbfont', 'pixconv', 'ddev', 'output' ]
+deps = [ 'codepage', 'console', 'drv', 'fbfont', 'pixconv', 'ddev', 'output',
+    'vt' ]
 src = files(
 	'ctl/serial.c',
@@ -34,5 +36,4 @@
 	'port/chardev.c',
 	'port/ddev.c',
-	'proto/vt100.c',
 	'output.c',
 )
Index: uspace/srv/hid/output/output.c
===================================================================
--- uspace/srv/hid/output/output.c	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/output/output.c	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2011 Martin Decky
  * All rights reserved.
@@ -40,4 +40,5 @@
 #include <ipc/output.h>
 #include <config.h>
+#include <vt/vt100.h>
 #include "port/ega.h"
 #include "port/chardev.h"
Index: uspace/srv/hid/output/port/chardev.c
===================================================================
--- uspace/srv/hid/output/port/chardev.c	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/output/port/chardev.c	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,5 +1,5 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2016 Jakub Jermar
- * Copyright (c) 2017 Jiri Svoboda
  * All rights reserved.
  *
@@ -65,7 +65,9 @@
 static FIBRIL_CONDVAR_INITIALIZE(discovery_cv);
 
-static void chardev_flush(void)
+static void chardev_flush(void *arg)
 {
 	size_t nwr;
+
+	(void)arg;
 
 	if (chardev_bused == 0)
@@ -81,9 +83,9 @@
 {
 	if (chardev_bused == chardev_buf_size)
-		chardev_flush();
+		chardev_flush(NULL);
 	chardev_buf[chardev_bused++] = (uint8_t) ch;
 }
 
-static void chardev_putuchar(char32_t ch)
+static void chardev_putuchar(void *arg, char32_t ch)
 {
 	char buf[STR_BOUNDS(1)];
@@ -92,4 +94,6 @@
 	errno_t rc;
 
+	(void)arg;
+
 	off = 0;
 	rc = chr_encode(ch, buf, &off, sizeof(buf));
@@ -101,5 +105,5 @@
 }
 
-static void chardev_control_puts(const char *str)
+static void chardev_control_puts(void *arg, const char *str)
 {
 	const char *p;
@@ -107,5 +111,5 @@
 	p = str;
 	while (*p != '\0')
-		chardev_putuchar(*p++);
+		chardev_putuchar(arg, *p++);
 }
 
Index: uspace/srv/hid/output/proto/vt100.c
===================================================================
--- uspace/srv/hid/output/proto/vt100.c	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ 	(revision )
@@ -1,249 +1,0 @@
-/*
- * Copyright (c) 2021 Jiri Svoboda
- * Copyright (c) 2011 Martin Decky
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * - Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- * - The name of the author may not be used to endorse or promote products
- *   derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/** @addtogroup output
- * @{
- */
-
-#include <inttypes.h>
-#include <errno.h>
-#include <stddef.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <io/color.h>
-#include <types/common.h>
-#include "vt100.h"
-
-/** Buffer size when creating actual VT100 commands.
- *
- * This is absurdly large but since we accept numbers via sysarg_t,
- * we make it big enough for the largest value to be on the safe side
- * (and to silence compiler too).
- *
- * TODO: find out if VT100 has some hard limits or perhaps simply cut-out
- * values larger than 16 bits or something.
- */
-#define MAX_CONTROL 64
-
-typedef enum {
-	CI_BLACK   = 0,
-	CI_RED     = 1,
-	CI_GREEN   = 2,
-	CI_BROWN   = 3,
-	CI_BLUE    = 4,
-	CI_MAGENTA = 5,
-	CI_CYAN    = 6,
-	CI_WHITE   = 7
-} sgr_color_index_t;
-
-typedef enum {
-	SGR_RESET       = 0,
-	SGR_BOLD        = 1,
-	SGR_UNDERLINE   = 4,
-	SGR_BLINK       = 5,
-	SGR_REVERSE     = 7,
-	SGR_FGCOLOR     = 30,
-	SGR_BGCOLOR     = 40
-} sgr_command_t;
-
-static sgr_color_index_t color_map[] = {
-	[COLOR_BLACK]   = CI_BLACK,
-	[COLOR_BLUE]    = CI_BLUE,
-	[COLOR_GREEN]   = CI_GREEN,
-	[COLOR_CYAN]    = CI_CYAN,
-	[COLOR_RED]     = CI_RED,
-	[COLOR_MAGENTA] = CI_MAGENTA,
-	[COLOR_YELLOW]  = CI_BROWN,
-	[COLOR_WHITE]   = CI_WHITE
-};
-
-/** ECMA-48 Set Graphics Rendition. */
-static void vt100_sgr(vt100_state_t *state, unsigned int mode)
-{
-	char control[MAX_CONTROL];
-
-	snprintf(control, MAX_CONTROL, "\033[%um", mode);
-	state->control_puts(control);
-}
-
-static void vt100_set_pos(vt100_state_t *state, sysarg_t col, sysarg_t row)
-{
-	char control[MAX_CONTROL];
-
-	snprintf(control, MAX_CONTROL, "\033[%" PRIun ";%" PRIun "f",
-	    row + 1, col + 1);
-	state->control_puts(control);
-}
-
-static void vt100_set_sgr(vt100_state_t *state, char_attrs_t attrs)
-{
-	switch (attrs.type) {
-	case CHAR_ATTR_STYLE:
-		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);
-			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);
-			break;
-		case STYLE_INVERTED:
-			vt100_sgr(state, SGR_RESET);
-			vt100_sgr(state, SGR_BGCOLOR + CI_BLACK);
-			vt100_sgr(state, 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);
-			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]);
-
-		if (attrs.val.index.attr & CATTR_BRIGHT)
-			vt100_sgr(state, SGR_BOLD);
-
-		break;
-	case CHAR_ATTR_RGB:
-		vt100_sgr(state, SGR_RESET);
-
-		if (attrs.val.rgb.bgcolor <= attrs.val.rgb.fgcolor)
-			vt100_sgr(state, SGR_REVERSE);
-
-		break;
-	}
-}
-
-vt100_state_t *vt100_state_create(sysarg_t cols, sysarg_t rows,
-    vt100_putuchar_t putuchar_fn, vt100_control_puts_t control_puts_fn,
-    vt100_flush_t flush_fn)
-{
-	vt100_state_t *state = malloc(sizeof(vt100_state_t));
-	if (state == NULL)
-		return NULL;
-
-	state->putuchar = putuchar_fn;
-	state->control_puts = control_puts_fn;
-	state->flush = flush_fn;
-
-	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;
-
-	/* Initialize graphic rendition attributes */
-	vt100_sgr(state, SGR_RESET);
-	vt100_sgr(state, SGR_FGCOLOR + CI_BLACK);
-	vt100_sgr(state, SGR_BGCOLOR + CI_WHITE);
-	state->control_puts("\033[2J");
-	state->control_puts("\033[?25l");
-
-	return state;
-}
-
-void vt100_state_destroy(vt100_state_t *state)
-{
-	free(state);
-}
-
-void vt100_get_dimensions(vt100_state_t *state, sysarg_t *cols,
-    sysarg_t *rows)
-{
-	*cols = state->cols;
-	*rows = state->rows;
-}
-
-errno_t vt100_yield(vt100_state_t *state)
-{
-	return EOK;
-}
-
-errno_t vt100_claim(vt100_state_t *state)
-{
-	return EOK;
-}
-
-void vt100_goto(vt100_state_t *state, sysarg_t col, sysarg_t row)
-{
-	if ((col >= state->cols) || (row >= state->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_state_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_state_t *state, bool visible)
-{
-	if (visible)
-		state->control_puts("\033[?25h");
-	else
-		state->control_puts("\033[?25l");
-}
-
-void vt100_putuchar(vt100_state_t *state, char32_t ch)
-{
-	state->putuchar(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_state_t *state)
-{
-	state->flush();
-}
-
-/** @}
- */
Index: uspace/srv/hid/output/proto/vt100.h
===================================================================
--- uspace/srv/hid/output/proto/vt100.h	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ 	(revision )
@@ -1,72 +1,0 @@
-/*
- * Copyright (c) 2011 Martin Decky
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * - Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- * - The name of the author may not be used to endorse or promote products
- *   derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/** @addtogroup output
- * @{
- */
-
-#ifndef OUTPUT_PROTO_VT100_H_
-#define OUTPUT_PROTO_VT100_H_
-
-#include <io/charfield.h>
-
-typedef void (*vt100_putuchar_t)(char32_t ch);
-typedef void (*vt100_control_puts_t)(const char *str);
-typedef void (*vt100_flush_t)(void);
-
-typedef struct {
-	sysarg_t cols;
-	sysarg_t rows;
-
-	sysarg_t cur_col;
-	sysarg_t cur_row;
-	char_attrs_t cur_attrs;
-
-	vt100_putuchar_t putuchar;
-	vt100_control_puts_t control_puts;
-	vt100_flush_t flush;
-} vt100_state_t;
-
-extern vt100_state_t *vt100_state_create(sysarg_t, sysarg_t, vt100_putuchar_t,
-    vt100_control_puts_t, vt100_flush_t);
-extern void vt100_state_destroy(vt100_state_t *);
-
-extern errno_t vt100_yield(vt100_state_t *);
-extern errno_t vt100_claim(vt100_state_t *);
-extern void vt100_get_dimensions(vt100_state_t *, sysarg_t *, sysarg_t *);
-
-extern void vt100_goto(vt100_state_t *, sysarg_t, sysarg_t);
-extern void vt100_set_attr(vt100_state_t *, char_attrs_t);
-extern void vt100_cursor_visibility(vt100_state_t *, bool);
-extern void vt100_putuchar(vt100_state_t *, char32_t);
-extern void vt100_flush(vt100_state_t *);
-
-#endif
-
-/** @}
- */
Index: uspace/srv/hid/remcons/meson.build
===================================================================
--- uspace/srv/hid/remcons/meson.build	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/remcons/meson.build	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,4 +1,4 @@
 #
-# Copyright (c) 2021 Jiri Svoboda
+# Copyright (c) 2024 Jiri Svoboda
 # Copyright (c) 2012 Vojtech Horky
 # All rights reserved.
@@ -28,4 +28,4 @@
 #
 
-deps = [ 'inet', 'console' ]
+deps = [ 'inet', 'console', 'vt' ]
 src = files('remcons.c', 'user.c')
Index: uspace/srv/hid/remcons/remcons.c
===================================================================
--- uspace/srv/hid/remcons/remcons.c	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/remcons/remcons.c	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2012 Vojtech Horky
  * All rights reserved.
@@ -51,6 +51,8 @@
 #include <inttypes.h>
 #include <str.h>
+#include <vt/vt100.h>
 #include "telnet.h"
 #include "user.h"
+#include "remcons.h"
 
 #define APP_GETTERM  "/app/getterm"
@@ -74,4 +76,5 @@
 static errno_t remcons_open(con_srvs_t *, con_srv_t *);
 static errno_t remcons_close(con_srv_t *);
+static errno_t remcons_read(con_srv_t *, void *, size_t, size_t *);
 static errno_t remcons_write(con_srv_t *, void *, size_t, size_t *);
 static void remcons_sync(con_srv_t *);
@@ -81,4 +84,11 @@
 static errno_t remcons_get_size(con_srv_t *, sysarg_t *, sysarg_t *);
 static errno_t remcons_get_color_cap(con_srv_t *, console_caps_t *);
+static void remcons_set_style(con_srv_t *, console_style_t);
+static void remcons_set_color(con_srv_t *, console_color_t,
+    console_color_t, console_color_attr_t);
+static void remcons_set_color(con_srv_t *, console_color_t,
+    console_color_t, console_color_attr_t);
+static void remcons_set_rgb_color(con_srv_t *, pixel_t, pixel_t);
+static void remcons_cursor_visibility(con_srv_t *, bool);
 static errno_t remcons_get_event(con_srv_t *, cons_event_t *);
 
@@ -86,5 +96,5 @@
 	.open = remcons_open,
 	.close = remcons_close,
-	.read = NULL,
+	.read = remcons_read,
 	.write = remcons_write,
 	.sync = remcons_sync,
@@ -94,8 +104,8 @@
 	.get_size = remcons_get_size,
 	.get_color_cap = remcons_get_color_cap,
-	.set_style = NULL,
-	.set_color = NULL,
-	.set_rgb_color = NULL,
-	.set_cursor_visibility = NULL,
+	.set_style = remcons_set_style,
+	.set_color = remcons_set_color,
+	.set_rgb_color = remcons_set_rgb_color,
+	.set_cursor_visibility = remcons_cursor_visibility,
 	.get_event = remcons_get_event
 };
@@ -115,5 +125,12 @@
 static telnet_user_t *srv_to_user(con_srv_t *srv)
 {
-	return srv->srvs->sarg;
+	remcons_t *remcons = (remcons_t *)srv->srvs->sarg;
+	return remcons->user;
+}
+
+static remcons_t *srv_to_remcons(con_srv_t *srv)
+{
+	remcons_t *remcons = (remcons_t *)srv->srvs->sarg;
+	return remcons;
 }
 
@@ -141,4 +158,17 @@
 }
 
+static errno_t remcons_read(con_srv_t *srv, void *data, size_t size,
+    size_t *nread)
+{
+	telnet_user_t *user = srv_to_user(srv);
+	errno_t rc;
+
+	rc = telnet_user_recv(user, data, size, nread);
+	if (rc != EOK)
+		return rc;
+
+	return EOK;
+}
+
 static errno_t remcons_write(con_srv_t *srv, void *data, size_t size, size_t *nwritten)
 {
@@ -161,12 +191,26 @@
 static void remcons_clear(con_srv_t *srv)
 {
-	(void) srv;
+	remcons_t *remcons = srv_to_remcons(srv);
+
+	if (remcons->enable_ctl) {
+		vt100_cls(remcons->vt);
+		vt100_set_pos(remcons->vt, 0, 0);
+		remcons->user->cursor_x = 0;
+		remcons->user->cursor_y = 0;
+	}
 }
 
 static void remcons_set_pos(con_srv_t *srv, sysarg_t col, sysarg_t row)
 {
-	telnet_user_t *user = srv_to_user(srv);
-
-	telnet_user_update_cursor_x(user, col);
+	remcons_t *remcons = srv_to_remcons(srv);
+	telnet_user_t *user = srv_to_user(srv);
+
+	if (remcons->enable_ctl) {
+		vt100_set_pos(remcons->vt, col, row);
+		remcons->user->cursor_x = col;
+		remcons->user->cursor_y = row;
+	} else {
+		telnet_user_update_cursor_x(user, col);
+	}
 }
 
@@ -176,5 +220,5 @@
 
 	*col = user->cursor_x;
-	*row = 0;
+	*row = user->cursor_y;
 
 	return EOK;
@@ -183,8 +227,13 @@
 static errno_t remcons_get_size(con_srv_t *srv, sysarg_t *cols, sysarg_t *rows)
 {
-	(void) srv;
-
-	*cols = 100;
-	*rows = 1;
+	remcons_t *remcons = srv_to_remcons(srv);
+
+	if (remcons->enable_ctl) {
+		*cols = 80;
+		*rows = 25;
+	} else {
+		*cols = 100;
+		*rows = 1;
+	}
 
 	return EOK;
@@ -193,8 +242,61 @@
 static errno_t remcons_get_color_cap(con_srv_t *srv, console_caps_t *ccaps)
 {
-	(void) srv;
-	*ccaps = CONSOLE_CAP_NONE;
-
-	return EOK;
+	remcons_t *remcons = srv_to_remcons(srv);
+
+	if (remcons->enable_ctl)
+		*ccaps = CONSOLE_CAP_INDEXED | CONSOLE_CAP_RGB;
+	else
+		*ccaps = 0;
+
+	return EOK;
+}
+
+static void remcons_set_style(con_srv_t *srv, console_style_t style)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	char_attrs_t attrs;
+
+	if (remcons->enable_ctl) {
+		attrs.type = CHAR_ATTR_STYLE;
+		attrs.val.style = style;
+		vt100_set_attr(remcons->vt, attrs);
+	}
+}
+
+static void remcons_set_color(con_srv_t *srv, console_color_t bgcolor,
+    console_color_t fgcolor, console_color_attr_t flags)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	char_attrs_t attrs;
+
+	if (remcons->enable_ctl) {
+		attrs.type = CHAR_ATTR_INDEX;
+		attrs.val.index.bgcolor = bgcolor;
+		attrs.val.index.fgcolor = fgcolor;
+		attrs.val.index.attr = flags;
+		vt100_set_attr(remcons->vt, attrs);
+	}
+}
+
+static void remcons_set_rgb_color(con_srv_t *srv, pixel_t bgcolor,
+    pixel_t fgcolor)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	char_attrs_t attrs;
+
+	if (remcons->enable_ctl) {
+		attrs.type = CHAR_ATTR_RGB;
+		attrs.val.rgb.bgcolor = bgcolor;
+		attrs.val.rgb.fgcolor = fgcolor;
+		vt100_set_attr(remcons->vt, attrs);
+	}
+}
+
+static void remcons_cursor_visibility(con_srv_t *srv, bool visible)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+
+	if (remcons->enable_ctl)
+		vt100_cursor_visibility(remcons->vt, visible);
 }
 
@@ -287,4 +389,34 @@
 }
 
+static void remcons_vt_putchar(void *arg, char32_t c)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+	char buf[STR_BOUNDS(1)];
+	size_t off;
+	errno_t rc;
+
+	(void)arg;
+
+	off = 0;
+	rc = chr_encode(c, buf, &off, sizeof(buf));
+	if (rc != EOK)
+		return;
+
+	(void)telnet_user_send_data(remcons->user, buf, off);
+}
+
+static void remcons_vt_cputs(void *arg, const char *str)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+
+	(void)telnet_user_send_data(remcons->user, str, str_size(str));
+}
+
+static void remcons_vt_flush(void *arg)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+	(void)remcons;
+}
+
 /** Handle network connection.
  *
@@ -294,10 +426,35 @@
 static void remcons_new_conn(tcp_listener_t *lst, tcp_conn_t *conn)
 {
+	char_attrs_t attrs;
+	remcons_t *remcons = calloc(1, sizeof(remcons_t));
+	assert(remcons != NULL); // XXX
 	telnet_user_t *user = telnet_user_create(conn);
 	assert(user);
 
+	remcons->enable_ctl = true;
+	remcons->user = user;
+
+	if (remcons->enable_ctl) {
+		user->rows = 25;
+	} else {
+		user->rows = 1;
+	}
+
+	remcons->vt = vt100_state_create((void *)remcons, 80, 25,
+	    remcons_vt_putchar, remcons_vt_cputs, remcons_vt_flush);
+	assert(remcons->vt != NULL); // XXX
+	remcons->vt->enable_rgb = true;
+
+	if (remcons->enable_ctl) {
+		attrs.type = CHAR_ATTR_STYLE;
+		attrs.val.style = STYLE_NORMAL;
+		vt100_set_sgr(remcons->vt, attrs);
+		vt100_cls(remcons->vt);
+		vt100_set_pos(remcons->vt, 0, 0);
+	}
+
 	con_srvs_init(&user->srvs);
 	user->srvs.ops = &con_ops;
-	user->srvs.sarg = user;
+	user->srvs.sarg = remcons;
 	user->srvs.abort_timeout = 1000;
 
Index: uspace/srv/hid/remcons/remcons.h
===================================================================
--- uspace/srv/hid/remcons/remcons.h	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/remcons/remcons.h	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2012 Vojtech Horky
  * All rights reserved.
@@ -36,6 +37,16 @@
 #define REMCONS_H_
 
+#include <stdbool.h>
+#include <vt/vt100.h>
+#include "user.h"
+
 #define NAME       "remcons"
 #define NAMESPACE  "term"
+
+typedef struct {
+	telnet_user_t *user;
+	vt100_state_t *vt;
+	bool enable_ctl;
+} remcons_t;
 
 #endif
Index: uspace/srv/hid/remcons/user.c
===================================================================
--- uspace/srv/hid/remcons/user.c	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/remcons/user.c	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2012 Vojtech Horky
  * All rights reserved.
@@ -37,4 +38,5 @@
 #include <adt/prodcons.h>
 #include <errno.h>
+#include <mem.h>
 #include <str_error.h>
 #include <loc.h>
@@ -48,4 +50,5 @@
 #include <inttypes.h>
 #include <assert.h>
+#include "remcons.h"
 #include "user.h"
 #include "telnet.h"
@@ -90,4 +93,5 @@
 
 	user->cursor_x = 0;
+	user->cursor_y = 0;
 
 	return user;
@@ -182,4 +186,26 @@
 }
 
+static errno_t telnet_user_fill_recv_buf(telnet_user_t *user)
+{
+	errno_t rc;
+	size_t recv_length;
+
+	rc = tcp_conn_recv_wait(user->conn, user->socket_buffer,
+	    BUFFER_SIZE, &recv_length);
+	if (rc != EOK)
+		return rc;
+
+	if (recv_length == 0) {
+		user->socket_closed = true;
+		user->srvs.aborted = true;
+		return ENOENT;
+	}
+
+	user->socket_buffer_len = recv_length;
+	user->socket_buffer_pos = 0;
+
+	return EOK;
+}
+
 /** Receive next byte from a socket (use buffering.
  * We need to return the value via extra argument because the read byte
@@ -188,26 +214,34 @@
 static errno_t telnet_user_recv_next_byte_no_lock(telnet_user_t *user, char *byte)
 {
+	errno_t rc;
+
 	/* No more buffered data? */
 	if (user->socket_buffer_len <= user->socket_buffer_pos) {
-		errno_t rc;
-		size_t recv_length;
-
-		rc = tcp_conn_recv_wait(user->conn, user->socket_buffer,
-		    BUFFER_SIZE, &recv_length);
+		rc = telnet_user_fill_recv_buf(user);
 		if (rc != EOK)
 			return rc;
-
-		if (recv_length == 0) {
-			user->socket_closed = true;
-			user->srvs.aborted = true;
-			return ENOENT;
-		}
-
-		user->socket_buffer_len = recv_length;
-		user->socket_buffer_pos = 0;
 	}
 
 	*byte = user->socket_buffer[user->socket_buffer_pos++];
-
+	return EOK;
+}
+
+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;
 }
@@ -283,10 +317,12 @@
 		/* Skip zeros, bail-out on error. */
 		while (next_byte == 0) {
+			fibril_mutex_unlock(&user->guard);
+
 			errno_t rc = telnet_user_recv_next_byte_no_lock(user, &next_byte);
-			if (rc != EOK) {
-				fibril_mutex_unlock(&user->guard);
+			if (rc != EOK)
 				return rc;
-			}
+
 			uint8_t byte = (uint8_t) next_byte;
+			fibril_mutex_lock(&user->guard);
 
 			/* Skip telnet commands. */
@@ -339,5 +375,6 @@
  * @param size Size of @p data buffer in bytes.
  */
-static errno_t telnet_user_send_data_no_lock(telnet_user_t *user, uint8_t *data, size_t size)
+static errno_t telnet_user_send_data_no_lock(telnet_user_t *user,
+    const char *data, size_t size)
 {
 	uint8_t *converted = malloc(3 * size + 1);
@@ -351,4 +388,6 @@
 			converted[converted_size++] = 10;
 			user->cursor_x = 0;
+			if (user->cursor_y < user->rows - 1)
+				++user->cursor_y;
 		} else {
 			converted[converted_size++] = data[i];
@@ -373,5 +412,6 @@
  * @param size Size of @p data buffer in bytes.
  */
-errno_t telnet_user_send_data(telnet_user_t *user, uint8_t *data, size_t size)
+errno_t telnet_user_send_data(telnet_user_t *user, const char *data,
+    size_t size)
 {
 	fibril_mutex_lock(&user->guard);
@@ -395,5 +435,5 @@
 	fibril_mutex_lock(&user->guard);
 	if (user->cursor_x - 1 == new_x) {
-		uint8_t data = '\b';
+		char data = '\b';
 		/* Ignore errors. */
 		telnet_user_send_data_no_lock(user, &data, 1);
Index: uspace/srv/hid/remcons/user.h
===================================================================
--- uspace/srv/hid/remcons/user.h	(revision 3fcea3438e30a096e9a5590a54508cfe3d5a697f)
+++ uspace/srv/hid/remcons/user.h	(revision d3109ffbe9773fed7630d7d9dae993d02f8967ac)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2012 Vojtech Horky
  * All rights reserved.
@@ -41,5 +42,4 @@
 #include <inttypes.h>
 #include <io/con_srv.h>
-#include "remcons.h"
 
 #define BUFFER_SIZE 32
@@ -79,4 +79,8 @@
 	/** X position of the cursor. */
 	int cursor_x;
+	/** Y position of the cursor. */
+	int cursor_y;
+	/** Total number of rows */
+	int rows;
 } telnet_user_t;
 
@@ -88,5 +92,6 @@
 extern void telnet_user_notify_client_disconnected(telnet_user_t *);
 extern errno_t telnet_user_get_next_keyboard_event(telnet_user_t *, kbd_event_t *);
-extern errno_t telnet_user_send_data(telnet_user_t *, uint8_t *, size_t);
+extern errno_t telnet_user_send_data(telnet_user_t *, const char *, size_t);
+extern errno_t telnet_user_recv(telnet_user_t *, void *, size_t, size_t *);
 extern void telnet_user_update_cursor_x(telnet_user_t *, int);
 
