Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

macos: native prompt.mjs runs \u2014 plot binding, richer __theme/system/wifi stubs

Adds the minimum bindings the Linux native prompt.mjs expects so it can
load on the macOS host without modification:

- plot(x, y) \u2014 single-pixel primitive (prompt uses it for cursor detail)
- __theme: full update() returning T with bg[]/fg/fgDim/fgMute/cursor[]/
accent[]/dark (shape matches what the Linux bindings produce)
- __theme.apply(name), ._forceDark, ._lastCheck \u2014 theme commands
- system: listPieces() returns fedac/native/pieces/ .mjs basenames
(populated by a readdir of the piece's parent dir in C); jump()
stashes target in globalThis.__pending_jump; readFile/writeFile,
reboot/poweroff, saveConfig, usbMidi.{enable,disable,refresh} are
safe no-ops; sshStarted/startSSH stubbed
- wifi: advertises connected/online on macOS so the network gate doesn't
block (we have real net via the host OS)
- Top-level globals: jump, kidlisp (no-op), user=null, handle=()=>null

Piece-directory scan runs once in piece_load before the piece evals, so
tab completion works immediately. Relative import normalizer added to
the module loader for any future piece that imports ./x or ../lib/y.

Verified: prompt.mjs boots cleanly, renders dark-mode canvas with the
blinking pink block cursor. Jump handling itself comes in Phase B.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+138 -10
+138 -10
fedac/native/macos/piece.c
··· 15 15 #include <string.h> 16 16 #include <math.h> 17 17 #include <time.h> 18 + #include <dirent.h> 19 + #include <libgen.h> 18 20 19 21 struct PieceCtx { 20 22 JSRuntime *rt; ··· 260 262 return JS_UNDEFINED; 261 263 } 262 264 265 + // plot(x, y) — single pixel at current ink color. Native prompt uses this 266 + // for the cursor's individual pixels when composing custom shapes. 267 + static JSValue js_plot(JSContext *jsctx, JSValueConst this_val, int argc, JSValueConst *argv) { 268 + (void)this_val; 269 + PieceCtx *pc = ctx_from(jsctx); 270 + int32_t x = 0, y = 0; 271 + if (argc >= 1) JS_ToInt32(jsctx, &x, argv[0]); 272 + if (argc >= 2) JS_ToInt32(jsctx, &y, argv[1]); 273 + if (x >= 0 && x < pc->fb->width && y >= 0 && y < pc->fb->height) { 274 + pc->fb->pixels[y * pc->fb->stride + x] = pc->ink_argb; 275 + } 276 + return JS_UNDEFINED; 277 + } 278 + 263 279 // write(text, { x, y, size, font }) 264 280 // Picks MatrixChunky8 for font: "matrix" (narrow, ~4px advance — matches 265 281 // notepat's layout math) and 6x10 for everything else. x/y default to 0; ··· 487 503 } 488 504 } 489 505 506 + // QuickJS module normalizer — invoked for every import, given the path of 507 + // the file doing the import. Handles relative imports like 508 + // `import './foo.mjs'` or `'../lib/platform.mjs'` by joining against the 509 + // parent dir of base. Absolute paths + /lib/ shortcuts fall through to 510 + // resolve_module() in the loader. 511 + static char *piece_module_normalize(JSContext *jsctx, const char *base, 512 + const char *name, void *opaque) { 513 + (void)opaque; 514 + if (name[0] == '.') { 515 + // base is e.g. "system/public/.../disks/prompt.mjs" — take its dir. 516 + char parent[1200]; 517 + snprintf(parent, sizeof parent, "%s", base ? base : ""); 518 + char *slash = strrchr(parent, '/'); 519 + if (slash) *slash = 0; else parent[0] = 0; 520 + 521 + // Repeatedly strip "./<seg>" or "../" from `name` relative to parent. 522 + const char *p = name; 523 + char acc[1200]; 524 + snprintf(acc, sizeof acc, "%s", parent); 525 + while (*p) { 526 + if (p[0] == '.' && p[1] == '/') { 527 + p += 2; 528 + } else if (p[0] == '.' && p[1] == '.' && p[2] == '/') { 529 + char *s = strrchr(acc, '/'); 530 + if (s) *s = 0; else acc[0] = 0; 531 + p += 3; 532 + } else { 533 + break; 534 + } 535 + } 536 + char out[1200]; 537 + if (acc[0]) snprintf(out, sizeof out, "%s/%s", acc, p); 538 + else snprintf(out, sizeof out, "%s", p); 539 + return js_strdup(jsctx, out); 540 + } 541 + // Non-relative: pass through. Loader will resolve "/lib/..." etc. 542 + return js_strdup(jsctx, name); 543 + } 544 + 490 545 // QuickJS module loader: read the file, compile as a module, return its 491 546 // JSModuleDef*. QuickJS wraps the def in a JSValue for us; we unwrap via 492 547 // JS_VALUE_GET_PTR. Signature matches JSModuleLoaderFunc. ··· 552 607 { "box", js_box, 0 }, 553 608 { "line", js_line, 0 }, 554 609 { "circle", js_circle, 0 }, 610 + { "plot", js_plot, 2 }, 555 611 { "write", js_write, 0 }, 556 612 { NULL, NULL, 0 } 557 613 }; ··· 592 648 " speak: noop,\n" 593 649 " };\n" 594 650 " const wifi = { scan: noop, connect: noop, disconnect: noop,\n" 595 - " networks: [], connected: false, state: 'idle',\n" 596 - " status: 'offline', ip: '', iface: '' };\n" 651 + " networks: [], connected: true, state: 'idle',\n" 652 + " status: 'online', ip: '127.0.0.1', iface: 'en0' };\n" 653 + " // system.jump(piece) stashes the target in __pending_jump; the C\n" 654 + " // host polls that between ticks and swaps pieces. Same story for\n" 655 + " // reboot / poweroff (logged only on macOS).\n" 656 + " // __native_pieces is populated by the C host from a readdir of\n" 657 + " // fedac/native/pieces/ before the piece module runs.\n" 658 + " const listPieces = () => (globalThis.__native_pieces || []).slice();\n" 597 659 " const system = { version,\n" 598 660 " readFile: () => null, writeFile: noop,\n" 661 + " saveConfig: noop,\n" 599 662 " fetchBinary: noop, mountMusic: noop, mountMusicMounted: false,\n" 600 - " hdmi: null, typec: [], usbMidi: { status: noop },\n" 601 - " ws: null, udp: null, jump: noop };\n" 663 + " hdmi: null, typec: [], usbMidi: { status: noop,\n" 664 + " enable: noop, disable: noop, refresh: noop },\n" 665 + " ws: null, udp: null,\n" 666 + " listPieces,\n" 667 + " jump(p) { globalThis.__pending_jump = p; },\n" 668 + " reboot() { console.log('[sys] reboot requested (noop on macOS)'); },\n" 669 + " poweroff() { console.log('[sys] poweroff requested (noop on macOS)'); },\n" 670 + " sshStarted: false, startSSH: noop,\n" 671 + " };\n" 672 + " // Top-level jump() + kidlisp() as globals (prompt + KidLisp return\n" 673 + " // path reach for them directly).\n" 674 + " globalThis.jump = (p) => { globalThis.__pending_jump = p; };\n" 675 + " globalThis.kidlisp = () => undefined; // TODO: wire to real eval\n" 676 + " globalThis.user = null; // no auth on macOS yet\n" 677 + " globalThis.handle = () => null;\n" 602 678 " const trackpad = { dx: 0, dy: 0 };\n" 603 679 " const pressures = [];\n" 604 - " // Host-provided globals the piece expects. __theme.dark tracks\n" 605 - " // macOS's effective appearance; update() re-queries via a C binding\n" 606 - " // installed later so notepat's 5s poll picks up Settings changes.\n" 680 + " // Full theme object — prompt.mjs reads T.bg/fg/fgDim/fgMute/\n" 681 + " // cursor/accent/dark on every paint, so update() must return the\n" 682 + " // same shape ac-native's JS theme helper produces on Linux.\n" 683 + " const THEMES = {\n" 684 + " dark: { dark: true, bg: [10, 12, 18], fg: 235, fgDim: 180, fgMute: 120, cursor: [255, 120, 180], accent: [120, 200, 255] },\n" 685 + " light: { dark: false, bg: [245, 242, 235], fg: 30, fgDim: 80, fgMute: 140, cursor: [220, 60, 120], accent: [80, 140, 200] },\n" 686 + " };\n" 607 687 " globalThis.__theme = {\n" 608 - " dark: false,\n" 609 - " update() { if (globalThis.__theme_dark) this.dark = !!globalThis.__theme_dark(); },\n" 688 + " _forceDark: undefined,\n" 689 + " _lastCheck: 0,\n" 690 + " _name: 'dark',\n" 691 + " dark: true, bg: THEMES.dark.bg, fg: THEMES.dark.fg,\n" 692 + " fgDim: THEMES.dark.fgDim, fgMute: THEMES.dark.fgMute,\n" 693 + " cursor: THEMES.dark.cursor, accent: THEMES.dark.accent,\n" 694 + " apply(name) { if (THEMES[name]) { this._name = name; Object.assign(this, THEMES[name]); } },\n" 695 + " update() {\n" 696 + " let dark;\n" 697 + " if (this._forceDark !== undefined) dark = !!this._forceDark;\n" 698 + " else if (globalThis.__theme_dark) dark = !!globalThis.__theme_dark();\n" 699 + " else dark = true;\n" 700 + " Object.assign(this, dark ? THEMES.dark : THEMES.light);\n" 701 + " this.dark = dark;\n" 702 + " return this;\n" 703 + " },\n" 610 704 " };\n" 611 705 " // Named exports visible both as globals and via the api arg.\n" 612 706 " for (const [k, v] of Object.entries({ sound, wifi, system, trackpad, pressures })) {\n" ··· 692 786 } 693 787 694 788 // Wire up module loader so `import "/lib/percussion.mjs"` works. 695 - JS_SetModuleLoaderFunc(pc->rt, NULL, piece_module_loader, pc); 789 + JS_SetModuleLoaderFunc(pc->rt, piece_module_normalize, piece_module_loader, pc); 696 790 697 791 size_t src_len = 0; 698 792 char *src = read_file(path, &src_len); ··· 701 795 JS_FreeValue(cx, global); 702 796 piece_destroy(pc); 703 797 return NULL; 798 + } 799 + 800 + // Populate globalThis.__native_pieces from the piece's parent directory 801 + // so the native prompt's system.listPieces() returns real entries (tab 802 + // completion + `list` command use it). Skips the current piece itself 803 + // plus non-.mjs entries; limited to a reasonable count so crashy dirs 804 + // don't OOM the runtime. 805 + { 806 + char parent[1200]; 807 + snprintf(parent, sizeof parent, "%s", path); 808 + char *sl = strrchr(parent, '/'); 809 + if (sl) *sl = 0; 810 + if (parent[0]) { 811 + JSValue arr = JS_NewArray(cx); 812 + uint32_t n = 0; 813 + DIR *d = opendir(parent); 814 + if (d) { 815 + struct dirent *ent; 816 + while ((ent = readdir(d)) && n < 256) { 817 + const char *nm = ent->d_name; 818 + size_t nl = strlen(nm); 819 + if (nl < 5) continue; 820 + if (strcmp(nm + nl - 4, ".mjs") != 0) continue; 821 + char base[128]; 822 + int copy_len = (int)(nl - 4); 823 + if (copy_len >= (int)sizeof base) copy_len = sizeof base - 1; 824 + memcpy(base, nm, (size_t)copy_len); 825 + base[copy_len] = 0; 826 + JS_SetPropertyUint32(cx, arr, n++, JS_NewString(cx, base)); 827 + } 828 + closedir(d); 829 + } 830 + JS_SetPropertyStr(cx, global, "__native_pieces", arr); 831 + } 704 832 } 705 833 706 834 // Register the piece under a stable module name so a subsequent