aboutsummaryrefslogtreecommitdiff
path: root/package/utils/ucode-mod-uline/src/uline.c
diff options
context:
space:
mode:
Diffstat (limited to 'package/utils/ucode-mod-uline/src/uline.c')
-rw-r--r--package/utils/ucode-mod-uline/src/uline.c945
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);
+}