Monorepo for Aesthetic.Computer
aesthetic.computer
1#define _GNU_SOURCE
2#include "pty.h"
3#include <stdio.h>
4#include <stdlib.h>
5#include <string.h>
6#include <unistd.h>
7#include <fcntl.h>
8#include <errno.h>
9#include <signal.h>
10#include <sys/ioctl.h>
11#include <sys/stat.h>
12#include <sys/wait.h>
13#include <pty.h>
14#include <termios.h>
15
16extern void ac_log(const char *fmt, ...);
17extern int ac_log_stderr_muted;
18
19// Standard ANSI 16-color palette
20static const uint8_t ansi_colors[16][3] = {
21 { 0, 0, 0}, // 0 black
22 {170, 0, 0}, // 1 red
23 { 0, 170, 0}, // 2 green
24 {170, 85, 0}, // 3 yellow/brown
25 { 0, 0, 170}, // 4 blue
26 {170, 0, 170}, // 5 magenta
27 { 0, 170, 170}, // 6 cyan
28 {170, 170, 170}, // 7 white (light gray)
29 { 85, 85, 85}, // 8 bright black (dark gray)
30 {255, 85, 85}, // 9 bright red
31 { 85, 255, 85}, // 10 bright green
32 {255, 255, 85}, // 11 bright yellow
33 { 85, 85, 255}, // 12 bright blue
34 {255, 85, 255}, // 13 bright magenta
35 { 85, 255, 255}, // 14 bright cyan
36 {255, 255, 255}, // 15 bright white
37};
38
39void pty_color_to_rgb(int color_index, int bold, uint8_t *r, uint8_t *g, uint8_t *b) {
40 if (color_index < 0 || color_index > 15) {
41 // Default: white fg or black bg
42 *r = *g = *b = (color_index == PTY_COLOR_DEFAULT_FG) ? 170 : 0;
43 return;
44 }
45 // Bold promotes colors 0-7 to 8-15
46 int idx = color_index;
47 if (bold && idx < 8) idx += 8;
48 *r = ansi_colors[idx][0];
49 *g = ansi_colors[idx][1];
50 *b = ansi_colors[idx][2];
51}
52
53void pty_clear(ACPty *pty) {
54 for (int y = 0; y < pty->rows; y++) {
55 for (int x = 0; x < pty->cols; x++) {
56 pty->grid[y][x] = (ACPtyCell){' ', PTY_COLOR_DEFAULT_FG, PTY_COLOR_DEFAULT_BG, 0, 1};
57 }
58 }
59 pty->cursor_x = 0;
60 pty->cursor_y = 0;
61 pty->grid_dirty = 1;
62}
63
64static void scroll_up(ACPty *pty) {
65 int top = pty->scroll_top;
66 int bot = pty->scroll_bottom;
67 // Move rows up by 1 within scroll region
68 for (int y = top; y < bot; y++) {
69 memcpy(pty->grid[y], pty->grid[y + 1], sizeof(ACPtyCell) * pty->cols);
70 for (int x = 0; x < pty->cols; x++) pty->grid[y][x].dirty = 1;
71 }
72 // Clear bottom row
73 for (int x = 0; x < pty->cols; x++) {
74 pty->grid[bot][x] = (ACPtyCell){' ', PTY_COLOR_DEFAULT_FG, PTY_COLOR_DEFAULT_BG, 0, 1};
75 }
76 pty->grid_dirty = 1;
77}
78
79static void scroll_down(ACPty *pty) {
80 int top = pty->scroll_top;
81 int bot = pty->scroll_bottom;
82 for (int y = bot; y > top; y--) {
83 memcpy(pty->grid[y], pty->grid[y - 1], sizeof(ACPtyCell) * pty->cols);
84 for (int x = 0; x < pty->cols; x++) pty->grid[y][x].dirty = 1;
85 }
86 for (int x = 0; x < pty->cols; x++) {
87 pty->grid[top][x] = (ACPtyCell){' ', PTY_COLOR_DEFAULT_FG, PTY_COLOR_DEFAULT_BG, 0, 1};
88 }
89 pty->grid_dirty = 1;
90}
91
92static void put_char(ACPty *pty, uint32_t ch) {
93 if (pty->cursor_x >= pty->cols) {
94 pty->cursor_x = 0;
95 pty->cursor_y++;
96 if (pty->cursor_y > pty->scroll_bottom) {
97 pty->cursor_y = pty->scroll_bottom;
98 scroll_up(pty);
99 }
100 }
101 if (pty->cursor_y < 0) pty->cursor_y = 0;
102 if (pty->cursor_y < pty->rows && pty->cursor_x < pty->cols) {
103 ACPtyCell *c = &pty->grid[pty->cursor_y][pty->cursor_x];
104 c->ch = ch;
105 c->fg = pty->cur_fg;
106 c->bg = pty->cur_bg;
107 c->bold = pty->cur_bold;
108 c->fg_r = pty->cur_fg_r; c->fg_g = pty->cur_fg_g; c->fg_b = pty->cur_fg_b;
109 c->bg_r = pty->cur_bg_r; c->bg_g = pty->cur_bg_g; c->bg_b = pty->cur_bg_b;
110 c->dirty = 1;
111 }
112 pty->cursor_x++;
113 pty->grid_dirty = 1;
114}
115
116// Parse CSI parameters: "1;2;3" → array of ints
117static int parse_csi_params(const char *buf, int len, int *params, int max_params) {
118 int count = 0;
119 int val = 0;
120 int has_val = 0;
121 for (int i = 0; i < len && count < max_params; i++) {
122 if (buf[i] >= '0' && buf[i] <= '9') {
123 val = val * 10 + (buf[i] - '0');
124 has_val = 1;
125 } else if (buf[i] == ';') {
126 params[count++] = has_val ? val : 0;
127 val = 0;
128 has_val = 0;
129 }
130 }
131 if (has_val || count == 0) params[count++] = val;
132 return count;
133}
134
135static void handle_sgr(ACPty *pty, int *params, int count) {
136 for (int i = 0; i < count; i++) {
137 int p = params[i];
138 if (p == 0) {
139 pty->cur_fg = PTY_COLOR_DEFAULT_FG;
140 pty->cur_bg = PTY_COLOR_DEFAULT_BG;
141 pty->cur_bold = 0;
142 } else if (p == 1) {
143 pty->cur_bold = 1;
144 } else if (p == 22) {
145 pty->cur_bold = 0;
146 } else if (p >= 30 && p <= 37) {
147 pty->cur_fg = p - 30;
148 } else if (p == 39) {
149 pty->cur_fg = PTY_COLOR_DEFAULT_FG;
150 } else if (p >= 40 && p <= 47) {
151 pty->cur_bg = p - 40;
152 } else if (p == 49) {
153 pty->cur_bg = PTY_COLOR_DEFAULT_BG;
154 } else if (p >= 90 && p <= 97) {
155 pty->cur_fg = p - 90 + 8;
156 } else if (p >= 100 && p <= 107) {
157 pty->cur_bg = p - 100 + 8;
158 }
159 // 256-color and truecolor support
160 else if (p == 38 || p == 48) {
161 if (i + 1 < count && params[i + 1] == 5) {
162 // 256-color: \e[38;5;Nm
163 if (i + 2 < count) {
164 int c = params[i + 2];
165 if (c < 16) {
166 if (p == 38) pty->cur_fg = c;
167 else pty->cur_bg = c;
168 } else if (c < 232) {
169 // 216-color cube (16-231): map to RGB
170 int ci = c - 16;
171 int cr = ci / 36, cg = (ci / 6) % 6, cb = ci % 6;
172 uint8_t r = cr ? 55 + cr * 40 : 0;
173 uint8_t g = cg ? 55 + cg * 40 : 0;
174 uint8_t b = cb ? 55 + cb * 40 : 0;
175 if (p == 38) { pty->cur_fg = 255; pty->cur_fg_r = r; pty->cur_fg_g = g; pty->cur_fg_b = b; }
176 else { pty->cur_bg = 255; pty->cur_bg_r = r; pty->cur_bg_g = g; pty->cur_bg_b = b; }
177 } else {
178 // Grayscale (232-255): 24 shades
179 uint8_t v = 8 + (c - 232) * 10;
180 if (p == 38) { pty->cur_fg = 255; pty->cur_fg_r = pty->cur_fg_g = pty->cur_fg_b = v; }
181 else { pty->cur_bg = 255; pty->cur_bg_r = pty->cur_bg_g = pty->cur_bg_b = v; }
182 }
183 i += 2;
184 }
185 } else if (i + 1 < count && params[i + 1] == 2) {
186 // Truecolor: \e[38;2;R;G;Bm
187 if (i + 4 < count) {
188 uint8_t r = params[i + 2], g = params[i + 3], b = params[i + 4];
189 if (p == 38) { pty->cur_fg = 255; pty->cur_fg_r = r; pty->cur_fg_g = g; pty->cur_fg_b = b; }
190 else { pty->cur_bg = 255; pty->cur_bg_r = r; pty->cur_bg_g = g; pty->cur_bg_b = b; }
191 }
192 i += 4;
193 }
194 }
195 }
196}
197
198static void erase_in_line(ACPty *pty, int mode) {
199 int y = pty->cursor_y;
200 if (y < 0 || y >= pty->rows) return;
201 int start = 0, end = pty->cols;
202 if (mode == 0) start = pty->cursor_x; // cursor to end
203 else if (mode == 1) end = pty->cursor_x + 1; // start to cursor
204 // mode == 2: entire line
205 for (int x = start; x < end; x++) {
206 pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1};
207 }
208 pty->grid_dirty = 1;
209}
210
211static void erase_in_display(ACPty *pty, int mode) {
212 if (mode == 0) {
213 // Cursor to end of screen
214 erase_in_line(pty, 0);
215 for (int y = pty->cursor_y + 1; y < pty->rows; y++) {
216 for (int x = 0; x < pty->cols; x++)
217 pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1};
218 }
219 } else if (mode == 1) {
220 // Start to cursor
221 for (int y = 0; y < pty->cursor_y; y++) {
222 for (int x = 0; x < pty->cols; x++)
223 pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1};
224 }
225 erase_in_line(pty, 1);
226 } else if (mode == 2 || mode == 3) {
227 // Entire screen
228 for (int y = 0; y < pty->rows; y++) {
229 for (int x = 0; x < pty->cols; x++)
230 pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1};
231 }
232 }
233 pty->grid_dirty = 1;
234}
235
236static void handle_csi(ACPty *pty, char final) {
237 int params[16] = {0};
238 int count = parse_csi_params(pty->csi_buf, pty->csi_len, params, 16);
239
240 switch (final) {
241 case 'A': // Cursor Up
242 pty->cursor_y -= (params[0] ? params[0] : 1);
243 if (pty->cursor_y < pty->scroll_top) pty->cursor_y = pty->scroll_top;
244 break;
245 case 'B': // Cursor Down
246 pty->cursor_y += (params[0] ? params[0] : 1);
247 if (pty->cursor_y > pty->scroll_bottom) pty->cursor_y = pty->scroll_bottom;
248 break;
249 case 'C': // Cursor Forward
250 pty->cursor_x += (params[0] ? params[0] : 1);
251 if (pty->cursor_x >= pty->cols) pty->cursor_x = pty->cols - 1;
252 break;
253 case 'D': // Cursor Back
254 pty->cursor_x -= (params[0] ? params[0] : 1);
255 if (pty->cursor_x < 0) pty->cursor_x = 0;
256 break;
257 case 'E': // Cursor Next Line
258 pty->cursor_x = 0;
259 pty->cursor_y += (params[0] ? params[0] : 1);
260 if (pty->cursor_y > pty->scroll_bottom) pty->cursor_y = pty->scroll_bottom;
261 break;
262 case 'F': // Cursor Previous Line
263 pty->cursor_x = 0;
264 pty->cursor_y -= (params[0] ? params[0] : 1);
265 if (pty->cursor_y < pty->scroll_top) pty->cursor_y = pty->scroll_top;
266 break;
267 case 'G': // Cursor Horizontal Absolute
268 pty->cursor_x = (params[0] ? params[0] - 1 : 0);
269 if (pty->cursor_x >= pty->cols) pty->cursor_x = pty->cols - 1;
270 break;
271 case 'H': case 'f': // Cursor Position
272 pty->cursor_y = (count > 0 && params[0] ? params[0] - 1 : 0);
273 pty->cursor_x = (count > 1 && params[1] ? params[1] - 1 : 0);
274 if (pty->cursor_y >= pty->rows) pty->cursor_y = pty->rows - 1;
275 if (pty->cursor_x >= pty->cols) pty->cursor_x = pty->cols - 1;
276 break;
277 case 'J': // Erase in Display
278 erase_in_display(pty, params[0]);
279 break;
280 case 'K': // Erase in Line
281 erase_in_line(pty, params[0]);
282 break;
283 case 'L': { // Insert Lines
284 int n = params[0] ? params[0] : 1;
285 for (int i = 0; i < n; i++) scroll_down(pty);
286 break;
287 }
288 case 'M': { // Delete Lines
289 int n = params[0] ? params[0] : 1;
290 for (int i = 0; i < n; i++) scroll_up(pty);
291 break;
292 }
293 case 'P': { // Delete Characters
294 int n = params[0] ? params[0] : 1;
295 int y = pty->cursor_y;
296 if (y >= 0 && y < pty->rows) {
297 for (int x = pty->cursor_x; x < pty->cols - n; x++)
298 pty->grid[y][x] = pty->grid[y][x + n];
299 for (int x = pty->cols - n; x < pty->cols; x++)
300 pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1};
301 }
302 pty->grid_dirty = 1;
303 break;
304 }
305 case 'S': { // Scroll Up
306 int n = params[0] ? params[0] : 1;
307 for (int i = 0; i < n; i++) scroll_up(pty);
308 break;
309 }
310 case 'T': { // Scroll Down
311 int n = params[0] ? params[0] : 1;
312 for (int i = 0; i < n; i++) scroll_down(pty);
313 break;
314 }
315 case 'd': // Cursor Vertical Absolute
316 pty->cursor_y = (params[0] ? params[0] - 1 : 0);
317 if (pty->cursor_y >= pty->rows) pty->cursor_y = pty->rows - 1;
318 break;
319 case 'm': // SGR (Select Graphic Rendition)
320 handle_sgr(pty, params, count);
321 break;
322 case 'r': // Set Scroll Region
323 pty->scroll_top = (count > 0 && params[0] ? params[0] - 1 : 0);
324 pty->scroll_bottom = (count > 1 && params[1] ? params[1] - 1 : pty->rows - 1);
325 if (pty->scroll_top < 0) pty->scroll_top = 0;
326 if (pty->scroll_bottom >= pty->rows) pty->scroll_bottom = pty->rows - 1;
327 pty->cursor_x = 0;
328 pty->cursor_y = pty->scroll_top;
329 break;
330 case 'h': case 'l': { // Set/Reset Mode
331 int set = (final == 'h');
332 // DEC private modes have '?' prefix in csi_buf
333 if (pty->csi_len > 0 && pty->csi_buf[0] == '?') {
334 int p2[8] = {0}; int c2;
335 c2 = parse_csi_params(pty->csi_buf + 1, pty->csi_len - 1, p2, 8);
336 int p = c2 > 0 ? p2[0] : 0;
337 if (p == 25) {
338 // DECTCEM: show/hide cursor
339 pty->cursor_visible = set;
340 } else if (p == 1049 && set) {
341 // Enter alternate screen: home cursor
342 pty->cursor_x = 0;
343 pty->cursor_y = 0;
344 }
345 }
346 break;
347 }
348 case '@': { // Insert Characters
349 int n = params[0] ? params[0] : 1;
350 int y = pty->cursor_y;
351 if (y >= 0 && y < pty->rows) {
352 for (int x = pty->cols - 1; x >= pty->cursor_x + n; x--)
353 pty->grid[y][x] = pty->grid[y][x - n];
354 for (int x = pty->cursor_x; x < pty->cursor_x + n && x < pty->cols; x++)
355 pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1};
356 }
357 pty->grid_dirty = 1;
358 break;
359 }
360 case 'X': { // Erase Characters
361 int n = params[0] ? params[0] : 1;
362 int y = pty->cursor_y;
363 if (y >= 0 && y < pty->rows) {
364 for (int x = pty->cursor_x; x < pty->cursor_x + n && x < pty->cols; x++)
365 pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1};
366 }
367 pty->grid_dirty = 1;
368 break;
369 }
370 default:
371 // Unknown CSI — ignore
372 break;
373 }
374}
375
376// Process a single byte through the VT100 state machine
377static void process_byte(ACPty *pty, uint8_t b) {
378 switch (pty->state) {
379 case 0: // Normal
380 if (b == 0x1B) {
381 pty->state = 1; // ESC
382 } else if (b == '\n') {
383 pty->cursor_y++;
384 if (pty->cursor_y > pty->scroll_bottom) {
385 pty->cursor_y = pty->scroll_bottom;
386 scroll_up(pty);
387 }
388 } else if (b == '\r') {
389 pty->cursor_x = 0;
390 } else if (b == '\t') {
391 pty->cursor_x = (pty->cursor_x + 8) & ~7;
392 if (pty->cursor_x >= pty->cols) pty->cursor_x = pty->cols - 1;
393 } else if (b == '\b') {
394 if (pty->cursor_x > 0) pty->cursor_x--;
395 } else if (b == 0x07) {
396 // BEL — ignore
397 } else if (b >= 0xC0 && b < 0xFE) {
398 // UTF-8 multi-byte sequence start
399 if (b < 0xE0) { pty->utf8_cp = b & 0x1F; pty->utf8_remaining = 1; }
400 else if (b < 0xF0) { pty->utf8_cp = b & 0x0F; pty->utf8_remaining = 2; }
401 else { pty->utf8_cp = b & 0x07; pty->utf8_remaining = 3; }
402 } else if (b >= 0x80 && b < 0xC0 && pty->utf8_remaining > 0) {
403 // UTF-8 continuation byte
404 pty->utf8_cp = (pty->utf8_cp << 6) | (b & 0x3F);
405 pty->utf8_remaining--;
406 if (pty->utf8_remaining == 0) {
407 put_char(pty, pty->utf8_cp);
408 }
409 } else if (b >= 0x20 && b < 0x80) {
410 // Printable ASCII
411 put_char(pty, b);
412 }
413 break;
414
415 case 1: // ESC received
416 if (b == '[') {
417 pty->state = 2; // CSI
418 pty->csi_len = 0;
419 } else if (b == ']') {
420 pty->state = 3; // OSC
421 pty->osc_len = 0;
422 } else if (b == '7') {
423 // Save cursor
424 pty->saved_x = pty->cursor_x;
425 pty->saved_y = pty->cursor_y;
426 pty->saved_fg = pty->cur_fg;
427 pty->saved_bg = pty->cur_bg;
428 pty->saved_bold = pty->cur_bold;
429 pty->state = 0;
430 } else if (b == '8') {
431 // Restore cursor
432 pty->cursor_x = pty->saved_x;
433 pty->cursor_y = pty->saved_y;
434 pty->cur_fg = pty->saved_fg;
435 pty->cur_bg = pty->saved_bg;
436 pty->cur_bold = pty->saved_bold;
437 pty->state = 0;
438 } else if (b == 'M') {
439 // Reverse Index (scroll down)
440 if (pty->cursor_y == pty->scroll_top) {
441 scroll_down(pty);
442 } else if (pty->cursor_y > 0) {
443 pty->cursor_y--;
444 }
445 pty->state = 0;
446 } else if (b == 'D') {
447 // Index (scroll up)
448 if (pty->cursor_y == pty->scroll_bottom) {
449 scroll_up(pty);
450 } else {
451 pty->cursor_y++;
452 }
453 pty->state = 0;
454 } else if (b == 'c') {
455 // Full Reset
456 pty_clear(pty);
457 pty->cur_fg = PTY_COLOR_DEFAULT_FG;
458 pty->cur_bg = PTY_COLOR_DEFAULT_BG;
459 pty->cur_bold = 0;
460 pty->scroll_top = 0;
461 pty->scroll_bottom = pty->rows - 1;
462 pty->state = 0;
463 } else {
464 pty->state = 0; // Unknown ESC sequence
465 }
466 break;
467
468 case 2: // CSI
469 if (b >= 0x40 && b <= 0x7E) {
470 // Final byte
471 handle_csi(pty, (char)b);
472 pty->state = 0;
473 } else if (pty->csi_len < (int)sizeof(pty->csi_buf) - 1) {
474 pty->csi_buf[pty->csi_len++] = (char)b;
475 }
476 break;
477
478 case 3: // OSC
479 if (b == 0x07 || b == 0x1B) {
480 // BEL or ESC terminates OSC — ignore the content
481 pty->state = (b == 0x1B) ? 1 : 0;
482 } else if (b == '\\' && pty->osc_len > 0 && pty->osc_buf[pty->osc_len - 1] == 0x1B) {
483 // ST (ESC \) terminates OSC
484 pty->state = 0;
485 } else if (pty->osc_len < (int)sizeof(pty->osc_buf) - 1) {
486 pty->osc_buf[pty->osc_len++] = (char)b;
487 }
488 break;
489 }
490}
491
492int pty_spawn(ACPty *pty, int cols, int rows, const char *cmd, char *const argv[]) {
493 memset(pty, 0, sizeof(*pty));
494 pty->cols = (cols > 0 && cols <= PTY_MAX_COLS) ? cols : 80;
495 pty->rows = (rows > 0 && rows <= PTY_MAX_ROWS) ? rows : 24;
496 pty->cur_fg = PTY_COLOR_DEFAULT_FG;
497 pty->cur_bg = PTY_COLOR_DEFAULT_BG;
498 pty->scroll_top = 0;
499 pty->scroll_bottom = pty->rows - 1;
500 pty->cursor_visible = 1;
501 pty_clear(pty);
502
503 // Pre-flight checks: log what we're about to spawn
504 ac_log("[pty] spawning: cmd='%s' cols=%d rows=%d\n", cmd, pty->cols, pty->rows);
505 for (int i = 0; argv[i]; i++) ac_log("[pty] argv[%d]='%s'\n", i, argv[i]);
506
507 // Check if command exists before forking
508 if (access(cmd, X_OK) != 0 && cmd[0] != '/') {
509 // Not an absolute path — check PATH
510 const char *path = getenv("PATH");
511 char check[512];
512 int found = 0;
513 if (path) {
514 char pathcopy[1024];
515 snprintf(pathcopy, sizeof(pathcopy), "%s", path);
516 char *saveptr;
517 for (char *dir = strtok_r(pathcopy, ":", &saveptr); dir; dir = strtok_r(NULL, ":", &saveptr)) {
518 snprintf(check, sizeof(check), "%s/%s", dir, cmd);
519 if (access(check, X_OK) == 0) {
520 ac_log("[pty] found: %s\n", check);
521 found = 1;
522 break;
523 }
524 }
525 }
526 if (!found) {
527 ac_log("[pty] WARNING: '%s' not found in PATH=%s\n", cmd, path ? path : "(null)");
528 }
529 }
530
531 // Check /dev/pts mount
532 if (access("/dev/pts", F_OK) != 0) {
533 ac_log("[pty] WARNING: /dev/pts does not exist — devpts not mounted?\n");
534 }
535
536 struct winsize ws = {
537 .ws_row = pty->rows,
538 .ws_col = pty->cols,
539 };
540
541 pid_t pid = forkpty(&pty->master_fd, NULL, NULL, &ws);
542 if (pid < 0) {
543 ac_log("[pty] forkpty failed: %s (errno=%d)\n", strerror(errno), errno);
544 ac_log("[pty] /dev/pts exists: %s\n", access("/dev/pts", F_OK) == 0 ? "yes" : "NO");
545 ac_log("[pty] /dev/ptmx exists: %s\n", access("/dev/ptmx", F_OK) == 0 ? "yes" : "NO");
546 return -1;
547 }
548
549 if (pid == 0) {
550 // Child process
551 setenv("TERM", "xterm-256color", 1);
552 setenv("HOME", "/tmp", 1); // force HOME=/tmp (credentials are here)
553 setenv("LANG", "en_US.UTF-8", 1);
554 setenv("PATH", "/tmp/.local/bin:/bin:/sbin:/usr/bin:/usr/sbin", 1);
555 // SHELL=/bin/bash is critical for Claude Code: its Bash tool
556 // spawns the shell from $SHELL (or /bin/sh fallback). busybox ash
557 // doesn't support arrays, process substitution, `local -n`, or
558 // bash parameter expansion (${var//a/b}) which Claude relies on.
559 // We ship real GNU bash in initramfs (see build-and-flash-initramfs.sh)
560 // and point SHELL at it here.
561 setenv("SHELL", "/bin/bash", 1);
562 setenv("USER", "root", 1);
563 setenv("LOGNAME", "root", 1);
564 setenv("GIT_TERMINAL_PROMPT", "0", 1);
565 setenv("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1", 1);
566 // SSL certs for API connections
567 setenv("SSL_CERT_FILE", "/etc/pki/tls/certs/ca-bundle.crt", 0);
568 setenv("SSL_CERT_DIR", "/etc/ssl/certs", 0);
569 setenv("CURL_CA_BUNDLE", "/etc/pki/tls/certs/ca-bundle.crt", 0);
570 setenv("NODE_EXTRA_CA_CERTS", "/etc/pki/tls/certs/ca-bundle.crt", 0);
571 if (access("/bin/ssh", X_OK) == 0 && access("/tmp/.ssh/config", R_OK) == 0) {
572 setenv("GIT_SSH_COMMAND", "/bin/ssh -F /tmp/.ssh/config -o BatchMode=yes", 1);
573 setenv("GIT_SSH_VARIANT", "ssh", 1);
574 }
575 // GitHub PAT (for git operations)
576 {
577 FILE *gf = fopen("/github-pat", "r");
578 if (gf) {
579 char pat[256] = {0};
580 if (fgets(pat, sizeof(pat), gf)) {
581 pat[strcspn(pat, "\r\n")] = '\0';
582 if (pat[0]) {
583 setenv("GH_TOKEN", pat, 1);
584 setenv("GITHUB_TOKEN", pat, 1);
585 // Configure git credential helper
586 mkdir("/tmp/.config", 0755);
587 mkdir("/tmp/.config/git", 0755);
588 FILE *gc = fopen("/tmp/.gitconfig", "w");
589 if (gc) {
590 fprintf(gc, "[user]\n\tname = Jeffrey Alan Scudder\n\temail = mail@aesthetic.computer\n");
591 fprintf(gc, "[credential \"https://github.com\"]\n\thelper = !f() { echo username=whistlegraph; echo password=%s; }; f\n", pat);
592 fclose(gc);
593 }
594 }
595 }
596 fclose(gf);
597 }
598 }
599 // Claude directories
600 mkdir("/tmp/.claude", 0755);
601 mkdir("/tmp/.config", 0755);
602 mkdir("/tmp/.local", 0755);
603 mkdir("/tmp/.local/bin", 0755);
604 // Claude Code settings: bypass permissions, trust /tmp/ac project
605 if (access("/tmp/.claude/settings.json", F_OK) != 0) {
606 FILE *sf = fopen("/tmp/.claude/settings.json", "w");
607 if (sf) {
608 fprintf(sf, "{\n"
609 " \"permissions\": {\n"
610 " \"allow\": [\"Bash(*)\", \"Read(*)\", \"Write(*)\", \"Edit(*)\", "
611 "\"Glob(*)\", \"Grep(*)\", \"WebFetch(*)\", \"WebSearch(*)\"]\n"
612 " },\n"
613 " \"autoUpdates\": false\n"
614 "}\n");
615 fclose(sf);
616 }
617 }
618 // Trust the /tmp/ac project so Claude doesn't ask every time
619 mkdir("/tmp/.claude/projects", 0755);
620 mkdir("/tmp/.claude/projects/-tmp-ac", 0755);
621 if (access("/tmp/.claude/projects/-tmp-ac/settings.json", F_OK) != 0) {
622 FILE *pf = fopen("/tmp/.claude/projects/-tmp-ac/settings.json", "w");
623 if (pf) {
624 fprintf(pf, "{\"isTrusted\": true}\n");
625 fclose(pf);
626 }
627 }
628 // Terminal capabilities
629 setenv("COLORTERM", "truecolor", 1);
630
631 // Working directory strategy:
632 // 1. /mnt/ac-repo (persistent shallow clone of aesthetic-computer,
633 // auto-cloned on WiFi connect or manually by the user) — PREFERRED
634 // so Claude can actually `git commit` real project files
635 // 2. /tmp/ac (tmpfs, persists per-session only) — FALLBACK for
636 // when there's no repo yet. Claude can still edit scratch
637 // files here. A placeholder git init makes it look like a
638 // project to Claude Code.
639 //
640 // We pick the repo clone if it exists AND has a .git directory.
641 // Otherwise fall through to /tmp/ac.
642 const char *work_dir = "/tmp/ac";
643 if (access("/mnt/ac-repo/.git", F_OK) == 0) {
644 work_dir = "/mnt/ac-repo";
645 }
646 // Always make sure /tmp/ac exists as a fallback.
647 mkdir("/tmp/ac", 0755);
648 chdir(work_dir);
649 // Get handle from config (parsed by parent)
650 const char *handle = getenv("AC_HANDLE");
651 if (!handle || !handle[0]) handle = "user";
652 // Ensure CLAUDE.md exists with full project context. Only write
653 // to /tmp/ac (the scratch fallback) — the real repo's CLAUDE.md
654 // lives in the source tree and shouldn't be clobbered.
655 if (access("/tmp/ac/CLAUDE.md", F_OK) != 0) {
656 FILE *cm = fopen("/tmp/ac/CLAUDE.md", "w");
657 if (cm) {
658 fprintf(cm,
659 "# AC Native Device — @%s\n\n"
660 "You are Claude Code running on an Aesthetic Computer (AC) device.\n"
661 "This is a minimal Linux system (custom kernel, initramfs, no package manager).\n\n"
662 "## Environment\n"
663 "- **User**: @%s\n"
664 "- **Working directory**: /mnt/ac-repo (aesthetic-computer repo, if cloned)\n"
665 " or /tmp/ac (scratch fallback) — you can cd between them\n"
666 "- **Shell**: /bin/bash (real GNU bash — arrays, subst, etc. all work)\n"
667 "- **Home**: /tmp\n"
668 "- **Tools**: bash, sh, curl, git, rg, jq, busybox utilities\n"
669 "- **No sudo** — you are already root\n"
670 "- **No package manager** — binaries are baked into initramfs\n\n"
671 "## Filesystem\n"
672 "- `/mnt` — USB boot drive (FAT32, has config.json and logs)\n"
673 "- `/mnt/ac-repo` — persistent shallow repo clone (GitHub fetch mirror)\n"
674 "- `/mnt/tapes` — MP4 tapes recorded via PrintScreen\n"
675 "- `/tmp` — tmpfs (lost on reboot)\n"
676 "- `/tmp/.ssh` — Tangled SSH key/config, when baked via `ac-os`\n"
677 "- `/bin` — busybox + baked binaries (bash, git, rg, jq, ssh)\n\n"
678 "## Networking\n"
679 "- WiFi auto-connects to saved networks\n"
680 "- `curl` is available for HTTP requests\n"
681 "- `/mnt/ac-repo` fetches from the GitHub mirror\n"
682 "- If `/tmp/.ssh/tangled` exists, `git push origin` mirrors to knot + GitHub\n\n"
683 "## What you can do\n"
684 "- Write and run shell scripts (bash, not busybox)\n"
685 "- Use curl to interact with APIs\n"
686 "- Edit files in /mnt/ac-repo and `git commit` them\n"
687 "- Use git (GitHub PAT is wired up; Tangled push works when the SSH key is baked)\n"
688 "- Read system logs at /mnt/ac-native.log\n",
689 handle, handle);
690 fclose(cm);
691 }
692 }
693 // Symlink SCORE.md if the baked version exists
694 if (access("/device-score.md", F_OK) == 0 && access("/tmp/ac/SCORE.md", F_OK) != 0) {
695 symlink("/device-score.md", "/tmp/ac/SCORE.md");
696 }
697 // Ensure /tmp/ac is a git repo so Claude Code recognizes it as a
698 // project (the real /mnt/ac-repo already is one).
699 if (access("/tmp/ac/.git", F_OK) != 0) {
700 system("git init -q /tmp/ac 2>/dev/null");
701 }
702
703 // Set Claude OAuth token from baked file (/claude-token is a plain text file)
704 {
705 FILE *tf = fopen("/claude-token", "r");
706 if (tf) {
707 char token[256] = {0};
708 if (fgets(token, sizeof(token), tf)) {
709 token[strcspn(token, "\r\n")] = '\0';
710 if (token[0]) setenv("CLAUDE_CODE_OAUTH_TOKEN", token, 1);
711 }
712 fclose(tf);
713 }
714 }
715 execvp(cmd, argv);
716 // exec failed — write error to stderr (flows through PTY to parent)
717 int err = errno;
718 fprintf(stderr, "\r\n[pty-child] exec '%s' failed: %s (errno=%d)\r\n", cmd, strerror(err), err);
719 if (err == ENOENT) {
720 fprintf(stderr, "[pty-child] command not found in PATH\r\n");
721 // Check common dependencies
722 if (access("/bin/sh", X_OK) != 0)
723 fprintf(stderr, "[pty-child] /bin/sh: MISSING\r\n");
724 if (access("/bin/bash", X_OK) != 0)
725 fprintf(stderr, "[pty-child] /bin/bash: MISSING\r\n");
726 if (access("/bin/node", X_OK) != 0)
727 fprintf(stderr, "[pty-child] /bin/node: MISSING\r\n");
728 if (access("/bin/claude", X_OK) != 0)
729 fprintf(stderr, "[pty-child] /bin/claude: MISSING\r\n");
730 if (access("/opt/claude-code/cli.js", R_OK) != 0)
731 fprintf(stderr, "[pty-child] /opt/claude-code/cli.js: MISSING\r\n");
732 } else if (err == EACCES) {
733 fprintf(stderr, "[pty-child] permission denied on '%s'\r\n", cmd);
734 }
735 _exit(127);
736 }
737
738 // Parent
739 pty->child_pid = pid;
740 pty->alive = 1;
741
742 // Make master non-blocking
743 int flags = fcntl(pty->master_fd, F_GETFL, 0);
744 fcntl(pty->master_fd, F_SETFL, flags | O_NONBLOCK);
745
746 ac_log("[pty] spawned pid=%d cmd=%s cols=%d rows=%d\n", pid, cmd, cols, rows);
747 ac_log_stderr_muted = 1; // Suppress stderr while PTY child is running
748 return 0;
749}
750
751int pty_pump(ACPty *pty) {
752 if (pty->master_fd < 0) return 0;
753
754 uint8_t buf[4096];
755 int total = 0;
756
757 for (;;) {
758 ssize_t n = read(pty->master_fd, buf, sizeof(buf));
759 if (n <= 0) break;
760 for (ssize_t i = 0; i < n; i++) {
761 process_byte(pty, buf[i]);
762 }
763 total += (int)n;
764 if (total > 32768) break; // don't block too long per frame
765 }
766
767 return total;
768}
769
770int pty_write(ACPty *pty, const char *data, int len) {
771 if (pty->master_fd < 0 || !data || len <= 0) return -1;
772 ssize_t written = write(pty->master_fd, data, len);
773 return (int)written;
774}
775
776int pty_resize(ACPty *pty, int cols, int rows) {
777 if (cols <= 0 || cols > PTY_MAX_COLS) cols = 80;
778 if (rows <= 0 || rows > PTY_MAX_ROWS) rows = 24;
779
780 // Resize the terminal
781 struct winsize ws = { .ws_row = rows, .ws_col = cols };
782 if (pty->master_fd >= 0) {
783 ioctl(pty->master_fd, TIOCSWINSZ, &ws);
784 }
785
786 // Expand or shrink grid
787 int old_rows = pty->rows;
788 int old_cols = pty->cols;
789 pty->rows = rows;
790 pty->cols = cols;
791 pty->scroll_bottom = rows - 1;
792
793 // Clear new cells
794 for (int y = 0; y < rows; y++) {
795 int start = (y < old_rows) ? old_cols : 0;
796 for (int x = start; x < cols; x++) {
797 pty->grid[y][x] = (ACPtyCell){' ', PTY_COLOR_DEFAULT_FG, PTY_COLOR_DEFAULT_BG, 0, 1};
798 }
799 }
800
801 if (pty->cursor_x >= cols) pty->cursor_x = cols - 1;
802 if (pty->cursor_y >= rows) pty->cursor_y = rows - 1;
803 pty->grid_dirty = 1;
804
805 ac_log("[pty] resized %dx%d -> %dx%d\n", old_cols, old_rows, cols, rows);
806 return 0;
807}
808
809int pty_check_alive(ACPty *pty) {
810 if (!pty->alive) return 0;
811 int status;
812 pid_t result = waitpid(pty->child_pid, &status, WNOHANG);
813 if (result == pty->child_pid) {
814 pty->alive = 0;
815 ac_log_stderr_muted = 0; // Restore stderr logging
816 if (WIFEXITED(status)) {
817 int code = WEXITSTATUS(status);
818 pty->exit_code = code;
819 if (code == 127) {
820 ac_log("[pty] child exited: command not found (exit 127)\n");
821 } else if (code == 126) {
822 ac_log("[pty] child exited: permission denied (exit 126)\n");
823 } else {
824 ac_log("[pty] child exited (code=%d)\n", code);
825 }
826 } else if (WIFSIGNALED(status)) {
827 int sig = WTERMSIG(status);
828 pty->exit_code = 128 + sig;
829 ac_log("[pty] child killed by signal %d (%s)\n", sig,
830 sig == SIGSEGV ? "SIGSEGV" :
831 sig == SIGABRT ? "SIGABRT" :
832 sig == SIGTERM ? "SIGTERM" :
833 sig == SIGKILL ? "SIGKILL" : "other");
834 } else {
835 pty->exit_code = -1;
836 ac_log("[pty] child exited (unknown status=%d)\n", status);
837 }
838 }
839 return pty->alive;
840}
841
842void pty_destroy(ACPty *pty) {
843 if (pty->child_pid > 0 && pty->alive) {
844 kill(pty->child_pid, SIGTERM);
845 usleep(100000); // 100ms grace
846 kill(pty->child_pid, SIGKILL);
847 waitpid(pty->child_pid, NULL, 0);
848 }
849 if (pty->master_fd >= 0) {
850 close(pty->master_fd);
851 pty->master_fd = -1;
852 }
853 pty->alive = 0;
854 pty->child_pid = 0;
855 ac_log_stderr_muted = 0; // Restore stderr logging
856 ac_log("[pty] destroyed\n");
857}