ucode-mod-uline: add package for ucode terminal line editing

It provides a ucode module with similar functionality as libreadline,
however with much smaller code and no dependencies aside from ucode and
libubox.
It also provides shell-style parsing/escaping code useful for building
a CLI.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
This commit is contained in:
Felix Fietkau 2025-01-13 12:28:30 +01:00
parent b6415e9fa9
commit be31d44bd9
8 changed files with 2680 additions and 0 deletions

View File

@ -0,0 +1,32 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=ucode-mod-uline
PKG_RELEASE:=$(AUTORELEASE)
PKG_LICENSE:=GPL-2.0-or-later
PKG_MAINTAINER:=Felix Fietkau <nbd@nbd.name>
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/cmake.mk
CMAKE_INSTALL := 1
define Package/ucode-mod-uline
SECTION:=utils
CATEGORY:=Utilities
TITLE:=ucode module for terminal line editing
DEPENDS:=+libucode +libubox
endef
CMAKE_OPTIONS += -DUSE_SYSTEM_WCHAR=ON
define Package/ucode-mod-uline/description
This module provides similar functionality as libreadline for ucode, without
depending on other libraries like ncurses.
endef
define Package/ucode-mod-uline/install
$(INSTALL_DIR) $(1)/usr/lib/ucode
$(CP) $(PKG_INSTALL_DIR)/usr/lib/ucode/uline.so $(1)/usr/lib/ucode/
endef
$(eval $(call BuildPackage,ucode-mod-uline))

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,905 @@
// SPDX-License-Identifier: ISC
/*
* Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
*/
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <poll.h>
#include <ucode/module.h>
#include <libubox/list.h>
#include <libubox/uloop.h>
#include "uline.h"
static uc_value_t *registry;
static uc_resource_type_t *state_type, *argp_type;
enum {
STATE_RES,
STATE_CB,
STATE_INPUT,
STATE_OUTPUT,
STATE_POLL_CB,
};
struct uc_uline_state {
struct uloop_fd fd;
struct uline_state s;
int registry_index;
uc_vm_t *vm;
uc_value_t *state, *cb, *res, *poll_cb;
uc_value_t *line;
uint32_t input_mask[256 / 32];
};
struct uc_arg_parser {
char line_sep;
};
static unsigned int
registry_set(uc_vm_t *vm, uc_value_t *val)
{
uc_value_t *registry;
size_t i, len;
registry = uc_vm_registry_get(vm, "uline.registry");
len = ucv_array_length(registry);
for (i = 0; i < len; i++)
if (ucv_array_get(registry, i) == NULL)
break;
ucv_array_set(registry, i, ucv_get(val));
return i;
}
static uc_value_t *
uc_uline_poll(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
uc_value_t *val;
if (!us)
return NULL;
uline_poll(&us->s);
val = us->line;
us->line = NULL;
return val;
}
static uc_value_t *
uc_uline_poll_key(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
uc_value_t *timeout_arg = uc_fn_arg(0);
struct pollfd pfd = {};
int timeout, len;
char c;
if (!us)
return NULL;
if (ucv_type(timeout_arg) == UC_INTEGER)
timeout = ucv_int64_get(timeout_arg);
else
timeout = -1;
pfd.fd = us->s.input;
pfd.events = POLLIN;
poll(&pfd, 1, timeout);
if (!(pfd.revents & POLLIN))
return NULL;
do {
len = read(pfd.fd, &c, 1);
} while (len < 0 && errno == EINTR);
if (len != 1)
return NULL;
return ucv_string_new_length(&c, 1);
}
static uc_value_t *
uc_uline_poll_stop(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
if (!us)
return NULL;
us->s.stop = true;
return NULL;
}
static uc_value_t *
uc_uline_get_window(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
uc_value_t *val;
if (!us)
return NULL;
val = ucv_object_new(vm);
ucv_object_add(val, "x", ucv_int64_new(us->s.cols));
ucv_object_add(val, "y", ucv_int64_new(us->s.rows));
return val;
}
static uc_value_t *
uc_uline_get_line(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
uc_value_t *line2 = uc_fn_arg(0);
uc_value_t *state, *val;
const char *line;
size_t len;
if (!us)
return NULL;
state = ucv_object_new(vm);
if (ucv_is_truish(line2))
uline_get_line2(&us->s, &line, &len);
else
uline_get_line(&us->s, &line, &len);
val = ucv_string_new_length(line, len);
ucv_object_add(state, "line", ucv_get(val));
ucv_object_add(state, "pos", ucv_int64_new(us->s.line.pos));
return state;
}
static uc_value_t *
uc_uline_set_state(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
uc_value_t *state = uc_fn_arg(0);
uc_value_t *arg;
bool found;
if (!us || ucv_type(state) != UC_OBJECT)
return NULL;
if ((arg = ucv_object_get(state, "prompt", NULL)) != NULL) {
if (ucv_type(arg) != UC_STRING)
return NULL;
uline_set_prompt(&us->s, ucv_string_get(arg));
}
if ((arg = ucv_object_get(state, "line", NULL)) != NULL) {
if (ucv_type(arg) != UC_STRING)
return NULL;
uline_set_line(&us->s, ucv_string_get(arg), ucv_string_length(arg));
}
if ((arg = ucv_object_get(state, "pos", NULL)) != NULL) {
if (ucv_type(arg) != UC_INTEGER)
return NULL;
uline_set_cursor(&us->s, ucv_int64_get(arg));
}
arg = ucv_object_get(state, "line2_prompt", &found);
if (found) {
if (!arg)
uline_set_line2_prompt(&us->s, NULL);
else if (ucv_type(arg) == UC_STRING)
uline_set_line2_prompt(&us->s, ucv_string_get(arg));
else
return NULL;
}
if ((arg = ucv_object_get(state, "line2", NULL)) != NULL) {
if (ucv_type(arg) != UC_STRING)
return NULL;
uline_set_line2(&us->s, ucv_string_get(arg), ucv_string_length(arg));
}
if ((arg = ucv_object_get(state, "line2_pos", NULL)) != NULL) {
if (ucv_type(arg) != UC_INTEGER)
return NULL;
uline_set_line2_cursor(&us->s, ucv_int64_get(arg));
}
return ucv_boolean_new(true);
}
static uc_value_t *
uc_uline_set_hint(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
uc_value_t *arg = uc_fn_arg(0);
if (!us || ucv_type(arg) != UC_STRING)
return NULL;
uline_set_hint(&us->s, ucv_string_get(arg), ucv_string_length(arg));
return ucv_boolean_new(true);
}
static uc_value_t *
uc_uline_set_uloop(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
uc_value_t *cb = uc_fn_arg(0);
if (!us || (cb && !ucv_is_callable(cb)))
return NULL;
us->poll_cb = cb;
ucv_array_set(us->state, STATE_POLL_CB, ucv_get(cb));
if (cb) {
uloop_fd_add(&us->fd, ULOOP_READ);
us->fd.cb(&us->fd, 0);
} else {
uloop_fd_delete(&us->fd);
}
return ucv_boolean_new(true);
}
static uc_value_t *
uc_uline_reset_key_input(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
us->s.repeat_char = 0;
return ucv_boolean_new(true);
}
static uc_value_t *
uc_uline_hide_prompt(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
if (!us)
return NULL;
uline_hide_prompt(&us->s);
return ucv_boolean_new(true);
}
static uc_value_t *
uc_uline_refresh_prompt(uc_vm_t *vm, size_t nargs)
{
struct uc_uline_state *us = uc_fn_thisval("uline.state");
if (!us)
return NULL;
uline_refresh_prompt(&us->s);
return ucv_boolean_new(true);
}
static bool
cb_prepare(struct uc_uline_state *us, const char *name)
{
uc_value_t *func;
func = ucv_object_get(us->cb, name, NULL);
if (!func)
return false;
uc_vm_stack_push(us->vm, ucv_get(us->res));
uc_vm_stack_push(us->vm, ucv_get(func));
return true;
}
static uc_value_t *
cb_call_ret(struct uc_uline_state *us, size_t args, ...)
{
va_list ap;
va_start(ap, args);
for (size_t i = 0; i < args; i++)
uc_vm_stack_push(us->vm, ucv_get(va_arg(ap, void *)));
va_end(ap);
if (uc_vm_call(us->vm, true, args) == EXCEPTION_NONE)
return uc_vm_stack_pop(us->vm);
return NULL;
}
#define cb_call(...) ucv_put(cb_call_ret(__VA_ARGS__))
static bool
uc_uline_cb_line(struct uline_state *s, const char *str, size_t len)
{
struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
bool complete = true;
uc_value_t *ret;
if (cb_prepare(us, "line_check")) {
ret = cb_call_ret(us, 1, ucv_string_new_length(str, len));
complete = ucv_is_truish(ret);
ucv_put(ret);
}
s->stop = complete;
if (complete)
us->line = ucv_string_new_length(str, len);
return complete;
}
static void
uc_uline_cb_event(struct uline_state *s, enum uline_event ev)
{
struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
static const char * const ev_types[] = {
[EDITLINE_EV_CURSOR_UP] = "cursor_up",
[EDITLINE_EV_CURSOR_DOWN] = "cursor_down",
[EDITLINE_EV_WINDOW_CHANGED] = "window_changed",
[EDITLINE_EV_EOF] = "eof",
[EDITLINE_EV_INTERRUPT] = "interrupt",
};
if (ev > ARRAY_SIZE(ev_types) || !ev_types[ev])
return;
if (!cb_prepare(us, ev_types[ev]))
return;
if (ev == EDITLINE_EV_WINDOW_CHANGED)
cb_call(us, 2, ucv_int64_new(s->cols), ucv_int64_new(s->rows));
else
cb_call(us, 0);
}
static void uc_uline_poll_cb(struct uloop_fd *fd, unsigned int events)
{
struct uc_uline_state *us = container_of(fd, struct uc_uline_state, fd);
uc_value_t *val;
while (!uloop_cancelled && us->poll_cb) {
uline_poll(&us->s);
val = us->line;
if (!val)
break;
us->line = NULL;
if (!ucv_is_callable(us->poll_cb))
return;
uc_vm_stack_push(us->vm, ucv_get(us->res));
uc_vm_stack_push(us->vm, ucv_get(us->poll_cb));
cb_call(us, 1, val);
}
}
static bool
uc_uline_cb_key_input(struct uline_state *s, unsigned char c, unsigned int count)
{
struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
uc_value_t *ret;
bool retval;
if (!(us->input_mask[c / 32] & (1 << (c % 32))))
return false;
if (!cb_prepare(us, "key_input"))
return false;
ret = cb_call_ret(us, 2, ucv_string_new_length((char *)&c, 1), ucv_int64_new(count));
retval = ucv_is_truish(ret);
ucv_put(ret);
return retval;
}
static void
uc_uline_cb_line2_update(struct uline_state *s, const char *str, size_t len)
{
struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
if (cb_prepare(us, "line2_update"))
cb_call(us, 1, ucv_string_new_length(str, len));
}
static bool
uc_uline_cb_line2_cursor(struct uline_state *s)
{
struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
uc_value_t *retval;
bool ret = true;
if (cb_prepare(us, "line2_cursor")) {
retval = cb_call_ret(us, 0);
ret = ucv_is_truish(retval);
ucv_put(retval);
}
return ret;
}
static bool
uc_uline_cb_line2_newline(struct uline_state *s, const char *str, size_t len)
{
struct uc_uline_state *us = container_of(s, struct uc_uline_state, s);
uc_value_t *retval;
bool ret = false;
if (cb_prepare(us, "line2_newline")) {
retval = cb_call_ret(us, 1, ucv_string_new_length(str, len));
ret = ucv_is_truish(retval);
ucv_put(retval);
}
return ret;
}
static uc_value_t *
uc_uline_new(uc_vm_t *vm, size_t nargs)
{
static const struct uline_cb uline_cb = {
#define _CB(_type) ._type = uc_uline_cb_##_type
_CB(key_input),
_CB(line),
_CB(event),
_CB(line2_update),
_CB(line2_cursor),
_CB(line2_newline),
#undef _CB
};
uc_value_t *data = uc_fn_arg(0);
struct uc_uline_state *us;
FILE *input, *output;
uc_value_t *arg, *cb, *state, *res;
if (ucv_type(data) != UC_OBJECT)
return NULL;
cb = ucv_object_get(data, "cb", NULL);
if (ucv_type(cb) != UC_OBJECT)
return NULL;
state = ucv_array_new(vm);
ucv_array_set(state, 0, ucv_get(cb));
if ((arg = ucv_object_get(data, "input", NULL)) != NULL) {
input = ucv_resource_data(arg, "fs.file");
ucv_array_set(state, STATE_INPUT, ucv_get(arg));
} else {
input = stdin;
}
if ((arg = ucv_object_get(data, "output", NULL)) != NULL) {
output = ucv_resource_data(arg, "fs.file");
ucv_array_set(state, STATE_OUTPUT, ucv_get(arg));
} else {
output = stdout;
}
if (!input || !output) {
input = output = NULL;
return NULL;
}
us = calloc(1, sizeof(*us));
us->vm = vm;
us->state = ucv_array_new(vm);
ucv_array_set(us->state, STATE_CB, ucv_get(cb));
us->cb = cb;
us->registry_index = registry_set(vm, state);
if ((arg = ucv_object_get(data, "key_input_list", NULL)) != NULL) {
uc_value_t *val;
size_t len;
if (ucv_type(arg) != UC_ARRAY)
goto free;
len = ucv_array_length(arg);
for (size_t i = 0; i < len; i++) {
unsigned char c;
val = ucv_array_get(arg, i);
if (ucv_type(val) != UC_STRING || ucv_string_length(val) != 1)
goto free;
c = ucv_string_get(val)[0];
us->input_mask[c / 32] |= 1 << (c % 32);
}
}
res = ucv_resource_new(state_type, us);
ucv_array_set(us->state, STATE_RES, ucv_get(res));
us->res = res;
us->fd.fd = fileno(input);
us->fd.cb = uc_uline_poll_cb;
uline_init(&us->s, &uline_cb, us->fd.fd, output, true);
return res;
free:
free(us);
return NULL;
}
static void free_state(void *ptr)
{
struct uc_uline_state *us = ptr;
uc_value_t *registry;
if (!us)
return;
registry = uc_vm_registry_get(us->vm, "uline.registry");
ucv_array_set(registry, us->registry_index, NULL);
uline_free(&us->s);
free(us);
}
static uc_value_t *
uc_uline_close(uc_vm_t *vm, size_t nargs)
{
struct uline_state **s = uc_fn_this("uline.state");
if (!s || !*s)
return NULL;
free_state(*s);
*s = NULL;
return NULL;
}
static bool
skip_space(const char **str, const char *end)
{
while (*str < end && isspace(**str))
(*str)++;
return *str < end;
}
static void
add_str(uc_stringbuf_t **buf, const char *str, const char *next)
{
if (str == next)
return;
if (!*buf)
*buf = ucv_stringbuf_new();
ucv_stringbuf_addstr(*buf, str, next - str);
}
static void
uc_uline_add_pos(uc_vm_t *vm, uc_value_t *list, ssize_t start, ssize_t end)
{
uc_value_t *val = ucv_array_new(vm);
ucv_array_push(val, ucv_int64_new(start));
ucv_array_push(val, ucv_int64_new(end));
ucv_array_push(list, ucv_get(val));
}
static uc_value_t *
uc_uline_parse_args(uc_vm_t *vm, size_t nargs, bool check)
{
struct uc_arg_parser *argp = uc_fn_thisval("uline.argp");
uc_value_t *list = NULL, *pos_list = NULL;
uc_value_t *args = NULL, *pos_args = NULL;
uc_value_t *str_arg = uc_fn_arg(0);
uc_stringbuf_t *buf = NULL;
uc_value_t *missing = NULL;
uc_value_t *ret;
const char *start, *str, *end;
ssize_t start_idx = -1, end_idx = 0;
enum {
UNQUOTED,
BACKSLASH,
SINGLE_QUOTE,
DOUBLE_QUOTE,
DOUBLE_QUOTE_BACKSLASH,
} state = UNQUOTED;
static const char * const state_str[] = {
[BACKSLASH] = "\\",
[SINGLE_QUOTE] = "'",
[DOUBLE_QUOTE] = "\"",
[DOUBLE_QUOTE_BACKSLASH] = "\\\"",
};
#define UNQUOTE_TOKENS " \t\r\n'\"\\"
char unquote_tok[] = UNQUOTE_TOKENS "\x00";
unquote_tok[strlen(UNQUOTE_TOKENS)] = argp->line_sep;
if (!argp || ucv_type(str_arg) != UC_STRING)
return NULL;
if (!check) {
list = ucv_array_new(vm);
pos_list = ucv_array_new(vm);
if (argp->line_sep) {
args = ucv_array_new(vm);
pos_args = ucv_array_new(vm);
ucv_array_push(args, ucv_get(list));
ucv_array_push(pos_args, ucv_get(pos_list));
} else {
args = list;
pos_args = pos_list;
}
}
start = str = ucv_string_get(str_arg);
end = str + ucv_string_length(str_arg);
skip_space(&str, end);
while (*str && str < end) {
const char *next;
switch (state) {
case UNQUOTED:
if (isspace(*str)) {
skip_space(&str, end);
if (!buf)
continue;
ucv_array_push(list, ucv_stringbuf_finish(buf));
uc_uline_add_pos(vm, pos_list, start_idx, end_idx);
start_idx = -1;
buf = NULL;
continue;
}
if (start_idx < 0)
start_idx = str - start;
next = str + strcspn(str, unquote_tok);
if (list)
add_str(&buf, str, next);
str = next;
end_idx = str - start;
switch (*str) {
case 0:
continue;
case '\'':
state = SINGLE_QUOTE;
break;
case '"':
state = DOUBLE_QUOTE;
break;
case '\\':
state = BACKSLASH;
break;
default:
if (argp->line_sep &&
*str == argp->line_sep) {
str++;
if (list) {
if (buf) {
ucv_array_push(list, ucv_stringbuf_finish(buf));
uc_uline_add_pos(vm, pos_list, start_idx, end_idx);
start_idx = -1;
}
buf = NULL;
list = ucv_array_new(vm);
ucv_array_push(args, ucv_get(list));
pos_list = ucv_array_new(vm);
ucv_array_push(pos_args, ucv_get(pos_list));
}
}
continue;
}
if (!buf)
buf = ucv_stringbuf_new();
str++;
break;
case BACKSLASH:
case DOUBLE_QUOTE_BACKSLASH:
if (start_idx < 0)
start_idx = str - start;
if (list && *str != '\n')
add_str(&buf, str, str + 1);
str++;
state--;
end_idx = str - start;
break;
case SINGLE_QUOTE:
if (start_idx < 0)
start_idx = str - start;
next = str + strcspn(str, "'");
if (list)
add_str(&buf, str, next);
str = next;
if (*str == '\'') {
state = UNQUOTED;
str++;
}
end_idx = str - start;
break;
case DOUBLE_QUOTE:
if (start_idx < 0)
start_idx = str - start;
next = str + strcspn(str, "\"\\");
if (list)
add_str(&buf, str, next);
str = next;
if (*str == '"') {
state = UNQUOTED;
str++;
} else if (*str == '\\') {
state = DOUBLE_QUOTE_BACKSLASH;
str++;
}
end_idx = str - start;
}
}
if (buf) {
ucv_array_push(list, ucv_get(ucv_stringbuf_finish(buf)));
uc_uline_add_pos(vm, pos_list, start_idx, end_idx);
}
if (state_str[state])
missing = ucv_string_new(state_str[state]);
if (!list)
return missing;
ret = ucv_object_new(vm);
ucv_object_add(ret, "args", ucv_get(args));
ucv_object_add(ret, "pos", ucv_get(pos_args));
if (missing)
ucv_object_add(ret, "missing", ucv_get(missing));
return ret;
}
static uc_value_t *
uc_uline_arg_parser(uc_vm_t *vm, size_t nargs)
{
uc_value_t *opts = uc_fn_arg(0);
struct uc_arg_parser *argp;
uc_value_t *a;
char sep = 0;
if ((a = ucv_object_get(opts, "line_separator", NULL)) != NULL) {
if (ucv_type(a) != UC_STRING || ucv_string_length(a) != 1)
return NULL;
sep = ucv_string_get(a)[0];
}
argp = calloc(1, sizeof(*argp));
argp->line_sep = sep;
return ucv_resource_new(argp_type, argp);
}
static uc_value_t *
uc_uline_argp_parse(uc_vm_t *vm, size_t nargs)
{
return uc_uline_parse_args(vm, nargs, false);
}
static uc_value_t *
uc_uline_argp_check(uc_vm_t *vm, size_t nargs)
{
return uc_uline_parse_args(vm, nargs, true);
}
static uc_value_t *
uc_uline_argp_escape(uc_vm_t *vm, size_t nargs)
{
uc_value_t *arg = uc_fn_arg(0);
uc_value_t *ref_arg = uc_fn_arg(1);
const char *str, *next;
uc_stringbuf_t *buf;
char ref = 0;
if (ucv_type(arg) != UC_STRING)
return NULL;
if (ucv_type(ref_arg) == UC_STRING)
ref = ucv_string_get(ref_arg)[0];
str = ucv_string_get(arg);
if (ref != '"' && ref != '\'') {
next = str + strcspn(str, "\n\t '\"");
if (*next)
ref = '"';
}
if (ref != '"' && ref != '\'')
return ucv_string_new(str);
buf = ucv_stringbuf_new();
ucv_stringbuf_addstr(buf, &ref, 1);
while (*str) {
next = strchr(str, ref);
if (!next) {
ucv_stringbuf_addstr(buf, str, strlen(str));
break;
}
if (next - str)
ucv_stringbuf_addstr(buf, str, next - str);
if (ref == '\'')
ucv_stringbuf_addstr(buf, "'\\''", 4);
else
ucv_stringbuf_addstr(buf, "\\\"", 2);
str = next + 1;
}
ucv_stringbuf_addstr(buf, &ref, 1);
return ucv_stringbuf_finish(buf);
}
static uc_value_t *
uc_uline_getpass(uc_vm_t *vm, size_t nargs)
{
uc_value_t *prompt = uc_fn_arg(0);
char *pw;
if (ucv_type(prompt) != UC_STRING)
return NULL;
pw = getpass(ucv_string_get(prompt));
if (!pw)
return NULL;
return ucv_string_new(pw);
}
static const uc_function_list_t argp_fns[] = {
{ "parse", uc_uline_argp_parse },
{ "check", uc_uline_argp_check },
{ "escape", uc_uline_argp_escape },
};
static const uc_function_list_t state_fns[] = {
{ "close", uc_uline_close },
{ "poll", uc_uline_poll },
{ "poll_stop", uc_uline_poll_stop },
{ "poll_key", uc_uline_poll_key },
{ "reset_key_input", uc_uline_reset_key_input },
{ "get_line", uc_uline_get_line },
{ "get_window", uc_uline_get_window },
{ "set_hint", uc_uline_set_hint },
{ "set_state", uc_uline_set_state },
{ "set_uloop", uc_uline_set_uloop },
{ "hide_prompt", uc_uline_hide_prompt },
{ "refresh_prompt", uc_uline_refresh_prompt },
};
static const uc_function_list_t global_fns[] = {
{ "new", uc_uline_new },
{ "arg_parser", uc_uline_arg_parser },
{ "getpass", uc_uline_getpass },
};
void uc_module_init(uc_vm_t *vm, uc_value_t *scope)
{
uc_function_list_register(scope, global_fns);
state_type = uc_type_declare(vm, "uline.state", state_fns, free_state);
argp_type = uc_type_declare(vm, "uline.argp", argp_fns, free);
registry = ucv_array_new(vm);
uc_vm_registry_set(vm, "uline.registry", registry);
}

