Line data Source code
1 : #include "input_line.h"
2 : #include "platform/terminal.h"
3 : #include <stdio.h>
4 : #include <string.h>
5 : #include <stdint.h>
6 :
7 : /* ── UTF-8 helpers ───────────────────────────────────────────────────── */
8 :
9 : /** True if byte is a UTF-8 continuation byte (10xxxxxx). */
10 2 : static int is_cont(unsigned char b) { return (b & 0xC0) == 0x80; }
11 :
12 : /** Byte length of the UTF-8 code point starting at s. */
13 1 : static size_t cp_len(const char *s) {
14 1 : unsigned char b = (unsigned char)*s;
15 1 : if (b < 0x80) return 1;
16 0 : if ((b & 0xE0) == 0xC0) return 2;
17 0 : if ((b & 0xF0) == 0xE0) return 3;
18 0 : return 4;
19 : }
20 :
21 : /**
22 : * Display column count for the first `bytes` bytes of s.
23 : * Uses terminal_wcwidth() for non-ASCII code points;
24 : * falls back to 1 column per code point if wcwidth returns <=0.
25 : */
26 142 : static int display_cols(const char *s, size_t bytes) {
27 142 : int cols = 0;
28 142 : size_t i = 0;
29 1853 : while (i < bytes) {
30 1711 : unsigned char b = (unsigned char)s[i];
31 1711 : if (b < 0x80) {
32 1711 : cols++;
33 1711 : i++;
34 : } else {
35 : /* Decode code point */
36 0 : uint32_t cp = 0;
37 0 : size_t clen = cp_len(s + i);
38 0 : if (clen == 2 && i + 1 < bytes)
39 0 : cp = ((uint32_t)(b & 0x1F) << 6) | ((unsigned char)s[i+1] & 0x3F);
40 0 : else if (clen == 3 && i + 2 < bytes)
41 0 : cp = ((uint32_t)(b & 0x0F) << 12)
42 0 : | ((uint32_t)((unsigned char)s[i+1] & 0x3F) << 6)
43 0 : | ((unsigned char)s[i+2] & 0x3F);
44 0 : else if (clen == 4 && i + 3 < bytes)
45 0 : cp = ((uint32_t)(b & 0x07) << 18)
46 0 : | ((uint32_t)((unsigned char)s[i+1] & 0x3F) << 12)
47 0 : | ((uint32_t)((unsigned char)s[i+2] & 0x3F) << 6)
48 0 : | ((unsigned char)s[i+3] & 0x3F);
49 : else
50 0 : cp = b; /* malformed — treat as 1 col */
51 0 : int w = terminal_wcwidth(cp);
52 0 : cols += (w > 0) ? w : 1;
53 0 : i += (clen <= bytes - i) ? clen : 1;
54 : }
55 : }
56 142 : return cols;
57 : }
58 :
59 : /* ── Editing operations ──────────────────────────────────────────────── */
60 :
61 2 : static void il_move_left(InputLine *il) {
62 2 : if (il->cur == 0) return;
63 2 : il->cur--;
64 2 : while (il->cur > 0 && is_cont((unsigned char)il->buf[il->cur]))
65 0 : il->cur--;
66 : }
67 :
68 1 : static void il_move_right(InputLine *il) {
69 1 : if (il->cur >= il->len) return;
70 1 : il->cur++;
71 1 : while (il->cur < il->len && is_cont((unsigned char)il->buf[il->cur]))
72 0 : il->cur++;
73 : }
74 :
75 1 : static void il_backspace(InputLine *il) {
76 1 : if (il->cur == 0) return;
77 1 : size_t end = il->cur;
78 1 : il_move_left(il);
79 1 : size_t dlen = end - il->cur;
80 1 : memmove(il->buf + il->cur, il->buf + end, il->len - end + 1);
81 1 : il->len -= dlen;
82 : }
83 :
84 1 : static void il_delete_fwd(InputLine *il) {
85 1 : if (il->cur >= il->len) return;
86 1 : size_t start = il->cur;
87 1 : size_t end = start + cp_len(il->buf + start);
88 1 : if (end > il->len) end = il->len;
89 1 : memmove(il->buf + start, il->buf + end, il->len - end + 1);
90 1 : il->len -= end - start;
91 : }
92 :
93 35 : static void il_insert(InputLine *il, char ch) {
94 35 : if (il->len + 1 >= il->bufsz) return;
95 35 : memmove(il->buf + il->cur + 1, il->buf + il->cur, il->len - il->cur + 1);
96 35 : il->buf[il->cur++] = ch;
97 35 : il->len++;
98 : }
99 :
100 : /* ── Rendering ───────────────────────────────────────────────────────── */
101 :
102 : /** Returns the 1-based cursor column for the current insertion point. */
103 71 : static int il_cursor_col(const InputLine *il, const char *prompt) {
104 71 : return 3 /* indent */ + display_cols(prompt, strlen(prompt))
105 71 : + display_cols(il->buf, il->cur);
106 : }
107 :
108 71 : static void il_render(const InputLine *il, int trow, const char *prompt) {
109 : /* Move to row, clear line, print prompt and text — do NOT position the
110 : * cursor yet; input_line_run does that after render_below so the cursor
111 : * always ends up on the input row regardless of what render_below drew. */
112 71 : printf("\033[%d;1H\033[2K %s%s", trow, prompt, il->buf);
113 71 : }
114 :
115 : /* ── Public API ──────────────────────────────────────────────────────── */
116 :
117 25 : void input_line_init(InputLine *il, char *buf, size_t bufsz,
118 : const char *initial_text) {
119 25 : il->buf = buf;
120 25 : il->bufsz = bufsz;
121 25 : il->trow = 0;
122 25 : il->render_below = NULL;
123 25 : il->tab_fn = NULL;
124 25 : il->shift_tab_fn = NULL;
125 25 : if (initial_text) {
126 24 : size_t n = strlen(initial_text);
127 24 : if (n >= bufsz) n = bufsz - 1;
128 24 : memcpy(buf, initial_text, n);
129 24 : buf[n] = '\0';
130 24 : il->len = n;
131 : } else {
132 1 : buf[0] = '\0';
133 1 : il->len = 0;
134 : }
135 25 : il->cur = il->len; /* cursor starts at end */
136 25 : }
137 :
138 25 : int input_line_run(InputLine *il, int trow, const char *prompt) {
139 25 : il->trow = trow;
140 46 : for (;;) {
141 71 : il_render(il, trow, prompt);
142 71 : if (il->render_below) il->render_below(il);
143 : /* Always reposition cursor on the input row after render_below
144 : * (which may have left the cursor elsewhere). */
145 71 : printf("\033[%d;%dH\033[?25h", trow, il_cursor_col(il, prompt));
146 71 : fflush(stdout);
147 :
148 71 : TermKey key = terminal_read_key();
149 70 : switch (key) {
150 21 : case TERM_KEY_ENTER:
151 21 : printf("\033[%d;1H\033[2K\033[?25l", trow + 1);
152 21 : fflush(stdout);
153 21 : return 1;
154 :
155 3 : case TERM_KEY_ESC:
156 : case TERM_KEY_QUIT:
157 3 : printf("\033[%d;1H\033[2K\033[?25l", trow + 1);
158 3 : fflush(stdout);
159 3 : return 0;
160 :
161 1 : case TERM_KEY_BACK:
162 1 : il_backspace(il);
163 1 : break;
164 :
165 1 : case TERM_KEY_DELETE:
166 1 : il_delete_fwd(il);
167 1 : break;
168 :
169 1 : case TERM_KEY_LEFT:
170 1 : il_move_left(il);
171 1 : break;
172 :
173 1 : case TERM_KEY_RIGHT:
174 1 : il_move_right(il);
175 1 : break;
176 :
177 2 : case TERM_KEY_HOME:
178 2 : il->cur = 0;
179 2 : break;
180 :
181 2 : case TERM_KEY_END:
182 2 : il->cur = il->len;
183 2 : break;
184 :
185 2 : case TERM_KEY_TAB:
186 2 : if (il->tab_fn) il->tab_fn(il);
187 2 : break;
188 :
189 1 : case TERM_KEY_SHIFT_TAB:
190 1 : if (il->shift_tab_fn) il->shift_tab_fn(il);
191 1 : break;
192 :
193 35 : case TERM_KEY_IGNORE: {
194 35 : int ch = terminal_last_printable();
195 35 : if (ch > 0) il_insert(il, (char)ch);
196 35 : break;
197 : }
198 :
199 0 : case TERM_KEY_NEXT_LINE:
200 : case TERM_KEY_PREV_LINE:
201 : case TERM_KEY_NEXT_PAGE:
202 : case TERM_KEY_PREV_PAGE:
203 0 : break; /* ignored in single-line context */
204 : }
205 : }
206 : }
|