LCOV - code coverage report
Current view: top level - libemail/src/core - input_line.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 97.0 % 134 130
Test Date: 2026-08-21 10:11:28 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            5 : 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            8 : static size_t cp_len(const char *s) {
      14            8 :     unsigned char b = (unsigned char)*s;
      15            8 :     if (b < 0x80) return 1;
      16            6 :     if ((b & 0xE0) == 0xC0) return 2;
      17            4 :     if ((b & 0xF0) == 0xE0) return 3;
      18            1 :     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          262 : static int display_cols(const char *s, size_t bytes) {
      27          262 :     int cols = 0;
      28          262 :     size_t i = 0;
      29         2191 :     while (i < bytes) {
      30         1929 :         unsigned char b = (unsigned char)s[i];
      31         1929 :         if (b < 0x80) {
      32         1924 :             cols++;
      33         1924 :             i++;
      34              :         } else {
      35              :             /* Decode code point */
      36            5 :             uint32_t cp = 0;
      37            5 :             size_t clen = cp_len(s + i);
      38            5 :             if (clen == 2 && i + 1 < bytes)
      39            2 :                 cp = ((uint32_t)(b & 0x1F) << 6) | ((unsigned char)s[i+1] & 0x3F);
      40            3 :             else if (clen == 3 && i + 2 < bytes)
      41            2 :                 cp = ((uint32_t)(b & 0x0F) << 12)
      42            2 :                    | ((uint32_t)((unsigned char)s[i+1] & 0x3F) << 6)
      43            2 :                    | ((unsigned char)s[i+2] & 0x3F);
      44            1 :             else if (clen == 4 && i + 3 < bytes)
      45            1 :                 cp = ((uint32_t)(b & 0x07) << 18)
      46            1 :                    | ((uint32_t)((unsigned char)s[i+1] & 0x3F) << 12)
      47            1 :                    | ((uint32_t)((unsigned char)s[i+2] & 0x3F) << 6)
      48            1 :                    | ((unsigned char)s[i+3] & 0x3F);
      49              :             else
      50            0 :                 cp = b;  /* malformed — treat as 1 col */
      51            5 :             int w = terminal_wcwidth(cp);
      52            5 :             cols += (w > 0) ? w : 1;
      53            5 :             i += (clen <= bytes - i) ? clen : 1;
      54              :         }
      55              :     }
      56          262 :     return cols;
      57              : }
      58              : 
      59              : /* ── Editing operations ──────────────────────────────────────────────── */
      60              : 
      61            6 : static void il_move_left(InputLine *il) {
      62            6 :     if (il->cur == 0) return;
      63            6 :     il->cur--;
      64            7 :     while (il->cur > 0 && is_cont((unsigned char)il->buf[il->cur]))
      65            1 :         il->cur--;
      66              : }
      67              : 
      68            2 : static void il_move_right(InputLine *il) {
      69            2 :     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            4 : static void il_backspace(InputLine *il) {
      76            4 :     if (il->cur == 0) return;
      77            3 :     size_t end = il->cur;
      78            3 :     il_move_left(il);
      79            3 :     size_t dlen = end - il->cur;
      80            3 :     memmove(il->buf + il->cur, il->buf + end, il->len - end + 1);
      81            3 :     il->len -= dlen;
      82              : }
      83              : 
      84            4 : static void il_delete_fwd(InputLine *il) {
      85            4 :     if (il->cur >= il->len) return;
      86            3 :     size_t start = il->cur;
      87            3 :     size_t end   = start + cp_len(il->buf + start);
      88            3 :     if (end > il->len) end = il->len;
      89            3 :     memmove(il->buf + start, il->buf + end, il->len - end + 1);
      90            3 :     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           44 : static void il_insert_utf8(InputLine *il, const char *seq) {
     102           44 :     size_t n = seq ? strlen(seq) : 0;
     103           44 :     if (n == 0 || il->len + n >= il->bufsz) return;
     104           43 :     memmove(il->buf + il->cur + n, il->buf + il->cur, il->len - il->cur + 1);
     105           43 :     memcpy(il->buf + il->cur, seq, n);
     106           43 :     il->cur += n;
     107           43 :     il->len += n;
     108              : }
     109              : 
     110              : /* ── Rendering ───────────────────────────────────────────────────────── */
     111              : 
     112              : /** Returns the 1-based cursor column for the current insertion point. */
     113          131 : static int il_cursor_col(const InputLine *il, const char *prompt) {
     114          131 :     return 3 /* indent */ + display_cols(prompt, strlen(prompt))
     115          131 :                           + display_cols(il->buf, il->cur);
     116              : }
     117              : 
     118          131 : 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          131 :     printf("\033[%d;1H\033[2K  %s%s", trow, prompt, il->buf);
     123          131 : }
     124              : 
     125              : /* ── Public API ──────────────────────────────────────────────────────── */
     126              : 
     127           74 : void input_line_init(InputLine *il, char *buf, size_t bufsz,
     128              :                      const char *initial_text) {
     129           74 :     il->buf           = buf;
     130           74 :     il->bufsz         = bufsz;
     131           74 :     il->trow          = 0;
     132           74 :     il->render_below  = NULL;
     133           74 :     il->tab_fn        = NULL;
     134           74 :     il->shift_tab_fn  = NULL;
     135           74 :     if (initial_text) {
     136           69 :         size_t n = strlen(initial_text);
     137           69 :         if (n >= bufsz) n = bufsz - 1;
     138           69 :         memcpy(buf, initial_text, n);
     139           69 :         buf[n] = '\0';
     140           69 :         il->len = n;
     141              :     } else {
     142            5 :         buf[0] = '\0';
     143            5 :         il->len = 0;
     144              :     }
     145           74 :     il->cur = il->len;   /* cursor starts at end */
     146           74 : }
     147              : 
     148           52 : int input_line_run(InputLine *il, int trow, const char *prompt) {
     149           52 :     il->trow = trow;
     150           79 :     for (;;) {
     151          131 :         il_render(il, trow, prompt);
     152          131 :         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          131 :         printf("\033[%d;%dH\033[?25h", trow, il_cursor_col(il, prompt));
     156          131 :         fflush(stdout);
     157              : 
     158          131 :         TermKey key = terminal_read_key();
     159          130 :         switch (key) {
     160           46 :         case TERM_KEY_ENTER:
     161           46 :             printf("\033[%d;1H\033[2K\033[?25l", trow + 1);
     162           46 :             fflush(stdout);
     163           46 :             return 1;
     164              : 
     165            5 :         case TERM_KEY_ESC:
     166              :         case TERM_KEY_QUIT:
     167            5 :             printf("\033[%d;1H\033[2K\033[?25l", trow + 1);
     168            5 :             fflush(stdout);
     169            5 :             return 0;
     170              : 
     171            4 :         case TERM_KEY_BACK:
     172            4 :             il_backspace(il);
     173            4 :             break;
     174              : 
     175            4 :         case TERM_KEY_DELETE:
     176            4 :             il_delete_fwd(il);
     177            4 :             break;
     178              : 
     179            3 :         case TERM_KEY_LEFT:
     180            3 :             il_move_left(il);
     181            3 :             break;
     182              : 
     183            2 :         case TERM_KEY_RIGHT:
     184            2 :             il_move_right(il);
     185            2 :             break;
     186              : 
     187           10 :         case TERM_KEY_HOME:
     188           10 :             il->cur = 0;
     189           10 :             break;
     190              : 
     191            5 :         case TERM_KEY_END:
     192            5 :             il->cur = il->len;
     193            5 :             break;
     194              : 
     195            3 :         case TERM_KEY_TAB:
     196            3 :             if (il->tab_fn) il->tab_fn(il);
     197            3 :             break;
     198              : 
     199            2 :         case TERM_KEY_SHIFT_TAB:
     200            2 :             if (il->shift_tab_fn) il->shift_tab_fn(il);
     201            2 :             break;
     202              : 
     203           46 :         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           46 :             const char *seq = terminal_last_utf8();
     207           46 :             if (seq && seq[0]) il_insert_utf8(il, seq);
     208           46 :             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