LCOV - code coverage report
Current view: top level - libemail/src/platform/posix - terminal.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 57.9 % 164 95
Test Date: 2026-05-07 15:53:08 Functions: 90.9 % 11 10

            Line data    Source code
       1              : /**
       2              :  * POSIX terminal implementation.
       3              :  * Uses termios(3), ioctl TIOCGWINSZ, wcwidth(3).
       4              :  */
       5              : #include "../terminal.h"
       6              : #include <stdio.h>
       7              : #include <stdlib.h>
       8              : #include <string.h>
       9              : #include <unistd.h>
      10              : #include <termios.h>
      11              : #include <sys/ioctl.h>
      12              : #include <wchar.h>
      13              : 
      14              : /** Opaque saved terminal state. Definition lives here (hidden from header). */
      15              : struct TermRawState {
      16              :     struct termios saved;
      17              :     int            active;
      18              : };
      19              : 
      20         1540 : int terminal_cols(void) {
      21              :     struct winsize ws;
      22         1540 :     if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0)
      23         1540 :         return (int)ws.ws_col;
      24            0 :     return 80;
      25              : }
      26              : 
      27         1660 : int terminal_rows(void) {
      28              :     struct winsize ws;
      29         1660 :     if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0)
      30         1660 :         return (int)ws.ws_row;
      31            0 :     return 0;
      32              : }
      33              : 
      34            0 : int terminal_is_tty(int fd) {
      35            0 :     return isatty(fd);
      36              : }
      37              : 
      38          371 : TermRawState *terminal_raw_enter(void) {
      39          371 :     TermRawState *state = malloc(sizeof(TermRawState));
      40          371 :     if (!state) return NULL;
      41              : 
      42          371 :     if (tcgetattr(STDIN_FILENO, &state->saved) != 0) {
      43            0 :         free(state);
      44            0 :         return NULL;
      45              :     }
      46              : 
      47          371 :     struct termios raw = state->saved;
      48          371 :     raw.c_lflag &= ~(unsigned)(ICANON | ECHO | ISIG);
      49          371 :     raw.c_cc[VMIN]  = 1;
      50          371 :     raw.c_cc[VTIME] = 0;
      51          371 :     tcsetattr(STDIN_FILENO, TCSANOW, &raw);
      52          371 :     state->active = 1;
      53          371 :     return state;
      54              : }
      55              : 
      56          369 : void terminal_raw_exit(TermRawState **state) {
      57          369 :     if (!state || !*state) return;
      58          279 :     if ((*state)->active)
      59          279 :         tcsetattr(STDIN_FILENO, TCSANOW, &(*state)->saved);
      60          279 :     free(*state);
      61          279 :     *state = NULL;
      62              : }
      63              : 
      64              : /**
      65              :  * Read one byte from STDIN_FILENO via read(2) — NOT via getchar()/stdio.
      66              :  *
      67              :  * Rationale: getchar() uses the C stdio buffer.  When getchar() calls
      68              :  * read(2) with VMIN=0 VTIME=1 and gets a 0-byte return (timeout for bare
      69              :  * ESC), stdio marks the FILE* EOF flag.  All subsequent getchar() calls
      70              :  * then return EOF immediately without blocking, causing an infinite
      71              :  * redraw loop in the TUI.  Using read(2) directly bypasses the stdio
      72              :  * layer entirely and avoids the EOF flag problem.
      73              :  *
      74              :  * Returns the byte value [0..255], or -1 on error/timeout.
      75              :  */
      76         2142 : static int read_byte(void) {
      77              :     unsigned char c;
      78         2142 :     ssize_t n = read(STDIN_FILENO, &c, 1);
      79         2084 :     return (n == 1) ? (int)c : -1;
      80              : }
      81              : 
      82              : static int  g_last_printable  = 0;
      83              : static char g_last_utf8[5]    = "";
      84              : 
      85          254 : int         terminal_last_printable(void) { return g_last_printable; }
      86           18 : const char *terminal_last_utf8(void)      { return g_last_utf8; }
      87              : 
      88         1039 : TermKey terminal_read_key(void) {
      89              :     /* The terminal must already be in raw mode (VMIN=1, VTIME=0). */
      90         1039 :     g_last_printable = 0;
      91         1039 :     g_last_utf8[0]   = '\0';
      92         1039 :     int c = read_byte();
      93          981 :     TermKey result = TERM_KEY_IGNORE;   /* unknown input → silent no-op */
      94              : 
      95          981 :     if (c == '\033') {
      96              :         /* Temporarily switch to VMIN=0 VTIME=1 (100 ms timeout) to drain
      97              :          * the escape sequence without blocking if it is a bare ESC. */
      98          566 :         struct termios t = {0};
      99          566 :         tcgetattr(STDIN_FILENO, &t);
     100          566 :         struct termios drain = t;
     101          566 :         drain.c_cc[VMIN]  = 0;
     102          566 :         drain.c_cc[VTIME] = 1;
     103          566 :         tcsetattr(STDIN_FILENO, TCSANOW, &drain);
     104              : 
     105          566 :         int c2 = read_byte();
     106          566 :         if (c2 == '[') {
     107          529 :             int c3 = read_byte();
     108          529 :             switch (c3) {
     109            6 :             case 'A': result = TERM_KEY_PREV_LINE; break;  /* ESC[A — Up          */
     110          429 :             case 'B': result = TERM_KEY_NEXT_LINE; break;  /* ESC[B — Down        */
     111            1 :             case 'C': result = TERM_KEY_RIGHT;     break;  /* ESC[C — Right       */
     112            1 :             case 'D': result = TERM_KEY_LEFT;      break;  /* ESC[D — Left        */
     113           80 :             case 'H': result = TERM_KEY_HOME;      break;  /* ESC[H — Home        */
     114            3 :             case 'F': result = TERM_KEY_END;       break;  /* ESC[F — End         */
     115            1 :             case 'Z': result = TERM_KEY_SHIFT_TAB; break;  /* ESC[Z — Shift+Tab   */
     116            0 :             case '1': { int nx=read_byte(); result=(nx=='~')?TERM_KEY_HOME:TERM_KEY_IGNORE; break; } /* ESC[1~ Home */
     117            1 :             case '3': { read_byte(); result = TERM_KEY_DELETE;    break; } /* ESC[3~ Delete */
     118            0 :             case '4': { read_byte(); result = TERM_KEY_END;       break; } /* ESC[4~ End    */
     119            3 :             case '5': { read_byte(); result = TERM_KEY_PREV_PAGE; break; } /* ESC[5~ PgUp   */
     120            4 :             case '6': { read_byte(); result = TERM_KEY_NEXT_PAGE; break; } /* ESC[6~ PgDn   */
     121            0 :             case '7': { read_byte(); result = TERM_KEY_HOME;      break; } /* ESC[7~ Home   */
     122            0 :             case '8': { read_byte(); result = TERM_KEY_END;       break; } /* ESC[8~ End    */
     123            0 :             default:
     124            0 :                 if (c3 != -1) {
     125              :                     int ch;
     126            0 :                     while ((ch = read_byte()) != -1) {
     127            0 :                         if ((ch >= 'A' && ch <= 'Z') ||
     128            0 :                             (ch >= 'a' && ch <= 'z') || ch == '~') break;
     129              :                     }
     130              :                 }
     131            0 :                 result = TERM_KEY_IGNORE;
     132            0 :                 break;
     133              :             }
     134              :         } else {
     135           37 :             result = TERM_KEY_ESC;   /* bare ESC — go back */
     136              :         }
     137              : 
     138              :         /* Restore VMIN=1 VTIME=0 raw mode. */
     139          566 :         t.c_cc[VMIN]  = 1;
     140          566 :         t.c_cc[VTIME] = 0;
     141          566 :         tcsetattr(STDIN_FILENO, TCSANOW, &t);
     142          415 :     } else if (c == '\n' || c == '\r') {
     143          244 :         result = TERM_KEY_ENTER;
     144          171 :     } else if (c == 3 /* Ctrl-C */) {
     145            0 :         result = TERM_KEY_QUIT;
     146          171 :     } else if (c == 127 || c == 8 /* DEL / Backspace */) {
     147           34 :         result = TERM_KEY_BACK;
     148          137 :     } else if (c == '\t') {
     149            4 :         result = TERM_KEY_TAB;
     150          133 :     } else if (c >= 32 && c <= 126) {
     151          132 :         g_last_printable = c;
     152          132 :         g_last_utf8[0] = (char)c;
     153          132 :         g_last_utf8[1] = '\0';
     154          132 :         result = TERM_KEY_IGNORE;
     155            1 :     } else if ((c & 0xE0) == 0xC0) {
     156              :         /* 2-byte UTF-8 sequence (U+0080..U+07FF, covers all Latin accented chars) */
     157            0 :         int c2 = read_byte();
     158            0 :         if ((c2 & 0xC0) == 0x80) {
     159            0 :             g_last_utf8[0] = (char)c;
     160            0 :             g_last_utf8[1] = (char)c2;
     161            0 :             g_last_utf8[2] = '\0';
     162            0 :             g_last_printable = 1;
     163            0 :             result = TERM_KEY_IGNORE;
     164              :         }
     165            1 :     } else if ((c & 0xF0) == 0xE0) {
     166              :         /* 3-byte UTF-8 sequence (U+0800..U+FFFF) */
     167            0 :         int c2 = read_byte(), c3 = read_byte();
     168            0 :         if ((c2 & 0xC0) == 0x80 && (c3 & 0xC0) == 0x80) {
     169            0 :             g_last_utf8[0] = (char)c;
     170            0 :             g_last_utf8[1] = (char)c2;
     171            0 :             g_last_utf8[2] = (char)c3;
     172            0 :             g_last_utf8[3] = '\0';
     173            0 :             g_last_printable = 1;
     174            0 :             result = TERM_KEY_IGNORE;
     175              :         }
     176            1 :     } else if ((c & 0xF8) == 0xF0) {
     177              :         /* 4-byte UTF-8 sequence (U+10000..U+10FFFF) */
     178            0 :         int c2 = read_byte(), c3 = read_byte(), c4 = read_byte();
     179            0 :         if ((c2 & 0xC0) == 0x80 && (c3 & 0xC0) == 0x80 && (c4 & 0xC0) == 0x80) {
     180            0 :             g_last_utf8[0] = (char)c;
     181            0 :             g_last_utf8[1] = (char)c2;
     182            0 :             g_last_utf8[2] = (char)c3;
     183            0 :             g_last_utf8[3] = (char)c4;
     184            0 :             g_last_utf8[4] = '\0';
     185            0 :             g_last_printable = 1;
     186            0 :             result = TERM_KEY_IGNORE;
     187              :         }
     188              :     }
     189              :     /* c == -1 (read error/timeout) → result stays TERM_KEY_IGNORE */
     190              : 
     191          981 :     return result;
     192              : }
     193              : 
     194       276559 : int terminal_wcwidth(uint32_t cp) {
     195       276559 :     int w = wcwidth((wchar_t)cp);
     196       276559 :     return (w < 0) ? 0 : w;
     197              : }
     198              : 
     199           14 : int terminal_read_password(const char *prompt, char *buf, size_t size) {
     200           14 :     if (!buf || size == 0) return -1;
     201              : 
     202           14 :     int fd = fileno(stdin);
     203           14 :     int is_tty = isatty(fd);
     204              : 
     205           14 :     if (is_tty) {
     206            0 :         printf("%s: ", prompt);
     207            0 :         fflush(stdout);
     208              : 
     209              :         struct termios oldt, newt;
     210            0 :         tcgetattr(fd, &oldt);
     211            0 :         newt = oldt;
     212            0 :         newt.c_lflag &= ~(unsigned)ECHO;
     213            0 :         tcsetattr(fd, TCSANOW, &newt);
     214              : 
     215            0 :         char *line = NULL;
     216            0 :         size_t len = 0;
     217            0 :         ssize_t nread = getline(&line, &len, stdin);
     218              : 
     219            0 :         tcsetattr(fd, TCSANOW, &oldt);
     220            0 :         printf("\n");
     221              : 
     222            0 :         if (nread == -1 || !line) {
     223            0 :             free(line);
     224            0 :             return -1;
     225              :         }
     226              : 
     227              :         /* Strip trailing newline */
     228            0 :         size_t slen = strlen(line);
     229            0 :         if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
     230            0 :             line[--slen] = '\0';
     231            0 :         if (slen > 0 && (line[slen-1] == '\r'))
     232            0 :             line[--slen] = '\0';
     233              : 
     234            0 :         if (slen >= size) slen = size - 1;
     235            0 :         memcpy(buf, line, slen);
     236            0 :         buf[slen] = '\0';
     237            0 :         free(line);
     238            0 :         return (int)slen;
     239              :     } else {
     240              :         /* Non-TTY: read from stdin without echo manipulation */
     241           14 :         char *line = NULL;
     242           14 :         size_t len = 0;
     243           14 :         ssize_t nread = getline(&line, &len, stdin);
     244           14 :         if (nread == -1 || !line) {
     245            0 :             free(line);
     246            0 :             return -1;
     247              :         }
     248           14 :         size_t slen = strlen(line);
     249           14 :         if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
     250           14 :             line[--slen] = '\0';
     251           14 :         if (slen > 0 && (line[slen-1] == '\r'))
     252            0 :             line[--slen] = '\0';
     253           14 :         if (slen >= size) slen = size - 1;
     254           14 :         memcpy(buf, line, slen);
     255           14 :         buf[slen] = '\0';
     256           14 :         free(line);
     257           14 :         return (int)slen;
     258              :     }
     259              : }
        

Generated by: LCOV version 2.0-1