• source navigation  • diff markup  • identifier search  • freetext search  • 

Sources/ucode/debug_lineedit.c

  1 /*
  2  * Copyright (C) 2026 Jo-Philipp Wich <jo@mein.io>
  3  *
  4  * Permission to use, copy, modify, and/or distribute this software for any
  5  * purpose with or without fee is hereby granted, provided that the above
  6  * copyright notice and this permission notice appear in all copies.
  7  *
  8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  9  * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF
 10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 15  */
 16 
 17 #include <stdio.h>
 18 #include <stdlib.h>
 19 #include <string.h>
 20 #include <unistd.h>
 21 #include <ctype.h>
 22 #include <termios.h>
 23 #include <fcntl.h>
 24 
 25 #include "debug_lineedit.h"
 26 
 27 #define EDITBUF_SIZE 4096
 28 #define HISTORY_SIZE 100
 29 
 30 /* -- raw terminal mode ----------------------------------------------------- */
 31 
 32 static struct termios orig_termios;
 33 static int orig_flags = -1;
 34 static bool raw_active = false;
 35 
 36 static void
 37 raw_mode_disable(void)
 38 {
 39         if (!raw_active)
 40                 return;
 41 
 42         tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
 43 
 44         if (orig_flags != -1)
 45                 fcntl(STDIN_FILENO, F_SETFL, orig_flags);
 46 
 47         raw_active = false;
 48 }
 49 
 50 void
 51 lineedit_init(void)
 52 {
 53         struct termios raw;
 54 
 55         if (raw_active || !isatty(STDIN_FILENO))
 56                 return;
 57 
 58         if (tcgetattr(STDIN_FILENO, &orig_termios) != 0)
 59                 return;
 60 
 61         raw = orig_termios;
 62 
 63         /* ISIG is deliberately cleared too: Ctrl-C is handled below as "cancel
 64          * the current line" (matching the original), not as SIGINT - this
 65          * client has no separate signal-based break-into-debugger path of its
 66          * own to preserve that for.
 67          *
 68          * VMIN/VTIME are deliberately left alone: with ICANON off, setting
 69          * VMIN=0/VTIME=0 makes every read() with nothing available return 0
 70          * immediately - indistinguishable from real EOF, which getc_nb() below
 71          * needs to detect. O_NONBLOCK (via fcntl, right below) already gives
 72          * the same "don't block" behavior while keeping that distinction: a
 73          * non-blocking read() returns -1/EAGAIN for "nothing yet" and only 0
 74          * for an actual EOF. */
 75         raw.c_lflag &= (tcflag_t)~(ICANON | ECHO | ISIG);
 76 
 77         if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0)
 78                 return;
 79 
 80         orig_flags = fcntl(STDIN_FILENO, F_GETFL);
 81 
 82         /* Non-blocking so a read() for the tail of an escape sequence can never
 83          * stall the process if, in some rare split-input scenario (e.g. a slow
 84          * network terminal), the rest hasn't arrived yet - see read_key(). */
 85         if (orig_flags != -1)
 86                 fcntl(STDIN_FILENO, F_SETFL, orig_flags | O_NONBLOCK);
 87 
 88         raw_active = true;
 89         atexit(raw_mode_disable);
 90 }
 91 
 92 bool
 93 lineedit_active(void)
 94 {
 95         return raw_active;
 96 }
 97 
 98 void
 99 lineedit_suspend(void)
