LCOV - code coverage report
Current view: top level - src/tui - screen.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 96.3 % 161 155
Test Date: 2026-04-20 19:54:22 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       104785 : static ScreenCell blank_cell(uint8_t attrs) {
      16       104785 :     ScreenCell c = { .cp = ' ', .width = 1, .attrs = attrs, ._pad = 0 };
      17       104785 :     return c;
      18              : }
      19              : 
      20          103 : int screen_init(Screen *s, int rows, int cols) {
      21          103 :     if (!s || rows <= 0 || cols <= 0) return -1;
      22           98 :     memset(s, 0, sizeof(*s));
      23           98 :     size_t n = (size_t)rows * (size_t)cols;
      24           98 :     s->rows = rows;
      25           98 :     s->cols = cols;
      26           98 :     s->front = (ScreenCell *)calloc(n, sizeof(ScreenCell));
      27           98 :     s->back  = (ScreenCell *)calloc(n, sizeof(ScreenCell));
      28           98 :     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        56583 :     for (size_t i = 0; i < n; i++) s->back[i] = blank_cell(0);
      36           98 :     s->out = stdout;
      37           98 :     s->force_full = 1;
      38           98 :     return 0;
      39              : }
      40              : 
      41           98 : void screen_free(Screen *s) {
      42           98 :     if (!s) return;
      43           98 :     free(s->front); free(s->back);
      44           98 :     memset(s, 0, sizeof(*s));
      45              : }
      46              : 
      47        26606 : static ScreenCell *back_at(Screen *s, int r, int c) {
      48        26606 :     return &s->back[(size_t)r * (size_t)s->cols + (size_t)c];
      49              : }
      50              : 
      51           17 : void screen_clear_back(Screen *s) {
      52           17 :     if (!s) return;
      53           17 :     size_t n = (size_t)s->rows * (size_t)s->cols;
      54        23477 :     for (size_t i = 0; i < n; i++) s->back[i] = blank_cell(0);
      55              : }
      56              : 
      57          653 : void screen_fill(Screen *s, int row, int col, int n, uint8_t attrs) {
      58          653 :     if (!s || row < 0 || row >= s->rows || col < 0 || col >= s->cols || n <= 0)
      59            0 :         return;
      60          653 :     if (col + n > s->cols) n = s->cols - col;
      61        25493 :     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         1766 : static int utf8_decode(const char *p, uint32_t *out_cp) {
      68         1766 :     unsigned char c = (unsigned char)p[0];
      69         1766 :     if (c == 0) { *out_cp = 0; return 0; }
      70         1766 :     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          127 : int screen_put_str_n(Screen *s, int row, int col, int max_cols,
     100              :                       const char *utf8, uint8_t attrs) {
     101          127 :     if (!s || !utf8 || row < 0 || row >= s->rows
     102          127 :         || col < 0 || col >= s->cols) return 0;
     103          123 :     int start = col;
     104          123 :     int hard_stop = s->cols;
     105          123 :     if (max_cols > 0 && col + max_cols < hard_stop) hard_stop = col + max_cols;
     106         1888 :     while (*utf8 && col < hard_stop) {
     107              :         uint32_t cp;
     108         1766 :         int n = utf8_decode(utf8, &cp);
     109         1767 :         if (n <= 0) break;
     110         1766 :         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         1766 :         if ((cp < 0x20 && cp != 0x09 && cp != 0x0A)
     117         1757 :                 || cp == 0x7F || cp == 0x9B) {
     118           15 :             cp = 0x00B7; /* U+00B7 MIDDLE DOT */
     119              :         }
     120         1766 :         int w = terminal_wcwidth(cp);
     121         1766 :         if (w <= 0) continue;
     122         1752 :         if (col + w > hard_stop) break;
     123         1751 :         ScreenCell *lead = back_at(s, row, col);
     124         1751 :         lead->cp = cp; lead->width = (uint8_t)w; lead->attrs = attrs; lead->_pad = 0;
     125         1751 :         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         1751 :         col += w;
     130              :     }
     131          123 :     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        12460 : static size_t utf8_encode(uint32_t cp, char out[4]) {
     145        12460 :     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           71 : static size_t sgr_encode(uint8_t attrs, char buf[16]) {
     167           71 :     size_t i = 0;
     168           71 :     buf[i++] = '\033'; buf[i++] = '['; buf[i++] = '0';
     169           71 :     if (attrs & SCREEN_ATTR_BOLD)    { buf[i++] = ';'; buf[i++] = '1'; }
     170           71 :     if (attrs & SCREEN_ATTR_DIM)     { buf[i++] = ';'; buf[i++] = '2'; }
     171           71 :     if (attrs & SCREEN_ATTR_REVERSE) { buf[i++] = ';'; buf[i++] = '7'; }
     172           71 :     buf[i++] = 'm';
     173           71 :     return i;
     174              : }
     175              : 
     176          183 : static size_t cup_encode(int row, int col, char buf[16]) {
     177          183 :     int n = snprintf(buf, 16, "\033[%d;%dH", row, col);
     178          183 :     return (n < 0) ? 0 : (size_t)n;
     179              : }
     180              : 
     181           41 : size_t screen_flip(Screen *s) {
     182           41 :     if (!s || !s->out) return 0;
     183           41 :     size_t total = 0;
     184           41 :     uint8_t cur_attrs = 0;
     185           41 :     int attrs_known = 0;
     186           41 :     int cur_row = -1, cur_col = -1;
     187              : 
     188          222 :     for (int r = 0; r < s->rows; r++) {
     189        12689 :         for (int c = 0; c < s->cols; c++) {
     190        12508 :             size_t idx = (size_t)r * (size_t)s->cols + (size_t)c;
     191        12508 :             ScreenCell *b = &s->back[idx];
     192        12508 :             ScreenCell *f = &s->front[idx];
     193        12543 :             if (b->width == 0) continue; /* trailer — handled by its lead */
     194        24990 :             int changed = s->force_full
     195           36 :                 || b->cp != f->cp
     196           35 :                 || b->width != f->width
     197        12531 :                 || b->attrs != f->attrs;
     198        12495 :             if (!changed) continue;
     199              : 
     200        12460 :             if (r != cur_row || c != cur_col) {
     201              :                 char buf[16];
     202          178 :                 size_t n = cup_encode(r + 1, c + 1, buf);
     203          178 :                 fwrite(buf, 1, n, s->out); total += n;
     204          178 :                 cur_row = r; cur_col = c;
     205              :             }
     206        12460 :             if (!attrs_known || b->attrs != cur_attrs) {
     207              :                 char buf[16];
     208           71 :                 size_t n = sgr_encode(b->attrs, buf);
     209           71 :                 fwrite(buf, 1, n, s->out); total += n;
     210           71 :                 cur_attrs = b->attrs; attrs_known = 1;
     211              :             }
     212              :             char buf[4];
     213        12460 :             size_t n = utf8_encode(b->cp ? b->cp : ' ', buf);
     214        12460 :             fwrite(buf, 1, n, s->out); total += n;
     215        12460 :             cur_col += b->width;
     216              : 
     217        12460 :             *f = *b;
     218        12460 :             if (b->width == 2 && c + 1 < s->cols) {
     219           13 :                 s->front[idx + 1] = s->back[idx + 1];
     220              :             }
     221              :         }
     222              :     }
     223           41 :     if (attrs_known && cur_attrs != 0) {
     224            6 :         const char *reset = "\033[0m";
     225            6 :         fwrite(reset, 1, 4, s->out); total += 4;
     226              :     }
     227           41 :     s->force_full = 0;
     228           41 :     fflush(s->out);
     229           41 :     return total;
     230              : }
     231              : 
     232            5 : void screen_cursor(Screen *s, int row, int col) {
     233            5 :     if (!s || !s->out) return;
     234              :     char buf[16];
     235            5 :     size_t n = cup_encode(row, col, buf);
     236            5 :     fwrite(buf, 1, n, s->out);
     237            5 :     fflush(s->out);
     238              : }
     239              : 
     240           12 : void screen_cursor_visible(Screen *s, int visible) {
     241           12 :     if (!s || !s->out) return;
     242           12 :     const char *seq = visible ? "\033[?25h" : "\033[?25l";
     243           12 :     fwrite(seq, 1, strlen(seq), s->out);
     244           12 :     fflush(s->out);
     245              : }
        

Generated by: LCOV version 2.0-1