From: Felix Fietkau Date: Mon, 13 Jan 2025 11:28:30 +0000 (+0100) Subject: ucode-mod-uline: add package for ucode terminal line editing X-Git-Url: http://git.cdn.openwrt.org/?a=commitdiff_plain;h=94c34ed5e2ff5495273c517aaf04a370aaf33328;p=openwrt%2Fstaging%2Fnbd.git ucode-mod-uline: add package for ucode terminal line editing Signed-off-by: Felix Fietkau --- diff --git a/package/utils/ucode-mod-uline/Makefile b/package/utils/ucode-mod-uline/Makefile new file mode 100644 index 0000000000..13d691b0d4 --- /dev/null +++ b/package/utils/ucode-mod-uline/Makefile @@ -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 + +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 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 + */ +#ifndef __EDITLINE_PRIVATE_H +#define __EDITLINE_PRIVATE_H + +#include + +#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..6a20cd2667 --- /dev/null +++ b/package/utils/ucode-mod-uline/src/ucode.c @@ -0,0 +1,748 @@ +// SPDX-License-Identifier: ISC +/* + * Copyright (C) 2025 Felix Fietkau + */ +#include +#include +#include + +#include +#include +#include + +#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 index 0000000000..1f30325356 --- /dev/null +++ b/package/utils/ucode-mod-uline/src/uline.c @@ -0,0 +1,909 @@ +// SPDX-License-Identifier: ISC +/* + * Copyright (C) 2025 Felix Fietkau + */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 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 + */ +#ifndef __EDITLINE_H +#define __EDITLINE_H + +#include +#include +#include +#include + +#include + +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 + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#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..522dd7f564 --- /dev/null +++ b/package/utils/ucode-mod-uline/src/vt100.c @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: ISC +/* + * Copyright (C) 2025 Felix Fietkau + */ +#include +#include +#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); +}