LCOV - code coverage report
Current view: top level - libs/libptytest - pty_screen.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 78.3 % 180 141
Test Date: 2026-05-07 15:53:08 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        21058 : PtyScreen *pty_screen_new(int cols, int rows) {
      23        21058 :     PtyScreen *scr = calloc(1, sizeof(*scr));
      24        21058 :     if (!scr) return NULL;
      25        21058 :     scr->cols = cols;
      26        21058 :     scr->rows = rows;
      27        21058 :     scr->cells = calloc((size_t)(cols * rows), sizeof(PtyCell));
      28        21058 :     if (!scr->cells) { free(scr); return NULL; }
      29              :     /* Initialise cells with spaces */
      30     59052978 :     for (int i = 0; i < cols * rows; i++) {
      31     59031920 :         scr->cells[i].ch[0] = ' ';
      32     59031920 :         scr->cells[i].ch[1] = '\0';
      33              :     }
      34        21058 :     return scr;
      35              : }
      36              : 
      37        20836 : void pty_screen_free(PtyScreen *scr) {
      38        20836 :     if (!scr) return;
      39        20836 :     free(scr->cells);
      40        20836 :     free(scr);
      41              : }
      42              : 
      43    154358712 : static PtyCell *cell_at(PtyScreen *scr, int row, int col) {
      44    154358712 :     if (row < 0 || row >= scr->rows || col < 0 || col >= scr->cols)
      45            0 :         return NULL;
      46    154358712 :     return &scr->cells[row * scr->cols + col];
      47              : }
      48              : 
      49              : /* ── Scroll up by one line ───────────────────────────────────────────── */
      50              : 
      51         3125 : static void scroll_up(PtyScreen *scr) {
      52         3125 :     memmove(&scr->cells[0],
      53         3125 :             &scr->cells[scr->cols],
      54         3125 :             (size_t)((scr->rows - 1) * scr->cols) * sizeof(PtyCell));
      55              :     /* Clear last row */
      56       327805 :     for (int c = 0; c < scr->cols; c++) {
      57       324680 :         PtyCell *cl = cell_at(scr, scr->rows - 1, c);
      58       324680 :         cl->ch[0] = ' '; cl->ch[1] = '\0';
      59       324680 :         cl->attr = PTY_ATTR_NONE;
      60              :     }
      61         3125 : }
      62              : 
      63              : /* ── VT100 parser state machine ──────────────────────────────────────── */
      64              : 
      65              : /** @brief Parses and applies a CSI sequence ending with the given final byte. */
      66      2318297 : static void apply_csi(PtyScreen *scr, const char *params, int param_len, char final) {
      67              :     /* Parse semicolon-separated integer parameters */
      68      2318297 :     int args[16] = {0};
      69      2318297 :     int argc = 0;
      70      2318297 :     const char *p = params;
      71      2318297 :     const char *end = params + param_len;
      72      4656421 :     while (p < end && argc < 16) {
      73      2338124 :         int val = 0;
      74      4975587 :         while (p < end && *p >= '0' && *p <= '9') {
      75      2637463 :             val = val * 10 + (*p - '0');
      76      2637463 :             p++;
      77              :         }
      78      2338124 :         args[argc++] = val;
      79      2338124 :         if (p < end && *p == ';') p++;
      80              :     }
      81              : 
      82              :     /* Any CSI sequence cancels pending wrap */
      83      2318297 :     scr->pending_wrap = 0;
      84              : 
      85      2318297 :     switch (final) {
      86       240055 :     case 'H': case 'f': /* CUP — cursor position */
      87       240055 :         scr->cur_row = (argc >= 1 && args[0] > 0) ? args[0] - 1 : 0;
      88       240055 :         scr->cur_col = (argc >= 2 && args[1] > 0) ? args[1] - 1 : 0;
      89       240055 :         if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1;
      90       240055 :         if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
      91       240055 :         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        94308 :     case 'J': /* ED — erase display */
     118        94308 :         if (args[0] == 2) {
     119              :             /* Erase entire display */
     120    223750856 :             for (int i = 0; i < scr->cols * scr->rows; i++) {
     121    223657000 :                 scr->cells[i].ch[0] = ' ';
     122    223657000 :                 scr->cells[i].ch[1] = '\0';
     123    223657000 :                 scr->cells[i].attr = PTY_ATTR_NONE;
     124              :             }
     125              :         }
     126        94308 :         break;
     127              : 
     128       135482 :     case 'K': /* EL — erase in line */
     129       135482 :         { int mode = (argc >= 1) ? args[0] : 0;
     130       135482 :           int start = (mode == 1 || mode == 2) ? 0 : scr->cur_col;
     131       135482 :           int stop  = (mode == 0 || mode == 2) ? scr->cols : scr->cur_col + 1;
     132      5931168 :           for (int c = start; c < stop; c++) {
     133      5795686 :               PtyCell *cl = cell_at(scr, scr->cur_row, c);
     134      5795686 :               if (cl) { cl->ch[0] = ' '; cl->ch[1] = '\0'; cl->attr = PTY_ATTR_NONE; }
     135              :           } }
     136       135482 :         break;
     137              : 
     138      1843808 :     case 'm': /* SGR — select graphic rendition */
     139      3687470 :         for (int i = 0; i < argc; i++) {
     140      1843662 :             if (args[i] == 0) {
     141       873964 :                 scr->cur_attr = PTY_ATTR_NONE;
     142       873964 :                 scr->cur_fg   = PTY_FG_DEFAULT;
     143       969698 :             } else if (args[i] == 1)  scr->cur_attr |= PTY_ATTR_BOLD;
     144       940101 :             else if (args[i] == 2)  scr->cur_attr |= PTY_ATTR_DIM;
     145       525319 :             else if (args[i] == 7)  scr->cur_attr |= PTY_ATTR_REVERSE;
     146       237735 :             else if (args[i] == 9)  scr->cur_attr |= PTY_ATTR_STRIKE;
     147       228835 :             else if (args[i] == 22) scr->cur_attr &= ~(PTY_ATTR_BOLD | PTY_ATTR_DIM);
     148       209618 :             else if (args[i] == 27) scr->cur_attr &= ~PTY_ATTR_REVERSE;
     149       209618 :             else if (args[i] == 29) scr->cur_attr &= ~PTY_ATTR_STRIKE;
     150              :             /* Standard fg colours: 30–37 */
     151       200718 :             else if (args[i] >= 30 && args[i] <= 37) scr->cur_fg = (uint8_t)args[i];
     152              :             /* Default fg colour */
     153        48634 :             else if (args[i] == 39) scr->cur_fg = PTY_FG_DEFAULT;
     154              :             /* 24-bit / 256-colour fg/bg: skip sub-args */
     155        34489 :             else if (args[i] == 38 || args[i] == 48) {
     156         5066 :                 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      1843808 :         break;
     162              : 
     163         4644 :     default:
     164              :         /* Unhandled CSI sequence — ignore */
     165         4644 :         break;
     166              :     }
     167      2318297 : }
     168              : 
     169       790089 : void pty_screen_feed(PtyScreen *scr, const char *data, size_t len) {
     170       790089 :     const char *end = data + len;
     171       790089 :     const char *p = data;
     172              : 
     173    154643759 :     while (p < end) {
     174    153853670 :         unsigned char ch = (unsigned char)*p;
     175              : 
     176    153853670 :         if (ch == '\033') {
     177              :             /* ESC sequence */
     178      4636594 :             if (p + 1 < end && p[1] == '[') {
     179              :                 /* CSI sequence: collect parameters and final byte */
     180      2318297 :                 const char *csi_start = p + 2;
     181      2318297 :                 const char *q = csi_start;
     182      5135732 :                 while (q < end && ((*q >= '0' && *q <= '9') || *q == ';' || *q == '?'))
     183      2817435 :                     q++;
     184      2318297 :                 if (q < end) {
     185      2318297 :                     apply_csi(scr, csi_start, (int)(q - csi_start), *q);
     186      2318297 :                     p = q + 1;
     187              :                 } else {
     188            0 :                     break; /* Incomplete sequence — wait for more data */
     189              :                 }
     190            0 :             } 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            0 :                 p++; /* Skip lone ESC */
     201              :             }
     202      2318297 :             continue;
     203              :         }
     204              : 
     205              :         /* Control characters */
     206    151535373 :         if (ch == '\n') {
     207      1619536 :             scr->pending_wrap = 0;
     208      1619536 :             scr->cur_row++;
     209      1619536 :             if (scr->cur_row >= scr->rows) {
     210         3125 :                 scr->cur_row = scr->rows - 1;
     211         3125 :                 scroll_up(scr);
     212              :             }
     213      1619536 :             p++;
     214      1619536 :             continue;
     215              :         }
     216    149915837 :         if (ch == '\r') {
     217      1672634 :             scr->pending_wrap = 0;
     218      1672634 :             scr->cur_col = 0;
     219      1672634 :             p++;
     220      1672634 :             continue;
     221              :         }
     222    148243203 :         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    148243203 :         if (ch == '\t') {
     229         4857 :             scr->pending_wrap = 0;
     230         4857 :             scr->cur_col = (scr->cur_col + 8) & ~7;
     231         4857 :             if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
     232         4857 :             p++;
     233         4857 :             continue;
     234              :         }
     235    148238346 :         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    148238346 :             if (scr->pending_wrap) {
     244          611 :                 scr->pending_wrap = 0;
     245          611 :                 scr->cur_col = 0;
     246          611 :                 scr->cur_row++;
     247          611 :                 if (scr->cur_row >= scr->rows) {
     248            0 :                     scr->cur_row = scr->rows - 1;
     249            0 :                     scroll_up(scr);
     250              :                 }
     251              :             }
     252              : 
     253    148238346 :             PtyCell *cl = cell_at(scr, scr->cur_row, scr->cur_col);
     254    148238346 :             if (!cl) { p++; continue; }
     255              : 
     256              :             /* Determine UTF-8 byte count */
     257    148238346 :             int bytes = 1;
     258    148238346 :             if (ch >= 0xC0 && ch < 0xE0) bytes = 2;
     259    148163663 :             else if (ch >= 0xE0 && ch < 0xF0) bytes = 3;
     260    126892591 :             else if (ch >= 0xF0 && ch < 0xF8) bytes = 4;
     261              : 
     262    148238346 :             if (p + bytes > end) break; /* Incomplete — wait for more */
     263              : 
     264    148238346 :             int copy = bytes < (int)sizeof(cl->ch) ? bytes : (int)sizeof(cl->ch) - 1;
     265    148238346 :             memcpy(cl->ch, p, (size_t)copy);
     266    148238346 :             cl->ch[copy] = '\0';
     267    148238346 :             cl->attr = scr->cur_attr;
     268    148238346 :             cl->fg   = scr->cur_fg;
     269              : 
     270    148238346 :             scr->cur_col++;
     271    148238346 :             if (scr->cur_col >= scr->cols) {
     272              :                 /* Pending wrap: stay at last column until next char */
     273      1241901 :                 scr->cur_col = scr->cols - 1;
     274      1241901 :                 scr->pending_wrap = 1;
     275              :             }
     276              : 
     277    148238346 :             p += bytes;
     278              :         }
     279              :     }
     280       790089 : }
        

Generated by: LCOV version 2.0-1