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 24675 : PtyScreen *pty_screen_new(int cols, int rows) {
23 24675 : PtyScreen *scr = calloc(1, sizeof(*scr));
24 24675 : if (!scr) return NULL;
25 24675 : scr->cols = cols;
26 24675 : scr->rows = rows;
27 24675 : scr->cells = calloc((size_t)(cols * rows), sizeof(PtyCell));
28 24675 : if (!scr->cells) { free(scr); return NULL; }
29 : /* Initialise cells with spaces */
30 74629675 : for (int i = 0; i < cols * rows; i++) {
31 74605000 : scr->cells[i].ch[0] = ' ';
32 74605000 : scr->cells[i].ch[1] = '\0';
33 : }
34 24675 : return scr;
35 : }
36 :
37 24294 : void pty_screen_free(PtyScreen *scr) {
38 24294 : if (!scr) return;
39 24294 : free(scr->cells);
40 24294 : free(scr);
41 : }
42 :
43 264706038 : static PtyCell *cell_at(PtyScreen *scr, int row, int col) {
44 264706038 : if (row < 0 || row >= scr->rows || col < 0 || col >= scr->cols)
45 0 : return NULL;
46 264706038 : return &scr->cells[row * scr->cols + col];
47 : }
48 :
49 : /* ── Scroll up by one line ───────────────────────────────────────────── */
50 :
51 2190 : static void scroll_up(PtyScreen *scr) {
52 2190 : memmove(&scr->cells[0],
53 2190 : &scr->cells[scr->cols],
54 2190 : (size_t)((scr->rows - 1) * scr->cols) * sizeof(PtyCell));
55 : /* Clear last row */
56 221190 : for (int c = 0; c < scr->cols; c++) {
57 219000 : PtyCell *cl = cell_at(scr, scr->rows - 1, c);
58 219000 : cl->ch[0] = ' '; cl->ch[1] = '\0';
59 219000 : cl->attr = PTY_ATTR_NONE;
60 : }
61 2190 : }
62 :
63 : /* ── VT100 parser state machine ──────────────────────────────────────── */
64 :
65 : /** @brief Parses and applies a CSI sequence ending with the given final byte. */
66 4711936 : static void apply_csi(PtyScreen *scr, const char *params, int param_len, char final) {
67 : /* Parse semicolon-separated integer parameters */
68 4711936 : int args[16] = {0};
69 4711936 : int argc = 0;
70 4711936 : const char *p = params;
71 4711936 : const char *end = params + param_len;
72 9654138 : while (p < end && argc < 16) {
73 4942202 : int val = 0;
74 10246737 : while (p < end && *p >= '0' && *p <= '9') {
75 5304535 : val = val * 10 + (*p - '0');
76 5304535 : p++;
77 : }
78 4942202 : args[argc++] = val;
79 4942202 : if (p < end && *p == ';') p++;
80 : }
81 :
82 : /* Any CSI sequence cancels pending wrap */
83 4711936 : scr->pending_wrap = 0;
84 :
85 4711936 : switch (final) {
86 934721 : case 'H': case 'f': /* CUP — cursor position */
87 934721 : scr->cur_row = (argc >= 1 && args[0] > 0) ? args[0] - 1 : 0;
88 934721 : scr->cur_col = (argc >= 2 && args[1] > 0) ? args[1] - 1 : 0;
89 934721 : if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1;
90 934721 : if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
91 934721 : 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 164308 : case 'J': /* ED — erase display */
118 164308 : if (args[0] == 2) {
119 : /* Erase entire display */
120 334183792 : for (int i = 0; i < scr->cols * scr->rows; i++) {
121 334055120 : scr->cells[i].ch[0] = ' ';
122 334055120 : scr->cells[i].ch[1] = '\0';
123 334055120 : scr->cells[i].attr = PTY_ATTR_NONE;
124 : }
125 : }
126 164308 : break;
127 :
128 476732 : case 'K': /* EL — erase in line */
129 476732 : { int mode = (argc >= 1) ? args[0] : 0;
130 476732 : int start = (mode == 1 || mode == 2) ? 0 : scr->cur_col;
131 476732 : int stop = (mode == 0 || mode == 2) ? scr->cols : scr->cur_col + 1;
132 43870913 : for (int c = start; c < stop; c++) {
133 43394181 : PtyCell *cl = cell_at(scr, scr->cur_row, c);
134 43394181 : if (cl) { cl->ch[0] = ' '; cl->ch[1] = '\0'; cl->attr = PTY_ATTR_NONE; }
135 : } }
136 476732 : break;
137 :
138 3129356 : case 'm': /* SGR — select graphic rendition */
139 6258566 : for (int i = 0; i < argc; i++) {
140 3129210 : if (args[i] == 0) {
141 1531891 : scr->cur_attr = PTY_ATTR_NONE;
142 1531891 : scr->cur_fg = PTY_FG_DEFAULT;
143 1597319 : } else if (args[i] == 1) scr->cur_attr |= PTY_ATTR_BOLD;
144 1527408 : else if (args[i] == 2) scr->cur_attr |= PTY_ATTR_DIM;
145 608418 : else if (args[i] == 7) scr->cur_attr |= PTY_ATTR_REVERSE;
146 200392 : else if (args[i] == 9) scr->cur_attr |= PTY_ATTR_STRIKE;
147 190206 : else if (args[i] == 22) scr->cur_attr &= ~(PTY_ATTR_BOLD | PTY_ATTR_DIM);
148 169679 : else if (args[i] == 27) scr->cur_attr &= ~PTY_ATTR_REVERSE;
149 169679 : else if (args[i] == 29) scr->cur_attr &= ~PTY_ATTR_STRIKE;
150 : /* Standard fg colours: 30–37 */
151 159493 : else if (args[i] >= 30 && args[i] <= 37) scr->cur_fg = (uint8_t)args[i];
152 : /* Default fg colour */
153 39285 : else if (args[i] == 39) scr->cur_fg = PTY_FG_DEFAULT;
154 : /* 24-bit / 256-colour fg/bg: skip sub-args */
155 33710 : else if (args[i] == 38 || args[i] == 48) {
156 4609 : 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 3129356 : break;
162 :
163 6819 : default:
164 : /* Unhandled CSI sequence — ignore */
165 6819 : break;
166 : }
167 4711936 : }
168 :
169 1560331 : void pty_screen_feed(PtyScreen *scr, const char *data, size_t len) {
170 1560331 : const char *end = data + len;
171 1560331 : const char *p = data;
172 :
173 231276648 : while (p < end) {
174 229716441 : unsigned char ch = (unsigned char)*p;
175 :
176 229716441 : if (ch == '\033') {
177 : /* ESC sequence */
178 9423907 : if (p + 1 < end && p[1] == '[') {
179 : /* CSI sequence: collect parameters and final byte */
180 4711968 : const char *csi_start = p + 2;
181 4711968 : const char *q = csi_start;
182 10825901 : while (q < end && ((*q >= '0' && *q <= '9') || *q == ';' || *q == '?'))
183 6113933 : q++;
184 4711968 : if (q < end) {
185 4711936 : apply_csi(scr, csi_start, (int)(q - csi_start), *q);
186 4711936 : p = q + 1;
187 : } else {
188 32 : break; /* Incomplete sequence — wait for more data */
189 : }
190 3 : } 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 3 : p++; /* Skip lone ESC */
201 : }
202 4711939 : continue;
203 : }
204 :
205 : /* Control characters */
206 225004470 : if (ch == '\n') {
207 1924385 : scr->pending_wrap = 0;
208 1924385 : scr->cur_row++;
209 1924385 : if (scr->cur_row >= scr->rows) {
210 2190 : scr->cur_row = scr->rows - 1;
211 2190 : scroll_up(scr);
212 : }
213 1924385 : p++;
214 1924385 : continue;
215 : }
216 223080085 : if (ch == '\r') {
217 1986499 : scr->pending_wrap = 0;
218 1986499 : scr->cur_col = 0;
219 1986499 : p++;
220 1986499 : continue;
221 : }
222 221093586 : 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 221093586 : if (ch == '\t') {
229 729 : scr->pending_wrap = 0;
230 729 : scr->cur_col = (scr->cur_col + 8) & ~7;
231 729 : if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
232 729 : p++;
233 729 : continue;
234 : }
235 221092857 : 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 221092857 : if (scr->pending_wrap) {
244 6771 : scr->pending_wrap = 0;
245 6771 : scr->cur_col = 0;
246 6771 : scr->cur_row++;
247 6771 : if (scr->cur_row >= scr->rows) {
248 0 : scr->cur_row = scr->rows - 1;
249 0 : scroll_up(scr);
250 : }
251 : }
252 :
253 221092857 : PtyCell *cl = cell_at(scr, scr->cur_row, scr->cur_col);
254 221092857 : if (!cl) { p++; continue; }
255 :
256 : /* Determine UTF-8 byte count */
257 221092857 : int bytes = 1;
258 221092857 : if (ch >= 0xC0 && ch < 0xE0) bytes = 2;
259 220997943 : else if (ch >= 0xE0 && ch < 0xF0) bytes = 3;
260 190476334 : else if (ch >= 0xF0 && ch < 0xF8) bytes = 4;
261 :
262 221092857 : if (p + bytes > end) break; /* Incomplete — wait for more */
263 :
264 221092765 : int copy = bytes < (int)sizeof(cl->ch) ? bytes : (int)sizeof(cl->ch) - 1;
265 221092765 : memcpy(cl->ch, p, (size_t)copy);
266 221092765 : cl->ch[copy] = '\0';
267 221092765 : cl->attr = scr->cur_attr;
268 221092765 : cl->fg = scr->cur_fg;
269 :
270 221092765 : scr->cur_col++;
271 221092765 : if (scr->cur_col >= scr->cols) {
272 : /* Pending wrap: stay at last column until next char */
273 1690524 : scr->cur_col = scr->cols - 1;
274 1690524 : scr->pending_wrap = 1;
275 : }
276 :
277 221092765 : p += bytes;
278 : }
279 : }
280 1560331 : }
|