LCOV - code coverage report
Current view: top level - libs/libptytest - pty_screen.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 56.7 % 171 97
Test Date: 2026-05-06 13:17:06 Functions: 83.3 % 6 5

            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          160 : PtyScreen *pty_screen_new(int cols, int rows) {
      22          160 :     PtyScreen *scr = calloc(1, sizeof(*scr));
      23          160 :     if (!scr) return NULL;
      24          160 :     scr->cols = cols;
      25          160 :     scr->rows = rows;
      26          160 :     scr->cells = calloc((size_t)(cols * rows), sizeof(PtyCell));
      27          160 :     if (!scr->cells) { free(scr); return NULL; }
      28              :     /* Initialise cells with spaces */
      29       313600 :     for (int i = 0; i < cols * rows; i++) {
      30       313440 :         scr->cells[i].ch[0] = ' ';
      31       313440 :         scr->cells[i].ch[1] = '\0';
      32              :     }
      33          160 :     return scr;
      34              : }
      35              : 
      36          124 : void pty_screen_free(PtyScreen *scr) {
      37          124 :     if (!scr) return;
      38          124 :     free(scr->cells);
      39          124 :     free(scr);
      40              : }
      41              : 
      42        34036 : static PtyCell *cell_at(PtyScreen *scr, int row, int col) {
      43        34036 :     if (row < 0 || row >= scr->rows || col < 0 || col >= scr->cols)
      44            0 :         return NULL;
      45        34036 :     return &scr->cells[row * scr->cols + col];
      46              : }
      47              : 
      48              : /* ── Scroll up by one line ───────────────────────────────────────────── */
      49              : 
      50            0 : static void scroll_up(PtyScreen *scr) {
      51            0 :     memmove(&scr->cells[0],
      52            0 :             &scr->cells[scr->cols],
      53            0 :             (size_t)((scr->rows - 1) * scr->cols) * sizeof(PtyCell));
      54              :     /* Clear last row */
      55            0 :     for (int c = 0; c < scr->cols; c++) {
      56            0 :         PtyCell *cl = cell_at(scr, scr->rows - 1, c);
      57            0 :         cl->ch[0] = ' '; cl->ch[1] = '\0';
      58            0 :         cl->attr = PTY_ATTR_NONE;
      59              :     }
      60            0 : }
      61              : 
      62              : /* ── VT100 parser state machine ──────────────────────────────────────── */
      63              : 
      64              : /** @brief Parses and applies a CSI sequence ending with the given final byte. */
      65          716 : static void apply_csi(PtyScreen *scr, const char *params, int param_len, char final) {
      66              :     /* Parse semicolon-separated integer parameters */
      67          716 :     int args[16] = {0};
      68          716 :     int argc = 0;
      69          716 :     const char *p = params;
      70          716 :     const char *end = params + param_len;
      71         1074 :     while (p < end && argc < 16) {
      72          358 :         int val = 0;
      73          769 :         while (p < end && *p >= '0' && *p <= '9') {
      74          411 :             val = val * 10 + (*p - '0');
      75          411 :             p++;
      76              :         }
      77          358 :         args[argc++] = val;
      78          358 :         if (p < end && *p == ';') p++;
      79              :     }
      80              : 
      81              :     /* Any CSI sequence cancels pending wrap */
      82          716 :     scr->pending_wrap = 0;
      83              : 
      84          716 :     switch (final) {
      85            0 :     case 'H': case 'f': /* CUP — cursor position */
      86            0 :         scr->cur_row = (argc >= 1 && args[0] > 0) ? args[0] - 1 : 0;
      87            0 :         scr->cur_col = (argc >= 2 && args[1] > 0) ? args[1] - 1 : 0;
      88            0 :         if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1;
      89            0 :         if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
      90            0 :         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            0 :     case 'm': /* SGR — select graphic rendition */
     138            0 :         for (int i = 0; i < argc; i++) {
     139            0 :             if (args[i] == 0)      scr->cur_attr = PTY_ATTR_NONE;
     140            0 :             else if (args[i] == 1) scr->cur_attr |= PTY_ATTR_BOLD;
     141            0 :             else if (args[i] == 2) scr->cur_attr |= PTY_ATTR_DIM;
     142            0 :             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            0 :         break;
     151              : 
     152            0 :     default:
     153              :         /* Unhandled CSI sequence — ignore */
     154            0 :         break;
     155              :     }
     156          716 : }
     157              : 
     158          487 : void pty_screen_feed(PtyScreen *scr, const char *data, size_t len) {
     159          487 :     const char *end = data + len;
     160          487 :     const char *p = data;
     161              : 
     162        11343 :     while (p < end) {
     163        10856 :         unsigned char ch = (unsigned char)*p;
     164              : 
     165        10856 :         if (ch == '\033') {
     166              :             /* ESC sequence */
     167         1432 :             if (p + 1 < end && p[1] == '[') {
     168              :                 /* CSI sequence: collect parameters and final byte */
     169          716 :                 const char *csi_start = p + 2;
     170          716 :                 const char *q = csi_start;
     171         1127 :                 while (q < end && ((*q >= '0' && *q <= '9') || *q == ';' || *q == '?'))
     172          411 :                     q++;
     173          716 :                 if (q < end) {
     174          716 :                     apply_csi(scr, csi_start, (int)(q - csi_start), *q);
     175          716 :                     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          716 :             continue;
     192              :         }
     193              : 
     194              :         /* Control characters */
     195        10140 :         if (ch == '\n') {
     196          451 :             scr->pending_wrap = 0;
     197          451 :             scr->cur_row++;
     198          451 :             if (scr->cur_row >= scr->rows) {
     199            0 :                 scr->cur_row = scr->rows - 1;
     200            0 :                 scroll_up(scr);
     201              :             }
     202          451 :             p++;
     203          451 :             continue;
     204              :         }
     205         9689 :         if (ch == '\r') {
     206         1271 :             scr->pending_wrap = 0;
     207         1271 :             scr->cur_col = 0;
     208         1271 :             p++;
     209         1271 :             continue;
     210              :         }
     211         8418 :         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         8418 :         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         8418 :         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         8418 :             if (scr->pending_wrap) {
     233           10 :                 scr->pending_wrap = 0;
     234           10 :                 scr->cur_col = 0;
     235           10 :                 scr->cur_row++;
     236           10 :                 if (scr->cur_row >= scr->rows) {
     237            0 :                     scr->cur_row = scr->rows - 1;
     238            0 :                     scroll_up(scr);
     239              :                 }
     240              :             }
     241              : 
     242         8418 :             PtyCell *cl = cell_at(scr, scr->cur_row, scr->cur_col);
     243         8418 :             if (!cl) { p++; continue; }
     244              : 
     245              :             /* Determine UTF-8 byte count */
     246         8418 :             int bytes = 1;
     247         8418 :             if (ch >= 0xC0 && ch < 0xE0) bytes = 2;
     248         8418 :             else if (ch >= 0xE0 && ch < 0xF0) bytes = 3;
     249         8418 :             else if (ch >= 0xF0 && ch < 0xF8) bytes = 4;
     250              : 
     251         8418 :             if (p + bytes > end) break; /* Incomplete — wait for more */
     252              : 
     253         8418 :             int copy = bytes < (int)sizeof(cl->ch) ? bytes : (int)sizeof(cl->ch) - 1;
     254         8418 :             memcpy(cl->ch, p, (size_t)copy);
     255         8418 :             cl->ch[copy] = '\0';
     256         8418 :             cl->attr = scr->cur_attr;
     257              : 
     258         8418 :             scr->cur_col++;
     259         8418 :             if (scr->cur_col >= scr->cols) {
     260              :                 /* Pending wrap: stay at last column until next char */
     261           10 :                 scr->cur_col = scr->cols - 1;
     262           10 :                 scr->pending_wrap = 1;
     263              :             }
     264              : 
     265         8418 :             p += bytes;
     266              :         }
     267              :     }
     268          487 : }
        

Generated by: LCOV version 2.0-1