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 21058 : PtyScreen *pty_screen_new(int cols, int rows) {
23 21058 : PtyScreen *scr = calloc(1, sizeof(*scr));
24 21058 : if (!scr) return NULL;
25 21058 : scr->cols = cols;
26 21058 : scr->rows = rows;
27 21058 : scr->cells = calloc((size_t)(cols * rows), sizeof(PtyCell));
28 21058 : if (!scr->cells) { free(scr); return NULL; }
29 : /* Initialise cells with spaces */
30 59052978 : for (int i = 0; i < cols * rows; i++) {
31 59031920 : scr->cells[i].ch[0] = ' ';
32 59031920 : scr->cells[i].ch[1] = '\0';
33 : }
34 21058 : return scr;
35 : }
36 :
37 20836 : void pty_screen_free(PtyScreen *scr) {
38 20836 : if (!scr) return;
39 20836 : free(scr->cells);
40 20836 : free(scr);
41 : }
42 :
43 154358712 : static PtyCell *cell_at(PtyScreen *scr, int row, int col) {
44 154358712 : if (row < 0 || row >= scr->rows || col < 0 || col >= scr->cols)
45 0 : return NULL;
46 154358712 : return &scr->cells[row * scr->cols + col];
47 : }
48 :
49 : /* ── Scroll up by one line ───────────────────────────────────────────── */
50 :
51 3125 : static void scroll_up(PtyScreen *scr) {
52 3125 : memmove(&scr->cells[0],
53 3125 : &scr->cells[scr->cols],
54 3125 : (size_t)((scr->rows - 1) * scr->cols) * sizeof(PtyCell));
55 : /* Clear last row */
56 327805 : for (int c = 0; c < scr->cols; c++) {
57 324680 : PtyCell *cl = cell_at(scr, scr->rows - 1, c);
58 324680 : cl->ch[0] = ' '; cl->ch[1] = '\0';
59 324680 : cl->attr = PTY_ATTR_NONE;
60 : }
61 3125 : }
62 :
63 : /* ── VT100 parser state machine ──────────────────────────────────────── */
64 :
65 : /** @brief Parses and applies a CSI sequence ending with the given final byte. */
66 2318297 : static void apply_csi(PtyScreen *scr, const char *params, int param_len, char final) {
67 : /* Parse semicolon-separated integer parameters */
68 2318297 : int args[16] = {0};
69 2318297 : int argc = 0;
70 2318297 : const char *p = params;
71 2318297 : const char *end = params + param_len;
72 4656421 : while (p < end && argc < 16) {
73 2338124 : int val = 0;
74 4975587 : while (p < end && *p >= '0' && *p <= '9') {
75 2637463 : val = val * 10 + (*p - '0');
76 2637463 : p++;
77 : }
78 2338124 : args[argc++] = val;
79 2338124 : if (p < end && *p == ';') p++;
80 : }
81 :
82 : /* Any CSI sequence cancels pending wrap */
83 2318297 : scr->pending_wrap = 0;
84 :
85 2318297 : switch (final) {
86 240055 : case 'H': case 'f': /* CUP — cursor position */
87 240055 : scr->cur_row = (argc >= 1 && args[0] > 0) ? args[0] - 1 : 0;
88 240055 : scr->cur_col = (argc >= 2 && args[1] > 0) ? args[1] - 1 : 0;
89 240055 : if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1;
90 240055 : if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
91 240055 : 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 94308 : case 'J': /* ED — erase display */
118 94308 : if (args[0] == 2) {
119 : /* Erase entire display */
120 223750856 : for (int i = 0; i < scr->cols * scr->rows; i++) {
121 223657000 : scr->cells[i].ch[0] = ' ';
122 223657000 : scr->cells[i].ch[1] = '\0';
123 223657000 : scr->cells[i].attr = PTY_ATTR_NONE;
124 : }
125 : }
126 94308 : break;
127 :
128 135482 : case 'K': /* EL — erase in line */
129 135482 : { int mode = (argc >= 1) ? args[0] : 0;
130 135482 : int start = (mode == 1 || mode == 2) ? 0 : scr->cur_col;
131 135482 : int stop = (mode == 0 || mode == 2) ? scr->cols : scr->cur_col + 1;
132 5931168 : for (int c = start; c < stop; c++) {
133 5795686 : PtyCell *cl = cell_at(scr, scr->cur_row, c);
134 5795686 : if (cl) { cl->ch[0] = ' '; cl->ch[1] = '\0'; cl->attr = PTY_ATTR_NONE; }
135 : } }
136 135482 : break;
137 :
138 1843808 : case 'm': /* SGR — select graphic rendition */
139 3687470 : for (int i = 0; i < argc; i++) {
140 1843662 : if (args[i] == 0) {
141 873964 : scr->cur_attr = PTY_ATTR_NONE;
142 873964 : scr->cur_fg = PTY_FG_DEFAULT;
143 969698 : } else if (args[i] == 1) scr->cur_attr |= PTY_ATTR_BOLD;
144 940101 : else if (args[i] == 2) scr->cur_attr |= PTY_ATTR_DIM;
145 525319 : else if (args[i] == 7) scr->cur_attr |= PTY_ATTR_REVERSE;
146 237735 : else if (args[i] == 9) scr->cur_attr |= PTY_ATTR_STRIKE;
147 228835 : else if (args[i] == 22) scr->cur_attr &= ~(PTY_ATTR_BOLD | PTY_ATTR_DIM);
148 209618 : else if (args[i] == 27) scr->cur_attr &= ~PTY_ATTR_REVERSE;
149 209618 : else if (args[i] == 29) scr->cur_attr &= ~PTY_ATTR_STRIKE;
150 : /* Standard fg colours: 30–37 */
151 200718 : else if (args[i] >= 30 && args[i] <= 37) scr->cur_fg = (uint8_t)args[i];
152 : /* Default fg colour */
153 48634 : else if (args[i] == 39) scr->cur_fg = PTY_FG_DEFAULT;
154 : /* 24-bit / 256-colour fg/bg: skip sub-args */
155 34489 : else if (args[i] == 38 || args[i] == 48) {
156 5066 : 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 1843808 : break;
162 :
163 4644 : default:
164 : /* Unhandled CSI sequence — ignore */
165 4644 : break;
166 : }
167 2318297 : }
168 :
169 790089 : void pty_screen_feed(PtyScreen *scr, const char *data, size_t len) {
170 790089 : const char *end = data + len;
171 790089 : const char *p = data;
172 :
173 154643759 : while (p < end) {
174 153853670 : unsigned char ch = (unsigned char)*p;
175 :
176 153853670 : if (ch == '\033') {
177 : /* ESC sequence */
178 4636594 : if (p + 1 < end && p[1] == '[') {
179 : /* CSI sequence: collect parameters and final byte */
180 2318297 : const char *csi_start = p + 2;
181 2318297 : const char *q = csi_start;
182 5135732 : while (q < end && ((*q >= '0' && *q <= '9') || *q == ';' || *q == '?'))
183 2817435 : q++;
184 2318297 : if (q < end) {
185 2318297 : apply_csi(scr, csi_start, (int)(q - csi_start), *q);
186 2318297 : p = q + 1;
187 : } else {
188 0 : break; /* Incomplete sequence — wait for more data */
189 : }
190 0 : } 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 0 : p++; /* Skip lone ESC */
201 : }
202 2318297 : continue;
203 : }
204 :
205 : /* Control characters */
206 151535373 : if (ch == '\n') {
207 1619536 : scr->pending_wrap = 0;
208 1619536 : scr->cur_row++;
209 1619536 : if (scr->cur_row >= scr->rows) {
210 3125 : scr->cur_row = scr->rows - 1;
211 3125 : scroll_up(scr);
212 : }
213 1619536 : p++;
214 1619536 : continue;
215 : }
216 149915837 : if (ch == '\r') {
217 1672634 : scr->pending_wrap = 0;
218 1672634 : scr->cur_col = 0;
219 1672634 : p++;
220 1672634 : continue;
221 : }
222 148243203 : 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 148243203 : if (ch == '\t') {
229 4857 : scr->pending_wrap = 0;
230 4857 : scr->cur_col = (scr->cur_col + 8) & ~7;
231 4857 : if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
232 4857 : p++;
233 4857 : continue;
234 : }
235 148238346 : 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 148238346 : if (scr->pending_wrap) {
244 611 : scr->pending_wrap = 0;
245 611 : scr->cur_col = 0;
246 611 : scr->cur_row++;
247 611 : if (scr->cur_row >= scr->rows) {
248 0 : scr->cur_row = scr->rows - 1;
249 0 : scroll_up(scr);
250 : }
251 : }
252 :
253 148238346 : PtyCell *cl = cell_at(scr, scr->cur_row, scr->cur_col);
254 148238346 : if (!cl) { p++; continue; }
255 :
256 : /* Determine UTF-8 byte count */
257 148238346 : int bytes = 1;
258 148238346 : if (ch >= 0xC0 && ch < 0xE0) bytes = 2;
259 148163663 : else if (ch >= 0xE0 && ch < 0xF0) bytes = 3;
260 126892591 : else if (ch >= 0xF0 && ch < 0xF8) bytes = 4;
261 :
262 148238346 : if (p + bytes > end) break; /* Incomplete — wait for more */
263 :
264 148238346 : int copy = bytes < (int)sizeof(cl->ch) ? bytes : (int)sizeof(cl->ch) - 1;
265 148238346 : memcpy(cl->ch, p, (size_t)copy);
266 148238346 : cl->ch[copy] = '\0';
267 148238346 : cl->attr = scr->cur_attr;
268 148238346 : cl->fg = scr->cur_fg;
269 :
270 148238346 : scr->cur_col++;
271 148238346 : if (scr->cur_col >= scr->cols) {
272 : /* Pending wrap: stay at last column until next char */
273 1241901 : scr->cur_col = scr->cols - 1;
274 1241901 : scr->pending_wrap = 1;
275 : }
276 :
277 148238346 : p += bytes;
278 : }
279 : }
280 790089 : }
|