// SPDX-License-Identifier: ISC /* * Copyright (C) 2025 Felix Fietkau */ #include #include #include #include #include #include #include #include #include #include #include #include #include "uline.h" #include "private.h" #define LINEBUF_CHUNK 64 static int sigwinch_count; static size_t nsyms(struct uline_state *s, const char *buf, size_t len) { if (!s->utf8) return len; return utf8_nsyms(buf, len); } static inline bool is_utf8_cont(unsigned char c) { return (c & 0xc0) == 0x80; } static size_t utf8_move_left(const char *line, size_t pos) { if (!pos) return 0; do { pos--; } while (pos > 0 && is_utf8_cont(line[pos])); return pos; } static size_t utf8_move_right(const char *line, size_t pos, size_t len) { if (pos == len) return pos; do { pos++; } while (pos < len && is_utf8_cont(line[pos])); return pos; } static char * linebuf_extend(struct linebuf *l, size_t size) { size_t tailroom = l->bufsize - l->len; char *buf; if (l->buf && tailroom > size) goto out; size -= tailroom; size += LINEBUF_CHUNK - 1; size -= size % LINEBUF_CHUNK; buf = realloc(l->buf, l->bufsize + size); if (!buf) return NULL; l->buf = buf; l->bufsize += size; out: return l->buf + l->len; } static void linebuf_free(struct linebuf *line) { free(line->buf); free(line->prompt); } static void update_window_size(struct uline_state *s, bool init) { unsigned int cols = 80, rows = 25; #ifdef TIOCGWINSZ struct winsize ws = {}; if (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 >= 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); }