LCOV - code coverage report
Current view: top level - src/core - readline.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 86.7 % 173 150
Test Date: 2026-04-20 19:54:22 Functions: 90.9 % 11 10

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file readline.c
       6              :  * @brief Custom interactive line editor built on top of terminal.h.
       7              :  */
       8              : 
       9              : #include "readline.h"
      10              : #include "platform/terminal.h"
      11              : 
      12              : #include <stdio.h>
      13              : #include <string.h>
      14              : #include <stdlib.h>
      15              : #include <unistd.h>
      16              : 
      17              : /* ---- History ---- */
      18              : 
      19           23 : void rl_history_init(LineHistory *h) {
      20           23 :     if (!h) return;
      21           22 :     memset(h, 0, sizeof(*h));
      22              : }
      23              : 
      24          279 : void rl_history_add(LineHistory *h, const char *line) {
      25          279 :     if (!h || !line || line[0] == '\0') return;
      26              : 
      27              :     /* Ignore duplicate of the most recent entry */
      28          277 :     if (h->count > 0) {
      29          266 :         int last = (h->head - 1 + RL_HISTORY_MAX) % RL_HISTORY_MAX;
      30          266 :         if (strcmp(h->entries[last], line) == 0) return;
      31              :     }
      32              : 
      33          275 :     strncpy(h->entries[h->head], line, RL_HISTORY_ENTRY_MAX - 1);
      34          275 :     h->entries[h->head][RL_HISTORY_ENTRY_MAX - 1] = '\0';
      35          275 :     h->head = (h->head + 1) % RL_HISTORY_MAX;
      36          275 :     if (h->count < RL_HISTORY_MAX) h->count++;
      37              : }
      38              : 
      39              : /* ---- Internal: get history entry by reverse index ----
      40              :  *
      41              :  * index 0 = most recent, 1 = second most recent, etc.
      42              :  * Returns NULL if out of range.
      43              :  */
      44            2 : static const char *history_get(const LineHistory *h, int index) {
      45            2 :     if (!h || index < 0 || index >= h->count) return NULL;
      46            2 :     int pos = (h->head - 1 - index + RL_HISTORY_MAX * 2) % RL_HISTORY_MAX;
      47            2 :     return h->entries[pos];
      48              : }
      49              : 
      50              : /* ---- Internal: line editor state ---- */
      51              : 
      52              : typedef struct {
      53              :     char   *buf;       /* output buffer (caller-supplied)          */
      54              :     size_t  size;      /* buf capacity in bytes                    */
      55              :     size_t  len;       /* current content length (excl. NUL)       */
      56              :     size_t  cur;       /* cursor position (0 .. len)               */
      57              :     const char *prompt;
      58              :     int     prompt_len;
      59              : } LineState;
      60              : 
      61              : /* ---- Internal: redraw the current line ---- */
      62              : 
      63           68 : static void redraw(const LineState *s) {
      64              :     /* Move cursor to beginning of line, clear to end, rewrite */
      65           68 :     fputs("\r", stdout);
      66           68 :     fputs(s->prompt, stdout);
      67           68 :     fwrite(s->buf, 1, s->len, stdout);
      68              :     /* Clear any leftover characters from a previous longer line */
      69           68 :     fputs("\033[K", stdout);
      70              :     /* Reposition cursor */
      71           68 :     size_t cursor_col = (size_t)s->prompt_len + s->cur;
      72              :     /* Move to column cursor_col (1-based): \r then CUF */
      73           68 :     if (cursor_col > 0) {
      74           68 :         printf("\r\033[%zuC", cursor_col);
      75              :     } else {
      76            0 :         fputs("\r", stdout);
      77              :     }
      78           68 :     fflush(stdout);
      79           68 : }
      80              : 
      81              : /* ---- Internal: insert a character at cursor ---- */
      82              : 
      83           56 : static void insert_char(LineState *s, char c) {
      84           56 :     if (s->len + 1 >= s->size) return; /* no space */
      85              :     /* Shift right */
      86           56 :     memmove(s->buf + s->cur + 1, s->buf + s->cur, s->len - s->cur);
      87           56 :     s->buf[s->cur] = c;
      88           56 :     s->len++;
      89           56 :     s->cur++;
      90           56 :     s->buf[s->len] = '\0';
      91              : }
      92              : 
      93              : /* ---- Internal: delete character before cursor (Backspace) ---- */
      94              : 
      95            1 : static void delete_before(LineState *s) {
      96            1 :     if (s->cur == 0) return;
      97            1 :     memmove(s->buf + s->cur - 1, s->buf + s->cur, s->len - s->cur);
      98            1 :     s->cur--;
      99            1 :     s->len--;
     100            1 :     s->buf[s->len] = '\0';
     101              : }
     102              : 
     103              : /* ---- Internal: delete character at cursor (Delete / Ctrl-D) ---- */
     104              : 
     105            0 : static void delete_at(LineState *s) {
     106            0 :     if (s->cur >= s->len) return;
     107            0 :     memmove(s->buf + s->cur, s->buf + s->cur + 1, s->len - s->cur - 1);
     108            0 :     s->len--;
     109            0 :     s->buf[s->len] = '\0';
     110              : }
     111              : 
     112              : /* ---- Internal: delete previous word (Ctrl-W) ---- */
     113              : 
     114            1 : static void delete_prev_word(LineState *s) {
     115            1 :     if (s->cur == 0) return;
     116            1 :     size_t end = s->cur;
     117              :     /* Skip trailing spaces */
     118            1 :     while (s->cur > 0 && s->buf[s->cur - 1] == ' ') s->cur--;
     119              :     /* Delete word characters */
     120            4 :     while (s->cur > 0 && s->buf[s->cur - 1] != ' ') s->cur--;
     121            1 :     size_t deleted = end - s->cur;
     122            1 :     memmove(s->buf + s->cur, s->buf + end, s->len - end);
     123            1 :     s->len -= deleted;
     124            1 :     s->buf[s->len] = '\0';
     125              : }
     126              : 
     127              : /* ---- Internal: kill to end of line (Ctrl-K) ---- */
     128              : 
     129            1 : static void kill_to_end(LineState *s) {
     130            1 :     s->len = s->cur;
     131            1 :     s->buf[s->len] = '\0';
     132            1 : }
     133              : 
     134              : /* ---- rl_readline: non-TTY fallback ----
     135              :  *
     136              :  * Uses read(STDIN_FILENO) directly to bypass FILE* buffering, which is
     137              :  * important when the caller has dup2'd a pipe or file into STDIN_FILENO.
     138              :  */
     139            4 : static int readline_nontty(const char *prompt, char *buf, size_t size) {
     140            4 :     if (prompt && *prompt) {
     141            1 :         fputs(prompt, stdout);
     142            1 :         fflush(stdout);
     143              :     }
     144            4 :     size_t len = 0;
     145           26 :     while (len + 1 < size) {
     146              :         unsigned char c;
     147           26 :         ssize_t n = read(STDIN_FILENO, &c, 1);
     148           26 :         if (n <= 0) {
     149            1 :             if (len == 0) return -1; /* EOF with no data */
     150            3 :             break;
     151              :         }
     152           25 :         if (c == '\n') break;
     153           22 :         if (c == '\r') continue;    /* skip CR in CR+LF sequences */
     154           21 :         buf[len++] = (char)c;
     155              :     }
     156            3 :     buf[len] = '\0';
     157            3 :     return (int)len;
     158              : }
     159              : 
     160              : /* ---- rl_readline: interactive (TTY) path ---- */
     161              : 
     162           27 : int rl_readline(const char *prompt, char *buf, size_t size,
     163              :                 LineHistory *history) {
     164           27 :     if (!buf || size == 0) return -1;
     165              : 
     166              :     /* Non-TTY fallback */
     167           25 :     if (!terminal_is_tty(STDIN_FILENO)) {
     168            4 :         return readline_nontty(prompt, buf, size);
     169              :     }
     170              : 
     171           42 :     RAII_TERM_RAW TermRawState *raw = terminal_raw_enter();
     172           21 :     if (!raw) {
     173              :         /* Raw mode failed — fall back to plain read */
     174            0 :         return readline_nontty(prompt, buf, size);
     175              :     }
     176              : 
     177              :     LineState s;
     178           21 :     s.buf        = buf;
     179           21 :     s.size       = size;
     180           21 :     s.len        = 0;
     181           21 :     s.cur        = 0;
     182           21 :     s.prompt     = prompt ? prompt : "";
     183           21 :     s.prompt_len = prompt ? (int)strlen(prompt) : 0;
     184           21 :     buf[0]       = '\0';
     185              : 
     186              :     /* History navigation state */
     187           21 :     int hist_idx = -1; /* -1 = not navigating */
     188              :     char saved_line[RL_HISTORY_ENTRY_MAX]; /* saved current edit when navigating */
     189           21 :     saved_line[0] = '\0';
     190              : 
     191              :     /* Initial prompt */
     192           21 :     fputs(s.prompt, stdout);
     193           21 :     fflush(stdout);
     194              : 
     195           68 :     for (;;) {
     196           89 :         TermKey key = terminal_read_key();
     197              : 
     198           89 :         switch (key) {
     199           10 :         case TERM_KEY_ENTER:
     200              :             /* Submit */
     201           10 :             buf[s.len] = '\0';
     202           10 :             fputs("\r\n", stdout);
     203           10 :             fflush(stdout);
     204           10 :             return (int)s.len;
     205              : 
     206            1 :         case TERM_KEY_QUIT:
     207              :             /* Ctrl-C: discard and signal abort */
     208            1 :             fputs("\r\n", stdout);
     209            1 :             fflush(stdout);
     210            1 :             buf[0] = '\0';
     211            1 :             return -1;
     212              : 
     213           10 :         case TERM_KEY_CTRL_D:
     214           10 :             if (s.len == 0) {
     215              :                 /* EOF on empty line */
     216           10 :                 fputs("\r\n", stdout);
     217           10 :                 fflush(stdout);
     218           10 :                 return -1;
     219              :             }
     220            0 :             delete_at(&s);
     221            0 :             break;
     222              : 
     223            1 :         case TERM_KEY_BACK:
     224            1 :             delete_before(&s);
     225            1 :             break;
     226              : 
     227            0 :         case TERM_KEY_DELETE:
     228            0 :             delete_at(&s);
     229            0 :             break;
     230              : 
     231            4 :         case TERM_KEY_LEFT:
     232            4 :             if (s.cur > 0) s.cur--;
     233            4 :             break;
     234              : 
     235            0 :         case TERM_KEY_RIGHT:
     236            0 :             if (s.cur < s.len) s.cur++;
     237            0 :             break;
     238              : 
     239            1 :         case TERM_KEY_HOME:
     240              :         case TERM_KEY_CTRL_A:
     241            1 :             s.cur = 0;
     242            1 :             break;
     243              : 
     244            1 :         case TERM_KEY_END:
     245              :         case TERM_KEY_CTRL_E:
     246            1 :             s.cur = s.len;
     247            1 :             break;
     248              : 
     249            1 :         case TERM_KEY_CTRL_K:
     250            1 :             kill_to_end(&s);
     251            1 :             break;
     252              : 
     253            1 :         case TERM_KEY_CTRL_W:
     254            1 :             delete_prev_word(&s);
     255            1 :             break;
     256              : 
     257            2 :         case TERM_KEY_PREV_LINE: /* Up — older history */
     258            2 :             if (!history || history->count == 0) break;
     259            2 :             if (hist_idx == -1) {
     260              :                 /* Save current edit */
     261            2 :                 strncpy(saved_line, buf, RL_HISTORY_ENTRY_MAX - 1);
     262            2 :                 saved_line[RL_HISTORY_ENTRY_MAX - 1] = '\0';
     263              :             }
     264            2 :             if (hist_idx + 1 < history->count) {
     265            2 :                 hist_idx++;
     266            2 :                 const char *entry = history_get(history, hist_idx);
     267            2 :                 if (entry) {
     268            2 :                     strncpy(buf, entry, size - 1);
     269            2 :                     buf[size - 1] = '\0';
     270            2 :                     s.len = strlen(buf);
     271            2 :                     s.cur = s.len;
     272              :                 }
     273              :             }
     274            2 :             break;
     275              : 
     276            1 :         case TERM_KEY_NEXT_LINE: /* Down — newer history / current edit */
     277            1 :             if (!history || hist_idx == -1) break;
     278            1 :             hist_idx--;
     279            1 :             if (hist_idx < 0) {
     280              :                 /* Restore saved edit */
     281            1 :                 hist_idx = -1;
     282            1 :                 strncpy(buf, saved_line, size - 1);
     283            1 :                 buf[size - 1] = '\0';
     284            1 :                 s.len = strlen(buf);
     285            1 :                 s.cur = s.len;
     286              :             } else {
     287            0 :                 const char *entry = history_get(history, hist_idx);
     288            0 :                 if (entry) {
     289            0 :                     strncpy(buf, entry, size - 1);
     290            0 :                     buf[size - 1] = '\0';
     291            0 :                     s.len = strlen(buf);
     292            0 :                     s.cur = s.len;
     293              :                 }
     294              :             }
     295            1 :             break;
     296              : 
     297           56 :         case TERM_KEY_IGNORE: {
     298           56 :             int ch = terminal_last_printable();
     299           56 :             if (ch >= 32 && ch <= 126) {
     300           56 :                 insert_char(&s, (char)ch);
     301              :             }
     302           56 :             break;
     303              :         }
     304              : 
     305            0 :         default:
     306              :             /* TERM_KEY_ESC, TERM_KEY_PREV_PAGE, TERM_KEY_NEXT_PAGE — ignore */
     307            0 :             break;
     308              :         }
     309              : 
     310           68 :         redraw(&s);
     311              :     }
     312              : }
        

Generated by: LCOV version 2.0-1