LCOV - code coverage report
Current view: top level - libs/libptytest - pty_screen.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 80.0 % 180 144
Test Date: 2026-08-21 10:11:30 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 (strikethrough)
       9              :  *   - Standard fg colours: CSI 30–37m (stored in cell.fg), CSI 39m (default)
      10              :  *   - 24-bit colour: CSI 38;2;R;G;Bm (fg), CSI 48;2;R;G;Bm (bg) — parsed, not stored
      11              :  *   - Basic bg colours: CSI 4Xm — parsed, not stored
      12              :  *   - Newline, carriage return, backspace, tab
      13              :  */
      14              : 
      15              : #include "pty_internal.h"
      16              : #include <stdio.h>
      17              : #include <string.h>
      18              : #include <stdlib.h>
      19              : 
      20              : /* ── Screen buffer management ────────────────────────────────────────── */
      21              : 
      22        24675 : PtyScreen *pty_screen_new(int cols, int rows) {
      23        24675 :     PtyScreen *scr = calloc(1, sizeof(*scr));
      24        24675 :     if (!scr) return NULL;
      25        24675 :     scr->cols = cols;
      26        24675 :     scr->rows = rows;
      27        24675 :     scr->cells = calloc((size_t)(cols * rows), sizeof(PtyCell));
      28        24675 :     if (!scr->cells) { free(scr); return NULL; }
      29              :     /* Initialise cells with spaces */
      30     74629675 :     for (int i = 0; i < cols * rows; i++) {
      31     74605000 :         scr->cells[i].ch[0] = ' ';
      32     74605000 :         scr->cells[i].ch[1] = '\0';
      33              :     }
      34        24675 :     return scr;
      35              : }
      36              : 
      37        24294 : void pty_screen_free(PtyScreen *scr) {
      38        24294 :     if (!scr) return;
      39        24294 :     free(scr->cells);
      40        24294 :     free(scr);
      41              : }
      42              : 
      43    264706038 : static PtyCell *cell_at(PtyScreen *scr, int row, int col) {
      44    264706038 :     if (row < 0 || row >= scr->rows || col < 0 || col >= scr->cols)
      45            0 :         return NULL;
      46    264706038 :     return &scr->cells[row * scr->cols + col];
      47              : }
      48              : 
      49              : /* ── Scroll up by one line ───────────────────────────────────────────── */
      50              : 
      51         2190 : static void scroll_up(PtyScreen *scr) {
      52         2190 :     memmove(&scr->cells[0],
      53         2190 :             &scr->cells[scr->cols],
      54         2190 :             (size_t)((scr->rows - 1) * scr->cols) * sizeof(PtyCell));
      55              :     /* Clear last row */
      56       221190 :     for (int c = 0; c < scr->cols; c++) {
      57       219000 :         PtyCell *cl = cell_at(scr, scr->rows - 1, c);
      58       219000 :         cl->ch[0] = ' '; cl->ch[1] = '\0';
      59       219000 :         cl->attr = PTY_ATTR_NONE;
      60              :     }
      61         2190 : }
      62              : 
      63              : /* ── VT100 parser state machine ──────────────────────────────────────── */
      64              : 
      65              : /** @brief Parses and applies a CSI sequence ending with the given final byte. */
      66      4711936 : static void apply_csi(PtyScreen *scr, const char *params, int param_len, char final) {
      67              :     /* Parse semicolon-separated integer parameters */
      68      4711936 :     int args[16] = {0};
      69      4711936 :     int argc = 0;
      70      4711936 :     const char *p = params;
      71      4711936 :     const char *end = params + param_len;
      72      9654138 :     while (p < end && argc < 16) {
      73      4942202 :         int val = 0;
      74     10246737 :         while (p < end && *p >= '0' && *p <= '9') {
      75      5304535 :             val = val * 10 + (*p - '0');
      76      5304535 :             p++;
      77              :         }
      78      4942202 :         args[argc++] = val;
      79      4942202 :         if (p < end && *p == ';') p++;
      80              :     }
      81              : 
      82              :     /* Any CSI sequence cancels pending wrap */
      83      4711936 :     scr->pending_wrap = 0;
      84              : 
      85      4711936 :     switch (final) {
      86       934721 :     case 'H': case 'f': /* CUP — cursor position */
      87       934721 :         scr->cur_row = (argc >= 1 && args[0] > 0) ? args[0] - 1 : 0;
      88       934721 :         scr->cur_col = (argc >= 2 && args[1] > 0) ? args[1] - 1 : 0;
      89       934721 :         if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1;
      90       934721 :         if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
      91       934721 :         break;
      92              : 
      93            0 :     case 'A': /* CUU — cursor up */
      94            0 :         { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
      95            0 :           scr->cur_row -= n;
      96            0 :           if (scr->cur_row < 0) scr->cur_row = 0; }
      97            0 :         break;
      98              : 
      99            0 :     case 'B': /* CUD — cursor down */
     100            0 :         { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
     101            0 :           scr->cur_row += n;
     102            0 :           if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1; }
     103            0 :         break;
     104              : 
     105            0 :     case 'C': /* CUF — cursor forward */
     106            0 :         { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
     107            0 :           scr->cur_col += n;
     108            0 :           if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1; }
     109            0 :         break;
     110              : 
     111            0 :     case 'D': /* CUB — cursor backward */
     112            0 :         { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
     113            0 :           scr->cur_col -= n;
     114            0 :           if (scr->cur_col < 0) scr->cur_col = 0; }
     115            0 :         break;
     116              : 
     117       164308 :     case 'J': /* ED — erase display */
     118       164308 :         if (args[0] == 2) {
     119              :             /* Erase entire display */
     120    334183792 :             for (int i = 0; i < scr->cols * scr->rows; i++) {
     121    334055120 :                 scr->cells[i].ch[0] = ' ';
     122    334055120 :                 scr->cells[i].ch[1] = '\0';
     123    334055120 :                 scr->cells[i].attr = PTY_ATTR_NONE;
     124              :             }
     125              :         }
     126       164308 :         break;
     127              : 
     128       476732 :     case 'K': /* EL — erase in line */
     129       476732 :         { int mode = (argc >= 1) ? args[0] : 0;
     130       476732 :           int start = (mode == 1 || mode == 2) ? 0 : scr->cur_col;
     131       476732 :           int stop  = (mode == 0 || mode == 2) ? scr->cols : scr->cur_col + 1;
     132     43870913 :           for (int c = start; c < stop; c++) {
     133     43394181 :               PtyCell *cl = cell_at(scr, scr->cur_row, c);
     134     43394181 :               if (cl) { cl->ch[0] = ' '; cl->ch[1] = '\0'; cl->attr = PTY_ATTR_NONE; }
     135              :           } }
     136       476732 :         break;
     137              : 
     138      3129356 :     case 'm': /* SGR — select graphic rendition */
     139      6258566 :         for (int i = 0; i < argc; i++) {
     140      3129210 :             if (args[i] == 0) {
     141      1531891 :                 scr->cur_attr = PTY_ATTR_NONE;
     142      1531891 :                 scr->cur_fg   = PTY_FG_DEFAULT;
     143      1597319 :             } else if (args[i] == 1)  scr->cur_attr |= PTY_ATTR_BOLD;
     144      1527408 :             else if (args[i] == 2)  scr->cur_attr |= PTY_ATTR_DIM;
     145       608418 :             else if (args[i] == 7)  scr->cur_attr |= PTY_ATTR_REVERSE;
     146       200392 :             else if (args[i] == 9)  scr->cur_attr |= PTY_ATTR_STRIKE;
     147       190206 :             else if (args[i] == 22) scr->cur_attr &= ~(PTY_ATTR_BOLD | PTY_ATTR_DIM);
     148       169679 :             else if (args[i] == 27) scr->cur_attr &= ~PTY_ATTR_REVERSE;
     149       169679 :             else if (args[i] == 29) scr->cur_attr &= ~PTY_ATTR_STRIKE;
     150              :             /* Standard fg colours: 30–37 */
     151       159493 :             else if (args[i] >= 30 && args[i] <= 37) scr->cur_fg = (uint8_t)args[i];
     152              :             /* Default fg colour */
     153        39285 :             else if (args[i] == 39) scr->cur_fg = PTY_FG_DEFAULT;
     154              :             /* 24-bit / 256-colour fg/bg: skip sub-args */
     155        33710 :             else if (args[i] == 38 || args[i] == 48) {
     156         4609 :                 if (i + 1 < argc && args[i + 1] == 2) i += 4; /* skip 2,R,G,B */
     157            0 :                 else if (i + 1 < argc && args[i + 1] == 5) i += 2; /* skip 5,N */
     158              :             }
     159              :             /* bg colours 40–47, 49, bright variants 90–107: ignored */
     160              :         }
     161      3129356 :         break;
     162              : 
     163         6819 :     default:
     164              :         /* Unhandled CSI sequence — ignore */
     165         6819 :         break;
     166              :     }
     167      4711936 : }
     168              : 
     169      1560331 : void pty_screen_feed(PtyScreen *scr, const char *data, size_t len) {
     170      1560331 :     const char *end = data + len;
     171      1560331 :     const char *p = data;
     172              : 
     173    231276648 :     while (p < end) {
     174    229716441 :         unsigned char ch = (unsigned char)*p;
     175              : 
     176    229716441 :         if (ch == '\033') {
     177              :             /* ESC sequence */
     178      9423907 :             if (p + 1 < end && p[1] == '[') {
     179              :                 /* CSI sequence: collect parameters and final byte */
     180      4711968 :                 const char *csi_start = p + 2;
     181      4711968 :                 const char *q = csi_start;
     182     10825901 :                 while (q < end && ((*q >= '0' && *q <= '9') || *q == ';' || *q == '?'))
     183      6113933 :                     q++;
     184      4711968 :                 if (q < end) {
     185      4711936 :                     apply_csi(scr, csi_start, (int)(q - csi_start), *q);
     186      4711936 :                     p = q + 1;
     187              :                 } else {
     188           32 :                     break; /* Incomplete sequence — wait for more data */
     189              :                 }
     190            3 :             } else if (p + 1 < end && p[1] == ']') {
     191              :                 /* OSC sequence: skip until ST (\033\\) or BEL (\007) */
     192            0 :                 const char *q = p + 2;
     193            0 :                 while (q < end) {
     194            0 :                     if (*q == '\007') { q++; break; }
     195            0 :                     if (*q == '\033' && q + 1 < end && q[1] == '\\') { q += 2; break; }
     196            0 :                     q++;
     197              :                 }
     198            0 :                 p = q;
     199              :             } else {
     200            3 :                 p++; /* Skip lone ESC */
     201              :             }
     202      4711939 :             continue;
     203              :         }
     204              : 
     205              :         /* Control characters */
     206    225004470 :         if (ch == '\n') {
     207      1924385 :             scr->pending_wrap = 0;
     208      1924385 :             scr->cur_row++;
     209      1924385 :             if (scr->cur_row >= scr->rows) {
     210         2190 :                 scr->cur_row = scr->rows - 1;
     211         2190 :                 scroll_up(scr);
     212              :             }
     213      1924385 :             p++;
     214      1924385 :             continue;
     215              :         }
     216    223080085 :         if (ch == '\r') {
     217      1986499 :             scr->pending_wrap = 0;
     218      1986499 :             scr->cur_col = 0;
     219      1986499 :             p++;
     220      1986499 :             continue;
     221              :         }
     222    221093586 :         if (ch == '\b') {
     223            0 :             scr->pending_wrap = 0;
     224            0 :             if (scr->cur_col > 0) scr->cur_col--;
     225            0 :             p++;
     226            0 :             continue;
     227              :         }
     228    221093586 :         if (ch == '\t') {
     229          729 :             scr->pending_wrap = 0;
     230          729 :             scr->cur_col = (scr->cur_col + 8) & ~7;
     231          729 :             if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
     232          729 :             p++;
     233          729 :             continue;
     234              :         }
     235    221092857 :         if (ch < 0x20) {
     236            0 :             p++; /* Skip other control chars */
     237            0 :             continue;
     238              :         }
     239              : 
     240              :         /* Printable character (ASCII or UTF-8 lead byte) */
     241              :         {
     242              :             /* Pending wrap: deferred line advance, matching real terminal behaviour */
     243    221092857 :             if (scr->pending_wrap) {
     244         6771 :                 scr->pending_wrap = 0;
     245         6771 :                 scr->cur_col = 0;
     246         6771 :                 scr->cur_row++;
     247         6771 :                 if (scr->cur_row >= scr->rows) {
     248            0 :                     scr->cur_row = scr->rows - 1;
     249            0 :                     scroll_up(scr);
     250              :                 }
     251              :             }
     252              : 
     253    221092857 :             PtyCell *cl = cell_at(scr, scr->cur_row, scr->cur_col);
     254    221092857 :             if (!cl) { p++; continue; }
     255              : 
     256              :             /* Determine UTF-8 byte count */
     257    221092857 :             int bytes = 1;
     258    221092857 :             if (ch >= 0xC0 && ch < 0xE0) bytes = 2;
     259    220997943 :             else if (ch >= 0xE0 && ch < 0xF0) bytes = 3;
     260    190476334 :             else if (ch >= 0xF0 && ch < 0xF8) bytes = 4;
     261              : 
     262    221092857 :             if (p + bytes > end) break; /* Incomplete — wait for more */
     263              : 
     264    221092765 :             int copy = bytes < (int)sizeof(cl->ch) ? bytes : (int)sizeof(cl->ch) - 1;
     265    221092765 :             memcpy(cl->ch, p, (size_t)copy);
     266    221092765 :             cl->ch[copy] = '\0';
     267    221092765 :             cl->attr = scr->cur_attr;
     268    221092765 :             cl->fg   = scr->cur_fg;
     269              : 
     270    221092765 :             scr->cur_col++;
     271    221092765 :             if (scr->cur_col >= scr->cols) {
     272              :                 /* Pending wrap: stay at last column until next char */
     273      1690524 :                 scr->cur_col = scr->cols - 1;
     274      1690524 :                 scr->pending_wrap = 1;
     275              :             }
     276              : 
     277    221092765 :             p += bytes;
     278              :         }
     279              :     }
     280      1560331 : }
        

Generated by: LCOV version 2.0-1