View File

@ -0,0 +1,919 @@
// 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;
if (!len)
return;
while ((next = memchr(str, '\n', len)) != NULL) {
size_t cur_len = next - str;
if (cur_len)
__pos_add_string(s, pos, str, cur_len);
pos_add_newline(s, pos);
len -= cur_len + 1;
str = next + 1;
}
if (len)
__pos_add_string(s, pos, str, len);
}
static struct pos
pos_diff(struct pos start, struct pos end)
{
struct pos diff = {
.x = end.x - start.x,
.y = end.y - start.y
};
return diff;
}
static void
set_cursor(struct uline_state *s, struct pos pos)
{
struct pos diff = pos_diff(s->cursor_pos, pos);
if (diff.x > 0)
vt100_cursor_forward(s->output, diff.x);
else if (diff.x < 0)
vt100_cursor_back(s->output, -diff.x);
if (diff.y > 0)
vt100_cursor_down(s->output, diff.y);
else if (diff.y < 0)
vt100_cursor_up(s->output, -diff.y);
s->cursor_pos = pos;
}
static void
display_output_string(struct uline_state *s, const char *str,
size_t len)
{
fwrite(str, len, 1, s->output);
pos_add_string(s, &s->cursor_pos, str, len);
}
static void
display_update_line(struct uline_state *s, struct linebuf *line,
struct pos *pos)
{
char *start = line->buf;
char *end = line->buf + line->len;
struct pos update_pos;
size_t prompt_len = 0;
if (line->prompt)
prompt_len = strlen(line->prompt);
if (s->full_update) {
display_output_string(s, line->prompt, prompt_len);
*pos = s->cursor_pos;
line->update_pos = 0;
} else {
pos_add_string(s, pos, line->prompt, prompt_len);
}
update_pos = *pos;
if (line->update_pos) {
start += line->update_pos;
pos_add_string(s, &update_pos, line->buf, line->update_pos);
}
set_cursor(s, update_pos);
vt100_erase_right(s->output);
line->update_pos = line->len;
if (end - start <= 0)
return;
display_output_string(s, start, end - start);
if (s->cursor_pos.x == 0 && end[-1] != '\n')
vt100_next_line(s->output);
}
static void
display_update(struct uline_state *s)
{
struct pos edit_pos, end_diff;
struct pos base_pos = {};
struct linebuf *line = &s->line;
if (s->full_update) {
set_cursor(s, (struct pos){});
fputc(KEY_CR, s->output);
vt100_erase_down(s->output);
}
display_update_line(s, line, &base_pos);
if (s->line2) {
line = s->line2;
if (s->cursor_pos.x != 0) {
vt100_next_line(s->output);
pos_add_newline(s, &s->cursor_pos);
}
base_pos = s->cursor_pos;
display_update_line(s, s->line2, &base_pos);
}
edit_pos = base_pos;
pos_add_string(s, &edit_pos, line->buf, line->pos);
end_diff = pos_diff(s->end_pos, s->cursor_pos);
s->end_pos = s->cursor_pos;
if (end_diff.y != 0)
vt100_erase_down(s->output);
else
vt100_erase_right(s->output);
set_cursor(s, edit_pos);
fflush(s->output);
s->full_update = false;
}
static bool
delete_symbol(struct uline_state *s, struct linebuf *line)
{
size_t len = 1;
if (line->pos == line->len)
return false;
if (s->utf8) {
len = utf8_move_right(line->buf, line->pos, line->len);
len -= line->pos;
}
linebuf_delete(line, len);
return true;
}
static bool
move_left(struct uline_state *s, struct linebuf *line)
{
if (!line->pos)
return false;
if (s->utf8)
line->pos = utf8_move_left(line->buf, line->pos);
else
line->pos--;
return true;
}
static bool
move_word_left(struct uline_state *s, struct linebuf *line)
{
char *buf = line->buf;
size_t pos;
if (!move_left(s, line))
return false;
pos = line->pos;
// remove trailing spaces
while (pos > 0 && isspace(buf[pos]))
pos--;
// skip word
while (pos > 0 && !isspace(buf[pos]))
pos--;
if (isspace(buf[pos]))
pos++;
line->pos = pos;
return true;
}
static bool
move_right(struct uline_state *s, struct linebuf *line)
{
if (line->pos >= line->len)
return false;
if (s->utf8)
line->pos = utf8_move_right(line->buf, line->pos, line->len);
else
line->pos++;
return true;
}
static bool
move_word_right(struct uline_state *s, struct linebuf *line)
{
char *buf = line->buf;
size_t pos = line->pos;
if (pos == line->len)
return false;
// skip word
while (!isspace(buf[pos]) && pos < line->len)
pos++;
// skip trailing whitespace
while (isspace(buf[pos]) && pos < line->len)
pos++;
line->pos = pos;
return true;
}
static bool
process_esc(struct uline_state *s, enum vt100_escape esc)
{
struct linebuf *line = &s->line;
if (s->line2 &&
(esc == VT100_DELETE ||
(s->cb->line2_cursor && s->cb->line2_cursor(s))))
line = s->line2;
switch (esc) {
case VT100_CURSOR_LEFT:
return move_left(s, line);
case VT100_CURSOR_WORD_LEFT:
return move_word_left(s, line);
case VT100_CURSOR_RIGHT:
return move_right(s, line);
case VT100_CURSOR_WORD_RIGHT:
return move_word_right(s, line);
case VT100_HOME:
line->pos = 0;
return true;
case VT100_END:
line->pos = line->len;
return true;
case VT100_CURSOR_UP:
s->cb->event(s, EDITLINE_EV_CURSOR_UP);
return true;
case VT100_CURSOR_DOWN:
s->cb->event(s, EDITLINE_EV_CURSOR_DOWN);
return true;
case VT100_DELETE:
return delete_symbol(s, line);
default:
vt100_ding(s->output);
return false;
}
}
static bool
process_backword(struct uline_state *s, struct linebuf *line)
{
size_t pos, len;
pos = line->pos - 1;
if (!move_word_left(s, line))
return false;
len = pos + 1 - line->pos;
linebuf_delete(line, len);
return true;
}
static void
linebuf_reset(struct linebuf *line)
{
line->pos = 0;
line->len = 0;
line->buf[0] = 0;
line->update_pos = 0;
}
static void
free_line2(struct uline_state *s)
{
if (!s->line2)
return;
linebuf_free(s->line2);
free(s->line2);
s->line2 = NULL;
}
static bool
process_newline(struct uline_state *s, bool drop)
{
bool ret;
if (drop)
goto reset;
termios_set_orig_mode(s);
if (s->line2 && s->cb->line2_newline &&
s->cb->line2_newline(s, s->line2->buf, s->line2->len)) {
termios_set_native_mode(s);
return true;
}
free_line2(s);
ret = s->cb->line(s, s->line.buf, s->line.len);
termios_set_native_mode(s);
if (!ret) {
linebuf_insert(&s->line, "\n", 1);
return true;
}
reset:
vt100_next_line(s->output);
vt100_erase_down(s->output);
s->cursor_pos = (struct pos) {};
s->full_update = true;
fflush(s->output);
if (!s->line.len)
return true;
linebuf_reset(&s->line);
return true;
}
static bool
process_ctrl(struct uline_state *s, char c)
{
struct linebuf *line = s->line2 ? s->line2 : &s->line;
switch (c) {
case KEY_LF:
case KEY_CR:
return process_newline(s, false);
case KEY_ETX:
s->cb->event(s, EDITLINE_EV_INTERRUPT);
process_newline(s, true);
s->stop = true;
return true;
case KEY_EOT:
if (s->line.len)
return false;
s->cb->event(s, EDITLINE_EV_EOF);
s->stop = true;
return true;
case KEY_BS:
case KEY_DEL:
if (!move_left(s, line))
return false;
delete_symbol(s, line);
if (s->line2 && s->cb->line2_update)
s->cb->line2_update(s, line->buf, line->len);
return true;
case KEY_FF:
vt100_cursor_home(s->output);
vt100_erase_down(s->output);
s->full_update = true;
return true;
case KEY_NAK:
linebuf_reset(line);
return true;
case KEY_SOH:
return process_esc(s, VT100_HOME);
case KEY_ENQ:
return process_esc(s, VT100_END);
case KEY_VT:
// TODO: kill
return false;
case KEY_EM:
// TODO: yank
return false;
case KEY_ETB:
return process_backword(s, line);
case KEY_ESC:
s->esc_idx = 0;
return false;
case KEY_SUB:
kill(getpid(), SIGTSTP);
return false;
default:
return false;
}
}
static void
check_key_repeat(struct uline_state *s, char c)
{
if (s->repeat_char != c)
s->repeat_count = 0;
s->repeat_char = c;
s->repeat_count++;
}
static void
process_char(struct uline_state *s, char c)
{
enum vt100_escape esc;
check_key_repeat(s, c);
if (s->esc_idx >= 0) {
s->esc_seq[s->esc_idx++] = c;
s->esc_seq[s->esc_idx] = 0;
esc = vt100_esc_decode(s->esc_seq);
if (esc == VT100_INCOMPLETE &&
s->esc_idx < (int)sizeof(s->esc_seq) - 1)
return;
s->esc_idx = -1;
if (!process_esc(s, esc))
return;
} else if (s->cb->key_input &&
!check_utf8(s, (unsigned char )c) &&
s->cb->key_input(s, c, s->repeat_count)) {
goto out;
} else if ((unsigned char)c < 32 || c == 127) {
if (!process_ctrl(s, c))
return;
} else {
struct linebuf *line = s->line2 ? s->line2 : &s->line;
if (!linebuf_insert(line, &c, 1) ||
handle_utf8(s, (unsigned char )c))
return;
if (s->line2 && s->cb->line2_update)
s->cb->line2_update(s, line->buf, line->len);
}
out:
if (s->stop)
return;
display_update(s);
}
void uline_poll(struct uline_state *s)
{
int ret;
char c;
uline_refresh_prompt(s);
s->stop = false;
while (!s->stop) {
ret = read(s->input, &c, 1);
if (ret < 0) {
if (errno == EINTR)
continue;
if (errno == EAGAIN)
return;
ret = 0;
}
if (!ret) {
s->cb->event(s, EDITLINE_EV_EOF);
termios_set_orig_mode(s);
return;
}
if (s->sigwinch_count != sigwinch_count)
update_window_size(s, false);
process_char(s, c);
}
}
void uline_set_prompt(struct uline_state *s, const char *str)
{
if (s->line.prompt && !strcmp(s->line.prompt, str))
return;
free(s->line.prompt);
s->line.prompt = strdup(str);
s->full_update = true;
}
void uline_set_line2_prompt(struct uline_state *s, const char *str)
{
if (!!str != !!s->line2) {
if (!str)
free_line2(s);
else
s->line2 = calloc(1, sizeof(*s->line2));
}
if (!str || (s->line2->prompt && !strcmp(s->line2->prompt, str)))
return;
free(s->line2->prompt);
s->line2->prompt = strdup(str);
s->full_update = true;
}
static void
__uline_set_line(struct uline_state *s, struct linebuf *line, const char *str, size_t len)
{
size_t i, prev_len = line->len;
line->len = 0;
linebuf_extend(line, len);
for (i = 0; i < prev_len && i < len; i++) {
if (line->buf[i] != str[i])
break;
}
if (i > prev_len)
i--;
if (s->utf8) {
// move back to the beginning of the utf-8 symbol
while (i > 0 && (str[i] & 0xc0) == 0x80)
i--;
}
line->update_pos = i;
memcpy(line->buf, str, len);
line->len = len;
if (line->pos > line->len)
line->pos = line->len;
}
void uline_set_line(struct uline_state *s, const char *str, size_t len)
{
__uline_set_line(s, &s->line, str, len);
}
void uline_set_line2(struct uline_state *s, const char *str, size_t len)
{
if (!s->line2)
return;
__uline_set_line(s, s->line2, str, len);
}
void uline_hide_prompt(struct uline_state *s)
{
set_cursor(s, (struct pos){});
vt100_erase_down(s->output);
s->full_update = true;
fflush(s->output);
}
void uline_refresh_prompt(struct uline_state *s)
{
termios_set_native_mode(s);
display_update(s);
}
void uline_set_hint(struct uline_state *s, const char *str, size_t len)
{
struct pos prev_pos = s->cursor_pos;
if (len) {
vt100_next_line(s->output);
pos_add_newline(s, &s->cursor_pos);
}
vt100_erase_down(s->output);
if (len) {
fwrite(str, len, 1, s->output);
pos_add_string(s, &s->cursor_pos, str, len);
}
set_cursor(s, prev_pos);
fflush(s->output);
}
void uline_init(struct uline_state *s, const struct uline_cb *cb,
int in_fd, FILE *out_stream, bool utf8)
{
struct sigaction sa = {
.sa_handler = handle_sigwinch,
};
s->cb = cb;
s->utf8 = utf8;
s->input = in_fd;
s->output = out_stream;
update_window_size(s, true);
reset_input_state(s);
#ifdef USE_SYSTEM_WCHAR
if (utf8)
setlocale(LC_CTYPE, "C.UTF-8");
#endif
sigaction(SIGWINCH, &sa, NULL);
s->full_update = true;
if (!tcgetattr(s->input, &s->orig_termios)) {
s->has_termios = true;
termios_set_native_mode(s);
}
}
void uline_free(struct uline_state *s)
{
free_line2(s);
termios_set_orig_mode(s);
linebuf_free(&s->line);
}

