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