LCOV - code coverage report
Current view: top level - libemail/src/core - input_line.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 82.1 % 134 110
Test Date: 2026-08-21 10:11:30 Functions: 100.0 % 12 12

            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            2 : 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            1 : static size_t cp_len(const char *s) {
      14            1 :     unsigned char b = (unsigned char)*s;
      15            1 :     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          142 : static int display_cols(const char *s, size_t bytes) {
      27          142 :     int cols = 0;
      28          142 :     size_t i = 0;
      29         1853 :     while (i < bytes) {
      30         1711 :         unsigned char b = (unsigned char)s[i];
      31         1711 :         if (b < 0x80) {
      32         1711 :             cols++;
      33         1711 :             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          142 :     return cols;
      57              : }
      58              : 
      59              : /* ── Editing operations ──────────────────────────────────────────────── */
      60              : 
      61            2 : static void il_move_left(InputLine *il) {
      62            2 :     if (il->cur == 0) return;
      63            2 :     il->cur--;
      64            2 :     while (il->cur > 0 && is_cont((unsigned char)il->buf[il->cur]))
      65            0 :         il->cur--;
      66              : }
      67              : 
      68            1 : static void il_move_right(InputLine *il) {
      69            1 :     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            1 : static void il_backspace(InputLine *il) {
      76            1 :     if (il->cur == 0) return;
      77            1 :     size_t end = il->cur;
      78            1 :     il_move_left(il);
      79            1 :     size_t dlen = end - il->cur;
      80            1 :     memmove(il->buf + il->cur, il->buf + end, il->len - end + 1);
      81            1 :     il->len -= dlen;
      82              : }
      83              : 
      84            1 : static void il_delete_fwd(InputLine *il) {
      85            1 :     if (il->cur >= il->len) return;
      86            1 :     size_t start = il->cur;
      87            1 :     size_t end   = start + cp_len(il->buf + start);
      88            1 :     if (end > il->len) end = il->len;
      89            1 :     memmove(il->buf + start, il->buf + end, il->len - end + 1);
      90            1 :     il->len -= end - start;
      91              : }
      92              : 
      93              : /* Insert a whole UTF-8 sequence at the cursor.
      94              :  *
      95              :  * Typed characters must be taken as complete code points: for anything above
      96              :  * ASCII, terminal_last_printable() only reports *that* a character arrived
      97              :  * (as the value 1) — the bytes live in terminal_last_utf8().  Inserting the
      98              :  * flag byte instead produced a literal 0x01 in the text, which both corrupted
      99              :  * the value (an accented letter became ^A) and desynchronised the rendered
     100              :  * cursor, since a control byte occupies no column. */
     101           35 : static void il_insert_utf8(InputLine *il, const char *seq) {
     102           35 :     size_t n = seq ? strlen(seq) : 0;
     103           35 :     if (n == 0 || il->len + n >= il->bufsz) return;
     104           35 :     memmove(il->buf + il->cur + n, il->buf + il->cur, il->len - il->cur + 1);
     105           35 :     memcpy(il->buf + il->cur, seq, n);
     106           35 :     il->cur += n;
     107           35 :     il->len += n;
     108              : }
     109              : 
     110              : /* ── Rendering ───────────────────────────────────────────────────────── */
     111              : 
     112              : /** Returns the 1-based cursor column for the current insertion point. */
     113           71 : static int il_cursor_col(const InputLine *il, const char *prompt) {
     114           71 :     return 3 /* indent */ + display_cols(prompt, strlen(prompt))
     115           71 :                           + display_cols(il->buf, il->cur);
     116              : }
     117              : 
     118           71 : static void il_render(const InputLine *il, int trow, const char *prompt) {
     119              :     /* Move to row, clear line, print prompt and text — do NOT position the
     120              :      * cursor yet; input_line_run does that after render_below so the cursor
     121              :      * always ends up on the input row regardless of what render_below drew. */
     122           71 :     printf("\033[%d;1H\033[2K  %s%s", trow, prompt, il->buf);
     123           71 : }
     124              : 
     125              : /* ── Public API ──────────────────────────────────────────────────────── */
     126              : 
     127           25 : void input_line_init(InputLine *il, char *buf, size_t bufsz,
     128              :                      const char *initial_text) {
     129           25 :     il->buf           = buf;
     130           25 :     il->bufsz         = bufsz;
     131           25 :     il->trow          = 0;
     132           25 :     il->render_below  = NULL;
     133           25 :     il->tab_fn        = NULL;
     134           25 :     il->shift_tab_fn  = NULL;
     135           25 :     if (initial_text) {
     136           24 :         size_t n = strlen(initial_text);
     137           24 :         if (n >= bufsz) n = bufsz - 1;
     138           24 :         memcpy(buf, initial_text, n);
     139           24 :         buf[n] = '\0';
     140           24 :         il->len = n;
     141              :     } else {
     142            1 :         buf[0] = '\0';
     143            1 :         il->len = 0;
     144              :     }
     145           25 :     il->cur = il->len;   /* cursor starts at end */
     146           25 : }
     147              : 
     148           25 : int input_line_run(InputLine *il, int trow, const char *prompt) {
     149           25 :     il->trow = trow;
     150           46 :     for (;;) {
     151           71 :         il_render(il, trow, prompt);
     152           71 :         if (il->render_below) il->render_below(il);
     153              :         /* Always reposition cursor on the input row after render_below
     154              :          * (which may have left the cursor elsewhere). */
     155           71 :         printf("\033[%d;%dH\033[?25h", trow, il_cursor_col(il, prompt));
     156           71 :         fflush(stdout);
     157              : 
     158           71 :         TermKey key = terminal_read_key();
     159           70 :         switch (key) {
     160           21 :         case TERM_KEY_ENTER:
     161           21 :             printf("\033[%d;1H\033[2K\033[?25l", trow + 1);
     162           21 :             fflush(stdout);
     163           21 :             return 1;
     164              : 
     165            3 :         case TERM_KEY_ESC:
     166              :         case TERM_KEY_QUIT:
     167            3 :             printf("\033[%d;1H\033[2K\033[?25l", trow + 1);
     168            3 :             fflush(stdout);
     169            3 :             return 0;
     170              : 
     171            1 :         case TERM_KEY_BACK:
     172            1 :             il_backspace(il);
     173            1 :             break;
     174              : 
     175            1 :         case TERM_KEY_DELETE:
     176            1 :             il_delete_fwd(il);
     177            1 :             break;
     178              : 
     179            1 :         case TERM_KEY_LEFT:
     180            1 :             il_move_left(il);
     181            1 :             break;
     182              : 
     183            1 :         case TERM_KEY_RIGHT:
     184            1 :             il_move_right(il);
     185            1 :             break;
     186              : 
     187            2 :         case TERM_KEY_HOME:
     188            2 :             il->cur = 0;
     189            2 :             break;
     190              : 
     191            2 :         case TERM_KEY_END:
     192            2 :             il->cur = il->len;
     193            2 :             break;
     194              : 
     195            2 :         case TERM_KEY_TAB:
     196            2 :             if (il->tab_fn) il->tab_fn(il);
     197            2 :             break;
     198              : 
     199            1 :         case TERM_KEY_SHIFT_TAB:
     200            1 :             if (il->shift_tab_fn) il->shift_tab_fn(il);
     201            1 :             break;
     202              : 
     203           35 :         case TERM_KEY_IGNORE: {
     204              :             /* terminal_last_utf8() carries the complete sequence for both
     205              :              * ASCII and multi-byte input, so it is the only correct source. */
     206           35 :             const char *seq = terminal_last_utf8();
     207           35 :             if (seq && seq[0]) il_insert_utf8(il, seq);
     208           35 :             break;
     209              :         }
     210              : 
     211            0 :         case TERM_KEY_NEXT_LINE:
     212              :         case TERM_KEY_PREV_LINE:
     213              :         case TERM_KEY_NEXT_PAGE:
     214              :         case TERM_KEY_PREV_PAGE:
     215            0 :             break;  /* ignored in single-line context */
     216              :         }
     217              :     }
     218              : }
        

Generated by: LCOV version 2.0-1