Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

ac-native: link/login rename, lisp dispatch fix, claude theme=auto, raylib soft renderer

- web + native: add `link` piece to pair credentials with a device. Web `link`
generates a code, claims it with the current user, and keeps it on screen with
a live countdown. Native `link CODE` polls and applies config. `device-key`,
`device-code`, `login` kept as aliases.
- native prompt: strip `.lisp` from piece names returned by listPieces so
`grid` and other lisp pieces dispatch instead of falling through to the
KidLisp evaluator.
- claude code: set `theme: auto` in both settings.json bootstrap paths so
the first-run theme picker no longer appears on every launch.
- raylib: add raylib-devel to the builder, pkg-config-detected in the Makefile
(HAVE_RAYLIB), and a new raylib-soft.c that renders a test pattern via the
Image* CPU APIs into a painting's ARGB32 buffer. Exposed as
`system.raylibTest(painting, frame)` and demonstrated by `raylibtest.mjs`.

+663 -223
+1 -1
fedac/native/Dockerfile.builder
··· 17 17 curl jq file git tar xz findutils pkgconf-pkg-config zstd xorriso \ 18 18 bash \ 19 19 ca-certificates \ 20 - alsa-lib-devel libdrm-devel flite-devel SDL3-devel \ 20 + alsa-lib-devel libdrm-devel flite-devel SDL3-devel raylib-devel \ 21 21 ffmpeg-free-devel \ 22 22 wpa_supplicant dhcp-client iw \ 23 23 iwlwifi-mvm-firmware wireless-regdb \
+17
fedac/native/Makefile
··· 43 43 AV_LIBS := $(shell pkg-config --libs libavcodec libavformat libavutil libswscale libswresample 2>/dev/null) 44 44 endif 45 45 46 + # raylib — used purely as a CPU-side software rasterizer via the Image* APIs 47 + # (no GL/window context). Fedora's libraylib.so dlopens windowing deps at 48 + # runtime, so as long as we never call InitWindow() the binary stays clean. 49 + HAVE_RAYLIB := $(shell pkg-config --exists raylib 2>/dev/null && echo 1) 50 + ifeq ($(HAVE_RAYLIB),1) 51 + RAYLIB_CFLAGS := $(shell pkg-config --cflags raylib 2>/dev/null) 52 + RAYLIB_LIBS := $(shell pkg-config --libs raylib 2>/dev/null) 53 + endif 54 + 46 55 CFLAGS += $(DRM_CFLAGS) $(ALSA_CFLAGS) 47 56 LDFLAGS += $(DRM_LIBS) $(ALSA_LIBS) $(FLITE_LIBS) 48 57 ifeq ($(HAVE_AVCODEC),1) 49 58 CFLAGS += -DHAVE_AVCODEC $(AV_CFLAGS) 50 59 LDFLAGS += $(AV_LIBS) 60 + endif 61 + ifeq ($(HAVE_RAYLIB),1) 62 + CFLAGS += -DHAVE_RAYLIB $(RAYLIB_CFLAGS) 63 + LDFLAGS += $(RAYLIB_LIBS) 51 64 endif 52 65 53 66 # SDL3 GPU-accelerated display: always compiled, loaded via dlopen at runtime. ··· 88 101 $(SRCDIR)/js-bindings.c \ 89 102 $(SRCDIR)/audio-decode.c \ 90 103 $(SRCDIR)/recorder.c 104 + 105 + ifeq ($(HAVE_RAYLIB),1) 106 + SRCS += $(SRCDIR)/raylib-soft.c 107 + endif 91 108 92 109 # Wayland display backend (conditional) 93 110 ifdef USE_WAYLAND
+3 -1
fedac/native/initramfs/init
··· 73 73 # CLI resolves "sonnet" to the latest Sonnet family member, which 74 74 # is currently Sonnet 4.7. Users who want a specific pinned version 75 75 # can override by writing a different model string into this file. 76 - printf '{"permissions":{"allow":["Bash(*)","Read(*)","Write(*)","Edit(*)","Glob(*)","Grep(*)","WebFetch(*)","WebSearch(*)"]},"autoUpdates":false,"installMethod":"native","model":"sonnet"}\n' > /tmp/.claude/settings.json 76 + # `theme: auto` matches the terminal's light/dark mode AND skips the 77 + # first-run theme picker, so Claude Code launches straight to a session. 78 + printf '{"permissions":{"allow":["Bash(*)","Read(*)","Write(*)","Edit(*)","Glob(*)","Grep(*)","WebFetch(*)","WebSearch(*)"]},"autoUpdates":false,"installMethod":"native","model":"sonnet","theme":"auto"}\n' > /tmp/.claude/settings.json 77 79 fi 78 80 if [ -f /tangled-key ] || [ -f /tangled-ssh-config ] || [ -f /tangled-known-hosts ]; then 79 81 mkdir -p /tmp/.ssh
+183
fedac/native/pieces/link.mjs
··· 1 + // link.mjs — AC Native identity pairing piece 2 + // Usage: `link ABCD12` (link code generated on aesthetic.computer with `link`) 3 + // 4 + // Flow: 5 + // 1. User types `link` on aesthetic.computer (web) — code is shown on screen 6 + // 2. On the device, types `link ABCD12` 7 + // 3. Device polls device-pair API with that code 8 + // 4. Gets back handle + tokens, writes config.json, reboots 9 + 10 + const API_BASE = "https://aesthetic.computer/.netlify/functions/device-pair"; 11 + let state = "init"; // init | polling | success | error 12 + let pairCode = ""; 13 + let message = ""; 14 + let frame = 0; 15 + let pollAttempts = 0; 16 + let newHandle = ""; 17 + let successFrame = 0; 18 + 19 + function boot({ system, params, colon }) { 20 + // Get code from params: `link ABCD12` or `link:ABCD12` 21 + const code = params?.[0] || colon?.[0] || ""; 22 + if (!code || code.length < 4) { 23 + state = "error"; 24 + message = "usage: link CODE"; 25 + return; 26 + } 27 + pairCode = code.toUpperCase(); 28 + state = "polling"; 29 + pollAttempts = 0; 30 + system?.fetch?.(API_BASE + "?code=" + pairCode); 31 + } 32 + 33 + function act({ event: e, system }) { 34 + if (!e.is("keyboard:down")) return; 35 + 36 + if (e.is("keyboard:down:escape") || e.is("keyboard:down:backspace")) { 37 + if (state !== "success") { 38 + system?.jump?.("prompt"); 39 + } 40 + } 41 + 42 + // Retry on error 43 + if (state === "error" && (e.is("keyboard:down:enter") || e.is("keyboard:down:return"))) { 44 + if (pairCode) { 45 + state = "polling"; 46 + pollAttempts = 0; 47 + system?.fetch?.(API_BASE + "?code=" + pairCode); 48 + } else { 49 + system?.jump?.("prompt"); 50 + } 51 + } 52 + } 53 + 54 + function paint({ wipe, ink, write, screen, system, sound }) { 55 + frame++; 56 + const T = __theme.update(); 57 + const h = screen.height; 58 + const pad = 10; 59 + const font = "font_1"; 60 + 61 + wipe(T.bg[0], T.bg[1], T.bg[2]); 62 + 63 + // Title 64 + ink(T.fg, T.fg + 10, T.fg); 65 + write("link", { x: pad, y: 10, size: 2, font: "matrix" }); 66 + 67 + // Current identity 68 + let currentHandle = ""; 69 + try { 70 + const raw = system?.readFile?.("/mnt/config.json"); 71 + if (raw) currentHandle = JSON.parse(raw).handle || ""; 72 + } catch (_) {} 73 + if (currentHandle) { 74 + ink(T.fgMute, T.fgMute + 10, T.fgMute); 75 + write("current: @" + currentHandle, { x: pad, y: 34, size: 1, font }); 76 + } 77 + 78 + const stateY = 52; 79 + 80 + // Poll for claimed credentials 81 + if (state === "polling") { 82 + const dots = ".".repeat((Math.floor(frame / 15) % 3) + 1); 83 + ink(120, 180, 255); 84 + write("checking code" + dots, { x: pad, y: stateY, size: 1, font }); 85 + ink(255, 255, 255); 86 + write(pairCode, { x: pad, y: stateY + 16, size: 2, font: "matrix" }); 87 + 88 + if (system?.fetchResult !== undefined && system?.fetchResult !== null) { 89 + try { 90 + const data = JSON.parse(system.fetchResult); 91 + if (data.status === "claimed" && data.handle) { 92 + // Success! Write new config 93 + newHandle = data.handle; 94 + const cfg = { 95 + handle: data.handle, 96 + piece: "notepat", 97 + sub: data.sub || "", 98 + email: data.email || "", 99 + token: data.token || "", 100 + }; 101 + if (data.claudeToken) cfg.claudeToken = data.claudeToken; 102 + if (data.githubPat) cfg.githubPat = data.githubPat; 103 + 104 + // Write main config 105 + system?.writeFile?.("/mnt/config.json", JSON.stringify(cfg)); 106 + 107 + // Write device tokens 108 + if (data.claudeToken) system?.writeFile?.("/claude-token", data.claudeToken); 109 + if (data.githubPat) system?.writeFile?.("/github-pat", data.githubPat); 110 + 111 + state = "success"; 112 + successFrame = frame; 113 + sound?.synth?.({ type: "sine", tone: 660, duration: 0.15, volume: 0.12, attack: 0.005, decay: 0.1 }); 114 + } else if (data.status === "pending") { 115 + // Not claimed yet — keep polling 116 + pollAttempts++; 117 + if (pollAttempts >= 60) { 118 + state = "error"; 119 + message = "code expired — generate a new one"; 120 + } 121 + } else if (data.message) { 122 + state = "error"; 123 + message = data.message; 124 + } else { 125 + state = "error"; 126 + message = "code not found or expired"; 127 + } 128 + } catch (err) { 129 + state = "error"; 130 + message = "bad response from server"; 131 + } 132 + } 133 + if (system?.fetchError) { 134 + state = "error"; 135 + message = "network error — check wifi"; 136 + } 137 + 138 + // Re-poll every ~2 seconds (120 frames at 60fps) if still pending 139 + if (state === "polling" && frame % 120 === 0 && pollAttempts > 0) { 140 + system?.fetch?.(API_BASE + "?code=" + pairCode); 141 + } 142 + } 143 + 144 + // Success — reboot 145 + if (state === "success") { 146 + ink(80, 255, 120); 147 + write("linked as @" + newHandle, { x: pad, y: stateY, size: 2, font: "matrix" }); 148 + 149 + const elapsed = frame - successFrame; 150 + if (elapsed < 120) { 151 + ink(200, 200, 100); 152 + write("rebooting in " + Math.ceil((120 - elapsed) / 60) + "...", { x: pad, y: stateY + 24, size: 1, font }); 153 + } else { 154 + system?.reboot?.(); 155 + } 156 + } 157 + 158 + // Error state 159 + if (state === "error") { 160 + ink(255, 80, 80); 161 + write("error", { x: pad, y: stateY, size: 1, font }); 162 + ink(200, 120, 100); 163 + write(message, { x: pad, y: stateY + 14, size: 1, font }); 164 + ink(T.fgMute); 165 + if (pairCode) { 166 + write("enter: retry esc: back", { x: pad, y: stateY + 30, size: 1, font }); 167 + } else { 168 + write("on aesthetic.computer, type: link", { x: pad, y: stateY + 30, size: 1, font }); 169 + write("then here: link CODE", { x: pad, y: stateY + 44, size: 1, font }); 170 + write("esc: back", { x: pad, y: stateY + 60, size: 1, font }); 171 + } 172 + } 173 + 174 + // Bottom hint 175 + if (state !== "success") { 176 + ink(T.fgMute, T.fgMute + 10, T.fgMute); 177 + write("esc: back", { x: pad, y: h - 12, size: 1, font }); 178 + } 179 + } 180 + 181 + function sim() {} 182 + 183 + export { boot, paint, act, sim };
+4 -183
fedac/native/pieces/login.mjs
··· 1 - // login.mjs — AC Native identity switching piece 2 - // Usage: `login ABCD12` (device key from aesthetic.computer `device-key` command) 3 - // 4 - // Flow: 5 - // 1. User generates a device key on their phone (web prompt: `device-key`) 6 - // 2. On native device, types `login ABCD12` 7 - // 3. Device polls device-pair API with that code 8 - // 4. Gets back handle + tokens, writes config.json, reboots 9 - 10 - const API_BASE = "https://aesthetic.computer/.netlify/functions/device-pair"; 11 - let state = "init"; // init | polling | success | error 12 - let pairCode = ""; 13 - let message = ""; 14 - let frame = 0; 15 - let pollAttempts = 0; 16 - let newHandle = ""; 17 - let successFrame = 0; 18 - 19 - function boot({ system, params, colon }) { 20 - // Get code from params: `login ABCD12` or `login:ABCD12` 21 - const code = params?.[0] || colon?.[0] || ""; 22 - if (!code || code.length < 4) { 23 - state = "error"; 24 - message = "usage: login CODE"; 25 - return; 26 - } 27 - pairCode = code.toUpperCase(); 28 - state = "polling"; 29 - pollAttempts = 0; 30 - system?.fetch?.(API_BASE + "?code=" + pairCode); 31 - } 32 - 33 - function act({ event: e, system, params, colon }) { 34 - if (!e.is("keyboard:down")) return; 35 - 36 - if (e.is("keyboard:down:escape") || e.is("keyboard:down:backspace")) { 37 - if (state !== "success") { 38 - system?.jump?.("prompt"); 39 - } 40 - } 41 - 42 - // Retry on error 43 - if (state === "error" && (e.is("keyboard:down:enter") || e.is("keyboard:down:return"))) { 44 - if (pairCode) { 45 - state = "polling"; 46 - pollAttempts = 0; 47 - system?.fetch?.(API_BASE + "?code=" + pairCode); 48 - } else { 49 - system?.jump?.("prompt"); 50 - } 51 - } 52 - } 53 - 54 - function paint({ wipe, ink, write, screen, system, sound }) { 55 - frame++; 56 - const T = __theme.update(); 57 - const w = screen.width, h = screen.height; 58 - const pad = 10; 59 - const font = "font_1"; 60 - 61 - wipe(T.bg[0], T.bg[1], T.bg[2]); 62 - 63 - // Title 64 - ink(T.fg, T.fg + 10, T.fg); 65 - write("login", { x: pad, y: 10, size: 2, font: "matrix" }); 66 - 67 - // Current identity 68 - let currentHandle = ""; 69 - try { 70 - const raw = system?.readFile?.("/mnt/config.json"); 71 - if (raw) currentHandle = JSON.parse(raw).handle || ""; 72 - } catch (_) {} 73 - if (currentHandle) { 74 - ink(T.fgMute, T.fgMute + 10, T.fgMute); 75 - write("current: @" + currentHandle, { x: pad, y: 34, size: 1, font }); 76 - } 77 - 78 - const stateY = 52; 79 - 80 - // Poll for claimed credentials 81 - if (state === "polling") { 82 - const dots = ".".repeat((Math.floor(frame / 15) % 3) + 1); 83 - ink(120, 180, 255); 84 - write("checking key" + dots, { x: pad, y: stateY, size: 1, font }); 85 - ink(255, 255, 255); 86 - write(pairCode, { x: pad, y: stateY + 16, size: 2, font: "matrix" }); 87 - 88 - if (system?.fetchResult !== undefined && system?.fetchResult !== null) { 89 - try { 90 - const data = JSON.parse(system.fetchResult); 91 - if (data.status === "claimed" && data.handle) { 92 - // Success! Write new config 93 - newHandle = data.handle; 94 - const cfg = { 95 - handle: data.handle, 96 - piece: "notepat", 97 - sub: data.sub || "", 98 - email: data.email || "", 99 - token: data.token || "", 100 - }; 101 - if (data.claudeToken) cfg.claudeToken = data.claudeToken; 102 - if (data.githubPat) cfg.githubPat = data.githubPat; 103 - 104 - // Write main config 105 - system?.writeFile?.("/mnt/config.json", JSON.stringify(cfg)); 106 - 107 - // Write device tokens 108 - if (data.claudeToken) system?.writeFile?.("/claude-token", data.claudeToken); 109 - if (data.githubPat) system?.writeFile?.("/github-pat", data.githubPat); 110 - 111 - state = "success"; 112 - successFrame = frame; 113 - sound?.synth?.({ type: "sine", tone: 660, duration: 0.15, volume: 0.12, attack: 0.005, decay: 0.1 }); 114 - } else if (data.status === "pending") { 115 - // Not claimed yet — keep polling 116 - pollAttempts++; 117 - if (pollAttempts >= 60) { 118 - state = "error"; 119 - message = "key expired — generate a new one"; 120 - } 121 - } else if (data.message) { 122 - state = "error"; 123 - message = data.message; 124 - } else { 125 - state = "error"; 126 - message = "key not found or expired"; 127 - } 128 - } catch (err) { 129 - state = "error"; 130 - message = "bad response from server"; 131 - } 132 - } 133 - if (system?.fetchError) { 134 - state = "error"; 135 - message = "network error — check wifi"; 136 - } 137 - 138 - // Re-poll every ~2 seconds (120 frames at 60fps) if still pending 139 - if (state === "polling" && frame % 120 === 0 && pollAttempts > 0) { 140 - system?.fetch?.(API_BASE + "?code=" + pairCode); 141 - } 142 - } 143 - 144 - // Success — reboot 145 - if (state === "success") { 146 - ink(80, 255, 120); 147 - write("logged in as @" + newHandle, { x: pad, y: stateY, size: 2, font: "matrix" }); 148 - 149 - const elapsed = frame - successFrame; 150 - if (elapsed < 120) { 151 - ink(200, 200, 100); 152 - write("rebooting in " + Math.ceil((120 - elapsed) / 60) + "...", { x: pad, y: stateY + 24, size: 1, font }); 153 - } else { 154 - system?.reboot?.(); 155 - } 156 - } 157 - 158 - // Error state 159 - if (state === "error") { 160 - ink(255, 80, 80); 161 - write("error", { x: pad, y: stateY, size: 1, font }); 162 - ink(200, 120, 100); 163 - write(message, { x: pad, y: stateY + 14, size: 1, font }); 164 - ink(T.fgMute); 165 - if (pairCode) { 166 - write("enter: retry esc: back", { x: pad, y: stateY + 30, size: 1, font }); 167 - } else { 168 - write("on your phone, type: device-key", { x: pad, y: stateY + 30, size: 1, font }); 169 - write("then here: login CODE", { x: pad, y: stateY + 44, size: 1, font }); 170 - write("esc: back", { x: pad, y: stateY + 60, size: 1, font }); 171 - } 172 - } 173 - 174 - // Bottom hint 175 - if (state !== "success") { 176 - ink(T.fgMute, T.fgMute + 10, T.fgMute); 177 - write("esc: back", { x: pad, y: h - 12, size: 1, font }); 178 - } 179 - } 180 - 181 - function sim() {} 182 - 183 - export { boot, paint, act, sim }; 1 + // login.mjs — legacy alias for link.mjs. 2 + // `link` is the canonical command; this re-export preserves any cached 3 + // references to the old `login` piece. 4 + export { boot, paint, act, sim } from "./link.mjs";
+13 -7
fedac/native/pieces/prompt.mjs
··· 22 22 let PIECE_NAMES = []; 23 23 // Built-in non-piece commands 24 24 const BUILTIN_COMMANDS = [ 25 - "version", "reboot", "off", "clear", "help", "ssh", "hi", "bye", "ls", "papers", "login", "midi", 25 + "version", "reboot", "off", "clear", "help", "ssh", "hi", "bye", "ls", "papers", "link", "login", "midi", 26 26 ]; 27 27 // All completable commands (built in boot) 28 28 let COMMANDS = []; ··· 55 55 "print": "printer / thermal", 56 56 "theme": "prompt theme", 57 57 "voice": "system voice", 58 - "login": "switch identity", 58 + "link": "pair this device with an account", 59 + "login": "→ link", 60 + "raylibtest": "raylib software renderer test", 59 61 "midi": "usb midi + udp relay", 60 62 "dark": "dark mode", 61 63 "light": "light mode", ··· 176 178 voiceOff = cfg.voice === "off"; 177 179 } 178 180 } catch (_) {} 179 - // Discover all available pieces dynamically 180 - PIECE_NAMES = (system?.listPieces?.() || []).filter(n => n !== "prompt" && n !== "lisp" && n !== "cc"); 181 + // Discover all available pieces dynamically. 182 + // listPieces returns .lisp files with the extension intact (so list.mjs can 183 + // bucket them); for prompt dispatch we want bare command names. 184 + PIECE_NAMES = (system?.listPieces?.() || []) 185 + .map(n => (n.endsWith && n.endsWith(".lisp")) ? n.slice(0, -5) : n) 186 + .filter(n => n !== "prompt" && n !== "lisp" && n !== "cc"); 181 187 PIECE_NAMES.sort(); 182 188 COMMANDS = [...PIECE_NAMES, ...BUILTIN_COMMANDS, ...CODE_NAMES]; 183 189 // Restore input from KidLisp return (backspace/escape preserves source) ··· 324 330 system?.poweroff?.(); 325 331 return; 326 332 } 327 - if (baseWord === "login") { 333 + if (baseWord === "link" || baseWord === "login") { 328 334 const code = cmd.slice(spaceIdx > 0 ? spaceIdx + 1 : cmd.length).trim(); 329 - message = "~> login"; 335 + message = "~> link"; 330 336 messageFrame = 0; 331 - system?.jump?.(code ? "login:" + code : "login"); 337 + system?.jump?.(code ? "link:" + code : "link"); 332 338 return; 333 339 } 334 340 if (lower === "clear") {
+62
fedac/native/pieces/raylibtest.mjs
··· 1 + // raylibtest.mjs — Smoke test for the raylib software-rendering bridge. 2 + // Allocates an off-screen painting buffer, asks ac-native to fill it via 3 + // raylib's Image* APIs, and pastes it onto the screen each frame. 4 + 5 + let canvas = null; 6 + let cw = 0, ch = 0; 7 + let frame = 0; 8 + let supported = true; 9 + let lastResult = true; 10 + 11 + function boot({ screen, painting }) { 12 + cw = Math.min(screen.width, 320); 13 + ch = Math.min(screen.height, 240); 14 + canvas = painting(cw, ch, (p) => p.wipe(0, 0, 0)); 15 + if (typeof globalThis.system?.raylibTest !== "function") { 16 + supported = false; 17 + } 18 + } 19 + 20 + function paint({ wipe, ink, write, screen, paste, system }) { 21 + frame++; 22 + const T = __theme.update(); 23 + wipe(T.bg[0], T.bg[1], T.bg[2]); 24 + 25 + if (!supported) { 26 + ink(255, 100, 100); 27 + write("raylib not compiled in", { x: 8, y: 12, size: 1, font: "font_1" }); 28 + ink(T.fgMute); 29 + write("rebuild ac-native with raylib-devel installed", { 30 + x: 8, y: 28, size: 1, font: "font_1", 31 + }); 32 + return; 33 + } 34 + 35 + if (canvas) { 36 + lastResult = system?.raylibTest?.(canvas, frame) === true; 37 + } 38 + 39 + if (!lastResult) { 40 + ink(255, 160, 80); 41 + write("raylib call failed", { x: 8, y: 12, size: 1, font: "font_1" }); 42 + return; 43 + } 44 + 45 + const ox = ((screen.width - cw) / 2) | 0; 46 + const oy = ((screen.height - ch) / 2) | 0; 47 + paste(canvas, ox, oy); 48 + 49 + // Tag the output so it's visually distinct from native primitives. 50 + ink(T.fg, T.fg + 10, T.fg); 51 + write("raylibtest", { x: 8, y: screen.height - 12, size: 1, font: "font_1" }); 52 + } 53 + 54 + function act({ event: e, system }) { 55 + if (e.is("keyboard:down:escape") || e.is("keyboard:down:backspace")) { 56 + system?.jump?.("prompt"); 57 + } 58 + } 59 + 60 + function sim() {} 61 + 62 + export { boot, paint, act, sim };
+29
fedac/native/src/js-bindings.c
··· 20 20 #include <errno.h> 21 21 #include "qrcodegen.h" 22 22 #include <alsa/asoundlib.h> 23 + #ifdef HAVE_RAYLIB 24 + #include "raylib-soft.h" 25 + #endif 23 26 24 27 // Defined in ac-native.c — logs to USB mount 25 28 extern void ac_log(const char *fmt, ...); ··· 2626 2629 2627 2630 graph_paste(current_rt->graph, src, dx, dy); 2628 2631 return JS_UNDEFINED; 2632 + } 2633 + 2634 + // system.raylibTest(painting, frame?) — fill a painting buffer with a 2635 + // raylib-rendered test pattern (CPU-side software path). Returns true on 2636 + // success, false when raylib isn't compiled in or the painting is invalid. 2637 + static JSValue js_raylib_test(JSContext *ctx, JSValueConst this_val, 2638 + int argc, JSValueConst *argv) { 2639 + (void)this_val; 2640 + if (argc < 1) return JS_FALSE; 2641 + ACFramebuffer *fb = JS_GetOpaque(argv[0], painting_class_id); 2642 + if (!fb || !fb->pixels) return JS_FALSE; 2643 + int frame = 0; 2644 + if (argc >= 2) JS_ToInt32(ctx, &frame, argv[1]); 2645 + #ifdef HAVE_RAYLIB 2646 + int rc = raylib_soft_test(fb->pixels, fb->width, fb->height, fb->stride, frame); 2647 + return rc == 0 ? JS_TRUE : JS_FALSE; 2648 + #else 2649 + (void)fb; (void)frame; 2650 + return JS_FALSE; 2651 + #endif 2629 2652 } 2630 2653 2631 2654 // sound.bpm(val?) — get or set BPM, returns current value ··· 7075 7098 // system.listPieces() — scan /pieces/*.mjs and return name array 7076 7099 JS_SetPropertyStr(ctx, sys, "listPieces", 7077 7100 JS_NewCFunction(ctx, js_list_pieces, "listPieces", 0)); 7101 + 7102 + // system.raylibTest(painting, frame?) — fill a painting buffer using 7103 + // raylib's software (Image*) APIs. Returns false when raylib isn't 7104 + // compiled in, so pieces can probe availability. 7105 + JS_SetPropertyStr(ctx, sys, "raylibTest", 7106 + JS_NewCFunction(ctx, js_raylib_test, "raylibTest", 2)); 7078 7107 7079 7108 // Printer detection and raw printing 7080 7109 JS_SetPropertyStr(ctx, sys, "listPrinters",
+3 -1
fedac/native/src/pty.c
··· 659 659 if (access("/tmp/.claude/settings.json", F_OK) != 0) { 660 660 FILE *sf = fopen("/tmp/.claude/settings.json", "w"); 661 661 if (sf) { 662 + // theme:auto skips the first-run theme picker. 662 663 fprintf(sf, "{\n" 663 664 " \"permissions\": {\n" 664 665 " \"allow\": [\"Bash(*)\", \"Read(*)\", \"Write(*)\", \"Edit(*)\", " 665 666 "\"Glob(*)\", \"Grep(*)\", \"WebFetch(*)\", \"WebSearch(*)\"]\n" 666 667 " },\n" 667 - " \"autoUpdates\": false\n" 668 + " \"autoUpdates\": false,\n" 669 + " \"theme\": \"auto\"\n" 668 670 "}\n"); 669 671 fclose(sf); 670 672 }
+78
fedac/native/src/raylib-soft.c
··· 1 + // raylib-soft.c — software rendering bridge into AC native's ARGB32 buffers. 2 + // 3 + // Built only when pkg-config finds raylib (HAVE_RAYLIB). Uses the Image* 4 + // CPU APIs exclusively, so no GL context, no InitWindow, and no windowing 5 + // libraries are touched at runtime — Fedora's libraylib.so dlopens those 6 + // only when its windowing path is exercised. 7 + 8 + #include "raylib-soft.h" 9 + 10 + #include <math.h> 11 + #include <stdint.h> 12 + #include <string.h> 13 + 14 + #include <raylib.h> 15 + 16 + int raylib_soft_test(uint32_t *dst, int width, int height, 17 + int stride_pixels, int frame) { 18 + if (!dst || width <= 0 || height <= 0 || stride_pixels < width) { 19 + return -1; 20 + } 21 + 22 + // Render into a temporary RGBA8 image so we never touch GPU paths. 23 + Image img = GenImageColor(width, height, (Color){ 12, 14, 22, 255 }); 24 + if (!img.data) return -1; 25 + 26 + // Animated diagonal stripes — clear visual confirmation that pixels 27 + // came out of raylib rather than ac-native's own primitives. 28 + float t = (float)frame * 0.05f; 29 + for (int y = 0; y < height; y++) { 30 + int phase = (int)(y + sinf(t) * 24.0f); 31 + for (int x = 0; x < width; x++) { 32 + int v = (x + phase) & 31; 33 + if (v < 6) { 34 + Color c = { 35 + (unsigned char)(60 + v * 12), 36 + (unsigned char)(40 + ((y * 255) / height)), 37 + (unsigned char)(120 + ((x * 100) / width)), 38 + 255, 39 + }; 40 + ImageDrawPixel(&img, x, y, c); 41 + } 42 + } 43 + } 44 + 45 + // Centerpiece: a wobbling circle + crosshair to show shape primitives. 46 + int cx = width / 2; 47 + int cy = height / 2; 48 + int r = (width < height ? width : height) / 5; 49 + int wobble = (int)(sinf(t * 1.7f) * (r * 0.25f)); 50 + ImageDrawCircle(&img, cx + wobble, cy, r, 51 + (Color){ 255, 200, 80, 255 }); 52 + ImageDrawCircleLines(&img, cx + wobble, cy, r + 4, 53 + (Color){ 255, 255, 255, 255 }); 54 + ImageDrawLine(&img, 0, cy, width - 1, cy, 55 + (Color){ 255, 100, 100, 180 }); 56 + ImageDrawLine(&img, cx, 0, cx, height - 1, 57 + (Color){ 100, 255, 100, 180 }); 58 + 59 + // Banner — uses raylib's bundled default font, also pure CPU. 60 + ImageDrawText(&img, "raylib soft", 8, 8, 20, 61 + (Color){ 230, 230, 255, 255 }); 62 + 63 + // Convert to RGBA8 (no-op if already, but cheap insurance) and copy 64 + // into the caller's ARGB32 buffer with stride. 65 + ImageFormat(&img, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); 66 + const unsigned char *src = (const unsigned char *)img.data; 67 + for (int y = 0; y < height; y++) { 68 + uint32_t *row = dst + (y * stride_pixels); 69 + for (int x = 0; x < width; x++) { 70 + const unsigned char *p = src + ((y * width + x) * 4); 71 + uint32_t r8 = p[0], g8 = p[1], b8 = p[2], a8 = p[3]; 72 + row[x] = (a8 << 24) | (r8 << 16) | (g8 << 8) | b8; 73 + } 74 + } 75 + 76 + UnloadImage(img); 77 + return 0; 78 + }
+14
fedac/native/src/raylib-soft.h
··· 1 + #ifndef AC_RAYLIB_SOFT_H 2 + #define AC_RAYLIB_SOFT_H 3 + 4 + #include <stdint.h> 5 + 6 + // Render a small test pattern using raylib's Image* CPU APIs (no GL/window 7 + // context required). Writes ARGB32 pixels (packed 0xAARRGGBB) into `dst`, 8 + // honoring `stride_pixels` for rows. Returns 0 on success, -1 on bad args. 9 + // 10 + // `frame` lets the pattern animate over time when called repeatedly. 11 + int raylib_soft_test(uint32_t *dst, int width, int height, 12 + int stride_pixels, int frame); 13 + 14 + #endif
+2 -1
fedac/native/wasm/build-offline-page.mjs
··· 733 733 pieces.lisp = makeStubPiece("lisp", () => "KidLisp eval is not wired into the browser host yet."); 734 734 pieces.list = makeStubPiece("list", ({ system }) => "Available offline pieces: " + system.listPieces().join(", ")); 735 735 pieces.claude = makeStubPiece("claude", () => "Claude integration is native-only for now."); 736 - pieces.login = makeStubPiece("login", ({ params }) => params[0] ? "Login code: " + params[0] : "No login code provided."); 736 + pieces.link = makeStubPiece("link", ({ params }) => params[0] ? "Link code: " + params[0] : "No link code provided."); 737 + pieces.login = pieces.link; // legacy alias 737 738 738 739 const promptUrl = URL.createObjectURL(new Blob([PROMPT_SOURCE], { type: "text/javascript" })); 739 740 const promptPiece = await import(promptUrl);
+246
system/public/aesthetic.computer/disks/link.mjs
··· 1 + // Link, 2026.04.25 2 + // Pair this account with an aesthetic computer device. 3 + // Generates a 6-char code, claims it with the current user's identity, 4 + // and keeps it on screen until the device fetches it (or it expires). 5 + 6 + const API_URL = "/.netlify/functions/device-pair"; 7 + const CODE_TTL_MS = 10 * 60 * 1000; // matches device-pair TTL (600s) 8 + 9 + let code = null; 10 + let userHandle = null; 11 + let expiresAt = 0; 12 + let status = "init"; // init | requesting | ready | expired | error | unauthed 13 + let errorMsg = ""; 14 + let frame = 0; 15 + let copiedFrame = -9999; 16 + let newBtn = null; 17 + let copyBtn = null; 18 + let signInBtn = null; 19 + 20 + async function generate(net) { 21 + status = "requesting"; 22 + errorMsg = ""; 23 + code = null; 24 + try { 25 + const createRes = await fetch(API_URL, { 26 + method: "POST", 27 + headers: { "Content-Type": "application/json" }, 28 + body: JSON.stringify({ action: "create" }), 29 + }); 30 + const createData = await createRes.json(); 31 + if (!createData.code) { 32 + throw new Error(createData.message || "create failed"); 33 + } 34 + 35 + const claimRes = await net.userRequest("POST", API_URL, { 36 + action: "claim", 37 + code: createData.code, 38 + }); 39 + if (!claimRes?.handle) { 40 + throw new Error(claimRes?.message || "claim failed"); 41 + } 42 + 43 + code = createData.code; 44 + expiresAt = Date.now() + CODE_TTL_MS; 45 + status = "ready"; 46 + console.log(`🔗 Link code ${code} (10 min)`); 47 + } catch (err) { 48 + status = "error"; 49 + errorMsg = (err && err.message) || "network error"; 50 + } 51 + } 52 + 53 + async function boot({ net, handle, user, hud }) { 54 + hud?.labelBack?.(); 55 + userHandle = handle?.() || null; 56 + const signedIn = !!(userHandle || user?.email); 57 + if (!signedIn) { 58 + status = "unauthed"; 59 + return; 60 + } 61 + await generate(net); 62 + } 63 + 64 + function fmtCountdown(ms) { 65 + const total = Math.max(0, ms); 66 + const mm = Math.floor(total / 60000); 67 + const ss = Math.floor((total % 60000) / 1000) 68 + .toString() 69 + .padStart(2, "0"); 70 + return `${mm}:${ss}`; 71 + } 72 + 73 + function paint({ wipe, ink, write, screen, ui }) { 74 + frame++; 75 + wipe(12, 14, 22); 76 + 77 + const cx = (screen.width / 2) | 0; 78 + let y = 18; 79 + 80 + ink(170, 200, 255).write("link", { center: "x", y, size: 2, screen }); 81 + y += 28; 82 + ink(140, 150, 180).write( 83 + "pair this account with a device", 84 + { center: "x", y, screen }, 85 + ); 86 + y += 16; 87 + 88 + if (userHandle) { 89 + ink(120, 180, 255).write(`@${userHandle}`, { center: "x", y, screen }); 90 + y += 16; 91 + } 92 + 93 + const codeY = Math.max(y + 40, ((screen.height / 2) | 0) - 20); 94 + 95 + if (status === "unauthed") { 96 + ink(255, 160, 100).write("sign in first", { 97 + center: "x", 98 + y: codeY, 99 + screen, 100 + }); 101 + ink(180, 180, 200).write( 102 + "log in on the prompt to generate a link code", 103 + { center: "x", y: codeY + 18, screen }, 104 + ); 105 + 106 + if (!signInBtn) signInBtn = new ui.TextButton("Sign in"); 107 + signInBtn.reposition({ center: "x", y: codeY + 44, screen }); 108 + signInBtn.paint( 109 + { ink, box: (b) => ink().box(b) }, 110 + [ 111 + [60, 90, 140], 112 + [120, 160, 220], 113 + 255, 114 + ], 115 + ); 116 + return; 117 + } 118 + 119 + if (status === "requesting") { 120 + const dots = ".".repeat(((frame / 15) | 0) % 4); 121 + ink(180, 200, 255).write(`requesting code${dots}`, { 122 + center: "x", 123 + y: codeY, 124 + screen, 125 + }); 126 + return; 127 + } 128 + 129 + if (status === "error") { 130 + ink(255, 110, 110).write("error", { 131 + center: "x", 132 + y: codeY, 133 + screen, 134 + }); 135 + ink(220, 160, 160).write(errorMsg || "unknown", { 136 + center: "x", 137 + y: codeY + 18, 138 + screen, 139 + }); 140 + } 141 + 142 + if (status === "ready" || status === "expired") { 143 + const c = code || "------"; 144 + const expired = status === "expired"; 145 + 146 + if (expired) { 147 + ink(140, 100, 100).write(c, { 148 + center: "x", 149 + y: codeY, 150 + size: 3, 151 + screen, 152 + }); 153 + ink(255, 140, 140).write("expired", { 154 + center: "x", 155 + y: codeY + 40, 156 + screen, 157 + }); 158 + } else { 159 + // Pulsing green for the code so it reads as live. 160 + const pulse = 200 + (((Math.sin(frame / 20) + 1) / 2) * 55) | 0; 161 + ink(120, pulse, 200).write(c, { 162 + center: "x", 163 + y: codeY, 164 + size: 3, 165 + screen, 166 + }); 167 + ink(170, 200, 220).write( 168 + `expires in ${fmtCountdown(expiresAt - Date.now())}`, 169 + { center: "x", y: codeY + 40, screen }, 170 + ); 171 + ink(140, 150, 180).write(`on the device: link ${c}`, { 172 + center: "x", 173 + y: codeY + 58, 174 + screen, 175 + }); 176 + } 177 + } 178 + 179 + // Action buttons. 180 + if (!newBtn) newBtn = new ui.TextButton("New code"); 181 + newBtn.reposition({ x: 8, bottom: 8, screen }); 182 + const newColor = [ 183 + [20, 60, 120], 184 + [60, 140, 220], 185 + 255, 186 + ]; 187 + newBtn.paint({ ink, box: (b) => ink().box(b) }, newColor); 188 + 189 + if (status === "ready" && code) { 190 + if (!copyBtn) copyBtn = new ui.TextButton("Copy"); 191 + copyBtn.reposition({ right: 8, bottom: 8, screen }); 192 + const justCopied = frame - copiedFrame < 90; 193 + const copyColor = justCopied 194 + ? [ 195 + [40, 120, 40], 196 + [80, 200, 80], 197 + 255, 198 + ] 199 + : newColor; 200 + copyBtn.paint({ ink, box: (b) => ink().box(b) }, copyColor); 201 + if (justCopied) { 202 + ink(180, 255, 180).write("copied", { 203 + center: "x", 204 + y: screen.height - 28, 205 + screen, 206 + }); 207 + } 208 + } 209 + } 210 + 211 + function sim() { 212 + if (status === "ready" && expiresAt && Date.now() > expiresAt) { 213 + status = "expired"; 214 + } 215 + } 216 + 217 + function act({ event: e, net, jump }) { 218 + if (status === "unauthed") { 219 + signInBtn?.act(e, () => { 220 + jump("prompt"); 221 + }); 222 + return; 223 + } 224 + newBtn?.act(e, () => { 225 + generate(net); 226 + }); 227 + if (status === "ready" && code) { 228 + copyBtn?.act(e, () => { 229 + try { 230 + navigator.clipboard?.writeText?.(code); 231 + copiedFrame = frame; 232 + } catch (_) { 233 + /* clipboard unavailable */ 234 + } 235 + }); 236 + } 237 + } 238 + 239 + function meta() { 240 + return { 241 + title: "Link", 242 + desc: "Pair this account with an aesthetic computer device.", 243 + }; 244 + } 245 + 246 + export { boot, paint, sim, act, meta };
+8 -29
system/public/aesthetic.computer/disks/prompt.mjs
··· 3855 3855 flashColor = "pink"; 3856 3856 makeFlash($); 3857 3857 return true; 3858 - } else if (slug === "device-key" || slug === "devicekey") { 3859 - // 🔑 Generate a device pairing key (pre-claimed with current user's identity). 3860 - // User types this key on a native device to switch it to their identity. 3861 - try { 3862 - // Step 1: Create a pairing code 3863 - const createRes = await fetch("/.netlify/functions/device-pair", { 3864 - method: "POST", 3865 - headers: { "Content-Type": "application/json" }, 3866 - body: JSON.stringify({ action: "create" }), 3867 - }); 3868 - const createData = await createRes.json(); 3869 - if (!createData.code) throw new Error(createData.message || "failed"); 3870 - 3871 - // Step 2: Immediately claim it with current user's auth 3872 - const claimRes = await net.userRequest( 3873 - "POST", 3874 - "/.netlify/functions/device-pair", 3875 - { action: "claim", code: createData.code }, 3876 - ); 3877 - if (!claimRes?.handle) throw new Error(claimRes?.message || "claim failed"); 3878 - 3879 - flashColor = [0, 255, 0]; 3880 - notice(createData.code, ["green", "white"]); 3881 - console.log(`🔑 Device key: ${createData.code} (expires in 10 min)`); 3882 - console.log(` On the device, type: login ${createData.code}`); 3883 - } catch (err) { 3884 - flashColor = [255, 0, 0]; 3885 - notice(err.message || "FAILED", ["yellow", "red"]); 3886 - } 3858 + } else if ( 3859 + slug === "device-key" || 3860 + slug === "devicekey" || 3861 + slug === "device-code" || 3862 + slug === "devicecode" 3863 + ) { 3864 + // 🔗 Aliases for the `link` piece — keep the code on screen. 3865 + jump("link"); 3887 3866 makeFlash($); 3888 3867 return true; 3889 3868 } else if (slugWithoutColon === "claude" && params[0]?.startsWith("sk-ant-")) {