LCOV - code coverage report
Current view: top level - src/tui - screen.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 85.7 % 161 138
Test Date: 2026-04-20 19:54:24 Functions: 81.2 % 16 13

            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        18168 : static ScreenCell blank_cell(uint8_t attrs) {
      16        18168 :     ScreenCell c = { .cp = ' ', .width = 1, .attrs = attrs, ._pad = 0 };
      17        18168 :     return c;
      18              : }
      19              : 
      20           15 : int screen_init(Screen *s, int rows, int cols) {
      21           15 :     if (!s || rows <= 0 || cols <= 0) return -1;
      22           15 :     memset(s, 0, sizeof(*s));
      23           15 :     size_t n = (size_t)rows * (size_t)cols;
      24           15 :     s->rows = rows;
      25           15 :     s->cols = cols;
      26           15 :     s->front = (ScreenCell *)calloc(n, sizeof(ScreenCell));
      27           15 :     s->back  = (ScreenCell *)calloc(n, sizeof(ScreenCell));
      28           15 :     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         6543 :     for (size_t i = 0; i < n; i++) s->back[i] = blank_cell(0);
      36           15 :     s->out = stdout;
      37           15 :     s->force_full = 1;
      38           15 :     return 0;
      39              : }
      40              : 
      41           15 : void screen_free(Screen *s) {
      42           15 :     if (!s) return;
      43           15 :     free(s->front); free(s->back);
      44           15 :     memset(s, 0, sizeof(*s));
      45              : }
      46              : 
      47         6116 : static ScreenCell *back_at(Screen *s, int r, int c) {
      48         6116 :     return &s->back[(size_t)r * (size_t)s->cols + (size_t)c];
      49              : }
      50              : 
      51            4 : void screen_clear_back(Screen *s) {
      52            4 :     if (!s) return;
      53            4 :     size_t n = (size_t)s->rows * (size_t)s->cols;
      54         5828 :     for (size_t i = 0; i < n; i++) s->back[i] = blank_cell(0);
      55              : }
      56              : 
      57          143 : void screen_fill(Screen *s, int row, int col, int n, uint8_t attrs) {
      58          143 :     if (!s || row < 0 || row >= s->rows || col < 0 || col >= s->cols || n <= 0)
      59            0 :         return;
      60          143 :     if (col + n > s->cols) n = s->cols - col;
      61         5959 :     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          301 : static int utf8_decode(const char *p, uint32_t *out_cp) {
      68          301 :     unsigned char c = (unsigned char)p[0];
      69          301 :     if (c == 0) { *out_cp = 0; return 0; }
      70          301 :     if (c < 0x80) { *out_cp = c; return 1; }
      71           24 :     if ((c & 0xE0) == 0xC0) {
      72            7 :         unsigned char c1 = (unsigned char)p[1];
      73            7 :         if ((c1 & 0xC0) != 0x80) { *out_cp = 0xFFFD; return 1; }
      74            6 :         *out_cp = (uint32_t)((c & 0x1F) << 6) | (c1 & 0x3F);
      75            6 :         return 2;
      76              :     }
      77           17 :     if ((c & 0xF0) == 0xE0) {
      78            6 :         unsigned char c1 = (unsigned char)p[1], c2 = (unsigned char)p[2];
      79            6 :         if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80) {
      80            0 :             *out_cp = 0xFFFD; return 1;
      81              :         }
      82            6 :         *out_cp = (uint32_t)((c & 0x0F) << 12) | ((c1 & 0x3F) << 6) | (c2 & 0x3F);
      83            6 :         return 3;
      84              :     }
      85           11 :     if ((c & 0xF8) == 0xF0) {
      86            4 :         unsigned char c1 = (unsigned char)p[1], c2 = (unsigned char)p[2],
      87            4 :                       c3 = (unsigned char)p[3];
      88            4 :         if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80) {
      89            0 :             *out_cp = 0xFFFD; return 1;
      90              :         }
      91            4 :         *out_cp = (uint32_t)((c & 0x07) << 18) | ((c1 & 0x3F) << 12)
      92            4 :                 | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
      93            4 :         return 4;
      94              :     }
      95            7 :     *out_cp = 0xFFFD;
      96            7 :     return 1;
      97              : }
      98              : 
      99           22 : int screen_put_str_n(Screen *s, int row, int col, int max_cols,
     100              :                       const char *utf8, uint8_t attrs) {
     101           22 :     if (!s || !utf8 || row < 0 || row >= s->rows
     102           22 :         || col < 0 || col >= s->cols) return 0;
     103           22 :     int start = col;
     104           22 :     int hard_stop = s->cols;
     105           22 :     if (max_cols > 0 && col + max_cols < hard_stop) hard_stop = col + max_cols;
     106          323 :     while (*utf8 && col < hard_stop) {
     107              :         uint32_t cp;
     108          301 :         int n = utf8_decode(utf8, &cp);
     109          301 :         if (n <= 0) break;
     110          301 :         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          301 :         if ((cp < 0x20 && cp != 0x09 && cp != 0x0A)
     117          297 :                 || cp == 0x7F || cp == 0x9B) {
     118            6 :             cp = 0x00B7; /* U+00B7 MIDDLE DOT */
     119              :         }
     120          301 :         int w = terminal_wcwidth(cp);
     121          301 :         if (w <= 0) continue;
     122          294 :         if (col + w > hard_stop) break;
     123          294 :         ScreenCell *lead = back_at(s, row, col);
     124          294 :         lead->cp = cp; lead->width = (uint8_t)w; lead->attrs = attrs; lead->_pad = 0;
     125          294 :         if (w == 2 && col + 1 < s->cols) {
     126            6 :             ScreenCell *tr = back_at(s, row, col + 1);
     127            6 :             tr->cp = cp; tr->width = 0; tr->attrs = attrs; tr->_pad = 0;
     128              :         }
     129          294 :         col += w;
     130              :     }
     131           22 :     return col - start;
     132              : }
     133              : 
     134           13 : int screen_put_str(Screen *s, int row, int col,
     135              :                     const char *utf8, uint8_t attrs) {
     136           13 :     return screen_put_str_n(s, row, col, 0, utf8, attrs);
     137              : }
     138              : 
     139            0 : void screen_invalidate(Screen *s) {
     140            0 :     if (s) s->force_full = 1;
     141            0 : }
     142              : 
     143              : /* Encode one codepoint to UTF-8. Returns bytes written (1..4). */
     144          762 : static size_t utf8_encode(uint32_t cp, char out[4]) {
     145          762 :     if (cp < 0x80) { out[0] = (char)cp; return 1; }
     146           20 :     if (cp < 0x800) {
     147            6 :         out[0] = (char)(0xC0 | (cp >> 6));
     148            6 :         out[1] = (char)(0x80 | (cp & 0x3F));
     149            6 :         return 2;
     150              :     }
     151           14 :     if (cp < 0x10000) {
     152           10 :         out[0] = (char)(0xE0 | (cp >> 12));
     153           10 :         out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
     154           10 :         out[2] = (char)(0x80 | (cp & 0x3F));
     155           10 :         return 3;
     156              :     }
     157            4 :     out[0] = (char)(0xF0 | (cp >> 18));
     158            4 :     out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
     159            4 :     out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
     160            4 :     out[3] = (char)(0x80 | (cp & 0x3F));
     161            4 :     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           12 : static size_t sgr_encode(uint8_t attrs, char buf[16]) {
     167           12 :     size_t i = 0;
     168           12 :     buf[i++] = '\033'; buf[i++] = '['; buf[i++] = '0';
     169           12 :     if (attrs & SCREEN_ATTR_BOLD)    { buf[i++] = ';'; buf[i++] = '1'; }
     170           12 :     if (attrs & SCREEN_ATTR_DIM)     { buf[i++] = ';'; buf[i++] = '2'; }
     171           12 :     if (attrs & SCREEN_ATTR_REVERSE) { buf[i++] = ';'; buf[i++] = '7'; }
     172           12 :     buf[i++] = 'm';
     173           12 :     return i;
     174              : }
     175              : 
     176           12 : static size_t cup_encode(int row, int col, char buf[16]) {
     177           12 :     int n = snprintf(buf, 16, "\033[%d;%dH", row, col);
     178           12 :     return (n < 0) ? 0 : (size_t)n;
     179              : }
     180              : 
     181           12 : size_t screen_flip(Screen *s) {
     182           12 :     if (!s || !s->out) return 0;
     183           12 :     size_t total = 0;
     184           12 :     uint8_t cur_attrs = 0;
     185           12 :     int attrs_known = 0;
     186           12 :     int cur_row = -1, cur_col = -1;
     187              : 
     188           24 :     for (int r = 0; r < s->rows; r++) {
     189          780 :         for (int c = 0; c < s->cols; c++) {
     190          768 :             size_t idx = (size_t)r * (size_t)s->cols + (size_t)c;
     191          768 :             ScreenCell *b = &s->back[idx];
     192          768 :             ScreenCell *f = &s->front[idx];
     193          768 :             if (b->width == 0) continue; /* trailer — handled by its lead */
     194         1524 :             int changed = s->force_full
     195            0 :                 || b->cp != f->cp
     196            0 :                 || b->width != f->width
     197          762 :                 || b->attrs != f->attrs;
     198          762 :             if (!changed) continue;
     199              : 
     200          762 :             if (r != cur_row || c != cur_col) {
     201              :                 char buf[16];
     202           12 :                 size_t n = cup_encode(r + 1, c + 1, buf);
     203           12 :                 fwrite(buf, 1, n, s->out); total += n;
     204           12 :                 cur_row = r; cur_col = c;
     205              :             }
     206          762 :             if (!attrs_known || b->attrs != cur_attrs) {
     207              :                 char buf[16];
     208           12 :                 size_t n = sgr_encode(b->attrs, buf);
     209           12 :                 fwrite(buf, 1, n, s->out); total += n;
     210           12 :                 cur_attrs = b->attrs; attrs_known = 1;
     211              :             }
     212              :             char buf[4];
     213          762 :             size_t n = utf8_encode(b->cp ? b->cp : ' ', buf);
     214          762 :             fwrite(buf, 1, n, s->out); total += n;
     215          762 :             cur_col += b->width;
     216              : 
     217          762 :             *f = *b;
     218          762 :             if (b->width == 2 && c + 1 < s->cols) {
     219            6 :                 s->front[idx + 1] = s->back[idx + 1];
     220              :             }
     221              :         }
     222              :     }
     223           12 :     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           12 :     s->force_full = 0;
     228           12 :     fflush(s->out);
     229           12 :     return total;
     230              : }
     231              : 
     232            0 : void screen_cursor(Screen *s, int row, int col) {
     233            0 :     if (!s || !s->out) return;
     234              :     char buf[16];
     235            0 :     size_t n = cup_encode(row, col, buf);
     236            0 :     fwrite(buf, 1, n, s->out);
     237            0 :     fflush(s->out);
     238              : }
     239              : 
     240            0 : void screen_cursor_visible(Screen *s, int visible) {
     241            0 :     if (!s || !s->out) return;
     242            0 :     const char *seq = visible ? "\033[?25h" : "\033[?25l";
     243            0 :     fwrite(seq, 1, strlen(seq), s->out);
     244            0 :     fflush(s->out);
     245              : }
        

Generated by: LCOV version 2.0-1