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