diff options
Diffstat (limited to 'package/utils/ucode-mod-uline/src/uline.c')
-rw-r--r-- | package/utils/ucode-mod-uline/src/uline.c | 945 |
1 files changed, 945 insertions, 0 deletions
diff --git a/package/utils/ucode-mod-uline/src/uline.c b/package/utils/ucode-mod-uline/src/uline.c new file mode 100644 index 0000000000..3d0ba8a587 --- /dev/null +++ b/package/utils/ucode-mod-uline/src/uline.c @@ -0,0 +1,945 @@ +// 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 (s->ioctl_winsize && + !ioctl(fileno(s->output), TIOCGWINSZ, &ws) && + ws.ws_col && ws.ws_row) { + cols = ws.ws_col; + rows = ws.ws_row; + } else +#endif + { + s->ioctl_winsize = false; + } + + 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, uint32_t data) +{ + 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_CURSOR_POS: + if (s->rows == (data & 0xffff) && + s->cols == data >> 16) + return false; + s->rows = data & 0xffff; + s->cols = data >> 16; + s->full_update = true; + s->cb->event(s, EDITLINE_EV_WINDOW_CHANGED); + return true; + 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, 0); + case KEY_ENQ: + return process_esc(s, VT100_END, 0); + 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; + uint32_t data = 0; + + 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, &data); + if (esc == VT100_INCOMPLETE && + s->esc_idx < (int)sizeof(s->esc_seq) - 1) + return; + + s->esc_idx = -1; + if (!process_esc(s, esc, data)) + 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); + } + + if (s->cursor_pos.y >= (int16_t)s->rows) { + if (s->cursor_pos.x > 0) + vt100_next_line(s->output); + s->cursor_pos = (struct pos){}; + s->full_update = true; + } else { + 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; + s->ioctl_winsize = 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); + } + + update_window_size(s, true); + if (!s->ioctl_winsize) { + vt100_request_window_size(s->output); + fflush(s->output); + } +} + +void uline_free(struct uline_state *s) +{ + free_line2(s); + termios_set_orig_mode(s); + linebuf_free(&s->line); +} |