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 162 : PtyScreen *pty_screen_new(int cols, int rows) {
22 162 : PtyScreen *scr = calloc(1, sizeof(*scr));
23 162 : if (!scr) return NULL;
24 162 : scr->cols = cols;
25 162 : scr->rows = rows;
26 162 : scr->cells = calloc((size_t)(cols * rows), sizeof(PtyCell));
27 162 : if (!scr->cells) { free(scr); return NULL; }
28 : /* Initialise cells with spaces */
29 316722 : for (int i = 0; i < cols * rows; i++) {
30 316560 : scr->cells[i].ch[0] = ' ';
31 316560 : scr->cells[i].ch[1] = '\0';
32 : }
33 162 : return scr;
34 : }
35 :
36 131 : void pty_screen_free(PtyScreen *scr) {
37 131 : if (!scr) return;
38 131 : free(scr->cells);
39 131 : free(scr);
40 : }
41 :
42 47059 : static PtyCell *cell_at(PtyScreen *scr, int row, int col) {
43 47059 : if (row < 0 || row >= scr->rows || col < 0 || col >= scr->cols)
44 0 : return NULL;
45 47059 : return &scr->cells[row * scr->cols + col];
46 : }
47 :
48 : /* ── Scroll up by one line ───────────────────────────────────────────── */
49 :
50 5 : static void scroll_up(PtyScreen *scr) {
51 5 : memmove(&scr->cells[0],
52 5 : &scr->cells[scr->cols],
53 5 : (size_t)((scr->rows - 1) * scr->cols) * sizeof(PtyCell));
54 : /* Clear last row */
55 405 : for (int c = 0; c < scr->cols; c++) {
56 400 : PtyCell *cl = cell_at(scr, scr->rows - 1, c);
57 400 : cl->ch[0] = ' '; cl->ch[1] = '\0';
58 400 : cl->attr = PTY_ATTR_NONE;
59 : }
60 5 : }
61 :
62 : /* ── VT100 parser state machine ──────────────────────────────────────── */
63 :
64 : /** @brief Parses and applies a CSI sequence ending with the given final byte. */
65 946 : static void apply_csi(PtyScreen *scr, const char *params, int param_len, char final) {
66 : /* Parse semicolon-separated integer parameters */
67 946 : int args[16] = {0};
68 946 : int argc = 0;
69 946 : const char *p = params;
70 946 : const char *end = params + param_len;
71 1904 : while (p < end && argc < 16) {
72 958 : int val = 0;
73 1883 : while (p < end && *p >= '0' && *p <= '9') {
74 925 : val = val * 10 + (*p - '0');
75 925 : p++;
76 : }
77 958 : args[argc++] = val;
78 958 : if (p < end && *p == ';') p++;
79 : }
80 :
81 : /* Any CSI sequence cancels pending wrap */
82 946 : scr->pending_wrap = 0;
83 :
84 946 : switch (final) {
85 169 : case 'H': case 'f': /* CUP — cursor position */
86 169 : scr->cur_row = (argc >= 1 && args[0] > 0) ? args[0] - 1 : 0;
87 169 : scr->cur_col = (argc >= 2 && args[1] > 0) ? args[1] - 1 : 0;
88 169 : if (scr->cur_row >= scr->rows) scr->cur_row = scr->rows - 1;
89 169 : if (scr->cur_col >= scr->cols) scr->cur_col = scr->cols - 1;
90 169 : 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 49 : case 'm': /* SGR — select graphic rendition */
138 119 : for (int i = 0; i < argc; i++) {
139 70 : if (args[i] == 0) scr->cur_attr = PTY_ATTR_NONE;
140 21 : else if (args[i] == 1) scr->cur_attr |= PTY_ATTR_BOLD;
141 21 : else if (args[i] == 2) scr->cur_attr |= PTY_ATTR_DIM;
142 7 : 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 49 : break;
151 :
152 12 : default:
153 : /* Unhandled CSI sequence — ignore */
154 12 : break;
155 : }
156 946 : }
157 :
158 515 : void pty_screen_feed(PtyScreen *scr, const char *data, size_t len) {
159 515 : const char *end = data + len;
160 515 : const char *p = data;
161 :
162 24277 : while (p < end) {
163 23762 : unsigned char ch = (unsigned char)*p;
164 :
165 23762 : if (ch == '\033') {
166 : /* ESC sequence */
167 1892 : if (p + 1 < end && p[1] == '[') {
168 : /* CSI sequence: collect parameters and final byte */
169 946 : const char *csi_start = p + 2;
170 946 : const char *q = csi_start;
171 2097 : while (q < end && ((*q >= '0' && *q <= '9') || *q == ';' || *q == '?'))
172 1151 : q++;
173 946 : if (q < end) {
174 946 : apply_csi(scr, csi_start, (int)(q - csi_start), *q);
175 946 : 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 946 : continue;
192 : }
193 :
194 : /* Control characters */
195 22816 : if (ch == '\n') {
196 474 : scr->pending_wrap = 0;
197 474 : scr->cur_row++;
198 474 : if (scr->cur_row >= scr->rows) {
199 5 : scr->cur_row = scr->rows - 1;
200 5 : scroll_up(scr);
201 : }
202 474 : p++;
203 474 : continue;
204 : }
205 22342 : if (ch == '\r') {
206 1301 : scr->pending_wrap = 0;
207 1301 : scr->cur_col = 0;
208 1301 : p++;
209 1301 : continue;
210 : }
211 21041 : 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 21041 : 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 21041 : 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 21041 : if (scr->pending_wrap) {
233 4 : scr->pending_wrap = 0;
234 4 : scr->cur_col = 0;
235 4 : scr->cur_row++;
236 4 : if (scr->cur_row >= scr->rows) {
237 0 : scr->cur_row = scr->rows - 1;
238 0 : scroll_up(scr);
239 : }
240 : }
241 :
242 21041 : PtyCell *cl = cell_at(scr, scr->cur_row, scr->cur_col);
243 21041 : if (!cl) { p++; continue; }
244 :
245 : /* Determine UTF-8 byte count */
246 21041 : int bytes = 1;
247 21041 : if (ch >= 0xC0 && ch < 0xE0) bytes = 2;
248 21041 : else if (ch >= 0xE0 && ch < 0xF0) bytes = 3;
249 21039 : else if (ch >= 0xF0 && ch < 0xF8) bytes = 4;
250 :
251 21041 : if (p + bytes > end) break; /* Incomplete — wait for more */
252 :
253 21041 : int copy = bytes < (int)sizeof(cl->ch) ? bytes : (int)sizeof(cl->ch) - 1;
254 21041 : memcpy(cl->ch, p, (size_t)copy);
255 21041 : cl->ch[copy] = '\0';
256 21041 : cl->attr = scr->cur_attr;
257 :
258 21041 : scr->cur_col++;
259 21041 : if (scr->cur_col >= scr->cols) {
260 : /* Pending wrap: stay at last column until next char */
261 168 : scr->cur_col = scr->cols - 1;
262 168 : scr->pending_wrap = 1;
263 : }
264 :
265 21041 : p += bytes;
266 : }
267 : }
268 515 : }
|