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 5 : 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 8 : static size_t cp_len(const char *s) {
14 8 : unsigned char b = (unsigned char)*s;
15 8 : if (b < 0x80) return 1;
16 6 : if ((b & 0xE0) == 0xC0) return 2;
17 4 : if ((b & 0xF0) == 0xE0) return 3;
18 1 : 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 262 : static int display_cols(const char *s, size_t bytes) {
27 262 : int cols = 0;
28 262 : size_t i = 0;
29 2191 : while (i < bytes) {
30 1929 : unsigned char b = (unsigned char)s[i];
31 1929 : if (b < 0x80) {
32 1924 : cols++;
33 1924 : i++;
34 : } else {
35 : /* Decode code point */
36 5 : uint32_t cp = 0;
37 5 : size_t clen = cp_len(s + i);
38 5 : if (clen == 2 && i + 1 < bytes)
39 2 : cp = ((uint32_t)(b & 0x1F) << 6) | ((unsigned char)s[i+1] & 0x3F);
40 3 : else if (clen == 3 && i + 2 < bytes)
41 2 : cp = ((uint32_t)(b & 0x0F) << 12)
42 2 : | ((uint32_t)((unsigned char)s[i+1] & 0x3F) << 6)
43 2 : | ((unsigned char)s[i+2] & 0x3F);
44 1 : else if (clen == 4 && i + 3 < bytes)
45 1 : cp = ((uint32_t)(b & 0x07) << 18)
46 1 : | ((uint32_t)((unsigned char)s[i+1] & 0x3F) << 12)
47 1 : | ((uint32_t)((unsigned char)s[i+2] & 0x3F) << 6)
48 1 : | ((unsigned char)s[i+3] & 0x3F);
49 : else
50 0 : cp = b; /* malformed — treat as 1 col */
51 5 : int w = terminal_wcwidth(cp);
52 5 : cols += (w > 0) ? w : 1;
53 5 : i += (clen <= bytes - i) ? clen : 1;
54 : }
55 : }
56 262 : return cols;
57 : }
58 :
59 : /* ── Editing operations ──────────────────────────────────────────────── */
60 :
61 6 : static void il_move_left(InputLine *il) {
62 6 : if (il->cur == 0) return;
63 6 : il->cur--;
64 7 : while (il->cur > 0 && is_cont((unsigned char)il->buf[il->cur]))
65 1 : il->cur--;
66 : }
67 :
68 2 : static void il_move_right(InputLine *il) {
69 2 : 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 4 : static void il_backspace(InputLine *il) {
76 4 : if (il->cur == 0) return;
77 3 : size_t end = il->cur;
78 3 : il_move_left(il);
79 3 : size_t dlen = end - il->cur;
80 3 : memmove(il->buf + il->cur, il->buf + end, il->len - end + 1);
81 3 : il->len -= dlen;
82 : }
83 :
84 4 : static void il_delete_fwd(InputLine *il) {
85 4 : if (il->cur >= il->len) return;
86 3 : size_t start = il->cur;
87 3 : size_t end = start + cp_len(il->buf + start);
88 3 : if (end > il->len) end = il->len;
89 3 : memmove(il->buf + start, il->buf + end, il->len - end + 1);
90 3 : il->len -= end - start;
91 : }
92 :
93 44 : static void il_insert(InputLine *il, char ch) {
94 44 : if (il->len + 1 >= il->bufsz) return;
95 43 : memmove(il->buf + il->cur + 1, il->buf + il->cur, il->len - il->cur + 1);
96 43 : il->buf[il->cur++] = ch;
97 43 : il->len++;
98 : }
99 :
100 : /* ── Rendering ───────────────────────────────────────────────────────── */
101 :
102 : /** Returns the 1-based cursor column for the current insertion point. */
103 131 : static int il_cursor_col(const InputLine *il, const char *prompt) {
104 131 : return 3 /* indent */ + display_cols(prompt, strlen(prompt))
105 131 : + display_cols(il->buf, il->cur);
106 : }
107 :
108 131 : 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 131 : printf("\033[%d;1H\033[2K %s%s", trow, prompt, il->buf);
113 131 : }
114 :
115 : /* ── Public API ──────────────────────────────────────────────────────── */
116 :
117 74 : void input_line_init(InputLine *il, char *buf, size_t bufsz,
118 : const char *initial_text) {
119 74 : il->buf = buf;
120 74 : il->bufsz = bufsz;
121 74 : il->trow = 0;
122 74 : il->render_below = NULL;
123 74 : il->tab_fn = NULL;
124 74 : il->shift_tab_fn = NULL;
125 74 : if (initial_text) {
126 69 : size_t n = strlen(initial_text);
127 69 : if (n >= bufsz) n = bufsz - 1;
128 69 : memcpy(buf, initial_text, n);
129 69 : buf[n] = '\0';
130 69 : il->len = n;
131 : } else {
132 5 : buf[0] = '\0';
133 5 : il->len = 0;
134 : }
135 74 : il->cur = il->len; /* cursor starts at end */
136 74 : }
137 :
138 52 : int input_line_run(InputLine *il, int trow, const char *prompt) {
139 52 : il->trow = trow;
140 79 : for (;;) {
141 131 : il_render(il, trow, prompt);
142 131 : 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 131 : printf("\033[%d;%dH\033[?25h", trow, il_cursor_col(il, prompt));
146 131 : fflush(stdout);
147 :
148 131 : TermKey key = terminal_read_key();
149 130 : switch (key) {
150 46 : case TERM_KEY_ENTER:
151 46 : printf("\033[%d;1H\033[2K\033[?25l", trow + 1);
152 46 : fflush(stdout);
153 46 : return 1;
154 :
155 5 : case TERM_KEY_ESC:
156 : case TERM_KEY_QUIT:
157 5 : printf("\033[%d;1H\033[2K\033[?25l", trow + 1);
158 5 : fflush(stdout);
159 5 : return 0;
160 :
161 4 : case TERM_KEY_BACK:
162 4 : il_backspace(il);
163 4 : break;
164 :
165 4 : case TERM_KEY_DELETE:
166 4 : il_delete_fwd(il);
167 4 : break;
168 :
169 3 : case TERM_KEY_LEFT:
170 3 : il_move_left(il);
171 3 : break;
172 :
173 2 : case TERM_KEY_RIGHT:
174 2 : il_move_right(il);
175 2 : break;
176 :
177 10 : case TERM_KEY_HOME:
178 10 : il->cur = 0;
179 10 : break;
180 :
181 5 : case TERM_KEY_END:
182 5 : il->cur = il->len;
183 5 : break;
184 :
185 3 : case TERM_KEY_TAB:
186 3 : if (il->tab_fn) il->tab_fn(il);
187 3 : break;
188 :
189 2 : case TERM_KEY_SHIFT_TAB:
190 2 : if (il->shift_tab_fn) il->shift_tab_fn(il);
191 2 : break;
192 :
193 46 : case TERM_KEY_IGNORE: {
194 46 : int ch = terminal_last_printable();
195 46 : if (ch > 0) il_insert(il, (char)ch);
196 46 : 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 : }
|