--- /dev/null
+// 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);
+}
--- /dev/null
+// 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;
+}