[openwrt/openwrt] ucode-mod-uline: add package for ucode terminal line editing
LEDE Commits
lede-commits at lists.infradead.org
Thu Feb 13 10:01:13 PST 2025
nbd pushed a commit to openwrt/openwrt.git, branch main:
https://git.openwrt.org/be31d44bd9a679162cdd53a4644fa1774553dcdf
commit be31d44bd9a679162cdd53a4644fa1774553dcdf
Author: Felix Fietkau <nbd at nbd.name>
AuthorDate: Mon Jan 13 12:28:30 2025 +0100
ucode-mod-uline: add package for ucode terminal line editing
It provides a ucode module with similar functionality as libreadline,
however with much smaller code and no dependencies aside from ucode and
libubox.
It also provides shell-style parsing/escaping code useful for building
a CLI.
Signed-off-by: Felix Fietkau <nbd at nbd.name>
---
package/utils/ucode-mod-uline/Makefile | 32 +
package/utils/ucode-mod-uline/src/CMakeLists.txt | 44 ++
package/utils/ucode-mod-uline/src/private.h | 194 +++++
package/utils/ucode-mod-uline/src/ucode.c | 905 ++++++++++++++++++++++
package/utils/ucode-mod-uline/src/uline.c | 919 +++++++++++++++++++++++
package/utils/ucode-mod-uline/src/uline.h | 151 ++++
package/utils/ucode-mod-uline/src/utf8.c | 340 +++++++++
package/utils/ucode-mod-uline/src/vt100.c | 95 +++
8 files changed, 2680 insertions(+)
diff --git a/package/utils/ucode-mod-uline/Makefile b/package/utils/ucode-mod-uline/Makefile
new file mode 100644
index 0000000000..50aecd4f7f
--- /dev/null
+++ b/package/utils/ucode-mod-uline/Makefile
@@ -0,0 +1,32 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=ucode-mod-uline
+PKG_RELEASE:=$(AUTORELEASE)
+PKG_LICENSE:=GPL-2.0-or-later
+PKG_MAINTAINER:=Felix Fietkau <nbd at nbd.name>
+
+include $(INCLUDE_DIR)/package.mk
+include $(INCLUDE_DIR)/cmake.mk
+
+CMAKE_INSTALL := 1
+
+define Package/ucode-mod-uline
+ SECTION:=utils
+ CATEGORY:=Utilities
+ TITLE:=ucode module for terminal line editing
+ DEPENDS:=+libucode +libubox
+endef
+
+CMAKE_OPTIONS += -DUSE_SYSTEM_WCHAR=ON
+
+define Package/ucode-mod-uline/description
+This module provides similar functionality as libreadline for ucode, without
+depending on other libraries like ncurses.
+endef
+
+define Package/ucode-mod-uline/install
+ $(INSTALL_DIR) $(1)/usr/lib/ucode
+ $(CP) $(PKG_INSTALL_DIR)/usr/lib/ucode/uline.so $(1)/usr/lib/ucode/
+endef
+
+$(eval $(call BuildPackage,ucode-mod-uline))
diff --git a/package/utils/ucode-mod-uline/src/CMakeLists.txt b/package/utils/ucode-mod-uline/src/CMakeLists.txt
new file mode 100644
index 0000000000..efa2d80f90
--- /dev/null
+++ b/package/utils/ucode-mod-uline/src/CMakeLists.txt
@@ -0,0 +1,44 @@
+cmake_minimum_required(VERSION 3.13)
+
+PROJECT(uline C)
+ADD_DEFINITIONS(-Os -ggdb -Wall -Werror --std=gnu99 -ffunction-sections -fwrapv -D_GNU_SOURCE -Wno-error=unused-function -Wno-parentheses -Wno-sign-compare)
+
+OPTION(USE_SYSTEM_WCHAR "Use system multibyte implementation for UTF-8" OFF)
+IF(CMAKE_C_COMPILER_VERSION VERSION_GREATER 6)
+ ADD_DEFINITIONS(-Wextra -Werror=implicit-function-declaration)
+ ADD_DEFINITIONS(-Wformat -Werror=format-security -Werror=format-nonliteral)
+ENDIF()
+ADD_DEFINITIONS(-Wmissing-declarations -Wno-error=unused-variable -Wno-unused-parameter)
+
+IF(APPLE)
+ SET(UCODE_MODULE_LINK_OPTIONS "LINKER:-undefined,dynamic_lookup")
+ELSE()
+ SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "-Wl,--gc-sections")
+ENDIF()
+
+IF(DEBUG)
+ ADD_DEFINITIONS(-DDEBUG -g3 -O0)
+ELSE()
+ ADD_DEFINITIONS(-DNDEBUG)
+ENDIF()
+
+FIND_LIBRARY(ucode NAMES ucode)
+FIND_LIBRARY(libubox NAMES ubox)
+FIND_PATH(uloop_include_dir NAMES libubox/uloop.h)
+FIND_PATH(ucode_include_dir NAMES ucode/module.h)
+INCLUDE_DIRECTORIES(${ucode_include_dir} ${uloop_include_dir})
+
+ADD_LIBRARY(uline STATIC uline.c utf8.c vt100.c)
+set_property(TARGET uline PROPERTY POSITION_INDEPENDENT_CODE ON)
+IF(USE_SYSTEM_WCHAR)
+ TARGET_COMPILE_DEFINITIONS(uline PUBLIC USE_SYSTEM_WCHAR)
+ENDIF()
+
+ADD_LIBRARY(uline_lib MODULE ucode.c)
+SET_TARGET_PROPERTIES(uline_lib PROPERTIES OUTPUT_NAME uline PREFIX "")
+TARGET_LINK_OPTIONS(uline_lib PRIVATE ${UCODE_MODULE_LINK_OPTIONS})
+TARGET_LINK_LIBRARIES(uline_lib uline ${libubox})
+
+install(FILES uline.h DESTINATION include)
+INSTALL(TARGETS uline LIBRARY DESTINATION lib)
+INSTALL(TARGETS uline_lib LIBRARY DESTINATION lib/ucode)
diff --git a/package/utils/ucode-mod-uline/src/private.h b/package/utils/ucode-mod-uline/src/private.h
new file mode 100644
index 0000000000..fa38d06737
--- /dev/null
+++ b/package/utils/ucode-mod-uline/src/private.h
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd at nbd.name>
+ */
+#ifndef __EDITLINE_PRIVATE_H
+#define __EDITLINE_PRIVATE_H
+
+#include <stdio.h>
+
+#define KEY_NUL 0 // ^@ Null character
+#define KEY_SOH 1 // ^A Start of heading, = console interrupt
+#define KEY_STX 2 // ^B Start of text, maintenance mode on HP console
+#define KEY_ETX 3 // ^C End of text
+#define KEY_EOT 4 // ^D End of transmission, not the same as ETB
+#define KEY_ENQ 5 // ^E Enquiry, goes with ACK; old HP flow control
+#define KEY_ACK 6 // ^F Acknowledge, clears ENQ logon hand
+#define KEY_BEL 7 // ^G Bell, rings the bell
+#define KEY_BS 8 // ^H Backspace, works on HP terminals/computers
+#define KEY_HT 9 // ^I Horizontal tab, move to next tab stop
+#define KEY_LF 10 // ^J Line Feed
+#define KEY_VT 11 // ^K Vertical tab
+#define KEY_FF 12 // ^L Form Feed, page eject
+#define KEY_CR 13 // ^M Carriage Return
+#define KEY_SO 14 // ^N Shift Out, alternate character set
+#define KEY_SI 15 // ^O Shift In, resume defaultn character set
+#define KEY_DLE 16 // ^P Data link escape
+#define KEY_DC1 17 // ^Q XON, with XOFF to pause listings; "okay to send"
+#define KEY_DC2 18 // ^R Device control 2, block-mode flow control
+#define KEY_DC3 19 // ^S XOFF, with XON is TERM=18 flow control
+#define KEY_DC4 20 // ^T Device control 4
+#define KEY_NAK 21 // ^U Negative acknowledge
+#define KEY_SYN 22 // ^V Synchronous idle
+#define KEY_ETB 23 // ^W End transmission block, not the same as EOT
+#define KEY_CAN 24 // ^X Cancel line, MPE echoes !!!
+#define KEY_EM 25 // ^Y End of medium, Control-Y interrupt
+#define KEY_SUB 26 // ^Z Substitute
+#define KEY_ESC 27 // ^[ Escape, next character is not echoed
+#define KEY_FS 28 // ^\ File separator
+#define KEY_GS 29 // ^] Group separator
+#define KEY_RS 30 // ^^ Record separator, block-mode terminator
+#define KEY_US 31 // ^_ Unit separator
+#define KEY_DEL 127 // Delete (not a real control character)
+
+// Types of escape code
+enum vt100_escape {
+ VT100_INCOMPLETE,
+ VT100_UNKNOWN,
+ VT100_IGNORE,
+ VT100_CURSOR_UP,
+ VT100_CURSOR_DOWN,
+ VT100_CURSOR_LEFT,
+ VT100_CURSOR_WORD_LEFT,
+ VT100_CURSOR_RIGHT,
+ VT100_CURSOR_WORD_RIGHT,
+ VT100_HOME,
+ VT100_END,
+ VT100_INSERT,
+ VT100_DELETE,
+ VT100_DELETE_LEFT,
+ VT100_DELETE_LEFT_WORD,
+ VT100_PAGE_UP,
+ VT100_PAGE_DOWN,
+};
+
+ssize_t utf8_nsyms(const char *str, size_t len);
+enum vt100_escape vt100_esc_decode(const char *str);
+
+// helpers:
+void __vt100_csi_num(FILE *out, int num, char code);
+void __vt100_csi2(FILE *out, char c1, char c2);
+void __vt100_esc(FILE *out, char c);
+static inline void __vt100_sgr(FILE *out, int code)
+{
+ __vt100_csi2(out, code + '0', 'm');
+}
+
+
+static inline void vt100_attr_reset(FILE *out)
+{
+ __vt100_sgr(out, 0);
+}
+
+static inline void vt100_attr_bright(FILE *out)
+{
+ __vt100_sgr(out, 1);
+}
+
+static inline void vt100_attr_dim(FILE *out)
+{
+ __vt100_sgr(out, 2);
+}
+
+static inline void vt100_attr_underscore(FILE *out)
+{
+ __vt100_sgr(out, 4);
+}
+
+static inline void vt100_attr_blink(FILE *out)
+{
+ __vt100_sgr(out, 5);
+}
+
+static inline void vt100_attr_reverse(FILE *out)
+{
+ __vt100_sgr(out, 7);
+}
+
+static inline void vt100_attr_hidden(FILE *out)
+{
+ __vt100_sgr(out, 8);
+}
+
+static inline void vt100_erase_line(FILE *out)
+{
+ __vt100_csi2(out, '2', 'K');
+}
+
+static inline void vt100_clear_screen(FILE *out)
+{
+ __vt100_csi2(out, '2', 'J');
+}
+
+static inline void vt100_cursor_save(FILE *out)
+{
+ __vt100_esc(out, '7');
+}
+
+static inline void vt100_cursor_restore(FILE *out)
+{
+ __vt100_esc(out, '8');
+}
+
+static inline void vt100_scroll_up(FILE *out)
+{
+ __vt100_esc(out, 'D');
+}
+
+static inline void vt100_scroll_down(FILE *out)
+{
+ __vt100_esc(out, 'M');
+}
+
+static inline void vt100_next_line(FILE *out)
+{
+ __vt100_esc(out, 'E');
+}
+
+static inline void vt100_cursor_up(FILE *out, int count)
+{
+ __vt100_csi_num(out, count, 'A');
+}
+
+static inline void vt100_cursor_down(FILE *out, int count)
+{
+ __vt100_csi_num(out, count, 'B');
+}
+
+static inline void vt100_cursor_forward(FILE *out, int count)
+{
+ __vt100_csi_num(out, count, 'C');
+}
+
+static inline void vt100_cursor_back(FILE *out, int count)
+{
+ __vt100_csi_num(out, count, 'D');
+}
+
+static inline void vt100_cursor_home(FILE *out)
+{
+ __vt100_csi2(out, 'H', 0);
+}
+
+static inline void vt100_erase(FILE *out, int count)
+{
+ __vt100_csi_num(out, count, 'P');
+}
+
+static inline void vt100_erase_down(FILE *out)
+{
+ __vt100_csi2(out, 'J', 0);
+}
+
+static inline void vt100_erase_right(FILE *out)
+{
+ __vt100_csi2(out, 'K', 0);
+}
+
+static inline void vt100_ding(FILE *out)
+{
+ fputc(7, out);
+ fflush(out);
+}
+
+#endif
diff --git a/package/utils/ucode-mod-uline/src/ucode.c b/package/utils/ucode-mod-uline/src/ucode.c
new file mode 100644
index 0000000000..7f1d9b1e34
--- /dev/null
+++ b/package/utils/ucode-mod-uline/src/ucode.c
@@ -0,0 +1,905 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd at nbd.name>
+ */
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <poll.h>
+
+#include <ucode/module.h>
+#include <libubox/list.h>
+#include <libubox/uloop.h>
+
+#include "uline.h"
+
+static uc_value_t *registry;
+static uc_resource_type_t *state_type, *argp_type;
+
+enum {
+ STATE_RES,
+ STATE_CB,
+ STATE_INPUT,
+ STATE_OUTPUT,
+ STATE_POLL_CB,
+};
+
+struct uc_uline_state {
+ struct uloop_fd fd;
+
+ struct uline_state s;
+ int registry_index;
+
+ uc_vm_t *vm;
+ uc_value_t *state, *cb, *res, *poll_cb;
+
+ uc_value_t *line;
+
+ uint32_t input_mask[256 / 32];
+};
+
+struct uc_arg_parser {
+ char line_sep;
+};
+
+static unsigned int
+registry_set(uc_vm_t *vm, uc_value_t *val)
+{
+ uc_value_t *registry;
+ size_t i, len;
+
+ registry = uc_vm_registry_get(vm, "uline.registry");
+ len = ucv_array_length(registry);
+ for (i = 0; i < len; i++)
+ if (ucv_array_get(registry, i) == NULL)
+ break;
+
+ ucv_array_set(registry, i, ucv_get(val));
+ return i;
+}
+
+static uc_value_t *
+uc_uline_poll(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+ uc_value_t *val;
+
+ if (!us)
+ return NULL;
+
+ uline_poll(&us->s);
+ val = us->line;
+ us->line = NULL;
+
+ return val;
+}
+
+static uc_value_t *
+uc_uline_poll_key(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+ uc_value_t *timeout_arg = uc_fn_arg(0);
+ struct pollfd pfd = {};
+ int timeout, len;
+ char c;
+
+ if (!us)
+ return NULL;
+
+ if (ucv_type(timeout_arg) == UC_INTEGER)
+ timeout = ucv_int64_get(timeout_arg);
+ else
+ timeout = -1;
+
+ pfd.fd = us->s.input;
+ pfd.events = POLLIN;
+ poll(&pfd, 1, timeout);
+ if (!(pfd.revents & POLLIN))
+ return NULL;
+
+ do {
+ len = read(pfd.fd, &c, 1);
+ } while (len < 0 && errno == EINTR);
+
+ if (len != 1)
+ return NULL;
+
+ return ucv_string_new_length(&c, 1);
+}
+
+static uc_value_t *
+uc_uline_poll_stop(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+
+ if (!us)
+ return NULL;
+
+ us->s.stop = true;
+
+ return NULL;
+}
+
+static uc_value_t *
+uc_uline_get_window(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+ uc_value_t *val;
+
+ if (!us)
+ return NULL;
+
+ val = ucv_object_new(vm);
+ ucv_object_add(val, "x", ucv_int64_new(us->s.cols));
+ ucv_object_add(val, "y", ucv_int64_new(us->s.rows));
+ return val;
+}
+
+static uc_value_t *
+uc_uline_get_line(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+ uc_value_t *line2 = uc_fn_arg(0);
+ uc_value_t *state, *val;
+ const char *line;
+ size_t len;
+
+ if (!us)
+ return NULL;
+
+ state = ucv_object_new(vm);
+ if (ucv_is_truish(line2))
+ uline_get_line2(&us->s, &line, &len);
+ else
+ uline_get_line(&us->s, &line, &len);
+ val = ucv_string_new_length(line, len);
+ ucv_object_add(state, "line", ucv_get(val));
+ ucv_object_add(state, "pos", ucv_int64_new(us->s.line.pos));
+
+ return state;
+}
+
+static uc_value_t *
+uc_uline_set_state(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+ uc_value_t *state = uc_fn_arg(0);
+ uc_value_t *arg;
+ bool found;
+
+ if (!us || ucv_type(state) != UC_OBJECT)
+ return NULL;
+
+ if ((arg = ucv_object_get(state, "prompt", NULL)) != NULL) {
+ if (ucv_type(arg) != UC_STRING)
+ return NULL;
+
+ uline_set_prompt(&us->s, ucv_string_get(arg));
+ }
+
+ if ((arg = ucv_object_get(state, "line", NULL)) != NULL) {
+ if (ucv_type(arg) != UC_STRING)
+ return NULL;
+
+ uline_set_line(&us->s, ucv_string_get(arg), ucv_string_length(arg));
+ }
+
+ if ((arg = ucv_object_get(state, "pos", NULL)) != NULL) {
+ if (ucv_type(arg) != UC_INTEGER)
+ return NULL;
+
+ uline_set_cursor(&us->s, ucv_int64_get(arg));
+ }
+
+ arg = ucv_object_get(state, "line2_prompt", &found);
+ if (found) {
+ if (!arg)
+ uline_set_line2_prompt(&us->s, NULL);
+ else if (ucv_type(arg) == UC_STRING)
+ uline_set_line2_prompt(&us->s, ucv_string_get(arg));
+ else
+ return NULL;
+ }
+
+ if ((arg = ucv_object_get(state, "line2", NULL)) != NULL) {
+ if (ucv_type(arg) != UC_STRING)
+ return NULL;
+
+ uline_set_line2(&us->s, ucv_string_get(arg), ucv_string_length(arg));
+ }
+
+ if ((arg = ucv_object_get(state, "line2_pos", NULL)) != NULL) {
+ if (ucv_type(arg) != UC_INTEGER)
+ return NULL;
+
+ uline_set_line2_cursor(&us->s, ucv_int64_get(arg));
+ }
+
+ return ucv_boolean_new(true);
+}
+
+static uc_value_t *
+uc_uline_set_hint(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+ uc_value_t *arg = uc_fn_arg(0);
+
+ if (!us || ucv_type(arg) != UC_STRING)
+ return NULL;
+
+ uline_set_hint(&us->s, ucv_string_get(arg), ucv_string_length(arg));
+
+ return ucv_boolean_new(true);
+}
+
+static uc_value_t *
+uc_uline_set_uloop(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+ uc_value_t *cb = uc_fn_arg(0);
+
+ if (!us || (cb && !ucv_is_callable(cb)))
+ return NULL;
+
+ us->poll_cb = cb;
+ ucv_array_set(us->state, STATE_POLL_CB, ucv_get(cb));
+ if (cb) {
+ uloop_fd_add(&us->fd, ULOOP_READ);
+ us->fd.cb(&us->fd, 0);
+ } else {
+ uloop_fd_delete(&us->fd);
+ }
+
+ return ucv_boolean_new(true);
+}
+
+static uc_value_t *
+uc_uline_reset_key_input(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+
+ us->s.repeat_char = 0;
+
+ return ucv_boolean_new(true);
+}
+
+static uc_value_t *
+uc_uline_hide_prompt(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+
+ if (!us)
+ return NULL;
+
+ uline_hide_prompt(&us->s);
+
+ return ucv_boolean_new(true);
+}
+
+static uc_value_t *
+uc_uline_refresh_prompt(uc_vm_t *vm, size_t nargs)
+{
+ struct uc_uline_state *us = uc_fn_thisval("uline.state");
+
+ if (!us)
+ return NULL;
+
+ uline_refresh_prompt(&us->s);
+
+ return ucv_boolean_new(true);
+}
+
+static bool
+cb_prepare(struct uc_uline_state *us, const char *name)
+{
+ uc_value_t *func;
+
+ func = ucv_object_get(us->cb, name, NULL);
+ if (!func)
+ return false;
+
+ uc_vm_stack_push(us->vm, ucv_get(us->res));
+ uc_vm_stack_push(us->vm, ucv_get(func));
+ return true;
+}
+
+static uc_value_t *
+cb_call_ret(struct uc_uline_state *us, size_t args, ...)
+{
+ va_list ap;
+
+ va_start(ap, args);
+ for (size_t i = 0; i < args; i++)
+ uc_vm_stack_push(us->vm, ucv_get(va_arg(ap, void *)));
+ va_end(ap);
+
+ if (uc_vm_call(us->vm, true, args) == EXCEPTION_NONE)
+ return uc_vm_stack_pop(us->vm);
+
+ return NULL;
+}
+#define cb_call(...) ucv_put(cb_call_ret(__VA_ARGS__))
+
+static bool
+uc_uline_cb_line(struct uline_state *s, const char *str, size_t len)
+{
+ struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
+ bool complete = true;
+ uc_value_t *ret;
+
+ if (cb_prepare(us, "line_check")) {
+ ret = cb_call_ret(us, 1, ucv_string_new_length(str, len));
+ complete = ucv_is_truish(ret);
+ ucv_put(ret);
+ }
+
+ s->stop = complete;
+ if (complete)
+ us->line = ucv_string_new_length(str, len);
+
+ return complete;
+}
+
+static void
+uc_uline_cb_event(struct uline_state *s, enum uline_event ev)
+{
+ struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
+ static const char * const ev_types[] = {
+ [EDITLINE_EV_CURSOR_UP] = "cursor_up",
+ [EDITLINE_EV_CURSOR_DOWN] = "cursor_down",
+ [EDITLINE_EV_WINDOW_CHANGED] = "window_changed",
+ [EDITLINE_EV_EOF] = "eof",
+ [EDITLINE_EV_INTERRUPT] = "interrupt",
+ };
+
+ if (ev > ARRAY_SIZE(ev_types) || !ev_types[ev])
+ return;
+
+ if (!cb_prepare(us, ev_types[ev]))
+ return;
+
+ if (ev == EDITLINE_EV_WINDOW_CHANGED)
+ cb_call(us, 2, ucv_int64_new(s->cols), ucv_int64_new(s->rows));
+ else
+ cb_call(us, 0);
+}
+
+static void uc_uline_poll_cb(struct uloop_fd *fd, unsigned int events)
+{
+ struct uc_uline_state *us = container_of(fd, struct uc_uline_state, fd);
+ uc_value_t *val;
+
+ while (!uloop_cancelled && us->poll_cb) {
+ uline_poll(&us->s);
+
+ val = us->line;
+ if (!val)
+ break;
+
+ us->line = NULL;
+ if (!ucv_is_callable(us->poll_cb))
+ return;
+
+ uc_vm_stack_push(us->vm, ucv_get(us->res));
+ uc_vm_stack_push(us->vm, ucv_get(us->poll_cb));
+ cb_call(us, 1, val);
+ }
+}
+
+static bool
+uc_uline_cb_key_input(struct uline_state *s, unsigned char c, unsigned int count)
+{
+ struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
+ uc_value_t *ret;
+ bool retval;
+
+ if (!(us->input_mask[c / 32] & (1 << (c % 32))))
+ return false;
+
+ if (!cb_prepare(us, "key_input"))
+ return false;
+
+ ret = cb_call_ret(us, 2, ucv_string_new_length((char *)&c, 1), ucv_int64_new(count));
+ retval = ucv_is_truish(ret);
+ ucv_put(ret);
+
+ return retval;
+}
+
+static void
+uc_uline_cb_line2_update(struct uline_state *s, const char *str, size_t len)
+{
+ struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
+
+ if (cb_prepare(us, "line2_update"))
+ cb_call(us, 1, ucv_string_new_length(str, len));
+}
+
+static bool
+uc_uline_cb_line2_cursor(struct uline_state *s)
+{
+ struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
+ uc_value_t *retval;
+ bool ret = true;
+
+ if (cb_prepare(us, "line2_cursor")) {
+ retval = cb_call_ret(us, 0);
+ ret = ucv_is_truish(retval);
+ ucv_put(retval);
+ }
+
+ return ret;
+}
+
+static bool
+uc_uline_cb_line2_newline(struct uline_state *s, const char *str, size_t len)
+{
+ struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
+ uc_value_t *retval;
+ bool ret = false;
+
+ if (cb_prepare(us, "line2_newline")) {
+ retval = cb_call_ret(us, 1, ucv_string_new_length(str, len));
+ ret = ucv_is_truish(retval);
+ ucv_put(retval);
+ }
+
+ return ret;
+}
+
+static uc_value_t *
+uc_uline_new(uc_vm_t *vm, size_t nargs)
+{
+ static const struct uline_cb uline_cb = {
+#define _CB(_type) ._type = uc_uline_cb_##_type
+ _CB(key_input),
+ _CB(line),
+ _CB(event),
+ _CB(line2_update),
+ _CB(line2_cursor),
+ _CB(line2_newline),
+#undef _CB
+ };
+ uc_value_t *data = uc_fn_arg(0);
+ struct uc_uline_state *us;
+ FILE *input, *output;
+ uc_value_t *arg, *cb, *state, *res;
+
+ if (ucv_type(data) != UC_OBJECT)
+ return NULL;
+
+ cb = ucv_object_get(data, "cb", NULL);
+ if (ucv_type(cb) != UC_OBJECT)
+ return NULL;
+
+ state = ucv_array_new(vm);
+ ucv_array_set(state, 0, ucv_get(cb));
+ if ((arg = ucv_object_get(data, "input", NULL)) != NULL) {
+ input = ucv_resource_data(arg, "fs.file");
+ ucv_array_set(state, STATE_INPUT, ucv_get(arg));
+ } else {
+ input = stdin;
+ }
+
+ if ((arg = ucv_object_get(data, "output", NULL)) != NULL) {
+ output = ucv_resource_data(arg, "fs.file");
+ ucv_array_set(state, STATE_OUTPUT, ucv_get(arg));
+ } else {
+ output = stdout;
+ }
+
+ if (!input || !output) {
+ input = output = NULL;
+ return NULL;
+ }
+
+ us = calloc(1, sizeof(*us));
+ us->vm = vm;
+ us->state = ucv_array_new(vm);
+ ucv_array_set(us->state, STATE_CB, ucv_get(cb));
+ us->cb = cb;
+ us->registry_index = registry_set(vm, state);
+
+ if ((arg = ucv_object_get(data, "key_input_list", NULL)) != NULL) {
+ uc_value_t *val;
+ size_t len;
+
+ if (ucv_type(arg) != UC_ARRAY)
+ goto free;
+
+ len = ucv_array_length(arg);
+ for (size_t i = 0; i < len; i++) {
+ unsigned char c;
+
+ val = ucv_array_get(arg, i);
+ if (ucv_type(val) != UC_STRING || ucv_string_length(val) != 1)
+ goto free;
+
+ c = ucv_string_get(val)[0];
+ us->input_mask[c / 32] |= 1 << (c % 32);
+ }
+ }
+
+ res = ucv_resource_new(state_type, us);
+ ucv_array_set(us->state, STATE_RES, ucv_get(res));
+ us->res = res;
+ us->fd.fd = fileno(input);
+ us->fd.cb = uc_uline_poll_cb;
+
+ uline_init(&us->s, &uline_cb, us->fd.fd, output, true);
+
+ return res;
+
+free:
+ free(us);
+ return NULL;
+}
+
+static void free_state(void *ptr)
+{
+ struct uc_uline_state *us = ptr;
+ uc_value_t *registry;
+
+ if (!us)
+ return;
+
+ registry = uc_vm_registry_get(us->vm, "uline.registry");
+ ucv_array_set(registry, us->registry_index, NULL);
+ uline_free(&us->s);
+ free(us);
+}
+
+static uc_value_t *
+uc_uline_close(uc_vm_t *vm, size_t nargs)
+{
+ struct uline_state **s = uc_fn_this("uline.state");
+
+ if (!s || !*s)
+ return NULL;
+
+ free_state(*s);
+ *s = NULL;
+
+ return NULL;
+}
+
+static bool
+skip_space(const char **str, const char *end)
+{
+ while (*str < end && isspace(**str))
+ (*str)++;
+ return *str < end;
+}
+
+static void
+add_str(uc_stringbuf_t **buf, const char *str, const char *next)
+{
+ if (str == next)
+ return;
+
+ if (!*buf)
+ *buf = ucv_stringbuf_new();
+ ucv_stringbuf_addstr(*buf, str, next - str);
+}
+
+static void
+uc_uline_add_pos(uc_vm_t *vm, uc_value_t *list, ssize_t start, ssize_t end)
+{
+ uc_value_t *val = ucv_array_new(vm);
+ ucv_array_push(val, ucv_int64_new(start));
+ ucv_array_push(val, ucv_int64_new(end));
+ ucv_array_push(list, ucv_get(val));
+}
+
+static uc_value_t *
+uc_uline_parse_args(uc_vm_t *vm, size_t nargs, bool check)
+{
+ struct uc_arg_parser *argp = uc_fn_thisval("uline.argp");
+ uc_value_t *list = NULL, *pos_list = NULL;
+ uc_value_t *args = NULL, *pos_args = NULL;
+ uc_value_t *str_arg = uc_fn_arg(0);
+ uc_stringbuf_t *buf = NULL;
+ uc_value_t *missing = NULL;
+ uc_value_t *ret;
+ const char *start, *str, *end;
+ ssize_t start_idx = -1, end_idx = 0;
+ enum {
+ UNQUOTED,
+ BACKSLASH,
+ SINGLE_QUOTE,
+ DOUBLE_QUOTE,
+ DOUBLE_QUOTE_BACKSLASH,
+ } state = UNQUOTED;
+ static const char * const state_str[] = {
+ [BACKSLASH] = "\\",
+ [SINGLE_QUOTE] = "'",
+ [DOUBLE_QUOTE] = "\"",
+ [DOUBLE_QUOTE_BACKSLASH] = "\\\"",
+ };
+#define UNQUOTE_TOKENS " \t\r\n'\"\\"
+ char unquote_tok[] = UNQUOTE_TOKENS "\x00";
+ unquote_tok[strlen(UNQUOTE_TOKENS)] = argp->line_sep;
+
+ if (!argp || ucv_type(str_arg) != UC_STRING)
+ return NULL;
+
+ if (!check) {
+ list = ucv_array_new(vm);
+ pos_list = ucv_array_new(vm);
+ if (argp->line_sep) {
+ args = ucv_array_new(vm);
+ pos_args = ucv_array_new(vm);
+ ucv_array_push(args, ucv_get(list));
+ ucv_array_push(pos_args, ucv_get(pos_list));
+ } else {
+ args = list;
+ pos_args = pos_list;
+ }
+ }
+
+ start = str = ucv_string_get(str_arg);
+ end = str + ucv_string_length(str_arg);
+ skip_space(&str, end);
+
+ while (*str && str < end) {
+ const char *next;
+
+ switch (state) {
+ case UNQUOTED:
+ if (isspace(*str)) {
+ skip_space(&str, end);
+ if (!buf)
+ continue;
+
+ ucv_array_push(list, ucv_stringbuf_finish(buf));
+ uc_uline_add_pos(vm, pos_list, start_idx, end_idx);
+ start_idx = -1;
+ buf = NULL;
+ continue;
+ }
+
+ if (start_idx < 0)
+ start_idx = str - start;
+ next = str + strcspn(str, unquote_tok);
+ if (list)
+ add_str(&buf, str, next);
+ str = next;
+ end_idx = str - start;
+
+ switch (*str) {
+ case 0:
+ continue;
+ case '\'':
+ state = SINGLE_QUOTE;
+ break;
+ case '"':
+ state = DOUBLE_QUOTE;
+ break;
+ case '\\':
+ state = BACKSLASH;
+ break;
+ default:
+ if (argp->line_sep &&
+ *str == argp->line_sep) {
+ str++;
+ if (list) {
+ if (buf) {
+ ucv_array_push(list, ucv_stringbuf_finish(buf));
+ uc_uline_add_pos(vm, pos_list, start_idx, end_idx);
+ start_idx = -1;
+ }
+
+ buf = NULL;
+ list = ucv_array_new(vm);
+ ucv_array_push(args, ucv_get(list));
+
+ pos_list = ucv_array_new(vm);
+ ucv_array_push(pos_args, ucv_get(pos_list));
+ }
+ }
+ continue;
+ }
+ if (!buf)
+ buf = ucv_stringbuf_new();
+ str++;
+ break;
+
+ case BACKSLASH:
+ case DOUBLE_QUOTE_BACKSLASH:
+ if (start_idx < 0)
+ start_idx = str - start;
+ if (list && *str != '\n')
+ add_str(&buf, str, str + 1);
+ str++;
+ state--;
+ end_idx = str - start;
+ break;
+
+ case SINGLE_QUOTE:
+ if (start_idx < 0)
+ start_idx = str - start;
+ next = str + strcspn(str, "'");
+ if (list)
+ add_str(&buf, str, next);
+ str = next;
+
+ if (*str == '\'') {
+ state = UNQUOTED;
+ str++;
+ }
+ end_idx = str - start;
+ break;
+
+ case DOUBLE_QUOTE:
+ if (start_idx < 0)
+ start_idx = str - start;
+ next = str + strcspn(str, "\"\\");
+ if (list)
+ add_str(&buf, str, next);
+ str = next;
+
+ if (*str == '"') {
+ state = UNQUOTED;
+ str++;
+ } else if (*str == '\\') {
+ state = DOUBLE_QUOTE_BACKSLASH;
+ str++;
+ }
+ end_idx = str - start;
+ }
+ }
+
+ if (buf) {
+ ucv_array_push(list, ucv_get(ucv_stringbuf_finish(buf)));
+ uc_uline_add_pos(vm, pos_list, start_idx, end_idx);
+ }
+
+ if (state_str[state])
+ missing = ucv_string_new(state_str[state]);
+
+ if (!list)
+ return missing;
+
+ ret = ucv_object_new(vm);
+ ucv_object_add(ret, "args", ucv_get(args));
+ ucv_object_add(ret, "pos", ucv_get(pos_args));
+ if (missing)
+ ucv_object_add(ret, "missing", ucv_get(missing));
+
+ return ret;
+}
+
+static uc_value_t *
+uc_uline_arg_parser(uc_vm_t *vm, size_t nargs)
+{
+ uc_value_t *opts = uc_fn_arg(0);
+ struct uc_arg_parser *argp;
+ uc_value_t *a;
+ char sep = 0;
+
+ if ((a = ucv_object_get(opts, "line_separator", NULL)) != NULL) {
+ if (ucv_type(a) != UC_STRING || ucv_string_length(a) != 1)
+ return NULL;
+
+ sep = ucv_string_get(a)[0];
+ }
+
+ argp = calloc(1, sizeof(*argp));
+ argp->line_sep = sep;
+
+ return ucv_resource_new(argp_type, argp);
+}
+
+static uc_value_t *
+uc_uline_argp_parse(uc_vm_t *vm, size_t nargs)
+{
+ return uc_uline_parse_args(vm, nargs, false);
+}
+
+static uc_value_t *
+uc_uline_argp_check(uc_vm_t *vm, size_t nargs)
+{
+ return uc_uline_parse_args(vm, nargs, true);
+}
+
+static uc_value_t *
+uc_uline_argp_escape(uc_vm_t *vm, size_t nargs)
+{
+ uc_value_t *arg = uc_fn_arg(0);
+ uc_value_t *ref_arg = uc_fn_arg(1);
+ const char *str, *next;
+ uc_stringbuf_t *buf;
+ char ref = 0;
+
+ if (ucv_type(arg) != UC_STRING)
+ return NULL;
+
+ if (ucv_type(ref_arg) == UC_STRING)
+ ref = ucv_string_get(ref_arg)[0];
+
+ str = ucv_string_get(arg);
+ if (ref != '"' && ref != '\'') {
+ next = str + strcspn(str, "\n\t '\"");
+ if (*next)
+ ref = '"';
+ }
+ if (ref != '"' && ref != '\'')
+ return ucv_string_new(str);
+
+ buf = ucv_stringbuf_new();
+ ucv_stringbuf_addstr(buf, &ref, 1);
+
+ while (*str) {
+ next = strchr(str, ref);
+ if (!next) {
+ ucv_stringbuf_addstr(buf, str, strlen(str));
+ break;
+ }
+
+ if (next - str)
+ ucv_stringbuf_addstr(buf, str, next - str);
+ if (ref == '\'')
+ ucv_stringbuf_addstr(buf, "'\\''", 4);
+ else
+ ucv_stringbuf_addstr(buf, "\\\"", 2);
+ str = next + 1;
+ }
+
+ ucv_stringbuf_addstr(buf, &ref, 1);
+
+ return ucv_stringbuf_finish(buf);
+}
+
+static uc_value_t *
+uc_uline_getpass(uc_vm_t *vm, size_t nargs)
+{
+ uc_value_t *prompt = uc_fn_arg(0);
+ char *pw;
+
+ if (ucv_type(prompt) != UC_STRING)
+ return NULL;
+
+ pw = getpass(ucv_string_get(prompt));
+ if (!pw)
+ return NULL;
+
+ return ucv_string_new(pw);
+}
+
+static const uc_function_list_t argp_fns[] = {
+ { "parse", uc_uline_argp_parse },
+ { "check", uc_uline_argp_check },
+ { "escape", uc_uline_argp_escape },
+};
+
+static const uc_function_list_t state_fns[] = {
+ { "close", uc_uline_close },
+ { "poll", uc_uline_poll },
+ { "poll_stop", uc_uline_poll_stop },
+ { "poll_key", uc_uline_poll_key },
+ { "reset_key_input", uc_uline_reset_key_input },
+ { "get_line", uc_uline_get_line },
+ { "get_window", uc_uline_get_window },
+ { "set_hint", uc_uline_set_hint },
+ { "set_state", uc_uline_set_state },
+ { "set_uloop", uc_uline_set_uloop },
+ { "hide_prompt", uc_uline_hide_prompt },
+ { "refresh_prompt", uc_uline_refresh_prompt },
+};
+
+static const uc_function_list_t global_fns[] = {
+ { "new", uc_uline_new },
+ { "arg_parser", uc_uline_arg_parser },
+ { "getpass", uc_uline_getpass },
+};
+
+void uc_module_init(uc_vm_t *vm, uc_value_t *scope)
+{
+ uc_function_list_register(scope, global_fns);
+
+ state_type = uc_type_declare(vm, "uline.state", state_fns, free_state);
+ argp_type = uc_type_declare(vm, "uline.argp", argp_fns, free);
+ registry = ucv_array_new(vm);
+ uc_vm_registry_set(vm, "uline.registry", registry);
+}
diff --git a/package/utils/ucode-mod-uline/src/uline.c b/package/utils/ucode-mod-uline/src/uline.c
new file mode 100644
index 0000000000..4bee2597c3
--- /dev/null
+++ b/package/utils/ucode-mod-uline/src/uline.c
@@ -0,0 +1,919 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd at nbd.name>
+ */
+#include <sys/types.h>
+#include <sys/ioctl.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <errno.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <locale.h>
+
+#include <libubox/list.h>
+
+#include "uline.h"
+#include "private.h"
+
+#define LINEBUF_CHUNK 64
+
+static int sigwinch_count;
+
+static size_t
+nsyms(struct uline_state *s, const char *buf, size_t len)
+{
+ if (!s->utf8)
+ return len;
+ return utf8_nsyms(buf, len);
+}
+
+static inline bool
+is_utf8_cont(unsigned char c)
+{
+ return (c & 0xc0) == 0x80;
+}
+
+static size_t
+utf8_move_left(const char *line, size_t pos)
+{
+ if (!pos)
+ return 0;
+ do {
+ pos--;
+ } while (pos > 0 && is_utf8_cont(line[pos]));
+
+ return pos;
+}
+
+static size_t
+utf8_move_right(const char *line, size_t pos, size_t len)
+{
+ if (pos == len)
+ return pos;
+
+ do {
+ pos++;
+ } while (pos < len && is_utf8_cont(line[pos]));
+
+ return pos;
+}
+
+static char *
+linebuf_extend(struct linebuf *l, size_t size)
+{
+ size_t tailroom = l->bufsize - l->len;
+ char *buf;
+
+ if (l->buf && tailroom > size)
+ goto out;
+
+ size -= tailroom;
+ size += LINEBUF_CHUNK - 1;
+ size -= size % LINEBUF_CHUNK;
+
+ buf = realloc(l->buf, l->bufsize + size);
+ if (!buf)
+ return NULL;
+
+ l->buf = buf;
+ l->bufsize += size;
+
+out:
+ return l->buf + l->len;
+}
+
+static void
+linebuf_free(struct linebuf *line)
+{
+ free(line->buf);
+ free(line->prompt);
+}
+
+static void
+update_window_size(struct uline_state *s, bool init)
+{
+ unsigned int cols = 80, rows = 25;
+#ifdef TIOCGWINSZ
+ struct winsize ws = {};
+
+ if (!ioctl(fileno(s->output), TIOCGWINSZ, &ws)) {
+ if (ws.ws_col)
+ cols = ws.ws_col;
+ if (ws.ws_row)
+ rows = ws.ws_row;
+ }
+#endif
+
+ s->sigwinch_count = sigwinch_count;
+ if (s->cols == cols && s->rows == rows)
+ return;
+
+ s->cols = cols;
+ s->rows = rows;
+ s->full_update = true;
+ s->cb->event(s, EDITLINE_EV_WINDOW_CHANGED);
+}
+
+static void
+handle_sigwinch(int signal)
+{
+ sigwinch_count++;
+}
+
+static void
+reset_input_state(struct uline_state *s)
+{
+ s->utf8_cont = 0;
+ s->esc_idx = -1;
+}
+
+static void
+termios_set_native_mode(struct uline_state *s)
+{
+ struct termios t = s->orig_termios;
+
+ if (!s->has_termios)
+ return;
+
+ t.c_iflag = 0;
+ t.c_oflag = OPOST | ONLCR;
+ t.c_lflag = 0;
+ t.c_cc[VMIN] = 1;
+ t.c_cc[VTIME] = 0;
+
+ tcsetattr(s->input, TCSADRAIN, &t);
+}
+
+static void
+termios_set_orig_mode(struct uline_state *s)
+{
+ if (!s->has_termios)
+ return;
+
+ tcsetattr(s->input, TCSADRAIN, &s->orig_termios);
+}
+
+static bool
+check_utf8(struct uline_state *s, unsigned char c)
+{
+ if (!s->utf8)
+ return false;
+ if (s->utf8_cont)
+ return true;
+ return (c & 0xc0) == 0xc0;
+}
+
+static bool
+handle_utf8(struct uline_state *s, unsigned char c)
+{
+ if (!s->utf8)
+ return false;
+
+ if (!s->utf8_cont) {
+ if ((c & 0xc0) != 0xc0)
+ return false;
+
+ c &= 0xf0;
+ c <<= 1;
+ while (c & 0x80) {
+ c <<= 1;
+ s->utf8_cont++;
+ }
+
+ return true;
+ }
+
+ if ((c & 0xc0) != 0x80) {
+ // invalid utf-8
+ s->utf8_cont = 0;
+ return false;
+ }
+
+ s->utf8_cont--;
+
+ return s->utf8_cont;
+}
+
+static bool
+linebuf_insert(struct linebuf *line, char *c, size_t len)
+{
+ char *dest;
+ ssize_t tail;
+
+ if (!linebuf_extend(line, len + 1))
+ return false;
+
+ dest = &line->buf[line->pos];
+ tail = line->len - line->pos;
+ if (tail > 0)
+ memmove(dest + len, dest, tail);
+ else
+ dest[len] = 0;
+
+ if (line->update_pos > line->pos)
+ line->update_pos = line->pos;
+
+ memcpy(dest, c, len);
+ line->len += len;
+ line->pos += len;
+ line->buf[line->len] = 0;
+
+ return true;
+}
+
+static void
+linebuf_delete(struct linebuf *line, size_t len)
+{
+ char *dest = &line->buf[line->pos];
+ ssize_t tail = line->len - line->pos;
+ size_t max_len = line->len - line->pos;
+
+ if (line->update_pos > line->pos)
+ line->update_pos = line->pos;
+
+ if (len > max_len)
+ len = max_len;
+
+ memmove(dest, dest + len, tail + 1);
+ line->len -= len;
+}
+
+static struct pos
+pos_convert(struct uline_state *s, ssize_t offset)
+{
+ struct pos pos;
+ pos.y = offset / s->cols;
+ pos.x = offset - (pos.y * s->cols);
+ return pos;
+}
+
+static void
+pos_add(struct uline_state *s, struct pos *pos, struct pos add)
+{
+ pos->x += add.x;
+ pos->y += add.y;
+ if (pos->x >= (int16_t)s->cols) {
+ pos->x -= s->cols;
+ pos->y++;
+ }
+ if (pos->x < 0) {
+ pos->x += s->cols;
+ pos->y--;
+ }
+ if (pos->y < 0)
+ pos->y = 0;
+}
+
+static void
+pos_add_ofs(struct uline_state *s, struct pos *pos, size_t offset)
+{
+ pos_add(s, pos, pos_convert(s, offset));
+}
+
+static void
+pos_add_newline(struct uline_state *s, struct pos *pos)
+{
+ pos->x = 0;
+ pos->y++;
+}
+
+static void
+__pos_add_string(struct uline_state *s, struct pos *pos, const char *str, size_t len)
+{
+ const char *next;
+
+ while ((next = memchr(str, KEY_ESC, len)) != NULL) {
+ size_t cur_len = next - str;
+
+ pos_add_ofs(s, pos, nsyms(s, str, cur_len));
+ next++;
+
+ if (*next == '[' || *next == 'O') {
+ next++;
+ while (*next <= 63)
+ next++;
+ }
+ next++;
+ len -= next - str;
+ str = next;
+ }
+
+ pos_add_ofs(s, pos, nsyms(s, str, len));
+}
+
+static void
+pos_add_string(struct uline_state *s, struct pos *pos, const char *str, size_t len)
+{
+ const char *next;
+
+ if (!len)
+ return;
+
+ while ((next = memchr(str, '\n', len)) != NULL) {
+ size_t cur_len = next - str;
+ if (cur_len)
+ __pos_add_string(s, pos, str, cur_len);
+ pos_add_newline(s, pos);
+ len -= cur_len + 1;
+ str = next + 1;
+ }
+
+ if (len)
+ __pos_add_string(s, pos, str, len);
+}
+
+static struct pos
+pos_diff(struct pos start, struct pos end)
+{
+ struct pos diff = {
+ .x = end.x - start.x,
+ .y = end.y - start.y
+ };
+
+ return diff;
+}
+
+static void
+set_cursor(struct uline_state *s, struct pos pos)
+{
+ struct pos diff = pos_diff(s->cursor_pos, pos);
+
+ if (diff.x > 0)
+ vt100_cursor_forward(s->output, diff.x);
+ else if (diff.x < 0)
+ vt100_cursor_back(s->output, -diff.x);
+
+ if (diff.y > 0)
+ vt100_cursor_down(s->output, diff.y);
+ else if (diff.y < 0)
+ vt100_cursor_up(s->output, -diff.y);
+
+ s->cursor_pos = pos;
+}
+
+static void
+display_output_string(struct uline_state *s, const char *str,
+ size_t len)
+{
+ fwrite(str, len, 1, s->output);
+ pos_add_string(s, &s->cursor_pos, str, len);
+}
+
+static void
+display_update_line(struct uline_state *s, struct linebuf *line,
+ struct pos *pos)
+{
+ char *start = line->buf;
+ char *end = line->buf + line->len;
+ struct pos update_pos;
+ size_t prompt_len = 0;
+
+ if (line->prompt)
+ prompt_len = strlen(line->prompt);
+
+ if (s->full_update) {
+ display_output_string(s, line->prompt, prompt_len);
+ *pos = s->cursor_pos;
+ line->update_pos = 0;
+ } else {
+ pos_add_string(s, pos, line->prompt, prompt_len);
+ }
+
+ update_pos = *pos;
+ if (line->update_pos) {
+ start += line->update_pos;
+ pos_add_string(s, &update_pos, line->buf, line->update_pos);
+ }
+ set_cursor(s, update_pos);
+ vt100_erase_right(s->output);
+ line->update_pos = line->len;
+
+ if (end - start <= 0)
+ return;
+
+ display_output_string(s, start, end - start);
+ if (s->cursor_pos.x == 0 && end[-1] != '\n')
+ vt100_next_line(s->output);
+}
+
+static void
+display_update(struct uline_state *s)
+{
+ struct pos edit_pos, end_diff;
+ struct pos base_pos = {};
+ struct linebuf *line = &s->line;
+
+ if (s->full_update) {
+ set_cursor(s, (struct pos){});
+ fputc(KEY_CR, s->output);
+ vt100_erase_down(s->output);
+ }
+
+ display_update_line(s, line, &base_pos);
+
+ if (s->line2) {
+ line = s->line2;
+
+ if (s->cursor_pos.x != 0) {
+ vt100_next_line(s->output);
+ pos_add_newline(s, &s->cursor_pos);
+ }
+
+ base_pos = s->cursor_pos;
+ display_update_line(s, s->line2, &base_pos);
+ }
+
+ edit_pos = base_pos;
+ pos_add_string(s, &edit_pos, line->buf, line->pos);
+
+ end_diff = pos_diff(s->end_pos, s->cursor_pos);
+ s->end_pos = s->cursor_pos;
+
+ if (end_diff.y != 0)
+ vt100_erase_down(s->output);
+ else
+ vt100_erase_right(s->output);
+
+ set_cursor(s, edit_pos);
+ fflush(s->output);
+
+ s->full_update = false;
+}
+
+static bool
+delete_symbol(struct uline_state *s, struct linebuf *line)
+{
+ size_t len = 1;
+
+ if (line->pos == line->len)
+ return false;
+
+ if (s->utf8) {
+ len = utf8_move_right(line->buf, line->pos, line->len);
+ len -= line->pos;
+ }
+
+ linebuf_delete(line, len);
+ return true;
+}
+
+static bool
+move_left(struct uline_state *s, struct linebuf *line)
+{
+ if (!line->pos)
+ return false;
+ if (s->utf8)
+ line->pos = utf8_move_left(line->buf, line->pos);
+ else
+ line->pos--;
+ return true;
+}
+
+static bool
+move_word_left(struct uline_state *s, struct linebuf *line)
+{
+ char *buf = line->buf;
+ size_t pos;
+
+ if (!move_left(s, line))
+ return false;
+
+ pos = line->pos;
+ // remove trailing spaces
+ while (pos > 0 && isspace(buf[pos]))
+ pos--;
+
+ // skip word
+ while (pos > 0 && !isspace(buf[pos]))
+ pos--;
+ if (isspace(buf[pos]))
+ pos++;
+
+ line->pos = pos;
+
+ return true;
+}
+
+static bool
+move_right(struct uline_state *s, struct linebuf *line)
+{
+ if (line->pos >= line->len)
+ return false;
+ if (s->utf8)
+ line->pos = utf8_move_right(line->buf, line->pos, line->len);
+ else
+ line->pos++;
+ return true;
+}
+
+static bool
+move_word_right(struct uline_state *s, struct linebuf *line)
+{
+ char *buf = line->buf;
+ size_t pos = line->pos;
+
+ if (pos == line->len)
+ return false;
+
+ // skip word
+ while (!isspace(buf[pos]) && pos < line->len)
+ pos++;
+
+ // skip trailing whitespace
+ while (isspace(buf[pos]) && pos < line->len)
+ pos++;
+
+ line->pos = pos;
+
+ return true;
+}
+
+static bool
+process_esc(struct uline_state *s, enum vt100_escape esc)
+{
+ struct linebuf *line = &s->line;
+
+ if (s->line2 &&
+ (esc == VT100_DELETE ||
+ (s->cb->line2_cursor && s->cb->line2_cursor(s))))
+ line = s->line2;
+
+ switch (esc) {
+ case VT100_CURSOR_LEFT:
+ return move_left(s, line);
+ case VT100_CURSOR_WORD_LEFT:
+ return move_word_left(s, line);
+ case VT100_CURSOR_RIGHT:
+ return move_right(s, line);
+ case VT100_CURSOR_WORD_RIGHT:
+ return move_word_right(s, line);
+ case VT100_HOME:
+ line->pos = 0;
+ return true;
+ case VT100_END:
+ line->pos = line->len;
+ return true;
+ case VT100_CURSOR_UP:
+ s->cb->event(s, EDITLINE_EV_CURSOR_UP);
+ return true;
+ case VT100_CURSOR_DOWN:
+ s->cb->event(s, EDITLINE_EV_CURSOR_DOWN);
+ return true;
+ case VT100_DELETE:
+ return delete_symbol(s, line);
+ default:
+ vt100_ding(s->output);
+ return false;
+ }
+}
+
+static bool
+process_backword(struct uline_state *s, struct linebuf *line)
+{
+ size_t pos, len;
+
+ pos = line->pos - 1;
+ if (!move_word_left(s, line))
+ return false;
+
+ len = pos + 1 - line->pos;
+ linebuf_delete(line, len);
+
+ return true;
+}
+
+static void
+linebuf_reset(struct linebuf *line)
+{
+ line->pos = 0;
+ line->len = 0;
+ line->buf[0] = 0;
+ line->update_pos = 0;
+}
+
+static void
+free_line2(struct uline_state *s)
+{
+ if (!s->line2)
+ return;
+
+ linebuf_free(s->line2);
+ free(s->line2);
+ s->line2 = NULL;
+}
+
+static bool
+process_newline(struct uline_state *s, bool drop)
+{
+ bool ret;
+
+ if (drop)
+ goto reset;
+
+ termios_set_orig_mode(s);
+ if (s->line2 && s->cb->line2_newline &&
+ s->cb->line2_newline(s, s->line2->buf, s->line2->len)) {
+ termios_set_native_mode(s);
+ return true;
+ }
+
+ free_line2(s);
+ ret = s->cb->line(s, s->line.buf, s->line.len);
+ termios_set_native_mode(s);
+ if (!ret) {
+ linebuf_insert(&s->line, "\n", 1);
+ return true;
+ }
+
+reset:
+ vt100_next_line(s->output);
+ vt100_erase_down(s->output);
+ s->cursor_pos = (struct pos) {};
+ s->full_update = true;
+ fflush(s->output);
+ if (!s->line.len)
+ return true;
+
+ linebuf_reset(&s->line);
+
+ return true;
+}
+
+static bool
+process_ctrl(struct uline_state *s, char c)
+{
+ struct linebuf *line = s->line2 ? s->line2 : &s->line;
+
+ switch (c) {
+ case KEY_LF:
+ case KEY_CR:
+ return process_newline(s, false);
+ case KEY_ETX:
+ s->cb->event(s, EDITLINE_EV_INTERRUPT);
+ process_newline(s, true);
+ s->stop = true;
+ return true;
+ case KEY_EOT:
+ if (s->line.len)
+ return false;
+ s->cb->event(s, EDITLINE_EV_EOF);
+ s->stop = true;
+ return true;
+ case KEY_BS:
+ case KEY_DEL:
+ if (!move_left(s, line))
+ return false;
+
+ delete_symbol(s, line);
+ if (s->line2 && s->cb->line2_update)
+ s->cb->line2_update(s, line->buf, line->len);
+ return true;
+ case KEY_FF:
+ vt100_cursor_home(s->output);
+ vt100_erase_down(s->output);
+ s->full_update = true;
+ return true;
+ case KEY_NAK:
+ linebuf_reset(line);
+ return true;
+ case KEY_SOH:
+ return process_esc(s, VT100_HOME);
+ case KEY_ENQ:
+ return process_esc(s, VT100_END);
+ case KEY_VT:
+ // TODO: kill
+ return false;
+ case KEY_EM:
+ // TODO: yank
+ return false;
+ case KEY_ETB:
+ return process_backword(s, line);
+ case KEY_ESC:
+ s->esc_idx = 0;
+ return false;
+ case KEY_SUB:
+ kill(getpid(), SIGTSTP);
+ return false;
+ default:
+ return false;
+ }
+}
+
+static void
+check_key_repeat(struct uline_state *s, char c)
+{
+ if (s->repeat_char != c)
+ s->repeat_count = 0;
+
+ s->repeat_char = c;
+ s->repeat_count++;
+}
+
+static void
+process_char(struct uline_state *s, char c)
+{
+ enum vt100_escape esc;
+
+ check_key_repeat(s, c);
+ if (s->esc_idx >= 0) {
+ s->esc_seq[s->esc_idx++] = c;
+ s->esc_seq[s->esc_idx] = 0;
+ esc = vt100_esc_decode(s->esc_seq);
+ if (esc == VT100_INCOMPLETE &&
+ s->esc_idx < (int)sizeof(s->esc_seq) - 1)
+ return;
+
+ s->esc_idx = -1;
+ if (!process_esc(s, esc))
+ return;
+ } else if (s->cb->key_input &&
+ !check_utf8(s, (unsigned char )c) &&
+ s->cb->key_input(s, c, s->repeat_count)) {
+ goto out;
+ } else if ((unsigned char)c < 32 || c == 127) {
+ if (!process_ctrl(s, c))
+ return;
+ } else {
+ struct linebuf *line = s->line2 ? s->line2 : &s->line;
+
+ if (!linebuf_insert(line, &c, 1) ||
+ handle_utf8(s, (unsigned char )c))
+ return;
+
+ if (s->line2 && s->cb->line2_update)
+ s->cb->line2_update(s, line->buf, line->len);
+ }
+
+out:
+ if (s->stop)
+ return;
+
+ display_update(s);
+}
+
+void uline_poll(struct uline_state *s)
+{
+ int ret;
+ char c;
+
+ uline_refresh_prompt(s);
+ s->stop = false;
+ while (!s->stop) {
+ ret = read(s->input, &c, 1);
+ if (ret < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno == EAGAIN)
+ return;
+ ret = 0;
+ }
+
+ if (!ret) {
+ s->cb->event(s, EDITLINE_EV_EOF);
+ termios_set_orig_mode(s);
+ return;
+ }
+
+ if (s->sigwinch_count != sigwinch_count)
+ update_window_size(s, false);
+
+ process_char(s, c);
+ }
+}
+
+void uline_set_prompt(struct uline_state *s, const char *str)
+{
+ if (s->line.prompt && !strcmp(s->line.prompt, str))
+ return;
+
+ free(s->line.prompt);
+ s->line.prompt = strdup(str);
+ s->full_update = true;
+}
+
+void uline_set_line2_prompt(struct uline_state *s, const char *str)
+{
+ if (!!str != !!s->line2) {
+ if (!str)
+ free_line2(s);
+ else
+ s->line2 = calloc(1, sizeof(*s->line2));
+ }
+
+ if (!str || (s->line2->prompt && !strcmp(s->line2->prompt, str)))
+ return;
+
+ free(s->line2->prompt);
+ s->line2->prompt = strdup(str);
+ s->full_update = true;
+}
+
+static void
+__uline_set_line(struct uline_state *s, struct linebuf *line, const char *str, size_t len)
+{
+ size_t i, prev_len = line->len;
+
+ line->len = 0;
+ linebuf_extend(line, len);
+ for (i = 0; i < prev_len && i < len; i++) {
+ if (line->buf[i] != str[i])
+ break;
+ }
+ if (i > prev_len)
+ i--;
+ if (s->utf8) {
+ // move back to the beginning of the utf-8 symbol
+ while (i > 0 && (str[i] & 0xc0) == 0x80)
+ i--;
+ }
+ line->update_pos = i;
+
+ memcpy(line->buf, str, len);
+ line->len = len;
+ if (line->pos > line->len)
+ line->pos = line->len;
+}
+
+void uline_set_line(struct uline_state *s, const char *str, size_t len)
+{
+ __uline_set_line(s, &s->line, str, len);
+}
+
+void uline_set_line2(struct uline_state *s, const char *str, size_t len)
+{
+ if (!s->line2)
+ return;
+ __uline_set_line(s, s->line2, str, len);
+}
+
+void uline_hide_prompt(struct uline_state *s)
+{
+ set_cursor(s, (struct pos){});
+ vt100_erase_down(s->output);
+ s->full_update = true;
+ fflush(s->output);
+}
+
+void uline_refresh_prompt(struct uline_state *s)
+{
+ termios_set_native_mode(s);
+ display_update(s);
+}
+
+void uline_set_hint(struct uline_state *s, const char *str, size_t len)
+{
+ struct pos prev_pos = s->cursor_pos;
+
+ if (len) {
+ vt100_next_line(s->output);
+ pos_add_newline(s, &s->cursor_pos);
+ }
+ vt100_erase_down(s->output);
+
+ if (len) {
+ fwrite(str, len, 1, s->output);
+ pos_add_string(s, &s->cursor_pos, str, len);
+ }
+
+ set_cursor(s, prev_pos);
+ fflush(s->output);
+}
+
+void uline_init(struct uline_state *s, const struct uline_cb *cb,
+ int in_fd, FILE *out_stream, bool utf8)
+{
+ struct sigaction sa = {
+ .sa_handler = handle_sigwinch,
+ };
+ s->cb = cb;
+ s->utf8 = utf8;
+ s->input = in_fd;
+ s->output = out_stream;
+ update_window_size(s, true);
+ reset_input_state(s);
+
+#ifdef USE_SYSTEM_WCHAR
+ if (utf8)
+ setlocale(LC_CTYPE, "C.UTF-8");
+#endif
+
+ sigaction(SIGWINCH, &sa, NULL);
+ s->full_update = true;
+
+ if (!tcgetattr(s->input, &s->orig_termios)) {
+ s->has_termios = true;
+ termios_set_native_mode(s);
+ }
+}
+
+void uline_free(struct uline_state *s)
+{
+ free_line2(s);
+ termios_set_orig_mode(s);
+ linebuf_free(&s->line);
+}
diff --git a/package/utils/ucode-mod-uline/src/uline.h b/package/utils/ucode-mod-uline/src/uline.h
new file mode 100644
index 0000000000..6f7b75542f
--- /dev/null
+++ b/package/utils/ucode-mod-uline/src/uline.h
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd at nbd.name>
+ */
+#ifndef __EDITLINE_H
+#define __EDITLINE_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <termios.h>
+#include <stdio.h>
+
+#include <libubox/utils.h>
+
+struct uline_state;
+
+struct linebuf {
+ char *buf;
+ size_t len;
+ size_t bufsize;
+
+ char *prompt;
+ size_t pos;
+ size_t update_pos;
+};
+
+struct pos {
+ int16_t x;
+ int16_t y;
+};
+
+enum uline_event {
+ EDITLINE_EV_CURSOR_UP,
+ EDITLINE_EV_CURSOR_DOWN,
+
+ EDITLINE_EV_WINDOW_CHANGED,
+ EDITLINE_EV_LINE_INPUT,
+
+ EDITLINE_EV_INTERRUPT,
+ EDITLINE_EV_EOF,
+};
+
+struct uline_cb {
+ // called on every key input. return true if handled by callback
+ bool (*key_input)(struct uline_state *s, unsigned char c, unsigned int count);
+
+ void (*event)(struct uline_state *s, enum uline_event ev);
+
+ // line: called on newline, returns true to accept the line, false to keep
+ // editing a multi-line string
+ bool (*line)(struct uline_state *s, const char *str, size_t len);
+
+ // called on any changes to the buffer of the secondary line editor
+ void (*line2_update)(struct uline_state *s, const char *str, size_t len);
+
+ // called on cursor button press during line2 editing
+ // return true to handle in line2, false to handle in primary line
+ bool (*line2_cursor)(struct uline_state *s);
+
+ // called on newline on the secondary line editor
+ // return true to ignore, false to process as primary line newline event
+ bool (*line2_newline)(struct uline_state *s, const char *str, size_t len);
+};
+
+struct uline_state {
+ const struct uline_cb *cb;
+
+ int input;
+ FILE *output;
+
+ int sigwinch_count;
+
+ struct termios orig_termios;
+ bool has_termios;
+
+ struct linebuf line;
+ struct linebuf *line2;
+
+ unsigned int repeat_count;
+ char repeat_char;
+
+ unsigned int rows, cols;
+ struct pos cursor_pos;
+ struct pos end_pos;
+ bool full_update;
+ bool stop;
+
+ bool utf8;
+
+ char esc_seq[8];
+ int8_t esc_idx;
+ uint8_t utf8_cont;
+};
+
+void uline_init(struct uline_state *s, const struct uline_cb *cb,
+ int in_fd, FILE *out_stream, bool utf8);
+void uline_poll(struct uline_state *s);
+
+void uline_set_line(struct uline_state *s, const char *str, size_t len);
+void uline_set_prompt(struct uline_state *s, const char *str);
+static inline void
+uline_set_cursor(struct uline_state *s, size_t pos)
+{
+ s->line.pos = pos;
+ if (s->line.pos > s->line.len)
+ s->line.pos = s->line.len;
+}
+static inline void
+uline_get_line(struct uline_state *s, const char **str, size_t *len)
+{
+ if (s->line.buf) {
+ *str = s->line.buf;
+ *len = s->line.len;
+ } else{
+ *str = "";
+ *len = 0;
+ }
+}
+
+
+
+void uline_set_line2(struct uline_state *s, const char *str, size_t len);
+void uline_set_line2_prompt(struct uline_state *s, const char *str);
+static inline void
+uline_set_line2_cursor(struct uline_state *s, size_t pos)
+{
+ if (!s->line2)
+ return;
+
+ s->line2->pos = pos;
+ if (s->line2->pos > s->line2->len)
+ s->line2->pos = s->line2->len;
+}
+static inline void
+uline_get_line2(struct uline_state *s, const char **str, size_t *len)
+{
+ if (s->line2 && s->line2->buf) {
+ *str = s->line2->buf;
+ *len = s->line2->len;
+ } else{
+ *str = "";
+ *len = 0;
+ }
+}
+
+void uline_set_hint(struct uline_state *s, const char *str, size_t len);
+void uline_hide_prompt(struct uline_state *s);
+void uline_refresh_prompt(struct uline_state *s);
+void uline_free(struct uline_state *s);
+
+#endif
diff --git a/package/utils/ucode-mod-uline/src/utf8.c b/package/utils/ucode-mod-uline/src/utf8.c
new file mode 100644
index 0000000000..3f7c75e68e
--- /dev/null
+++ b/package/utils/ucode-mod-uline/src/utf8.c
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd at nbd.name>
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <unistd.h>
+#include <stdint.h>
+#include <wchar.h>
+
+#include "private.h"
+
+#ifndef USE_SYSTEM_WCHAR
+/*
+ * adapted from musl code:
+ *
+ * Copyright © 2005-2020 Rich Felker, et al.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#undef MB_CUR_MAX
+#define MB_CUR_MAX 4
+
+static const unsigned char table[] = {
+16,16,16,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,16,33,16,16,16,34,35,36,
+37,38,39,40,16,16,41,16,16,16,16,16,16,16,16,16,16,16,42,43,16,16,44,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,45,16,46,47,48,49,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,50,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,51,16,16,52,
+53,16,54,55,56,16,16,16,16,16,16,57,16,16,58,16,59,60,61,62,63,64,65,66,67,68,
+69,70,16,71,72,73,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,74,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,75,76,16,16,16,77,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,78,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,79,80,16,16,16,16,16,16,16,81,16,16,16,16,16,82,83,84,16,16,16,16,16,85,
+86,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,248,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,254,255,255,255,255,191,182,0,0,0,0,0,0,0,63,0,255,23,0,0,0,0,0,248,255,
+255,0,0,1,0,0,0,0,0,0,0,0,0,0,0,192,191,159,61,0,0,0,128,2,0,0,0,255,255,255,
+7,0,0,0,0,0,0,0,0,0,0,192,255,1,0,0,0,0,0,0,248,15,32,0,0,192,251,239,62,0,0,
+0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,255,255,255,255,
+255,7,0,0,0,0,0,0,20,254,33,254,0,12,0,0,0,2,0,0,0,0,0,0,16,30,32,0,0,12,0,0,
+64,6,0,0,0,0,0,0,16,134,57,2,0,0,0,35,0,6,0,0,0,0,0,0,16,190,33,0,0,12,0,0,
+252,2,0,0,0,0,0,0,144,30,32,64,0,12,0,0,0,4,0,0,0,0,0,0,0,1,32,0,0,0,0,0,0,17,
+0,0,0,0,0,0,192,193,61,96,0,12,0,0,0,2,0,0,0,0,0,0,144,64,48,0,0,12,0,0,0,3,0,
+0,0,0,0,0,24,30,32,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,4,92,0,0,0,0,0,0,0,0,0,0,0,
+242,7,128,127,0,0,0,0,0,0,0,0,0,0,0,0,242,31,0,63,0,0,0,0,0,0,0,0,0,3,0,0,160,
+2,0,0,0,0,0,0,254,127,223,224,255,254,255,255,255,31,64,0,0,0,0,0,0,0,0,0,0,0,
+0,224,253,102,0,0,0,195,1,0,30,0,100,32,0,32,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,224,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,28,0,0,0,28,0,0,0,12,0,0,0,12,0,0,0,0,0,0,0,176,63,64,254,
+15,32,0,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96,0,0,0,0,2,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,135,1,4,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+128,9,0,0,0,0,0,0,64,127,229,31,248,159,0,0,0,0,0,0,255,127,0,0,0,0,0,0,0,0,
+15,0,0,0,0,0,208,23,4,0,0,0,0,248,15,0,3,0,0,0,60,59,0,0,0,0,0,0,64,163,3,0,0,
+0,0,0,0,240,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,247,255,253,33,16,
+3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,
+251,0,248,0,0,0,124,0,0,0,0,0,0,223,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,
+255,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,3,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,
+0,60,0,0,0,0,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,128,247,63,0,0,0,192,0,0,0,0,0,0,0,0,0,0,3,0,68,8,0,0,96,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,48,0,0,0,255,255,3,128,0,0,0,0,192,63,0,0,128,255,3,0,
+0,0,0,0,7,0,0,0,0,0,200,51,0,0,0,0,32,0,0,
+0,0,0,0,0,0,126,102,0,8,16,0,0,0,0,0,16,0,0,0,0,0,0,157,193,2,0,0,0,0,48,64,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,33,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,0,0,0,
+64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,0,0,255,
+255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,1,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,110,240,0,
+0,0,0,0,135,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96,0,0,0,0,0,0,0,240,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,255,1,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,255,127,0,0,0,0,0,0,128,
+3,0,0,0,0,0,120,38,0,32,0,0,0,0,0,0,7,0,0,0,128,239,31,0,0,0,0,0,0,0,8,0,3,0,
+0,0,0,0,192,127,0,30,0,0,0,0,0,0,0,0,0,0,0,128,211,64,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,128,248,7,0,0,3,0,0,0,0,0,0,24,1,0,0,0,192,31,31,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,255,92,0,0,64,0,0,0,0,0,0,0,0,0,0,248,133,13,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,60,176,1,0,0,48,0,0,0,0,0,0,0,0,0,0,
+248,167,1,0,0,0,0,0,0,0,0,0,0,0,0,40,191,0,0,0,0,0,0,0,0,0,0,0,0,224,188,15,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,255,6,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,240,12,1,0,0,0,254,7,0,0,0,0,248,121,128,0,126,14,0,0,0,0,0,252,
+127,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,191,0,0,0,0,0,0,0,0,0,0,252,255,
+255,252,109,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,126,180,191,0,0,0,0,0,0,0,0,0,163,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,0,0,0,0,0,0,0,255,
+1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,0,0,0,0,0,0,0,127,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,0,0,0,0,0,0,0,128,7,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96,15,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,3,248,255,231,15,0,0,0,60,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,
+255,255,255,255,127,248,255,255,255,255,255,31,32,0,16,0,0,248,254,255,0,0,0,
+0,0,0,0,0,0,0,127,255,255,249,219,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,240,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,0,0,0,0,0,0,0,0,0,0,0,0,0,240,7,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,};
+
+static const unsigned char wtable[] = {
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,18,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,19,16,20,21,22,16,16,16,23,16,16,24,25,26,27,28,17,
+17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,29,
+17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,
+17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,
+17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,
+17,17,17,17,17,17,17,17,30,16,16,16,16,31,16,16,17,17,17,17,17,17,17,17,17,17,
+17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,
+17,17,17,17,17,17,17,32,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,17,17,16,16,16,33,
+34,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,35,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,
+17,17,17,17,17,17,36,17,17,37,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,17,38,39,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,
+16,16,16,16,16,16,16,40,41,42,43,44,45,46,47,16,48,49,16,16,16,16,
+16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,6,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,30,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96,0,0,48,0,0,0,0,0,0,255,15,0,0,0,0,128,0,0,8,
+0,2,12,0,96,48,64,16,0,0,4,44,36,32,12,0,0,0,1,0,0,0,80,184,0,0,0,0,0,0,0,224,
+0,0,0,1,128,0,0,0,0,0,0,0,0,0,0,0,24,0,0,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,251,255,255,255,255,255,255,255,
+255,255,255,15,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,63,0,0,0,255,15,255,255,255,255,
+255,255,255,127,254,255,255,255,255,255,255,255,255,255,127,254,255,255,255,
+255,255,255,255,255,255,255,255,255,224,255,255,255,255,255,254,255,255,255,
+255,255,255,255,255,255,255,127,255,255,255,255,255,7,255,255,255,255,15,0,
+255,255,255,255,255,127,255,255,255,255,255,0,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,
+0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,31,255,255,255,255,255,255,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,
+255,255,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,15,0,0,0,0,0,0,0,0,0,0,0,0,0,255,3,0,0,255,255,255,255,247,255,127,15,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,254,255,255,255,255,255,255,255,255,255,255,
+255,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,7,0,255,255,255,127,0,0,0,0,0,
+0,7,0,240,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+15,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,64,254,7,0,0,0,0,0,0,0,0,0,0,0,0,7,0,255,255,255,
+255,255,15,255,1,3,0,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,
+1,224,191,255,255,255,255,255,255,255,255,223,255,255,15,0,255,255,255,255,
+255,135,15,0,255,255,17,255,255,255,255,255,255,255,255,127,253,255,255,255,
+255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,
+159,255,255,255,255,255,255,255,63,0,120,255,255,255,0,0,4,0,0,96,0,16,0,0,0,
+0,0,0,0,0,0,0,248,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,
+255,255,255,255,255,255,63,16,39,0,0,24,240,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,255,15,0,
+0,0,224,255,255,255,255,255,255,255,255,255,255,255,255,123,252,255,255,255,
+255,231,199,255,255,255,231,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,15,7,7,0,63,0,0,0,0,0,0,0,0,0,0,0,0,0,
+};
+
+/* Upper 6 state bits are a negative integer offset to bound-check next byte */
+/* equivalent to: ( (b-0x80) | (b+offset) ) & ~0x3f */
+#define OOB(c,b) (((((b)>>3)-0x10)|(((b)>>3)+((int32_t)(c)>>26))) & ~7)
+
+/* Interval [a,b). Either a must be 80 or b must be c0, lower 3 bits clear. */
+#define R(a,b) ((uint32_t)((a==0x80 ? 0x40u-b : 0u-a) << 23))
+#define FAILSTATE R(0x80,0x80)
+
+#define SA 0xc2u
+#define SB 0xf4u
+
+/* Arbitrary encoding for representing code units instead of characters. */
+#define CODEUNIT(c) (0xdfff & (signed char)(c))
+#define IS_CODEUNIT(c) ((unsigned)(c)-0xdf80 < 0x80)
+
+static int
+internal_mbtowc(wchar_t *restrict wc, const char *restrict src, size_t n)
+{
+#define C(x) ( x<2 ? -1 : ( R(0x80,0xc0) | x ) )
+#define D(x) C((x+16))
+#define E(x) ( ( x==0 ? R(0xa0,0xc0) : \
+ x==0xd ? R(0x80,0xa0) : \
+ R(0x80,0xc0) ) \
+ | ( R(0x80,0xc0) >> 6 ) \
+ | x )
+#define F(x) ( ( x>=5 ? 0 : \
+ x==0 ? R(0x90,0xc0) : \
+ x==4 ? R(0x80,0x90) : \
+ R(0x80,0xc0) ) \
+ | ( R(0x80,0xc0) >> 6 ) \
+ | ( R(0x80,0xc0) >> 12 ) \
+ | x )
+
+ static const uint32_t bittab[] = {
+ C(0x2),C(0x3),C(0x4),C(0x5),C(0x6),C(0x7),
+ C(0x8),C(0x9),C(0xa),C(0xb),C(0xc),C(0xd),C(0xe),C(0xf),
+ D(0x0),D(0x1),D(0x2),D(0x3),D(0x4),D(0x5),D(0x6),D(0x7),
+ D(0x8),D(0x9),D(0xa),D(0xb),D(0xc),D(0xd),D(0xe),D(0xf),
+ E(0x0),E(0x1),E(0x2),E(0x3),E(0x4),E(0x5),E(0x6),E(0x7),
+ E(0x8),E(0x9),E(0xa),E(0xb),E(0xc),E(0xd),E(0xe),E(0xf),
+ F(0x0),F(0x1),F(0x2),F(0x3),F(0x4)
+ };
+ unsigned c;
+ const unsigned char *s = (const void *)src;
+ wchar_t dummy;
+
+ if (!s) return 0;
+ if (!n) goto ilseq;
+ if (!wc) wc = &dummy;
+
+ if (*s < 0x80) return !!(*wc = *s);
+ if (MB_CUR_MAX==1) return (*wc = CODEUNIT(*s)), 1;
+ if (*s-SA > SB-SA) goto ilseq;
+ c = bittab[*s++-SA];
+
+ /* Avoid excessive checks against n: If shifting the state n-1
+ * times does not clear the high bit, then the value of n is
+ * insufficient to read a character */
+ if (n<4 && ((c<<(6*n-6)) & (1U<<31))) goto ilseq;
+
+ if (OOB(c,*s)) goto ilseq;
+ c = c<<6 | *s++-0x80;
+ if (!(c&(1U<<31))) {
+ *wc = c;
+ return 2;
+ }
+
+ if (*s-0x80u >= 0x40) goto ilseq;
+ c = c<<6 | *s++-0x80;
+ if (!(c&(1U<<31))) {
+ *wc = c;
+ return 3;
+ }
+
+ if (*s-0x80u >= 0x40) goto ilseq;
+ *wc = c<<6 | *s++-0x80;
+ return 4;
+
+ilseq:
+ errno = EILSEQ;
+ return -1;
+}
+
+static int internal_wcwidth(wchar_t wc)
+{
+ if (wc < 0xff)
+ return (wc+1 & 0x7f) >= 0x21 ? 1 : wc ? -1 : 0;
+ if ((wc & 0xfffeffffU) < 0xfffe) {
+ if ((table[table[wc>>8]*32+((wc&255)>>3)]>>(wc&7))&1)
+ return 0;
+ if ((wtable[wtable[wc>>8]*32+((wc&255)>>3)]>>(wc&7))&1)
+ return 2;
+ return 1;
+ }
+ if ((wc & 0xfffe) == 0xfffe)
+ return -1;
+ if (wc-0x20000U < 0x20000)
+ return 2;
+ if (wc == 0xe0001 || wc-0xe0020U < 0x5f || wc-0xe0100U < 0xef)
+ return 0;
+ return 1;
+}
+
+#define mbtowc internal_mbtowc
+#define wcwidth internal_wcwidth
+
+#endif
+
+ssize_t utf8_nsyms(const char *str, size_t len)
+{
+ size_t nsyms = 0;
+ size_t ofs = 0;
+
+ while (ofs < len) {
+ wchar_t sym;
+ int ret;
+
+ ret = mbtowc(&sym, str + ofs, len - ofs);
+ if (ret <= 0) {
+ ret = 1;
+ sym = 'A';
+ } else if ((size_t)ret > len) {
+ ret = len;
+ }
+
+ ofs += ret;
+ ret = wcwidth(sym);
+ if (ret < 0)
+ continue;
+
+ nsyms += ret;
+ }
+
+ return nsyms;
+}
diff --git a/package/utils/ucode-mod-uline/src/vt100.c b/package/utils/ucode-mod-uline/src/vt100.c
new file mode 100644
index 0000000000..b13e6a6722
--- /dev/null
+++ b/package/utils/ucode-mod-uline/src/vt100.c
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd at nbd.name>
+ */
+#include <string.h>
+#include <stdlib.h>
+#include "uline.h"
+#include "private.h"
+
+enum vt100_escape vt100_esc_decode(const char *str)
+{
+ unsigned long code;
+ size_t idx;
+
+ switch (*(str++)) {
+ case 0:
+ return VT100_INCOMPLETE;
+ case '[':
+ case 'O':
+ switch (*(str++)) {
+ case 0:
+ return VT100_INCOMPLETE;
+ case 'A':
+ return VT100_CURSOR_UP;
+ case 'B':
+ return VT100_CURSOR_DOWN;
+ case 'C':
+ return VT100_CURSOR_RIGHT;
+ case 'D':
+ return VT100_CURSOR_LEFT;
+ case 'F':
+ return VT100_END;
+ case 'H':
+ return VT100_HOME;
+ case '5':
+ switch (*str) {
+ case 'C':
+ return VT100_CURSOR_WORD_RIGHT;
+ case 'D':
+ return VT100_CURSOR_WORD_LEFT;
+ default:
+ break;
+ }
+ /* fallthrough */
+ case '0' ... '4':
+ case '6' ... '9':
+ str--;
+ idx = strspn(str, "0123456789");
+ if (!str[idx])
+ return VT100_INCOMPLETE;
+ if (str[idx] != '~')
+ return VT100_UNKNOWN;
+ code = strtoul(str, NULL, 10);
+ switch (code) {
+ case 1:
+ return VT100_HOME;
+ case 3:
+ return VT100_DELETE;
+ case 4:
+ return VT100_END;
+ case 200:
+ case 201:
+ // paste start/end
+ return VT100_IGNORE;
+ default:
+ return VT100_UNKNOWN;
+ }
+ default:
+ return VT100_UNKNOWN;
+ }
+ default:
+ return VT100_UNKNOWN;
+ }
+}
+
+void __vt100_csi_num(FILE *out, int num, char code)
+{
+ fprintf(out, "\e[%d%c", num, code);
+}
+
+void __vt100_esc(FILE *out, char c)
+{
+ char seq[] = "\eX";
+ seq[1] = c;
+ fputs(seq, out);
+}
+
+void __vt100_csi2(FILE *out, char c1, char c2)
+{
+ char seq[] = "\e[XX";
+
+ seq[2] = c1;
+ seq[3] = c2;
+ fputs(seq, out);
+}
More information about the lede-commits
mailing list