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 : * - 24-bit colour: CSI 38;2;R;G;Bm (fg), CSI 48;2;R;G;Bm (bg) — parsed, not stored
10 : * - Basic colours: CSI 3Xm, CSI 4Xm — parsed, not stored
11 : * - Newline, carriage return, backspace, tab
12 : */
13 :
14 : #include "pty_internal.h"
15 : #include <stdio.h>
16 : #include <string.h>
17 : #include <stdlib.h>
18 :
19 : /* ── Screen buffer management ────────────────────────────────────────── */
20 :
21 160 : PtyScreen *pty_screen_new(int cols, int rows) {
22 160 : PtyScreen *scr = calloc(1, sizeof(*scr));
23 160 : if (!scr) return NULL;
24 160 : scr->cols = cols;
25 160 : scr->rows = rows;
26 160 : scr->cells = calloc((size_t)(cols * rows), sizeof(PtyCell));
27 160 : if (!scr->cells) { free(scr); return NULL; }
28 : /* Initialise cells with spaces */
29 313600 : for (int i = 0; i < cols * rows; i++) {
30 313440 : scr->cells[i].ch[0] = ' ';
31 313440 : scr->cells[i].ch[1] = '\0';
32 : }
33 160 : return scr;
34 : }
35 :
36 124 : void pty_screen_free(PtyScreen *scr) {
37 124 : if (!scr) return;
38 124 : free(scr->cells);
39 124 : free(scr);
40 : }
41 :
42 34036 : static PtyCell *cell_at(PtyScreen *scr, int row, int col) {
43 34036 : if (row < 0 || row >= scr->rows || col < 0 || col >= scr->cols)
44 0 : return NULL;
45 34036 : return &scr->cells[row * scr->cols + col];
46 : }
47 :
48 : /* ── Scroll up by one line ───────────────────────────────────────────── */
49 :
50 0 : static void scroll_up(PtyScreen *scr) {
51 0 : memmove(&scr->cells[0],
52 0 : &scr->cells[scr->cols],
53 0 : (size_t)((scr->rows - 1) * scr->cols) * sizeof(PtyCell));
54 : /* Clear last row */
55 0 : for (int c = 0; c < scr->cols; c++) {
56 0 : PtyCell *cl = cell_at(scr, scr->rows - 1, c);
57 0 : cl->ch[0] = ' '; cl->ch[1] = '\0';
58 0 : cl->attr = PTY_ATTR_NONE;
59 : }
60 0 : }
61 :
62 : /* ── VT100 parser state machine ──────────────────────────────────────── */
63 :
64 : /** @brief Parses and applies a CSI sequence ending with the given final byte. */
65 716 : static void apply_csi(PtyScreen *scr, const char *params, int param_len, char final) {
66 : /* Parse semicolon-separated integer parameters */
67 716 : int args[16] = {0};
68 716 : int argc = 0;
69 716 : const char *p = params;
70 716 : const char *end = params + param_len;
71 1074 : while (p < end && argc < 16) {
72 358 : int val = 0;
73 769 : while (p < end && *p >= '0' && *p <= '9') {
74 411 : val = val * 10 + (*p - '0');
75 411 : p++;
76 : }
77 358 : args[argc++] = val;
78 358 : if (p < end && *p == ';') p++;
79 : }
80 :
81 : /* Any CSI sequence cancels pending wrap */
82 716 : scr->pending_wrap = 0;
83 :
84 716 : switch (final) {
85 0 : case 'H': case 'f': /* CUP — cursor position */
86 0 : scr->cur_row = (argc >= 1 && args[0] > 0) ? args[0] - 1 : 0;
87 0 : scr->cur_col = (argc >= 2 && args[1] > 0) ? args[1] - 1 : 0;
88 0 : if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1;
89 0 : if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
90 0 : break;
91 :
92 0 : case 'A': /* CUU — cursor up */
93 0 : { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
94 0 : scr->cur_row -= n;
95 0 : if (scr->cur_row < 0) scr->cur_row = 0; }
96 0 : break;
97 :
98 0 : case 'B': /* CUD — cursor down */
99 0 : { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
100 0 : scr->cur_row += n;
101 0 : if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1; }
102 0 : break;
103 :
104 358 : case 'C': /* CUF — cursor forward */
105 358 : { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
106 358 : scr->cur_col += n;
107 358 : if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1; }
108 358 : break;
109 :
110 0 : case 'D': /* CUB — cursor backward */
111 0 : { int n = (argc >= 1 && args[0] > 0) ? args[0] : 1;
112 0 : scr->cur_col -= n;
113 0 : if (scr->cur_col < 0) scr->cur_col = 0; }
114 0 : break;
115 :
116 0 : case 'J': /* ED — erase display */
117 0 : if (args[0] == 2) {
118 : /* Erase entire display */
119 0 : for (int i = 0; i < scr->cols * scr->rows; i++) {
120 0 : scr->cells[i].ch[0] = ' ';
121 0 : scr->cells[i].ch[1] = '\0';
122 0 : scr->cells[i].attr = PTY_ATTR_NONE;
123 : }
124 : }
125 0 : break;
126 :
127 358 : case 'K': /* EL — erase in line */
128 358 : { int mode = (argc >= 1) ? args[0] : 0;
129 358 : int start = (mode == 1 || mode == 2) ? 0 : scr->cur_col;
130 358 : int stop = (mode == 0 || mode == 2) ? scr->cols : scr->cur_col + 1;
131 25976 : for (int c = start; c < stop; c++) {
132 25618 : PtyCell *cl = cell_at(scr, scr->cur_row, c);
133 25618 : if (cl) { cl->ch[0] = ' '; cl->ch[1] = '\0'; cl->attr = PTY_ATTR_NONE; }
134 : } }
135 358 : break;
136 :
137 0 : case 'm': /* SGR — select graphic rendition */
138 0 : for (int i = 0; i < argc; i++) {
139 0 : if (args[i] == 0) scr->cur_attr = PTY_ATTR_NONE;
140 0 : else if (args[i] == 1) scr->cur_attr |= PTY_ATTR_BOLD;
141 0 : else if (args[i] == 2) scr->cur_attr |= PTY_ATTR_DIM;
142 0 : else if (args[i] == 7) scr->cur_attr |= PTY_ATTR_REVERSE;
143 : /* Skip colour args: 30-37,38,39, 40-47,48,49, 90-97, 100-107 */
144 0 : else if (args[i] == 38 || args[i] == 48) {
145 : /* 38;2;R;G;B or 38;5;N — skip remaining args */
146 0 : if (i + 1 < argc && args[i + 1] == 2) i += 4; /* skip 2,R,G,B */
147 0 : else if (i + 1 < argc && args[i + 1] == 5) i += 2; /* skip 5,N */
148 : }
149 : }
150 0 : break;
151 :
152 0 : default:
153 : /* Unhandled CSI sequence — ignore */
154 0 : break;
155 : }
156 716 : }
157 :
158 487 : void pty_screen_feed(PtyScreen *scr, const char *data, size_t len) {
159 487 : const char *end = data + len;
160 487 : const char *p = data;
161 :
162 11343 : while (p < end) {
163 10856 : unsigned char ch = (unsigned char)*p;
164 :
165 10856 : if (ch == '\033') {
166 : /* ESC sequence */
167 1432 : if (p + 1 < end && p[1] == '[') {
168 : /* CSI sequence: collect parameters and final byte */
169 716 : const char *csi_start = p + 2;
170 716 : const char *q = csi_start;
171 1127 : while (q < end && ((*q >= '0' && *q <= '9') || *q == ';' || *q == '?'))
172 411 : q++;
173 716 : if (q < end) {
174 716 : apply_csi(scr, csi_start, (int)(q - csi_start), *q);
175 716 : p = q + 1;
176 : } else {
177 0 : break; /* Incomplete sequence — wait for more data */
178 : }
179 0 : } else if (p + 1 < end && p[1] == ']') {
180 : /* OSC sequence: skip until ST (\033\\) or BEL (\007) */
181 0 : const char *q = p + 2;
182 0 : while (q < end) {
183 0 : if (*q == '\007') { q++; break; }
184 0 : if (*q == '\033' && q + 1 < end && q[1] == '\\') { q += 2; break; }
185 0 : q++;
186 : }
187 0 : p = q;
188 : } else {
189 0 : p++; /* Skip lone ESC */
190 : }
191 716 : continue;
192 : }
193 :
194 : /* Control characters */
195 10140 : if (ch == '\n') {
196 451 : scr->pending_wrap = 0;
197 451 : scr->cur_row++;
198 451 : if (scr->cur_row >= scr->rows) {
199 0 : scr->cur_row = scr->rows - 1;
200 0 : scroll_up(scr);
201 : }
202 451 : p++;
203 451 : continue;
204 : }
205 9689 : if (ch == '\r') {
206 1271 : scr->pending_wrap = 0;
207 1271 : scr->cur_col = 0;
208 1271 : p++;
209 1271 : continue;
210 : }
211 8418 : if (ch == '\b') {
212 0 : scr->pending_wrap = 0;
213 0 : if (scr->cur_col > 0) scr->cur_col--;
214 0 : p++;
215 0 : continue;
216 : }
217 8418 : if (ch == '\t') {
218 0 : scr->pending_wrap = 0;
219 0 : scr->cur_col = (scr->cur_col + 8) & ~7;
220 0 : if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
221 0 : p++;
222 0 : continue;
223 : }
224 8418 : if (ch < 0x20) {
225 0 : p++; /* Skip other control chars */
226 0 : continue;
227 : }
228 :
229 : /* Printable character (ASCII or UTF-8 lead byte) */
230 : {
231 : /* Pending wrap: deferred line advance, matching real terminal behaviour */
232 8418 : if (scr->pending_wrap) {
233 10 : scr->pending_wrap = 0;
234 10 : scr->cur_col = 0;
235 10 : scr->cur_row++;
236 10 : if (scr->cur_row >= scr->rows) {
237 0 : scr->cur_row = scr->rows - 1;
238 0 : scroll_up(scr);
239 : }
240 : }
241 :
242 8418 : PtyCell *cl = cell_at(scr, scr->cur_row, scr->cur_col);
243 8418 : if (!cl) { p++; continue; }
244 :
245 : /* Determine UTF-8 byte count */
246 8418 : int bytes = 1;
247 8418 : if (ch >= 0xC0 && ch < 0xE0) bytes = 2;
248 8418 : else if (ch >= 0xE0 && ch < 0xF0) bytes = 3;
249 8418 : else if (ch >= 0xF0 && ch < 0xF8) bytes = 4;
250 :
251 8418 : if (p + bytes > end) break; /* Incomplete — wait for more */
252 :
253 8418 : int copy = bytes < (int)sizeof(cl->ch) ? bytes : (int)sizeof(cl->ch) - 1;
254 8418 : memcpy(cl->ch, p, (size_t)copy);
255 8418 : cl->ch[copy] = '\0';
256 8418 : cl->attr = scr->cur_attr;
257 :
258 8418 : scr->cur_col++;
259 8418 : if (scr->cur_col >= scr->cols) {
260 : /* Pending wrap: stay at last column until next char */
261 10 : scr->cur_col = scr->cols - 1;
262 10 : scr->pending_wrap = 1;
263 : }
264 :
265 8418 : p += bytes;
266 : }
267 : }
268 487 : }
|