100 {
101         raw_mode_disable();
102 }
103 
104 void
105 lineedit_resume(void)
106 {
107         lineedit_init();
108 }
109 
110 /* -- non-blocking key decoding, ported from term_getc()/term_getc_raw()
111  * (formerly lib/debug.c) ---------------------------------------------------- */
112 
113 enum {
114         LE_NODATA = -1, /* nothing available right now - stop reading */
115         LE_EOF    = -2, /* stdin hit real EOF (terminal hung up) */
116 
117         KEY_HOME = 0x110000,
118         KEY_END,
119         KEY_DEL,
120         KEY_ARROW_UP,
121         KEY_ARROW_DOWN,
122         KEY_ARROW_LEFT,
123         KEY_ARROW_RIGHT,
124         KEY_CTRL_LEFT,
125         KEY_CTRL_RIGHT,
126 };
127 
128 static int
129 getc_nb(void)
130 {
131         unsigned char c;
132         ssize_t n = read(STDIN_FILENO, &c, 1);
133 
134         if (n == 1)
135                 return c;
136 
137         if (n == 0)
138                 return LE_EOF;
139 
140         return LE_NODATA;
141 }
142 
143 /* Decode one keypress, including multi-byte escape sequences for arrow/
144  * home/end/delete keys. If a sequence is only partially available, it
145  * degrades to a bare ESC (0x1b) rather than blocking or losing the bytes
146  * already read - see the header comment on why that's an acceptable
147  * simplification here. */
148 static int
149 read_key(void)
150 {
151         int c = getc_nb();
152         int seq[3];
153 
154         if (c != 0x1b)
155                 return c;
156 
157         if ((seq[0] = getc_nb()) < 0) return 0x1b;
158         if ((seq[1] = getc_nb()) < 0) return 0x1b;
159 
160         if (seq[0] == '[') {
161                 if (seq[1] >= '' && seq[1] <= '9') {
162                         if ((seq[2] = getc_nb()) < 0) return 0x1b;
163 
164                         if (seq[2] == '~') {
165                                 switch (seq[1]) {
166                                 case '1': case '7': return KEY_HOME;
167                                 case '3': return KEY_DEL;
168                                 case '4': case '8': return KEY_END;
169                                 }
170                         }
171                         else if (seq[2] == ';') {
172                                 int mod = getc_nb();
173                                 int fin = (mod < 0) ? LE_NODATA : getc_nb();
174 
175                                 if (mod == '5') {
176                                         switch (fin) {
177                                         case 'C': return KEY_CTRL_RIGHT;
178                                         case 'D': return KEY_CTRL_LEFT;
179                                         }
180                                 }
181                         }
182 
183                         return LE_NODATA; /* unrecognized sequence, swallow it */
184                 }
185 
186                 switch (seq[1]) {
187                 case 'A': return KEY_ARROW_UP;
188                 case 'B': return KEY_ARROW_DOWN;
189                 case 'C': return KEY_ARROW_RIGHT;
190                 case 'D': return KEY_ARROW_LEFT;
191                 case 'H': return KEY_HOME;
192                 case 'F': return KEY_END;
193                 }
194         }
195         else if (seq[0] == 'O') {
196                 switch (seq[1]) {
197                 case 'H': return KEY_HOME;
198                 case 'F': return KEY_END;
199                 }
200         }
201 
202         return LE_NODATA;
203 }
204 
205 /* -- line buffer + cursor --------------------------------------------------- */
206 
207 static char linebuf[EDITBUF_SIZE];
208 static size_t linelen = 0, cursor = 0;
209 static char cur_prompt[64];
210 
211 static void
212 buf_insert(const char *s, size_t n)
213 {
214         if (linelen + n >= sizeof(linebuf))
215                 n = sizeof(linebuf) - 1 - linelen;
216 
217         if (!n)
218                 return;
219 
220         memmove(linebuf + cursor + n, linebuf + cursor, linelen - cursor);
221         memcpy(linebuf + cursor, s, n);
222         linelen += n;
223         cursor += n;
224 }
225 
226 static void
227 buf_delete(size_t from, size_t to)
228 {
229         if (to > linelen)
230                 to = linelen;
231 
232         if (from >= to)
233                 return;
234 
235         memmove(linebuf + from, linebuf + to, linelen - to);
236         linelen -= (to - from);
237 
238         if (cursor > from)
239                 cursor = (cursor >= to) ? cursor - (to - from) : from;
240 }
241 
242 static size_t
243 word_left(size_t pos)
244 {
245         while (pos > 0 && isspace((unsigned char)linebuf[pos - 1])) pos--;
246         while (pos > 0 && !isspace((unsigned char)linebuf[pos - 1])) pos--;
247 
248         return pos;
249 }
250 
251 static size_t
252 word_right(size_t pos)
253 {
254         while (pos < linelen && isspace((unsigned char)linebuf[pos])) pos++;
255         while (pos < linelen && !isspace((unsigned char)linebuf[pos])) pos++;
256 
257         return pos;
258 }
259 
260 static void
261 redraw(void)
262 {
263         printf("\r\033[K%s%.*s", cur_prompt, (int)linelen, linebuf);
264 
265         if (cursor < linelen)
266                 printf("\033[%zuD", linelen - cursor);
267 
268         fflush(stdout);
269 }
270 
271 /* -- history, ported from termstate.history/HISTORY_SIZE (formerly
272  * lib/debug.c) -------------------------------------------------------------- */
273 
274 static char *history[HISTORY_SIZE];
275 static size_t history_count = 0;
276 static size_t history_browse = 0; /* == history_count: editing the live line */
277 static char history_saved[EDITBUF_SIZE];
278 
279 static void
280 history_push(const char *line)
281 {
282         if (!*line)
283                 return;
284 
285         if (history_count > 0 && !strcmp(history[history_count - 1], line))
286                 return;
287 
288         if (history_count >= HISTORY_SIZE) {
289                 free(history[0]);
290                 memmove(&history[0], &history[1], (HISTORY_SIZE - 1) * sizeof(history[0]));
291                 history_count--;
292         }
293 
294         history[history_count++] = strdup(line);
295 }
296 
297 /* -- Tab completion, ported from term_line_tabcomplete() (formerly
298  * lib/debug.c), restricted to command-name completion only -----------------
299  * (the original also completed breakpoint specs/function names/file paths
300  * depending on argument position - that needs live data from the server
301  * and is future work, not something this port takes on). */
302 
303 static const lineedit_completion_t *completions = NULL;
304 static size_t ncompletions = 0;
305 
306 void
307 lineedit_set_completions(const lineedit_completion_t *c, size_t n)
308 {
309         completions = c;
310         ncompletions = n;
311 }
312 
313 static void
314 try_complete(void)
315 {
316         const char *matches[64];
317         size_t nmatch = 0, maxlen = 0, wend = 0, i;
318 
319         while (wend < linelen && !isspace((unsigned char)linebuf[wend]))
320                 wend++;
321 
322         /* only complete the command word itself, not its arguments */
323         if (!completions || cursor != wend || wend == 0)
324                 return;
325 
326         for (i = 0; i < ncompletions; i++) {
327                 const char *c;
328 
329                 for (c = completions[i].names; *c; c += strlen(c) + 1) {
330                         size_t len = strlen(c);
331 
332                         if (len >= wend && !strncmp(c, linebuf, wend)) {
333                                 if (nmatch < sizeof(matches) / sizeof(matches[0]))
334                                         matches[nmatch++] = c;
335 
336                                 if (len > maxlen)
337                                         maxlen = len;
338                         }
339                 }
340         }
341 
342         if (nmatch == 0)
343                 return;
344 
345         if (nmatch == 1) {
346                 buf_delete(0, wend);
347                 cursor = 0;
348                 buf_insert(matches[0], strlen(matches[0]));
349                 buf_insert(" ", 1);
350         }
351         else {
352                 printf("\n");
353 
354                 for (i = 0; i < nmatch; i++)
355                         printf("%-*s  ", (int)maxlen, matches[i]);
356 
357                 printf("\n");
358         }
359 }
360 
361 /* -- prompt + feed ----------------------------------------------------------- */
362 
363 void
364 lineedit_begin(const char *prompt)
365 {
366         snprintf(cur_prompt, sizeof(cur_prompt), "%s", prompt);
367         linelen = cursor = 0;
368         history_browse = history_count;
369 
370         if (raw_active) {
371                 redraw();
372         }
373         else {
374                 fputs(prompt, stdout);
375                 fflush(stdout);
376         }
377 }
378 
379 bool
380 lineedit_feed(char *out, size_t outsz, bool *eof)
381 {
382         int key;
383 
384         *eof = false;
385 
386         /* Non-interactive input (piped/scripted, or stdin isn't a tty): no
387          * editing possible or needed, just read one line the plain way. */
388         if (!raw_active) {
389                 if (!fgets(out, (int)outsz, stdin)) {
390                         *eof = true;
391                         return false;
392                 }
393 
394                 out[strcspn(out, "\n")] = '\0';
395 
396                 return true;
397         }
398 
399         while ((key = read_key()) != LE_NODATA) {
400                 if (key == LE_EOF) {
401                         *eof = true;
402                         return false;
403                 }
404 
405                 switch (key) {
406                 case '\r': case '\n':
407                         printf("\n");
408                         snprintf(out, outsz, "%.*s", (int)linelen, linebuf);
409                         history_push(out);
410 
411                         return true;
412 
413                 case 3: /* Ctrl-C: cancel the line in place, like the original */
414                         linelen = cursor = 0;
415                         history_browse = history_count;
416                         break;
417 
418                 case 127: case 8: /* backspace */
419                         if (cursor > 0)
420                                 buf_delete(cursor - 1, cursor);
421 
422                         break;
423 
424                 case KEY_DEL:
425                         buf_delete(cursor, cursor + 1);
426                         break;
427 
428                 case KEY_HOME:
429                         cursor = 0;
430                         break;
431 
432                 case KEY_END:
433                         cursor = linelen;
434                         break;
435 
436                 case KEY_ARROW_LEFT:
437                         if (cursor > 0)
438                                 cursor--;
439 
440                         break;
441 
442                 case KEY_ARROW_RIGHT:
443                         if (cursor < linelen)
444                                 cursor++;
445 
446                         break;
447 
448                 case KEY_CTRL_LEFT:
449                         cursor = word_left(cursor);
450                         break;
451 
452                 case KEY_CTRL_RIGHT:
453                         cursor = word_right(cursor);
454                         break;
455 
456                 case KEY_ARROW_UP:
457                         if (history_browse > 0) {
458                                 if (history_browse == history_count) {
459                                         linebuf[linelen] = '\0';
460                                         snprintf(history_saved, sizeof(history_saved), "%s", linebuf);
461                                 }
462 
463                                 history_browse--;
464                                 snprintf(linebuf, sizeof(linebuf), "%s", history[history_browse]);
465                                 linelen = cursor = strlen(linebuf);
466                         }
467 
468                         break;
469 
470                 case KEY_ARROW_DOWN:
471                         if (history_browse < history_count) {
472                                 history_browse++;
473 
474                                 if (history_browse == history_count)
475                                         snprintf(linebuf, sizeof(linebuf), "%s", history_saved);
476                                 else
477                                         snprintf(linebuf, sizeof(linebuf), "%s", history[history_browse]);
478 
479                                 linelen = cursor = strlen(linebuf);
480                         }
481 
482                         break;
483 
484                 case 23: /* Ctrl-W */
485                         buf_delete(word_left(cursor), cursor);
486                         break;
487 
488                 case 9: /* Tab */
489                         try_complete();
490                         break;
491 
492                 default:
493                         if (key >= ' ' && key < 127) {
494                                 char c = (char)key;
495 
496                                 buf_insert(&c, 1);
497                         }
498 
499                         break;
500                 }
501 
502                 redraw();
503         }
504 
505         return false;
506 }
507 

This page was automatically generated by LXR 0.3.1.  •  OpenWrt