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 : }
|