ucode-mod-uline: add package for ucode terminal line editing
authorFelix Fietkau <nbd@nbd.name>
Mon, 13 Jan 2025 11:28:30 +0000 (12:28 +0100)
committerFelix Fietkau <nbd@nbd.name>
Mon, 13 Jan 2025 11:28:30 +0000 (12:28 +0100)
Signed-off-by: Felix Fietkau <nbd@nbd.name>
package/utils/ucode-mod-uline/Makefile [new file with mode: 0644]
package/utils/ucode-mod-uline/src/CMakeLists.txt [new file with mode: 0644]
package/utils/ucode-mod-uline/src/private.h [new file with mode: 0644]
package/utils/ucode-mod-uline/src/ucode.c [new file with mode: 0644]
package/utils/ucode-mod-uline/src/uline.c [new file with mode: 0644]
package/utils/ucode-mod-uline/src/uline.h [new file with mode: 0644]
package/utils/ucode-mod-uline/src/utf8.c [new file with mode: 0644]
package/utils/ucode-mod-uline/src/vt100.c [new file with mode: 0644]

diff --git a/package/utils/ucode-mod-uline/Makefile b/package/utils/ucode-mod-uline/Makefile
new file mode 100644 (file)
index 0000000..13d691b
--- /dev/null
@@ -0,0 +1,32 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=ucode-mod-uline
+PKG_RELEASE:=1
+PKG_LICENSE:=GPL-2.0-or-later
+PKG_MAINTAINER:=Felix Fietkau <nbd@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_UTF8=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 (file)
index 0000000..efa2d80
--- /dev/null
@@ -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 (file)
index 0000000..fa38d06
--- /dev/null
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd@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 (file)
index 0000000..6a20cd2
--- /dev/null
@@ -0,0 +1,748 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
+ */
+#include <stdio.h>
+#include <string.h>
+#include <ctype.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_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;
+
+       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 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 *str_arg = uc_fn_arg(0);
+       uc_stringbuf_t *buf = NULL;
+       uc_value_t *missing = NULL;
+       uc_value_t *args = NULL;
+       uc_value_t *list = NULL;
+       uc_value_t *ret;
+       const char *str, *end;
+       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);
+               if (argp->line_sep) {
+                       args = ucv_array_new(vm);
+                       ucv_array_push(args, ucv_get(list));
+               } else {
+                       args = list;
+               }
+       }
+
+       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));
+                               buf = NULL;
+                               continue;
+                       }
+
+                       next = str + strcspn(str, unquote_tok);
+                       if (list)
+                               add_str(&buf, str, next);
+                       str = next;
+
+                       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));
+                                               buf = NULL;
+                                               list = ucv_array_new(vm);
+                                               ucv_array_push(args, ucv_get(list));
+                                       }
+                               }
+                               continue;
+                       }
+                       if (!buf)
+                               buf = ucv_stringbuf_new();
+                       str++;
+                       break;
+
+               case BACKSLASH:
+               case DOUBLE_QUOTE_BACKSLASH:
+                       if (list && *str != '\n')
+                               add_str(&buf, str, str + 1);
+                       str++;
+                       state--;
+                       break;
+
+               case SINGLE_QUOTE:
+                       next = str + strcspn(str, "'");
+                       if (list)
+                               add_str(&buf, str, next);
+                       str = next;
+
+                       if (*str == '\'') {
+                               state = UNQUOTED;
+                               str++;
+                       }
+                       break;
+
+               case DOUBLE_QUOTE:
+                       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++;
+                       }
+               }
+       }
+
+       if (buf)
+               ucv_array_push(list, ucv_get(ucv_stringbuf_finish(buf)));
+
+       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));
+       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 const uc_function_list_t argp_fns[] = {
+       { "parse", uc_uline_argp_parse },
+       { "check", uc_uline_argp_check },
+};
+
+static const uc_function_list_t state_fns[] = {
+       { "close", uc_uline_close },
+       { "poll", uc_uline_poll },
+       { "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 },
+};
+
+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 (file)
index 0000000..1f30325
--- /dev/null
@@ -0,0 +1,909 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd@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;
+
+       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 = 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->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;
+               }
+
+               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 (file)
index 0000000..6f7b755
--- /dev/null
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd@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 (file)
index 0000000..3f7c75e
--- /dev/null
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd@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 (file)
index 0000000..522dd7f
--- /dev/null
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: ISC
+/*
+ * Copyright (C) 2025 Felix Fietkau <nbd@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 '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);
+}