LCOV - code coverage report
Current view: top level - libemail/src/platform/posix - terminal.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 46.0 % 137 63
Test Date: 2026-04-15 21:12:52 Functions: 90.0 % 10 9

            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           43 : int terminal_cols(void) {
      21           43 :     struct winsize ws;
      22           43 :     if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0)
      23            0 :         return (int)ws.ws_col;
      24           43 :     return 80;
      25              : }
      26              : 
      27            2 : int terminal_rows(void) {
      28            2 :     struct winsize ws;
      29            2 :     if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0)
      30            0 :         return (int)ws.ws_row;
      31            2 :     return 0;
      32              : }
      33              : 
      34           25 : int terminal_is_tty(int fd) {
      35           25 :     return isatty(fd);
      36              : }
      37              : 
      38            1 : TermRawState *terminal_raw_enter(void) {
      39            1 :     TermRawState *state = malloc(sizeof(TermRawState));
      40            1 :     if (!state) return NULL;
      41              : 
      42            1 :     if (tcgetattr(STDIN_FILENO, &state->saved) != 0) {
      43            1 :         free(state);
      44            1 :         return NULL;
      45              :     }
      46              : 
      47            0 :     struct termios raw = state->saved;
      48            0 :     raw.c_lflag &= ~(unsigned)(ICANON | ECHO | ISIG);
      49            0 :     raw.c_cc[VMIN]  = 1;
      50            0 :     raw.c_cc[VTIME] = 0;
      51            0 :     tcsetattr(STDIN_FILENO, TCSANOW, &raw);
      52            0 :     state->active = 1;
      53            0 :     return state;
      54              : }
      55              : 
      56           43 : void terminal_raw_exit(TermRawState **state) {
      57           43 :     if (!state || !*state) return;
      58            0 :     if ((*state)->active)
      59            0 :         tcsetattr(STDIN_FILENO, TCSANOW, &(*state)->saved);
      60            0 :     free(*state);
      61            0 :     *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            2 : static int read_byte(void) {
      77            2 :     unsigned char c;
      78            2 :     ssize_t n = read(STDIN_FILENO, &c, 1);
      79            2 :     return (n == 1) ? (int)c : -1;
      80              : }
      81              : 
      82              : static int g_last_printable = 0;
      83              : 
      84            0 : int terminal_last_printable(void) { return g_last_printable; }
      85              : 
      86            1 : TermKey terminal_read_key(void) {
      87              :     /* The terminal must already be in raw mode (VMIN=1, VTIME=0). */
      88            1 :     g_last_printable = 0;
      89            1 :     int c = read_byte();
      90            1 :     TermKey result = TERM_KEY_IGNORE;   /* unknown input → silent no-op */
      91              : 
      92            1 :     if (c == '\033') {
      93              :         /* Temporarily switch to VMIN=0 VTIME=1 (100 ms timeout) to drain
      94              :          * the escape sequence without blocking if it is a bare ESC. */
      95            1 :         struct termios t = {0};
      96            1 :         tcgetattr(STDIN_FILENO, &t);
      97            1 :         struct termios drain = t;
      98            1 :         drain.c_cc[VMIN]  = 0;
      99            1 :         drain.c_cc[VTIME] = 1;
     100            1 :         tcsetattr(STDIN_FILENO, TCSANOW, &drain);
     101              : 
     102            1 :         int c2 = read_byte();
     103            1 :         if (c2 == '[') {
     104            0 :             int c3 = read_byte();
     105            0 :             switch (c3) {
     106            0 :             case 'A': result = TERM_KEY_PREV_LINE; break;  /* ESC[A — Up          */
     107            0 :             case 'B': result = TERM_KEY_NEXT_LINE; break;  /* ESC[B — Down        */
     108            0 :             case 'C': result = TERM_KEY_RIGHT;     break;  /* ESC[C — Right       */
     109            0 :             case 'D': result = TERM_KEY_LEFT;      break;  /* ESC[D — Left        */
     110            0 :             case 'H': result = TERM_KEY_HOME;      break;  /* ESC[H — Home        */
     111            0 :             case 'F': result = TERM_KEY_END;       break;  /* ESC[F — End         */
     112            0 :             case 'Z': result = TERM_KEY_SHIFT_TAB; break;  /* ESC[Z — Shift+Tab   */
     113            0 :             case '1': { int nx=read_byte(); result=(nx=='~')?TERM_KEY_HOME:TERM_KEY_IGNORE; break; } /* ESC[1~ Home */
     114            0 :             case '3': { read_byte(); result = TERM_KEY_DELETE;    break; } /* ESC[3~ Delete */
     115            0 :             case '4': { read_byte(); result = TERM_KEY_END;       break; } /* ESC[4~ End    */
     116            0 :             case '5': { read_byte(); result = TERM_KEY_PREV_PAGE; break; } /* ESC[5~ PgUp   */
     117            0 :             case '6': { read_byte(); result = TERM_KEY_NEXT_PAGE; break; } /* ESC[6~ PgDn   */
     118            0 :             case '7': { read_byte(); result = TERM_KEY_HOME;      break; } /* ESC[7~ Home   */
     119            0 :             case '8': { read_byte(); result = TERM_KEY_END;       break; } /* ESC[8~ End    */
     120            0 :             default:
     121            0 :                 if (c3 != -1) {
     122              :                     int ch;
     123            0 :                     while ((ch = read_byte()) != -1) {
     124            0 :                         if ((ch >= 'A' && ch <= 'Z') ||
     125            0 :                             (ch >= 'a' && ch <= 'z') || ch == '~') break;
     126              :                     }
     127              :                 }
     128            0 :                 result = TERM_KEY_IGNORE;
     129            0 :                 break;
     130              :             }
     131              :         } else {
     132            1 :             result = TERM_KEY_ESC;   /* bare ESC — go back */
     133              :         }
     134              : 
     135              :         /* Restore VMIN=1 VTIME=0 raw mode. */
     136            1 :         t.c_cc[VMIN]  = 1;
     137            1 :         t.c_cc[VTIME] = 0;
     138            1 :         tcsetattr(STDIN_FILENO, TCSANOW, &t);
     139            0 :     } else if (c == '\n' || c == '\r') {
     140            0 :         result = TERM_KEY_ENTER;
     141            0 :     } else if (c == 3 /* Ctrl-C */) {
     142            0 :         result = TERM_KEY_QUIT;
     143            0 :     } else if (c == 127 || c == 8 /* DEL / Backspace */) {
     144            0 :         result = TERM_KEY_BACK;
     145            0 :     } else if (c == '\t') {
     146            0 :         result = TERM_KEY_TAB;
     147            0 :     } else if (c >= 32 && c <= 126) {
     148            0 :         g_last_printable = c;
     149            0 :         result = TERM_KEY_IGNORE;
     150              :     }
     151              :     /* c == -1 (read error/timeout) → result stays TERM_KEY_IGNORE */
     152              : 
     153            1 :     return result;
     154              : }
     155              : 
     156         5333 : int terminal_wcwidth(uint32_t cp) {
     157         5333 :     int w = wcwidth((wchar_t)cp);
     158         5333 :     return (w < 0) ? 0 : w;
     159              : }
     160              : 
     161            4 : int terminal_read_password(const char *prompt, char *buf, size_t size) {
     162            4 :     if (!buf || size == 0) return -1;
     163              : 
     164            2 :     int fd = fileno(stdin);
     165            2 :     int is_tty = isatty(fd);
     166              : 
     167            2 :     if (is_tty) {
     168            0 :         printf("%s: ", prompt);
     169            0 :         fflush(stdout);
     170              : 
     171            0 :         struct termios oldt, newt;
     172            0 :         tcgetattr(fd, &oldt);
     173            0 :         newt = oldt;
     174            0 :         newt.c_lflag &= ~(unsigned)ECHO;
     175            0 :         tcsetattr(fd, TCSANOW, &newt);
     176              : 
     177            0 :         char *line = NULL;
     178            0 :         size_t len = 0;
     179            0 :         ssize_t nread = getline(&line, &len, stdin);
     180              : 
     181            0 :         tcsetattr(fd, TCSANOW, &oldt);
     182            0 :         printf("\n");
     183              : 
     184            0 :         if (nread == -1 || !line) {
     185            0 :             free(line);
     186            0 :             return -1;
     187              :         }
     188              : 
     189              :         /* Strip trailing newline */
     190            0 :         size_t slen = strlen(line);
     191            0 :         if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
     192            0 :             line[--slen] = '\0';
     193            0 :         if (slen > 0 && (line[slen-1] == '\r'))
     194            0 :             line[--slen] = '\0';
     195              : 
     196            0 :         if (slen >= size) slen = size - 1;
     197            0 :         memcpy(buf, line, slen);
     198            0 :         buf[slen] = '\0';
     199            0 :         free(line);
     200            0 :         return (int)slen;
     201              :     } else {
     202              :         /* Non-TTY: read from stdin without echo manipulation */
     203            2 :         char *line = NULL;
     204            2 :         size_t len = 0;
     205            2 :         ssize_t nread = getline(&line, &len, stdin);
     206            2 :         if (nread == -1 || !line) {
     207            1 :             free(line);
     208            1 :             return -1;
     209              :         }
     210            1 :         size_t slen = strlen(line);
     211            1 :         if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
     212            1 :             line[--slen] = '\0';
     213            1 :         if (slen > 0 && (line[slen-1] == '\r'))
     214            0 :             line[--slen] = '\0';
     215            1 :         if (slen >= size) slen = size - 1;
     216            1 :         memcpy(buf, line, slen);
     217            1 :         buf[slen] = '\0';
     218            1 :         free(line);
     219            1 :         return (int)slen;
     220              :     }
     221              : }
        

Generated by: LCOV version 2.0-1