LCOV - code coverage report
Current view: top level - src/tui - screen.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 95.0 % 161 153
Test Date: 2026-05-06 13:17:06 Functions: 100.0 % 16 16

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file tui/screen.c
       6              :  * @brief Double-buffered terminal screen implementation (US-11 v2).
       7              :  */
       8              : 
       9              : #include "tui/screen.h"
      10              : #include "platform/terminal.h"
      11              : 
      12              : #include <stdlib.h>
      13              : #include <string.h>
      14              : 
      15        72385 : static ScreenCell blank_cell(uint8_t attrs) {
      16        72385 :     ScreenCell c = { .cp = ' ', .width = 1, .attrs = attrs, ._pad = 0 };
      17        72385 :     return c;
      18              : }
      19              : 
      20           97 : int screen_init(Screen *s, int rows, int cols) {
      21           97 :     if (!s || rows <= 0 || cols <= 0) return -1;
      22           92 :     memset(s, 0, sizeof(*s));
      23           92 :     size_t n = (size_t)rows * (size_t)cols;
      24           92 :     s->rows = rows;
      25           92 :     s->cols = cols;
      26           92 :     s->front = (ScreenCell *)calloc(n, sizeof(ScreenCell));
      27           92 :     s->back  = (ScreenCell *)calloc(n, sizeof(ScreenCell));
      28           92 :     if (!s->front || !s->back) {
      29            0 :         free(s->front); free(s->back);
      30            0 :         memset(s, 0, sizeof(*s));
      31            0 :         return -1;
      32              :     }
      33              :     /* front is "unknown" (cp=0, width=1) so the first flip emits everything.
      34              :      * back starts as a blank canvas so writers can just paint over it. */
      35        45777 :     for (size_t i = 0; i < n; i++) s->back[i] = blank_cell(0);
      36           92 :     s->out = stdout;
      37           92 :     s->force_full = 1;
      38           92 :     return 0;
      39              : }
      40              : 
      41           92 : void screen_free(Screen *s) {
      42           92 :     if (!s) return;
      43           92 :     free(s->front); free(s->back);
      44           92 :     memset(s, 0, sizeof(*s));
      45              : }
      46              : 
      47        15326 : static ScreenCell *back_at(Screen *s, int r, int c) {
      48        15326 :     return &s->back[(size_t)r * (size_t)s->cols + (size_t)c];
      49              : }
      50              : 
      51           11 : void screen_clear_back(Screen *s) {
      52           11 :     if (!s) return;
      53           11 :     size_t n = (size_t)s->rows * (size_t)s->cols;
      54        12671 :     for (size_t i = 0; i < n; i++) s->back[i] = blank_cell(0);
      55              : }
      56              : 
      57          379 : void screen_fill(Screen *s, int row, int col, int n, uint8_t attrs) {
      58          379 :     if (!s || row < 0 || row >= s->rows || col < 0 || col >= s->cols || n <= 0)
      59            0 :         return;
      60          379 :     if (col + n > s->cols) n = s->cols - col;
      61        14419 :     for (int i = 0; i < n; i++) *back_at(s, row, col + i) = blank_cell(attrs);
      62              : }
      63              : 
      64              : /* Decode one UTF-8 codepoint from @p p. Returns bytes consumed, or 0 at end
      65              :  * of string. Writes U+FFFD on malformed input and consumes a single byte so
      66              :  * the caller always makes progress. */
      67         1286 : static int utf8_decode(const char *p, uint32_t *out_cp) {
      68         1286 :     unsigned char c = (unsigned char)p[0];
      69         1286 :     if (c == 0) { *out_cp = 0; return 0; }
      70         1286 :     if (c < 0x80) { *out_cp = c; return 1; }
      71           53 :     if ((c & 0xE0) == 0xC0) {
      72           15 :         unsigned char c1 = (unsigned char)p[1];
      73           15 :         if ((c1 & 0xC0) != 0x80) { *out_cp = 0xFFFD; return 1; }
      74           13 :         *out_cp = (uint32_t)((c & 0x1F) << 6) | (c1 & 0x3F);
      75           13 :         return 2;
      76              :     }
      77           38 :     if ((c & 0xF0) == 0xE0) {
      78           16 :         unsigned char c1 = (unsigned char)p[1], c2 = (unsigned char)p[2];
      79           16 :         if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80) {
      80            0 :             *out_cp = 0xFFFD; return 1;
      81              :         }
      82           16 :         *out_cp = (uint32_t)((c & 0x0F) << 12) | ((c1 & 0x3F) << 6) | (c2 & 0x3F);
      83           16 :         return 3;
      84              :     }
      85           22 :     if ((c & 0xF8) == 0xF0) {
      86            8 :         unsigned char c1 = (unsigned char)p[1], c2 = (unsigned char)p[2],
      87            8 :                       c3 = (unsigned char)p[3];
      88            8 :         if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80) {
      89            0 :             *out_cp = 0xFFFD; return 1;
      90              :         }
      91            8 :         *out_cp = (uint32_t)((c & 0x07) << 18) | ((c1 & 0x3F) << 12)
      92            8 :                 | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
      93            8 :         return 4;
      94              :     }
      95           14 :     *out_cp = 0xFFFD;
      96           14 :     return 1;
      97              : }
      98              : 
      99          109 : int screen_put_str_n(Screen *s, int row, int col, int max_cols,
     100              :                       const char *utf8, uint8_t attrs) {
     101          109 :     if (!s || !utf8 || row < 0 || row >= s->rows
     102          109 :         || col < 0 || col >= s->cols) return 0;
     103          105 :     int start = col;
     104          105 :     int hard_stop = s->cols;
     105          105 :     if (max_cols > 0 && col + max_cols < hard_stop) hard_stop = col + max_cols;
     106         1390 :     while (*utf8 && col < hard_stop) {
     107              :         uint32_t cp;
     108         1286 :         int n = utf8_decode(utf8, &cp);
     109         1287 :         if (n <= 0) break;
     110         1286 :         utf8 += n;
     111              :         /* SEC-01: sanitize control characters before storing in a cell.
     112              :          * Replace codepoints that could carry ANSI escape sequences with
     113              :          * U+00B7 MIDDLE DOT so malicious message text cannot hijack the
     114              :          * terminal.  Allowed low-controls: \t (U+0009) and \n (U+000A).
     115              :          * Also block U+007F (DEL) and U+009B (8-bit CSI introducer). */
     116         1286 :         if ((cp < 0x20 && cp != 0x09 && cp != 0x0A)
     117         1277 :                 || cp == 0x7F || cp == 0x9B) {
     118           15 :             cp = 0x00B7; /* U+00B7 MIDDLE DOT */
     119              :         }
     120         1286 :         int w = terminal_wcwidth(cp);
     121         1286 :         if (w <= 0) continue;
     122         1272 :         if (col + w > hard_stop) break;
     123         1271 :         ScreenCell *lead = back_at(s, row, col);
     124         1271 :         lead->cp = cp; lead->width = (uint8_t)w; lead->attrs = attrs; lead->_pad = 0;
     125         1271 :         if (w == 2 && col + 1 < s->cols) {
     126           15 :             ScreenCell *tr = back_at(s, row, col + 1);
     127           15 :             tr->cp = cp; tr->width = 0; tr->attrs = attrs; tr->_pad = 0;
     128              :         }
     129         1271 :         col += w;
     130              :     }
     131          105 :     return col - start;
     132              : }
     133              : 
     134           50 : int screen_put_str(Screen *s, int row, int col,
     135              :                     const char *utf8, uint8_t attrs) {
     136           50 :     return screen_put_str_n(s, row, col, 0, utf8, attrs);
     137              : }
     138              : 
     139            1 : void screen_invalidate(Screen *s) {
     140            1 :     if (s) s->force_full = 1;
     141            1 : }
     142              : 
     143              : /* Encode one codepoint to UTF-8. Returns bytes written (1..4). */
     144         1660 : static size_t utf8_encode(uint32_t cp, char out[4]) {
     145         1660 :     if (cp < 0x80) { out[0] = (char)cp; return 1; }
     146           42 :     if (cp < 0x800) {
     147           13 :         out[0] = (char)(0xC0 | (cp >> 6));
     148           13 :         out[1] = (char)(0x80 | (cp & 0x3F));
     149           13 :         return 2;
     150              :     }
     151           29 :     if (cp < 0x10000) {
     152           21 :         out[0] = (char)(0xE0 | (cp >> 12));
     153           21 :         out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
     154           21 :         out[2] = (char)(0x80 | (cp & 0x3F));
     155           21 :         return 3;
     156              :     }
     157            8 :     out[0] = (char)(0xF0 | (cp >> 18));
     158            8 :     out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
     159            8 :     out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
     160            8 :     out[3] = (char)(0x80 | (cp & 0x3F));
     161            8 :     return 4;
     162              : }
     163              : 
     164              : /* Emit CSI m sequence for @p attrs. Always begins with a reset so the
     165              :  * previous cell's attributes do not leak through. */
     166           35 : static size_t sgr_encode(uint8_t attrs, char buf[16]) {
     167           35 :     size_t i = 0;
     168           35 :     buf[i++] = '\033'; buf[i++] = '['; buf[i++] = '0';
     169           35 :     if (attrs & SCREEN_ATTR_BOLD)    { buf[i++] = ';'; buf[i++] = '1'; }
     170           35 :     if (attrs & SCREEN_ATTR_DIM)     { buf[i++] = ';'; buf[i++] = '2'; }
     171           35 :     if (attrs & SCREEN_ATTR_REVERSE) { buf[i++] = ';'; buf[i++] = '7'; }
     172           35 :     buf[i++] = 'm';
     173           35 :     return i;
     174              : }
     175              : 
     176           39 : static size_t cup_encode(int row, int col, char buf[16]) {
     177           39 :     int n = snprintf(buf, 16, "\033[%d;%dH", row, col);
     178           39 :     return (n < 0) ? 0 : (size_t)n;
     179              : }
     180              : 
     181           35 : size_t screen_flip(Screen *s) {
     182           35 :     if (!s || !s->out) return 0;
     183           35 :     size_t total = 0;
     184           35 :     uint8_t cur_attrs = 0;
     185           35 :     int attrs_known = 0;
     186           35 :     int cur_row = -1, cur_col = -1;
     187              : 
     188           76 :     for (int r = 0; r < s->rows; r++) {
     189         1749 :         for (int c = 0; c < s->cols; c++) {
     190         1708 :             size_t idx = (size_t)r * (size_t)s->cols + (size_t)c;
     191         1708 :             ScreenCell *b = &s->back[idx];
     192         1708 :             ScreenCell *f = &s->front[idx];
     193         1743 :             if (b->width == 0) continue; /* trailer — handled by its lead */
     194         3390 :             int changed = s->force_full
     195           36 :                 || b->cp != f->cp
     196           35 :                 || b->width != f->width
     197         1731 :                 || b->attrs != f->attrs;
     198         1695 :             if (!changed) continue;
     199              : 
     200         1660 :             if (r != cur_row || c != cur_col) {
     201              :                 char buf[16];
     202           38 :                 size_t n = cup_encode(r + 1, c + 1, buf);
     203           38 :                 fwrite(buf, 1, n, s->out); total += n;
     204           38 :                 cur_row = r; cur_col = c;
     205              :             }
     206         1660 :             if (!attrs_known || b->attrs != cur_attrs) {
     207              :                 char buf[16];
     208           35 :                 size_t n = sgr_encode(b->attrs, buf);
     209           35 :                 fwrite(buf, 1, n, s->out); total += n;
     210           35 :                 cur_attrs = b->attrs; attrs_known = 1;
     211              :             }
     212              :             char buf[4];
     213         1660 :             size_t n = utf8_encode(b->cp ? b->cp : ' ', buf);
     214         1660 :             fwrite(buf, 1, n, s->out); total += n;
     215         1660 :             cur_col += b->width;
     216              : 
     217         1660 :             *f = *b;
     218         1660 :             if (b->width == 2 && c + 1 < s->cols) {
     219           13 :                 s->front[idx + 1] = s->back[idx + 1];
     220              :             }
     221              :         }
     222              :     }
     223           35 :     if (attrs_known && cur_attrs != 0) {
     224            0 :         const char *reset = "\033[0m";
     225            0 :         fwrite(reset, 1, 4, s->out); total += 4;
     226              :     }
     227           35 :     s->force_full = 0;
     228           35 :     fflush(s->out);
     229           35 :     return total;
     230              : }
     231              : 
     232            1 : void screen_cursor(Screen *s, int row, int col) {
     233            1 :     if (!s || !s->out) return;
     234              :     char buf[16];
     235            1 :     size_t n = cup_encode(row, col, buf);
     236            1 :     fwrite(buf, 1, n, s->out);
     237            1 :     fflush(s->out);
     238              : }
     239              : 
     240            2 : void screen_cursor_visible(Screen *s, int visible) {
     241            2 :     if (!s || !s->out) return;
     242            2 :     const char *seq = visible ? "\033[?25h" : "\033[?25l";
     243            2 :     fwrite(seq, 1, strlen(seq), s->out);
     244            2 :     fflush(s->out);
     245              : }
        

Generated by: LCOV version 2.0-1