LCOV - code coverage report
Current view: top level - src/platform/posix - terminal.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 1.6 % 190 3
Test Date: 2026-04-20 19:54:24 Functions: 6.2 % 16 1

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * POSIX terminal implementation.
       6              :  * Uses termios(3), ioctl TIOCGWINSZ, wcwidth(3).
       7              :  */
       8              : #include "../terminal.h"
       9              : #include <errno.h>
      10              : #include <poll.h>
      11              : #include <signal.h>
      12              : #include <stdio.h>
      13              : #include <stdlib.h>
      14              : #include <string.h>
      15              : #include <unistd.h>
      16              : #include <wchar.h>
      17              : #include <termios.h>
      18              : #include <sys/ioctl.h>
      19              : #include <wchar.h>
      20              : 
      21              : /** Opaque saved terminal state. Definition lives here (hidden from header). */
      22              : struct TermRawState {
      23              :     struct termios saved;
      24              :     int            active;
      25              : };
      26              : 
      27            0 : int terminal_cols(void) {
      28              :     struct winsize ws;
      29            0 :     if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0)
      30            0 :         return (int)ws.ws_col;
      31            0 :     return 80;
      32              : }
      33              : 
      34            0 : int terminal_rows(void) {
      35              :     struct winsize ws;
      36            0 :     if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_row > 0)
      37            0 :         return (int)ws.ws_row;
      38            0 :     return 0;
      39              : }
      40              : 
      41            0 : int terminal_is_tty(int fd) {
      42            0 :     return isatty(fd);
      43              : }
      44              : 
      45            0 : TermRawState *terminal_raw_enter(void) {
      46            0 :     TermRawState *state = malloc(sizeof(TermRawState));
      47            0 :     if (!state) return NULL;
      48              : 
      49            0 :     if (tcgetattr(STDIN_FILENO, &state->saved) != 0) {
      50            0 :         free(state);
      51            0 :         return NULL;
      52              :     }
      53              : 
      54            0 :     struct termios raw = state->saved;
      55            0 :     raw.c_lflag &= ~(unsigned)(ICANON | ECHO | ISIG);
      56            0 :     raw.c_cc[VMIN]  = 1;
      57            0 :     raw.c_cc[VTIME] = 0;
      58            0 :     tcsetattr(STDIN_FILENO, TCSANOW, &raw);
      59            0 :     state->active = 1;
      60            0 :     return state;
      61              : }
      62              : 
      63            0 : void terminal_raw_exit(TermRawState **state) {
      64            0 :     if (!state || !*state) return;
      65            0 :     if ((*state)->active)
      66            0 :         tcsetattr(STDIN_FILENO, TCSANOW, &(*state)->saved);
      67            0 :     free(*state);
      68            0 :     *state = NULL;
      69              : }
      70              : 
      71              : /**
      72              :  * Read one byte from STDIN_FILENO via read(2) — NOT via getchar()/stdio.
      73              :  *
      74              :  * Rationale: getchar() uses the C stdio buffer.  When getchar() calls
      75              :  * read(2) with VMIN=0 VTIME=1 and gets a 0-byte return (timeout for bare
      76              :  * ESC), stdio marks the FILE* EOF flag.  All subsequent getchar() calls
      77              :  * then return EOF immediately without blocking, causing an infinite
      78              :  * redraw loop in the TUI.  Using read(2) directly bypasses the stdio
      79              :  * layer entirely and avoids the EOF flag problem.
      80              :  *
      81              :  * Returns the byte value [0..255], or -1 on error/timeout.
      82              :  */
      83            0 : static int read_byte(void) {
      84              :     unsigned char c;
      85            0 :     ssize_t n = read(STDIN_FILENO, &c, 1);
      86            0 :     return (n == 1) ? (int)c : -1;
      87              : }
      88              : 
      89              : static int g_last_printable = 0;
      90              : 
      91            0 : int terminal_last_printable(void) { return g_last_printable; }
      92              : 
      93            0 : TermKey terminal_read_key(void) {
      94              :     /* The terminal must already be in raw mode (VMIN=1, VTIME=0). */
      95            0 :     g_last_printable = 0;
      96            0 :     int c = read_byte();
      97            0 :     TermKey result = TERM_KEY_IGNORE;   /* unknown input → silent no-op */
      98              : 
      99            0 :     if (c == '\033') {
     100              :         /* Temporarily switch to VMIN=0 VTIME=1 (100 ms timeout) to drain
     101              :          * the escape sequence without blocking if it is a bare ESC. */
     102              :         struct termios t;
     103            0 :         tcgetattr(STDIN_FILENO, &t);
     104            0 :         struct termios drain = t;
     105            0 :         drain.c_cc[VMIN]  = 0;
     106            0 :         drain.c_cc[VTIME] = 1;
     107            0 :         tcsetattr(STDIN_FILENO, TCSANOW, &drain);
     108              : 
     109            0 :         int c2 = read_byte();
     110            0 :         if (c2 == '[') {
     111            0 :             int c3 = read_byte();
     112            0 :             switch (c3) {
     113            0 :             case 'A': result = TERM_KEY_PREV_LINE; break;  /* ESC[A — Up arrow    */
     114            0 :             case 'B': result = TERM_KEY_NEXT_LINE; break;  /* ESC[B — Down arrow  */
     115            0 :             case 'C': result = TERM_KEY_RIGHT;     break;  /* ESC[C — Right arrow */
     116            0 :             case 'D': result = TERM_KEY_LEFT;      break;  /* ESC[D — Left arrow  */
     117            0 :             case 'H': result = TERM_KEY_HOME;      break;  /* ESC[H — Home        */
     118            0 :             case 'F': result = TERM_KEY_END;       break;  /* ESC[F — End         */
     119            0 :             case '1': { /* ESC[1~ Home or ESC[1;...  */
     120            0 :                 int c4 = read_byte();
     121            0 :                 if (c4 == '~') result = TERM_KEY_HOME;
     122            0 :                 else           result = TERM_KEY_IGNORE; /* ESC[1;... modifier */
     123            0 :                 break;
     124              :             }
     125            0 :             case '3': read_byte(); result = TERM_KEY_DELETE;    break; /* ESC[3~ Del  */
     126            0 :             case '4': read_byte(); result = TERM_KEY_END;       break; /* ESC[4~ End  */
     127            0 :             case '5': read_byte(); result = TERM_KEY_PREV_PAGE; break; /* ESC[5~ PgUp */
     128            0 :             case '6': read_byte(); result = TERM_KEY_NEXT_PAGE; break; /* ESC[6~ PgDn */
     129            0 :             case '7': read_byte(); result = TERM_KEY_HOME;      break; /* ESC[7~ Home */
     130            0 :             case '8': read_byte(); result = TERM_KEY_END;       break; /* ESC[8~ End  */
     131            0 :             default:
     132            0 :                 if (c3 != -1) {
     133              :                     int ch;
     134            0 :                     while ((ch = read_byte()) != -1) {
     135            0 :                         if ((ch >= 'A' && ch <= 'Z') ||
     136            0 :                             (ch >= 'a' && ch <= 'z') || ch == '~') break;
     137              :                     }
     138              :                 }
     139            0 :                 result = TERM_KEY_IGNORE;
     140            0 :                 break;
     141              :             }
     142            0 :         } else if (c2 == 'O') {
     143            0 :             int c3 = read_byte();
     144            0 :             switch (c3) {
     145            0 :             case 'H': result = TERM_KEY_HOME; break; /* ESC O H — Home */
     146            0 :             case 'F': result = TERM_KEY_END;  break; /* ESC O F — End  */
     147            0 :             default:  result = TERM_KEY_IGNORE; break;
     148              :             }
     149              :         } else {
     150            0 :             result = TERM_KEY_ESC;   /* bare ESC — go back */
     151              :         }
     152              : 
     153              :         /* Restore VMIN=1 VTIME=0 raw mode. */
     154            0 :         t.c_cc[VMIN]  = 1;
     155            0 :         t.c_cc[VTIME] = 0;
     156            0 :         tcsetattr(STDIN_FILENO, TCSANOW, &t);
     157            0 :     } else if (c == '\n' || c == '\r') {
     158            0 :         result = TERM_KEY_ENTER;
     159            0 :     } else if (c == 3 /* Ctrl-C */) {
     160            0 :         result = TERM_KEY_QUIT;
     161            0 :     } else if (c == 4 /* Ctrl-D */) {
     162            0 :         result = TERM_KEY_CTRL_D;
     163            0 :     } else if (c == 1 /* Ctrl-A */) {
     164            0 :         result = TERM_KEY_CTRL_A;
     165            0 :     } else if (c == 5 /* Ctrl-E */) {
     166            0 :         result = TERM_KEY_CTRL_E;
     167            0 :     } else if (c == 11 /* Ctrl-K */) {
     168            0 :         result = TERM_KEY_CTRL_K;
     169            0 :     } else if (c == 23 /* Ctrl-W */) {
     170            0 :         result = TERM_KEY_CTRL_W;
     171            0 :     } else if (c == 127 || c == 8 /* DEL / Backspace */) {
     172            0 :         result = TERM_KEY_BACK;
     173            0 :     } else if (c >= 32 && c <= 126) {
     174            0 :         g_last_printable = c;
     175            0 :         result = TERM_KEY_IGNORE;
     176              :     }
     177              :     /* c == -1 (read error/timeout) → result stays TERM_KEY_IGNORE */
     178              : 
     179            0 :     return result;
     180              : }
     181              : 
     182          301 : int terminal_wcwidth(uint32_t cp) {
     183          301 :     int w = wcwidth((wchar_t)cp);
     184          301 :     return (w < 0) ? 0 : w;
     185              : }
     186              : 
     187            0 : int terminal_wait_key(int timeout_ms) {
     188            0 :     struct pollfd pfd = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
     189            0 :     int rc = poll(&pfd, 1, timeout_ms);
     190            0 :     if (rc < 0) {
     191              :         /* EINTR (e.g. SIGWINCH) is not a hard error — caller retries. */
     192            0 :         return (errno == EINTR) ? -1 : -1;
     193              :     }
     194            0 :     if (rc == 0) return 0;
     195            0 :     return (pfd.revents & POLLIN) ? 1 : 0;
     196              : }
     197              : 
     198              : /* ---- SIGTERM / SIGHUP / SIGINT cleanup handlers ---- */
     199              : 
     200              : /**
     201              :  * Pointer to the saved termios inside the active TermRawState.
     202              :  * Written once from terminal_install_cleanup_handlers() before
     203              :  * any signal can arrive; read only from signal handlers thereafter.
     204              :  * volatile to prevent the compiler from caching the load.
     205              :  */
     206              : static volatile struct termios *g_saved_termios = NULL;
     207              : 
     208              : /**
     209              :  * Async-signal-safe terminal cleanup and re-raise.
     210              :  *
     211              :  * Uses only async-signal-safe functions:
     212              :  *   tcsetattr(3)  — POSIX async-signal-safe
     213              :  *   write(2)      — POSIX async-signal-safe
     214              :  *   signal(2)     — POSIX async-signal-safe
     215              :  *   raise(3)      — POSIX async-signal-safe
     216              :  */
     217            0 : static void cleanup_signal_handler(int sig) {
     218              :     /* Restore cooked mode if we have a saved state. */
     219            0 :     if (g_saved_termios) {
     220              :         /* Cast away volatile: tcsetattr requires a non-volatile pointer.
     221              :          * The cast is safe because we only write this pointer once and
     222              :          * the pointed-to struct outlives the signal handler. */
     223              :         struct termios saved;
     224            0 :         saved = *(struct termios *)g_saved_termios;
     225            0 :         tcsetattr(STDIN_FILENO, TCSANOW, &saved);
     226              :     }
     227              :     /* Show the cursor (ESC[?25h) — write(2) is async-signal-safe.
     228              :      * Ignore the return value: if the write fails there is nothing
     229              :      * sensible to do inside a signal handler. */
     230              :     static const char show_cursor[] = "\033[?25h";
     231            0 :     ssize_t _wr_unused = write(STDOUT_FILENO, show_cursor,
     232              :                                sizeof(show_cursor) - 1);
     233              :     (void)_wr_unused;
     234              :     /* Reset to default handler and re-raise so the shell sees the
     235              :      * correct exit status (e.g. 128 + SIGTERM). */
     236            0 :     signal(sig, SIG_DFL);
     237            0 :     raise(sig);
     238            0 : }
     239              : 
     240            0 : void terminal_install_cleanup_handlers(TermRawState *state) {
     241            0 :     if (!state) return;
     242              :     /* Expose the saved termios to the signal handler. */
     243            0 :     g_saved_termios = &state->saved;
     244              : 
     245              :     struct sigaction sa;
     246            0 :     memset(&sa, 0, sizeof(sa));
     247            0 :     sa.sa_handler = cleanup_signal_handler;
     248            0 :     sigemptyset(&sa.sa_mask);
     249            0 :     sa.sa_flags = 0;   /* no SA_RESTART — we re-raise immediately */
     250              : 
     251            0 :     sigaction(SIGTERM, &sa, NULL);
     252            0 :     sigaction(SIGHUP,  &sa, NULL);
     253            0 :     sigaction(SIGINT,  &sa, NULL);
     254              : }
     255              : 
     256              : /* ---- SIGWINCH / resize notifications ---- */
     257              : 
     258              : static volatile sig_atomic_t g_resize_pending = 0;
     259              : static int g_resize_handler_installed = 0;
     260              : 
     261            0 : static void resize_handler(int sig) {
     262              :     (void)sig;
     263            0 :     g_resize_pending = 1;
     264            0 : }
     265              : 
     266            0 : void terminal_enable_resize_notifications(void) {
     267            0 :     if (g_resize_handler_installed) return;
     268              :     struct sigaction sa;
     269            0 :     memset(&sa, 0, sizeof(sa));
     270            0 :     sa.sa_handler = resize_handler;
     271            0 :     sigemptyset(&sa.sa_mask);
     272              :     /* No SA_RESTART on purpose: we want blocking read(2) in
     273              :      * terminal_read_key to return with EINTR so the TUI loop can
     274              :      * observe the resize between keystrokes. */
     275            0 :     sa.sa_flags = 0;
     276            0 :     sigaction(SIGWINCH, &sa, NULL);
     277            0 :     g_resize_handler_installed = 1;
     278              : }
     279              : 
     280            0 : int terminal_consume_resize(void) {
     281            0 :     if (g_resize_pending) {
     282            0 :         g_resize_pending = 0;
     283            0 :         return 1;
     284              :     }
     285            0 :     return 0;
     286              : }
     287              : 
     288            0 : int terminal_read_password(const char *prompt, char *buf, size_t size) {
     289            0 :     if (!buf || size == 0) return -1;
     290              : 
     291            0 :     int fd = fileno(stdin);
     292            0 :     int is_tty = isatty(fd);
     293              : 
     294            0 :     if (is_tty) {
     295            0 :         printf("%s: ", prompt);
     296            0 :         fflush(stdout);
     297              : 
     298              :         struct termios oldt, newt;
     299            0 :         tcgetattr(fd, &oldt);
     300            0 :         newt = oldt;
     301            0 :         newt.c_lflag &= ~(unsigned)ECHO;
     302            0 :         tcsetattr(fd, TCSANOW, &newt);
     303              : 
     304            0 :         char *line = NULL;
     305            0 :         size_t len = 0;
     306            0 :         ssize_t nread = getline(&line, &len, stdin);
     307              : 
     308            0 :         tcsetattr(fd, TCSANOW, &oldt);
     309            0 :         printf("\n");
     310              : 
     311            0 :         if (nread == -1 || !line) {
     312            0 :             free(line);
     313            0 :             return -1;
     314              :         }
     315              : 
     316              :         /* Strip trailing newline */
     317            0 :         size_t slen = strlen(line);
     318            0 :         if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
     319            0 :             line[--slen] = '\0';
     320            0 :         if (slen > 0 && (line[slen-1] == '\r'))
     321            0 :             line[--slen] = '\0';
     322              : 
     323            0 :         if (slen >= size) slen = size - 1;
     324            0 :         memcpy(buf, line, slen);
     325            0 :         buf[slen] = '\0';
     326            0 :         free(line);
     327            0 :         return (int)slen;
     328              :     } else {
     329              :         /* Non-TTY: read from stdin without echo manipulation */
     330            0 :         char *line = NULL;
     331            0 :         size_t len = 0;
     332            0 :         ssize_t nread = getline(&line, &len, stdin);
     333            0 :         if (nread == -1 || !line) {
     334            0 :             free(line);
     335            0 :             return -1;
     336              :         }
     337            0 :         size_t slen = strlen(line);
     338            0 :         if (slen > 0 && (line[slen-1] == '\n' || line[slen-1] == '\r'))
     339            0 :             line[--slen] = '\0';
     340            0 :         if (slen > 0 && (line[slen-1] == '\r'))
     341            0 :             line[--slen] = '\0';
     342            0 :         if (slen >= size) slen = size - 1;
     343            0 :         memcpy(buf, line, slen);
     344            0 :         buf[slen] = '\0';
     345            0 :         free(line);
     346            0 :         return (int)slen;
     347              :     }
     348              : }
        

Generated by: LCOV version 2.0-1