LCOV - code coverage report
Current view: top level - libs/libptytest - pty_screen.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 71.9 % 171 123
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 6 6

            Line data    Source code
       1              : /**
       2              :  * @file pty_screen.c
       3              :  * @brief Virtual screen buffer with VT100 escape sequence parser.
       4              :  *
       5              :  * Minimal VT100 subset — only what TUI programs typically use:
       6              :  *   - Cursor positioning: CSI row;col H, CSI H (home)
       7              :  *   - Screen erase: CSI 2J (full), CSI K (to end of line), CSI 2K (full line)
       8              :  *   - SGR attributes: 0 (reset), 1 (bold), 2 (dim), 7 (reverse)
       9              :  *   - 24-bit colour: CSI 38;2;R;G;Bm (fg), CSI 48;2;R;G;Bm (bg) — parsed, not stored
      10              :  *   - Basic colours: CSI 3Xm, CSI 4Xm — parsed, not stored
      11              :  *   - Newline, carriage return, backspace, tab
      12              :  */
      13              : 
      14              : #include "pty_internal.h"
      15              : #include <stdio.h>
      16              : #include <string.h>
      17              : #include <stdlib.h>
      18              : 
      19              : /* ── Screen buffer management ────────────────────────────────────────── */
      20              : 
      21          162 : PtyScreen *pty_screen_new(int cols, int rows) {
      22          162 :     PtyScreen *scr = calloc(1, sizeof(*scr));
      23          162 :     if (!scr) return NULL;
      24          162 :     scr->cols = cols;
      25          162 :     scr->rows = rows;
      26          162 :     scr->cells = calloc((size_t)(cols * rows), sizeof(PtyCell));
      27          162 :     if (!scr->cells) { free(scr); return NULL; }
      28              :     /* Initialise cells with spaces */
      29       316722 :     for (int i = 0; i < cols * rows; i++) {
      30       316560 :         scr->cells[i].ch[0] = ' ';
      31       316560 :         scr->cells[i].ch[1] = '\0';
      32              :     }
      33          162 :     return scr;
      34              : }
      35              : 
      36          131 : void pty_screen_free(PtyScreen *scr) {
      37          131 :     if (!scr) return;
      38          131 :     free(scr->cells);
      39          131 :     free(scr);
      40              : }
      41              : 
      42        47059 : static PtyCell *cell_at(PtyScreen *scr, int row, int col) {
      43        47059 :     if (row < 0 || row >= scr->rows || col < 0 || col >= scr->cols)
      44            0 :         return NULL;
      45        47059 :     return &scr->cells[row * scr->cols + col];
      46              : }
      47              : 
      48              : /* ── Scroll up by one line ───────────────────────────────────────────── */
      49              : 
      50            5 : static void scroll_up(PtyScreen *scr) {
      51            5 :     memmove(&scr->cells[0],
      52            5 :             &scr->cells[scr->cols],
      53            5 :             (size_t)((scr->rows - 1) * scr->cols) * sizeof(PtyCell));
      54              :     /* Clear last row */
      55          405 :     for (int c = 0; c < scr->cols; c++) {
      56          400 :         PtyCell *cl = cell_at(scr, scr->rows - 1, c);
      57          400 :         cl->ch[0] = ' '; cl->ch[1] = '\0';
      58          400 :         cl->attr = PTY_ATTR_NONE;
      59              :     }
      60            5 : }
      61              : 
      62              : /* ── VT100 parser state machine ──────────────────────────────────────── */
      63              : 
      64              : /** @brief Parses and applies a CSI sequence ending with the given final byte. */
      65          946 : static void apply_csi(PtyScreen *scr, const char *params, int param_len, char final) {
      66              :     /* Parse semicolon-separated integer parameters */
      67          946 :     int args[16] = {0};
      68          946 :     int argc = 0;
      69          946 :     const char *p = params;
      70          946 :     const char *end = params + param_len;
      71         1904 :     while (p < end && argc < 16) {
      72          958 :         int val = 0;
      73         1883 :         while (p < end && *p >= '0' && *p <= '9') {
      74          925 :             val = val * 10 + (*p - '0');
      75          925 :             p++;
      76              :         }
      77          958 :         args[argc++] = val;
      78          958 :         if (p < end && *p == ';') p++;
      79              :     }
      80              : 
      81              :     /* Any CSI sequence cancels pending wrap */
      82          946 :     scr->pending_wrap = 0;
      83              : 
      84          946 :     switch (final) {
      85          169 :     case 'H': case 'f': /* CUP — cursor position */
      86          169 :         scr->cur_row = (argc >= 1 && args[0] > 0) ? args[0] - 1 : 0;
      87          169 :         scr->cur_col = (argc >= 2 && args[1] > 0) ? args[1] - 1 : 0;
      88          169 :         if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1;
      89          169 :         if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
      90          169 :         break;
      91              : 
      92            0 :     case 'A': /* CUU — cursor up */
      93            0 :         { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
      94            0 :           scr->cur_row -= n;
      95            0 :           if (scr->cur_row < 0) scr->cur_row = 0; }
      96            0 :         break;
      97              : 
      98            0 :     case 'B': /* CUD — cursor down */
      99            0 :         { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
     100            0 :           scr->cur_row += n;
     101            0 :           if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1; }
     102            0 :         break;
     103              : 
     104          358 :     case 'C': /* CUF — cursor forward */
     105          358 :         { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
     106          358 :           scr->cur_col += n;
     107          358 :           if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1; }
     108          358 :         break;
     109              : 
     110            0 :     case 'D': /* CUB — cursor backward */
     111            0 :         { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
     112            0 :           scr->cur_col -= n;
     113            0 :           if (scr->cur_col < 0) scr->cur_col = 0; }
     114            0 :         break;
     115              : 
     116            0 :     case 'J': /* ED — erase display */
     117            0 :         if (args[0] == 2) {
     118              :             /* Erase entire display */
     119            0 :             for (int i = 0; i < scr->cols * scr->rows; i++) {
     120            0 :                 scr->cells[i].ch[0] = ' ';
     121            0 :                 scr->cells[i].ch[1] = '\0';
     122            0 :                 scr->cells[i].attr = PTY_ATTR_NONE;
     123              :             }
     124              :         }
     125            0 :         break;
     126              : 
     127          358 :     case 'K': /* EL — erase in line */
     128          358 :         { int mode = (argc >= 1) ? args[0] : 0;
     129          358 :           int start = (mode == 1 || mode == 2) ? 0 : scr->cur_col;
     130          358 :           int stop  = (mode == 0 || mode == 2) ? scr->cols : scr->cur_col + 1;
     131        25976 :           for (int c = start; c < stop; c++) {
     132        25618 :               PtyCell *cl = cell_at(scr, scr->cur_row, c);
     133        25618 :               if (cl) { cl->ch[0] = ' '; cl->ch[1] = '\0'; cl->attr = PTY_ATTR_NONE; }
     134              :           } }
     135          358 :         break;
     136              : 
     137           49 :     case 'm': /* SGR — select graphic rendition */
     138          119 :         for (int i = 0; i < argc; i++) {
     139           70 :             if (args[i] == 0)      scr->cur_attr = PTY_ATTR_NONE;
     140           21 :             else if (args[i] == 1) scr->cur_attr |= PTY_ATTR_BOLD;
     141           21 :             else if (args[i] == 2) scr->cur_attr |= PTY_ATTR_DIM;
     142            7 :             else if (args[i] == 7) scr->cur_attr |= PTY_ATTR_REVERSE;
     143              :             /* Skip colour args: 30-37,38,39, 40-47,48,49, 90-97, 100-107 */
     144            0 :             else if (args[i] == 38 || args[i] == 48) {
     145              :                 /* 38;2;R;G;B or 38;5;N — skip remaining args */
     146            0 :                 if (i + 1 < argc && args[i + 1] == 2) i += 4; /* skip 2,R,G,B */
     147            0 :                 else if (i + 1 < argc && args[i + 1] == 5) i += 2; /* skip 5,N */
     148              :             }
     149              :         }
     150           49 :         break;
     151              : 
     152           12 :     default:
     153              :         /* Unhandled CSI sequence — ignore */
     154           12 :         break;
     155              :     }
     156          946 : }
     157              : 
     158          515 : void pty_screen_feed(PtyScreen *scr, const char *data, size_t len) {
     159          515 :     const char *end = data + len;
     160          515 :     const char *p = data;
     161              : 
     162        24277 :     while (p < end) {
     163        23762 :         unsigned char ch = (unsigned char)*p;
     164              : 
     165        23762 :         if (ch == '\033') {
     166              :             /* ESC sequence */
     167         1892 :             if (p + 1 < end && p[1] == '[') {
     168              :                 /* CSI sequence: collect parameters and final byte */
     169          946 :                 const char *csi_start = p + 2;
     170          946 :                 const char *q = csi_start;
     171         2097 :                 while (q < end && ((*q >= '0' && *q <= '9') || *q == ';' || *q == '?'))
     172         1151 :                     q++;
     173          946 :                 if (q < end) {
     174          946 :                     apply_csi(scr, csi_start, (int)(q - csi_start), *q);
     175          946 :                     p = q + 1;
     176              :                 } else {
     177            0 :                     break; /* Incomplete sequence — wait for more data */
     178              :                 }
     179            0 :             } else if (p + 1 < end && p[1] == ']') {
     180              :                 /* OSC sequence: skip until ST (\033\\) or BEL (\007) */
     181            0 :                 const char *q = p + 2;
     182            0 :                 while (q < end) {
     183            0 :                     if (*q == '\007') { q++; break; }
     184            0 :                     if (*q == '\033' && q + 1 < end && q[1] == '\\') { q += 2; break; }
     185            0 :                     q++;
     186              :                 }
     187            0 :                 p = q;
     188              :             } else {
     189            0 :                 p++; /* Skip lone ESC */
     190              :             }
     191          946 :             continue;
     192              :         }
     193              : 
     194              :         /* Control characters */
     195        22816 :         if (ch == '\n') {
     196          474 :             scr->pending_wrap = 0;
     197          474 :             scr->cur_row++;
     198          474 :             if (scr->cur_row >= scr->rows) {
     199            5 :                 scr->cur_row = scr->rows - 1;
     200            5 :                 scroll_up(scr);
     201              :             }
     202          474 :             p++;
     203          474 :             continue;
     204              :         }
     205        22342 :         if (ch == '\r') {
     206         1301 :             scr->pending_wrap = 0;
     207         1301 :             scr->cur_col = 0;
     208         1301 :             p++;
     209         1301 :             continue;
     210              :         }
     211        21041 :         if (ch == '\b') {
     212            0 :             scr->pending_wrap = 0;
     213            0 :             if (scr->cur_col > 0) scr->cur_col--;
     214            0 :             p++;
     215            0 :             continue;
     216              :         }
     217        21041 :         if (ch == '\t') {
     218            0 :             scr->pending_wrap = 0;
     219            0 :             scr->cur_col = (scr->cur_col + 8) & ~7;
     220            0 :             if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
     221            0 :             p++;
     222            0 :             continue;
     223              :         }
     224        21041 :         if (ch < 0x20) {
     225            0 :             p++; /* Skip other control chars */
     226            0 :             continue;
     227              :         }
     228              : 
     229              :         /* Printable character (ASCII or UTF-8 lead byte) */
     230              :         {
     231              :             /* Pending wrap: deferred line advance, matching real terminal behaviour */
     232        21041 :             if (scr->pending_wrap) {
     233            4 :                 scr->pending_wrap = 0;
     234            4 :                 scr->cur_col = 0;
     235            4 :                 scr->cur_row++;
     236            4 :                 if (scr->cur_row >= scr->rows) {
     237            0 :                     scr->cur_row = scr->rows - 1;
     238            0 :                     scroll_up(scr);
     239              :                 }
     240              :             }
     241              : 
     242        21041 :             PtyCell *cl = cell_at(scr, scr->cur_row, scr->cur_col);
     243        21041 :             if (!cl) { p++; continue; }
     244              : 
     245              :             /* Determine UTF-8 byte count */
     246        21041 :             int bytes = 1;
     247        21041 :             if (ch >= 0xC0 && ch < 0xE0) bytes = 2;
     248        21041 :             else if (ch >= 0xE0 && ch < 0xF0) bytes = 3;
     249        21039 :             else if (ch >= 0xF0 && ch < 0xF8) bytes = 4;
     250              : 
     251        21041 :             if (p + bytes > end) break; /* Incomplete — wait for more */
     252              : 
     253        21041 :             int copy = bytes < (int)sizeof(cl->ch) ? bytes : (int)sizeof(cl->ch) - 1;
     254        21041 :             memcpy(cl->ch, p, (size_t)copy);
     255        21041 :             cl->ch[copy] = '\0';
     256        21041 :             cl->attr = scr->cur_attr;
     257              : 
     258        21041 :             scr->cur_col++;
     259        21041 :             if (scr->cur_col >= scr->cols) {
     260              :                 /* Pending wrap: stay at last column until next char */
     261          168 :                 scr->cur_col = scr->cols - 1;
     262          168 :                 scr->pending_wrap = 1;
     263              :             }
     264              : 
     265        21041 :             p += bytes;
     266              :         }
     267              :     }
     268          515 : }
        

Generated by: LCOV version 2.0-1