View File

@ -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

View File

@ -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;
}

View File

@ -0,0 +1,95 @@
// 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 'F':
return VT100_END;
case 'H':
return VT100_HOME;
case '5':
switch (*str) {
case 'C':
return VT100_CURSOR_WORD_RIGHT;
case 'D':
return VT100_CURSOR_WORD_LEFT;
default:
break;
}
/* fallthrough */
case '0' ... '4':
case '6' ... '9':
str--;
idx = strspn(str, "0123456789");
if (!str[idx])
return VT100_INCOMPLETE;
if (str[idx] != '~')
return VT100_UNKNOWN;
code = strtoul(str, NULL, 10);
switch (code) {
case 1:
return VT100_HOME;
case 3:
return VT100_DELETE;
case 4:
return VT100_END;
case 200:
case 201:
// paste start/end
return VT100_IGNORE;
default:
return VT100_UNKNOWN;
}
default:
return VT100_UNKNOWN;
}
default:
return VT100_UNKNOWN;
}
}
void __vt100_csi_num(FILE *out, int num, char code)
{
fprintf(out, "\e[%d%c", num, code);
}
void __vt100_esc(FILE *out, char c)
{
char seq[] = "\eX";
seq[1] = c;
fputs(seq, out);
}
void __vt100_csi2(FILE *out, char c1, char c2)
{
char seq[] = "\e[XX";
seq[2] = c1;
seq[3] = c2;
fputs(seq, out);
}