Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914#define _GNU_SOURCE#include "pty.h"#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <fcntl.h>#include <errno.h>#include <signal.h>#include <sys/ioctl.h>#include <sys/stat.h>#include <sys/wait.h>#include <pty.h>#include <termios.h>
extern void ac_log(const char *fmt, ...);extern int ac_log_stderr_muted;
// Standard ANSI 16-color palettestatic const uint8_t ansi_colors[16][3] = { { 0, 0, 0}, // 0 black {170, 0, 0}, // 1 red { 0, 170, 0}, // 2 green {170, 85, 0}, // 3 yellow/brown { 0, 0, 170}, // 4 blue {170, 0, 170}, // 5 magenta { 0, 170, 170}, // 6 cyan {170, 170, 170}, // 7 white (light gray) { 85, 85, 85}, // 8 bright black (dark gray) {255, 85, 85}, // 9 bright red { 85, 255, 85}, // 10 bright green {255, 255, 85}, // 11 bright yellow { 85, 85, 255}, // 12 bright blue {255, 85, 255}, // 13 bright magenta { 85, 255, 255}, // 14 bright cyan {255, 255, 255}, // 15 bright white};
void pty_color_to_rgb(int color_index, int bold, uint8_t *r, uint8_t *g, uint8_t *b) { if (color_index < 0 || color_index > 15) { // Default: white fg or black bg *r = *g = *b = (color_index == PTY_COLOR_DEFAULT_FG) ? 170 : 0; return; } // Bold promotes colors 0-7 to 8-15 int idx = color_index; if (bold && idx < 8) idx += 8; *r = ansi_colors[idx][0]; *g = ansi_colors[idx][1]; *b = ansi_colors[idx][2];}
void pty_clear(ACPty *pty) { for (int y = 0; y < pty->rows; y++) { for (int x = 0; x < pty->cols; x++) { pty->grid[y][x] = (ACPtyCell){' ', PTY_COLOR_DEFAULT_FG, PTY_COLOR_DEFAULT_BG, 0, 1}; } } pty->cursor_x = 0; pty->cursor_y = 0; pty->grid_dirty = 1;}
static void scroll_up(ACPty *pty) { int top = pty->scroll_top; int bot = pty->scroll_bottom; // Move rows up by 1 within scroll region for (int y = top; y < bot; y++) { memcpy(pty->grid[y], pty->grid[y + 1], sizeof(ACPtyCell) * pty->cols); for (int x = 0; x < pty->cols; x++) pty->grid[y][x].dirty = 1; } // Clear bottom row for (int x = 0; x < pty->cols; x++) { pty->grid[bot][x] = (ACPtyCell){' ', PTY_COLOR_DEFAULT_FG, PTY_COLOR_DEFAULT_BG, 0, 1}; } pty->grid_dirty = 1;}
static void scroll_down(ACPty *pty) { int top = pty->scroll_top; int bot = pty->scroll_bottom; for (int y = bot; y > top; y--) { memcpy(pty->grid[y], pty->grid[y - 1], sizeof(ACPtyCell) * pty->cols); for (int x = 0; x < pty->cols; x++) pty->grid[y][x].dirty = 1; } for (int x = 0; x < pty->cols; x++) { pty->grid[top][x] = (ACPtyCell){' ', PTY_COLOR_DEFAULT_FG, PTY_COLOR_DEFAULT_BG, 0, 1}; } pty->grid_dirty = 1;}
static void put_char(ACPty *pty, uint32_t ch) { if (pty->cursor_x >= pty->cols) { pty->cursor_x = 0; pty->cursor_y++; if (pty->cursor_y > pty->scroll_bottom) { pty->cursor_y = pty->scroll_bottom; scroll_up(pty); } } if (pty->cursor_y < 0) pty->cursor_y = 0; if (pty->cursor_y < pty->rows && pty->cursor_x < pty->cols) { ACPtyCell *c = &pty->grid[pty->cursor_y][pty->cursor_x]; c->ch = ch; c->fg = pty->cur_fg; c->bg = pty->cur_bg; c->bold = pty->cur_bold; c->fg_r = pty->cur_fg_r; c->fg_g = pty->cur_fg_g; c->fg_b = pty->cur_fg_b; c->bg_r = pty->cur_bg_r; c->bg_g = pty->cur_bg_g; c->bg_b = pty->cur_bg_b; c->dirty = 1; } pty->cursor_x++; pty->grid_dirty = 1;}
// Parse CSI parameters: "1;2;3" → array of intsstatic int parse_csi_params(const char *buf, int len, int *params, int max_params) { int count = 0; int val = 0; int has_val = 0; for (int i = 0; i < len && count < max_params; i++) { if (buf[i] >= '0' && buf[i] <= '9') { val = val * 10 + (buf[i] - '0'); has_val = 1; } else if (buf[i] == ';') { params[count++] = has_val ? val : 0; val = 0; has_val = 0; } } if (has_val || count == 0) params[count++] = val; return count;}
static void handle_sgr(ACPty *pty, int *params, int count) { for (int i = 0; i < count; i++) { int p = params[i]; if (p == 0) { pty->cur_fg = PTY_COLOR_DEFAULT_FG; pty->cur_bg = PTY_COLOR_DEFAULT_BG; pty->cur_bold = 0; } else if (p == 1) { pty->cur_bold = 1; } else if (p == 22) { pty->cur_bold = 0; } else if (p >= 30 && p <= 37) { pty->cur_fg = p - 30; } else if (p == 39) { pty->cur_fg = PTY_COLOR_DEFAULT_FG; } else if (p >= 40 && p <= 47) { pty->cur_bg = p - 40; } else if (p == 49) { pty->cur_bg = PTY_COLOR_DEFAULT_BG; } else if (p >= 90 && p <= 97) { pty->cur_fg = p - 90 + 8; } else if (p >= 100 && p <= 107) { pty->cur_bg = p - 100 + 8; } // 256-color and truecolor support else if (p == 38 || p == 48) { if (i + 1 < count && params[i + 1] == 5) { // 256-color: \e[38;5;Nm if (i + 2 < count) { int c = params[i + 2]; if (c < 16) { if (p == 38) pty->cur_fg = c; else pty->cur_bg = c; } else if (c < 232) { // 216-color cube (16-231): map to RGB int ci = c - 16; int cr = ci / 36, cg = (ci / 6) % 6, cb = ci % 6; uint8_t r = cr ? 55 + cr * 40 : 0; uint8_t g = cg ? 55 + cg * 40 : 0; uint8_t b = cb ? 55 + cb * 40 : 0; if (p == 38) { pty->cur_fg = 255; pty->cur_fg_r = r; pty->cur_fg_g = g; pty->cur_fg_b = b; } else { pty->cur_bg = 255; pty->cur_bg_r = r; pty->cur_bg_g = g; pty->cur_bg_b = b; } } else { // Grayscale (232-255): 24 shades uint8_t v = 8 + (c - 232) * 10; if (p == 38) { pty->cur_fg = 255; pty->cur_fg_r = pty->cur_fg_g = pty->cur_fg_b = v; } else { pty->cur_bg = 255; pty->cur_bg_r = pty->cur_bg_g = pty->cur_bg_b = v; } } i += 2; } } else if (i + 1 < count && params[i + 1] == 2) { // Truecolor: \e[38;2;R;G;Bm if (i + 4 < count) { uint8_t r = params[i + 2], g = params[i + 3], b = params[i + 4]; if (p == 38) { pty->cur_fg = 255; pty->cur_fg_r = r; pty->cur_fg_g = g; pty->cur_fg_b = b; } else { pty->cur_bg = 255; pty->cur_bg_r = r; pty->cur_bg_g = g; pty->cur_bg_b = b; } } i += 4; } } }}
static void erase_in_line(ACPty *pty, int mode) { int y = pty->cursor_y; if (y < 0 || y >= pty->rows) return; int start = 0, end = pty->cols; if (mode == 0) start = pty->cursor_x; // cursor to end else if (mode == 1) end = pty->cursor_x + 1; // start to cursor // mode == 2: entire line for (int x = start; x < end; x++) { pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1}; } pty->grid_dirty = 1;}
static void erase_in_display(ACPty *pty, int mode) { if (mode == 0) { // Cursor to end of screen erase_in_line(pty, 0); for (int y = pty->cursor_y + 1; y < pty->rows; y++) { for (int x = 0; x < pty->cols; x++) pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1}; } } else if (mode == 1) { // Start to cursor for (int y = 0; y < pty->cursor_y; y++) { for (int x = 0; x < pty->cols; x++) pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1}; } erase_in_line(pty, 1); } else if (mode == 2 || mode == 3) { // Entire screen for (int y = 0; y < pty->rows; y++) { for (int x = 0; x < pty->cols; x++) pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1}; } } pty->grid_dirty = 1;}
static void handle_csi(ACPty *pty, char final) { int params[16] = {0}; int count = parse_csi_params(pty->csi_buf, pty->csi_len, params, 16);
switch (final) { case 'A': // Cursor Up pty->cursor_y -= (params[0] ? params[0] : 1); if (pty->cursor_y < pty->scroll_top) pty->cursor_y = pty->scroll_top; break; case 'B': // Cursor Down pty->cursor_y += (params[0] ? params[0] : 1); if (pty->cursor_y > pty->scroll_bottom) pty->cursor_y = pty->scroll_bottom; break; case 'C': // Cursor Forward pty->cursor_x += (params[0] ? params[0] : 1); if (pty->cursor_x >= pty->cols) pty->cursor_x = pty->cols - 1; break; case 'D': // Cursor Back pty->cursor_x -= (params[0] ? params[0] : 1); if (pty->cursor_x < 0) pty->cursor_x = 0; break; case 'E': // Cursor Next Line pty->cursor_x = 0; pty->cursor_y += (params[0] ? params[0] : 1); if (pty->cursor_y > pty->scroll_bottom) pty->cursor_y = pty->scroll_bottom; break; case 'F': // Cursor Previous Line pty->cursor_x = 0; pty->cursor_y -= (params[0] ? params[0] : 1); if (pty->cursor_y < pty->scroll_top) pty->cursor_y = pty->scroll_top; break; case 'G': // Cursor Horizontal Absolute pty->cursor_x = (params[0] ? params[0] - 1 : 0); if (pty->cursor_x >= pty->cols) pty->cursor_x = pty->cols - 1; break; case 'H': case 'f': // Cursor Position pty->cursor_y = (count > 0 && params[0] ? params[0] - 1 : 0); pty->cursor_x = (count > 1 && params[1] ? params[1] - 1 : 0); if (pty->cursor_y >= pty->rows) pty->cursor_y = pty->rows - 1; if (pty->cursor_x >= pty->cols) pty->cursor_x = pty->cols - 1; break; case 'J': // Erase in Display erase_in_display(pty, params[0]); break; case 'K': // Erase in Line erase_in_line(pty, params[0]); break; case 'L': { // Insert Lines int n = params[0] ? params[0] : 1; for (int i = 0; i < n; i++) scroll_down(pty); break; } case 'M': { // Delete Lines int n = params[0] ? params[0] : 1; for (int i = 0; i < n; i++) scroll_up(pty); break; } case 'P': { // Delete Characters int n = params[0] ? params[0] : 1; int y = pty->cursor_y; if (y >= 0 && y < pty->rows) { for (int x = pty->cursor_x; x < pty->cols - n; x++) pty->grid[y][x] = pty->grid[y][x + n]; for (int x = pty->cols - n; x < pty->cols; x++) pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1}; } pty->grid_dirty = 1; break; } case 'S': { // Scroll Up int n = params[0] ? params[0] : 1; for (int i = 0; i < n; i++) scroll_up(pty); break; } case 'T': { // Scroll Down int n = params[0] ? params[0] : 1; for (int i = 0; i < n; i++) scroll_down(pty); break; } case 'd': // Cursor Vertical Absolute pty->cursor_y = (params[0] ? params[0] - 1 : 0); if (pty->cursor_y >= pty->rows) pty->cursor_y = pty->rows - 1; break; case 'm': // SGR (Select Graphic Rendition) handle_sgr(pty, params, count); break; case 'r': // Set Scroll Region pty->scroll_top = (count > 0 && params[0] ? params[0] - 1 : 0); pty->scroll_bottom = (count > 1 && params[1] ? params[1] - 1 : pty->rows - 1); if (pty->scroll_top < 0) pty->scroll_top = 0; if (pty->scroll_bottom >= pty->rows) pty->scroll_bottom = pty->rows - 1; pty->cursor_x = 0; pty->cursor_y = pty->scroll_top; break; case 'h': case 'l': { // Set/Reset Mode int set = (final == 'h'); // DEC private modes have '?' prefix in csi_buf if (pty->csi_len > 0 && pty->csi_buf[0] == '?') { int p2[8] = {0}; int c2; c2 = parse_csi_params(pty->csi_buf + 1, pty->csi_len - 1, p2, 8); int p = c2 > 0 ? p2[0] : 0; if (p == 25) { // DECTCEM: show/hide cursor pty->cursor_visible = set; } else if (p == 1049 && set) { // Enter alternate screen: home cursor pty->cursor_x = 0; pty->cursor_y = 0; } } break; } case '@': { // Insert Characters int n = params[0] ? params[0] : 1; int y = pty->cursor_y; if (y >= 0 && y < pty->rows) { for (int x = pty->cols - 1; x >= pty->cursor_x + n; x--) pty->grid[y][x] = pty->grid[y][x - n]; for (int x = pty->cursor_x; x < pty->cursor_x + n && x < pty->cols; x++) pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1}; } pty->grid_dirty = 1; break; } case 'X': { // Erase Characters int n = params[0] ? params[0] : 1; int y = pty->cursor_y; if (y >= 0 && y < pty->rows) { for (int x = pty->cursor_x; x < pty->cursor_x + n && x < pty->cols; x++) pty->grid[y][x] = (ACPtyCell){' ', pty->cur_fg, pty->cur_bg, 0, 1}; } pty->grid_dirty = 1; break; } default: // Unknown CSI — ignore break; }}
// Process a single byte through the VT100 state machinestatic void process_byte(ACPty *pty, uint8_t b) { switch (pty->state) { case 0: // Normal if (b == 0x1B) { pty->state = 1; // ESC } else if (b == '\n') { pty->cursor_y++; if (pty->cursor_y > pty->scroll_bottom) { pty->cursor_y = pty->scroll_bottom; scroll_up(pty); } } else if (b == '\r') { pty->cursor_x = 0; } else if (b == '\t') { pty->cursor_x = (pty->cursor_x + 8) & ~7; if (pty->cursor_x >= pty->cols) pty->cursor_x = pty->cols - 1; } else if (b == '\b') { if (pty->cursor_x > 0) pty->cursor_x--; } else if (b == 0x07) { // BEL — ignore } else if (b >= 0xC0 && b < 0xFE) { // UTF-8 multi-byte sequence start if (b < 0xE0) { pty->utf8_cp = b & 0x1F; pty->utf8_remaining = 1; } else if (b < 0xF0) { pty->utf8_cp = b & 0x0F; pty->utf8_remaining = 2; } else { pty->utf8_cp = b & 0x07; pty->utf8_remaining = 3; } } else if (b >= 0x80 && b < 0xC0 && pty->utf8_remaining > 0) { // UTF-8 continuation byte pty->utf8_cp = (pty->utf8_cp << 6) | (b & 0x3F); pty->utf8_remaining--; if (pty->utf8_remaining == 0) { put_char(pty, pty->utf8_cp); } } else if (b >= 0x20 && b < 0x80) { // Printable ASCII put_char(pty, b); } break;
case 1: // ESC received if (b == '[') { pty->state = 2; // CSI pty->csi_len = 0; } else if (b == ']') { pty->state = 3; // OSC pty->osc_len = 0; } else if (b == '7') { // Save cursor pty->saved_x = pty->cursor_x; pty->saved_y = pty->cursor_y; pty->saved_fg = pty->cur_fg; pty->saved_bg = pty->cur_bg; pty->saved_bold = pty->cur_bold; pty->state = 0; } else if (b == '8') { // Restore cursor pty->cursor_x = pty->saved_x; pty->cursor_y = pty->saved_y; pty->cur_fg = pty->saved_fg; pty->cur_bg = pty->saved_bg; pty->cur_bold = pty->saved_bold; pty->state = 0; } else if (b == 'M') { // Reverse Index (scroll down) if (pty->cursor_y == pty->scroll_top) { scroll_down(pty); } else if (pty->cursor_y > 0) { pty->cursor_y--; } pty->state = 0; } else if (b == 'D') { // Index (scroll up) if (pty->cursor_y == pty->scroll_bottom) { scroll_up(pty); } else { pty->cursor_y++; } pty->state = 0; } else if (b == 'c') { // Full Reset pty_clear(pty); pty->cur_fg = PTY_COLOR_DEFAULT_FG; pty->cur_bg = PTY_COLOR_DEFAULT_BG; pty->cur_bold = 0; pty->scroll_top = 0; pty->scroll_bottom = pty->rows - 1; pty->state = 0; } else { pty->state = 0; // Unknown ESC sequence } break;
case 2: // CSI if (b >= 0x40 && b <= 0x7E) { // Final byte handle_csi(pty, (char)b); pty->state = 0; } else if (pty->csi_len < (int)sizeof(pty->csi_buf) - 1) { pty->csi_buf[pty->csi_len++] = (char)b; } break;
case 3: // OSC if (b == 0x07 || b == 0x1B) { // BEL or ESC terminates OSC — ignore the content pty->state = (b == 0x1B) ? 1 : 0; } else if (b == '\\' && pty->osc_len > 0 && pty->osc_buf[pty->osc_len - 1] == 0x1B) { // ST (ESC \) terminates OSC pty->state = 0; } else if (pty->osc_len < (int)sizeof(pty->osc_buf) - 1) { pty->osc_buf[pty->osc_len++] = (char)b; } break; }}
int pty_spawn(ACPty *pty, int cols, int rows, const char *cmd, char *const argv[]) { memset(pty, 0, sizeof(*pty)); pty->cols = (cols > 0 && cols <= PTY_MAX_COLS) ? cols : 80; pty->rows = (rows > 0 && rows <= PTY_MAX_ROWS) ? rows : 24; pty->cur_fg = PTY_COLOR_DEFAULT_FG; pty->cur_bg = PTY_COLOR_DEFAULT_BG; pty->scroll_top = 0; pty->scroll_bottom = pty->rows - 1; pty->cursor_visible = 1; pty_clear(pty);
// Pre-flight checks: log what we're about to spawn ac_log("[pty] spawning: cmd='%s' cols=%d rows=%d\n", cmd, pty->cols, pty->rows); for (int i = 0; argv[i]; i++) ac_log("[pty] argv[%d]='%s'\n", i, argv[i]);
// Check if command exists before forking if (access(cmd, X_OK) != 0 && cmd[0] != '/') { // Not an absolute path — check PATH const char *path = getenv("PATH"); char check[512]; int found = 0; if (path) { char pathcopy[1024]; snprintf(pathcopy, sizeof(pathcopy), "%s", path); char *saveptr; for (char *dir = strtok_r(pathcopy, ":", &saveptr); dir; dir = strtok_r(NULL, ":", &saveptr)) { snprintf(check, sizeof(check), "%s/%s", dir, cmd); if (access(check, X_OK) == 0) { ac_log("[pty] found: %s\n", check); found = 1; break; } } } if (!found) { ac_log("[pty] WARNING: '%s' not found in PATH=%s\n", cmd, path ? path : "(null)"); } }
// Check /dev/pts mount if (access("/dev/pts", F_OK) != 0) { ac_log("[pty] WARNING: /dev/pts does not exist — devpts not mounted?\n"); }
struct winsize ws = { .ws_row = pty->rows, .ws_col = pty->cols, };
pid_t pid = forkpty(&pty->master_fd, NULL, NULL, &ws); if (pid < 0) { ac_log("[pty] forkpty failed: %s (errno=%d)\n", strerror(errno), errno); ac_log("[pty] /dev/pts exists: %s\n", access("/dev/pts", F_OK) == 0 ? "yes" : "NO"); ac_log("[pty] /dev/ptmx exists: %s\n", access("/dev/ptmx", F_OK) == 0 ? "yes" : "NO"); return -1; }
if (pid == 0) { // Child process setenv("TERM", "xterm-256color", 1); setenv("HOME", "/tmp", 1); // force HOME=/tmp (credentials are here) setenv("LANG", "en_US.UTF-8", 1); setenv("PATH", "/tmp/.local/bin:/bin:/sbin:/usr/bin:/usr/sbin", 1); // SHELL=/bin/bash is critical for Claude Code: its Bash tool // spawns the shell from $SHELL (or /bin/sh fallback). busybox ash // doesn't support arrays, process substitution, `local -n`, or // bash parameter expansion (${var//a/b}) which Claude relies on. // We ship real GNU bash in initramfs (see build-and-flash-initramfs.sh) // and point SHELL at it here. setenv("SHELL", "/bin/bash", 1); setenv("USER", "root", 1); setenv("LOGNAME", "root", 1); setenv("GIT_TERMINAL_PROMPT", "0", 1); setenv("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1", 1); // Pin Claude's default model to "sonnet" — Anthropic's CLI resolves // that alias to the latest Sonnet family member, currently 4.7. // Setting ANTHROPIC_MODEL belt-and-suspenders alongside the same // key in /tmp/.claude/settings.json so either resolution path // lands on 4.7 instead of whatever the CLI's built-in default is. setenv("ANTHROPIC_MODEL", "sonnet", 0); // SSL certs for API connections setenv("SSL_CERT_FILE", "/etc/pki/tls/certs/ca-bundle.crt", 0); setenv("SSL_CERT_DIR", "/etc/ssl/certs", 0); setenv("CURL_CA_BUNDLE", "/etc/pki/tls/certs/ca-bundle.crt", 0); setenv("NODE_EXTRA_CA_CERTS", "/etc/pki/tls/certs/ca-bundle.crt", 0); if (access("/bin/ssh", X_OK) == 0 && access("/tmp/.ssh/config", R_OK) == 0) { setenv("GIT_SSH_COMMAND", "/bin/ssh -F /tmp/.ssh/config -o BatchMode=yes", 1); setenv("GIT_SSH_VARIANT", "ssh", 1); } // GitHub PAT (for git operations) { FILE *gf = fopen("/github-pat", "r"); if (gf) { char pat[256] = {0}; if (fgets(pat, sizeof(pat), gf)) { pat[strcspn(pat, "\r\n")] = '\0'; if (pat[0]) { setenv("GH_TOKEN", pat, 1); setenv("GITHUB_TOKEN", pat, 1); // Derive git user identity from /mnt/config.json so // `git commit` attributes to the actual device owner // rather than being hardcoded. Falls back to a // generic "aesthetic.computer" identity if config is // unreadable (e.g. device booted from a USB that // lost its config partition). char cfg_handle[64] = ""; char cfg_email[128] = ""; FILE *cfg = fopen("/mnt/config.json", "r"); if (cfg) { char line[1024]; size_t n = fread(line, 1, sizeof(line) - 1, cfg); line[n] = 0; fclose(cfg); // Cheap JSON field extraction — the config is a // single flat object written by flash-mac / oven. const char *p; if ((p = strstr(line, "\"handle\""))) { p = strchr(p, ':'); if (p) p = strchr(p, '"'); if (p) { p++; const char *q = strchr(p, '"'); if (q && q - p < (long)sizeof(cfg_handle)) { memcpy(cfg_handle, p, q - p); cfg_handle[q - p] = 0; } } } if ((p = strstr(line, "\"email\""))) { p = strchr(p, ':'); if (p) p = strchr(p, '"'); if (p) { p++; const char *q = strchr(p, '"'); if (q && q - p < (long)sizeof(cfg_email)) { memcpy(cfg_email, p, q - p); cfg_email[q - p] = 0; } } } } const char *git_name = cfg_handle[0] ? cfg_handle : "ac-native"; const char *git_email = cfg_email[0] ? cfg_email : "noreply@aesthetic.computer"; // GitHub credential helper uses the handle as the // HTTPS username (GitHub's own rule: any username // paired with a valid PAT authenticates as the PAT's // owner). For @jeffrey that resolves to the admin // `whistlegraph` account naturally. const char *gh_user = cfg_handle[0] ? cfg_handle : "ac-native"; mkdir("/tmp/.config", 0755); mkdir("/tmp/.config/git", 0755); FILE *gc = fopen("/tmp/.gitconfig", "w"); if (gc) { fprintf(gc, "[user]\n\tname = %s\n\temail = %s\n", git_name, git_email); fprintf(gc, "[credential \"https://github.com\"]\n\thelper = !f() { echo username=%s; echo password=%s; }; f\n", gh_user, pat); // Enable `compush` alias — commit all staged + untracked // changes and push origin HEAD in one verb. Matches the // notation in CLAUDE.md ("compush = commit & push"). fprintf(gc, "[alias]\n" "\tcompush = !f() { git add -A && git commit -m \"${1:-compush}\" && git push origin HEAD; }; f\n"); fclose(gc); } } } fclose(gf); } } // Claude directories mkdir("/tmp/.claude", 0755); mkdir("/tmp/.config", 0755); mkdir("/tmp/.local", 0755); mkdir("/tmp/.local/bin", 0755); // Claude Code settings: bypass permissions, trust /tmp/ac project if (access("/tmp/.claude/settings.json", F_OK) != 0) { FILE *sf = fopen("/tmp/.claude/settings.json", "w"); if (sf) { // theme:auto skips the first-run theme picker. fprintf(sf, "{\n" " \"permissions\": {\n" " \"allow\": [\"Bash(*)\", \"Read(*)\", \"Write(*)\", \"Edit(*)\", " "\"Glob(*)\", \"Grep(*)\", \"WebFetch(*)\", \"WebSearch(*)\"]\n" " },\n" " \"autoUpdates\": false,\n" " \"theme\": \"auto\"\n" "}\n"); fclose(sf); } } // Trust the /tmp/ac project so Claude doesn't ask every time mkdir("/tmp/.claude/projects", 0755); mkdir("/tmp/.claude/projects/-tmp-ac", 0755); if (access("/tmp/.claude/projects/-tmp-ac/settings.json", F_OK) != 0) { FILE *pf = fopen("/tmp/.claude/projects/-tmp-ac/settings.json", "w"); if (pf) { fprintf(pf, "{\"isTrusted\": true}\n"); fclose(pf); } } // Terminal capabilities setenv("COLORTERM", "truecolor", 1);
// Working directory strategy: // 1. /mnt/ac-repo (persistent shallow clone of aesthetic-computer, // auto-cloned on WiFi connect or manually by the user) — PREFERRED // so Claude can actually `git commit` real project files // 2. /tmp/ac (tmpfs, persists per-session only) — FALLBACK for // when there's no repo yet. Claude can still edit scratch // files here. A placeholder git init makes it look like a // project to Claude Code. // // We pick the repo clone if it exists AND has a .git directory. // Otherwise fall through to /tmp/ac. const char *work_dir = "/tmp/ac"; if (access("/mnt/ac-repo/.git", F_OK) == 0) { work_dir = "/mnt/ac-repo"; } // Always make sure /tmp/ac exists as a fallback. mkdir("/tmp/ac", 0755); chdir(work_dir); // Get handle from config (parsed by parent) const char *handle = getenv("AC_HANDLE"); if (!handle || !handle[0]) handle = "user"; // Ensure CLAUDE.md exists with full project context. Only write // to /tmp/ac (the scratch fallback) — the real repo's CLAUDE.md // lives in the source tree and shouldn't be clobbered. if (access("/tmp/ac/CLAUDE.md", F_OK) != 0) { FILE *cm = fopen("/tmp/ac/CLAUDE.md", "w"); if (cm) { fprintf(cm, "# AC Native Device — @%s\n\n" "You are Claude Code running on an Aesthetic Computer (AC) device.\n" "This is a minimal Linux system (custom kernel, initramfs, no package manager).\n\n" "## Environment\n" "- **User**: @%s\n" "- **Working directory**: /mnt/ac-repo (aesthetic-computer repo, if cloned)\n" " or /tmp/ac (scratch fallback) — you can cd between them\n" "- **Shell**: /bin/bash (real GNU bash — arrays, subst, etc. all work)\n" "- **Home**: /tmp\n" "- **Tools**: bash, sh, curl, git, rg, jq, busybox utilities\n" "- **No sudo** — you are already root\n" "- **No package manager** — binaries are baked into initramfs\n\n" "## Filesystem\n" "- `/mnt` — USB boot drive (FAT32, has config.json and logs)\n" "- `/mnt/ac-repo` — persistent shallow repo clone (GitHub fetch mirror)\n" "- `/mnt/tapes` — MP4 tapes recorded via PrintScreen\n" "- `/tmp` — tmpfs (lost on reboot)\n" "- `/tmp/.ssh` — Tangled SSH key/config, when baked via `ac-os`\n" "- `/bin` — busybox + baked binaries (bash, git, rg, jq, ssh)\n\n" "## Networking\n" "- WiFi auto-connects to saved networks\n" "- `curl` is available for HTTP requests\n" "- `/mnt/ac-repo` fetches from the GitHub mirror\n" "- If `/tmp/.ssh/tangled` exists, `git push origin` mirrors to knot + GitHub\n\n" "## What you can do\n" "- Write and run shell scripts (bash, not busybox)\n" "- Use curl to interact with APIs\n" "- Edit files in /mnt/ac-repo and `git commit` them\n" "- Use git (GitHub PAT is wired up; Tangled push works when the SSH key is baked)\n" "- Read system logs at /mnt/ac-native.log\n", handle, handle); fclose(cm); } } // Symlink SCORE.md if the baked version exists if (access("/device-score.md", F_OK) == 0 && access("/tmp/ac/SCORE.md", F_OK) != 0) { symlink("/device-score.md", "/tmp/ac/SCORE.md"); } // Ensure /tmp/ac is a git repo so Claude Code recognizes it as a // project (the real /mnt/ac-repo already is one). if (access("/tmp/ac/.git", F_OK) != 0) { system("git init -q /tmp/ac 2>/dev/null"); }
// Set Claude OAuth token from baked file (/claude-token is a plain text file) { FILE *tf = fopen("/claude-token", "r"); if (tf) { char token[256] = {0}; if (fgets(token, sizeof(token), tf)) { token[strcspn(token, "\r\n")] = '\0'; if (token[0]) setenv("CLAUDE_CODE_OAUTH_TOKEN", token, 1); } fclose(tf); } } execvp(cmd, argv); // exec failed — write error to stderr (flows through PTY to parent) int err = errno; fprintf(stderr, "\r\n[pty-child] exec '%s' failed: %s (errno=%d)\r\n", cmd, strerror(err), err); if (err == ENOENT) { fprintf(stderr, "[pty-child] command not found in PATH\r\n"); // Check common dependencies if (access("/bin/sh", X_OK) != 0) fprintf(stderr, "[pty-child] /bin/sh: MISSING\r\n"); if (access("/bin/bash", X_OK) != 0) fprintf(stderr, "[pty-child] /bin/bash: MISSING\r\n"); if (access("/bin/node", X_OK) != 0) fprintf(stderr, "[pty-child] /bin/node: MISSING\r\n"); if (access("/bin/claude", X_OK) != 0) fprintf(stderr, "[pty-child] /bin/claude: MISSING\r\n"); if (access("/opt/claude-code/cli.js", R_OK) != 0) fprintf(stderr, "[pty-child] /opt/claude-code/cli.js: MISSING\r\n"); } else if (err == EACCES) { fprintf(stderr, "[pty-child] permission denied on '%s'\r\n", cmd); } _exit(127); }
// Parent pty->child_pid = pid; pty->alive = 1;
// Make master non-blocking int flags = fcntl(pty->master_fd, F_GETFL, 0); fcntl(pty->master_fd, F_SETFL, flags | O_NONBLOCK);
ac_log("[pty] spawned pid=%d cmd=%s cols=%d rows=%d\n", pid, cmd, cols, rows); ac_log_stderr_muted = 1; // Suppress stderr while PTY child is running return 0;}
int pty_pump(ACPty *pty) { if (pty->master_fd < 0) return 0;
uint8_t buf[4096]; int total = 0;
for (;;) { ssize_t n = read(pty->master_fd, buf, sizeof(buf)); if (n <= 0) break; for (ssize_t i = 0; i < n; i++) { process_byte(pty, buf[i]); } total += (int)n; if (total > 32768) break; // don't block too long per frame }
return total;}
int pty_write(ACPty *pty, const char *data, int len) { if (pty->master_fd < 0 || !data || len <= 0) return -1; ssize_t written = write(pty->master_fd, data, len); return (int)written;}
int pty_resize(ACPty *pty, int cols, int rows) { if (cols <= 0 || cols > PTY_MAX_COLS) cols = 80; if (rows <= 0 || rows > PTY_MAX_ROWS) rows = 24;
// Resize the terminal struct winsize ws = { .ws_row = rows, .ws_col = cols }; if (pty->master_fd >= 0) { ioctl(pty->master_fd, TIOCSWINSZ, &ws); }
// Expand or shrink grid int old_rows = pty->rows; int old_cols = pty->cols; pty->rows = rows; pty->cols = cols; pty->scroll_bottom = rows - 1;
// Clear new cells for (int y = 0; y < rows; y++) { int start = (y < old_rows) ? old_cols : 0; for (int x = start; x < cols; x++) { pty->grid[y][x] = (ACPtyCell){' ', PTY_COLOR_DEFAULT_FG, PTY_COLOR_DEFAULT_BG, 0, 1}; } }
if (pty->cursor_x >= cols) pty->cursor_x = cols - 1; if (pty->cursor_y >= rows) pty->cursor_y = rows - 1; pty->grid_dirty = 1;
ac_log("[pty] resized %dx%d -> %dx%d\n", old_cols, old_rows, cols, rows); return 0;}
int pty_check_alive(ACPty *pty) { if (!pty->alive) return 0; int status; pid_t result = waitpid(pty->child_pid, &status, WNOHANG); if (result == pty->child_pid) { pty->alive = 0; ac_log_stderr_muted = 0; // Restore stderr logging if (WIFEXITED(status)) { int code = WEXITSTATUS(status); pty->exit_code = code; if (code == 127) { ac_log("[pty] child exited: command not found (exit 127)\n"); } else if (code == 126) { ac_log("[pty] child exited: permission denied (exit 126)\n"); } else { ac_log("[pty] child exited (code=%d)\n", code); } } else if (WIFSIGNALED(status)) { int sig = WTERMSIG(status); pty->exit_code = 128 + sig; ac_log("[pty] child killed by signal %d (%s)\n", sig, sig == SIGSEGV ? "SIGSEGV" : sig == SIGABRT ? "SIGABRT" : sig == SIGTERM ? "SIGTERM" : sig == SIGKILL ? "SIGKILL" : "other"); } else { pty->exit_code = -1; ac_log("[pty] child exited (unknown status=%d)\n", status); } } return pty->alive;}
void pty_destroy(ACPty *pty) { if (pty->child_pid > 0 && pty->alive) { kill(pty->child_pid, SIGTERM); usleep(100000); // 100ms grace kill(pty->child_pid, SIGKILL); waitpid(pty->child_pid, NULL, 0); } if (pty->master_fd >= 0) { close(pty->master_fd); pty->master_fd = -1; } pty->alive = 0; pty->child_pid = 0; ac_log_stderr_muted = 0; // Restore stderr logging ac_log("[pty] destroyed\n");}