Monorepo for Aesthetic.Computer aesthetic.computer
at main 857 lines 34 kB view raw
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}