Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

native: add speed.mjs write-speed test + deleteFile/diskInfo/blockDevices

New JS bindings (all on the system.* namespace):
- system.deleteFile(path) — unlink + sync, returns bool
- system.diskInfo(path) — statvfs wrapper, returns {total, free,
available, blockSize} in bytes
- system.blockDevices() — scans /sys/block and returns an array of
{name, sizeBytes, removable, vendor, model}
for every real disk (skips loop/ram/dm)

New piece: speed.mjs
- Lists every block device with size + model + removable badge
- Benchmarks sequential write throughput against three mount points
(usb /mnt, hdd /tmp/hd, tmpfs /tmp) at 1/4/16 MiB sizes
- Reports MB/s color-coded (green > 50, amber > 10, red below)
- Also renders per-mount free-space bars so you can see at a glance
where capacity vs speed is limiting you
- Controls: SPACE/ENTER to run, ESC to return

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

+304
+219
fedac/native/pieces/speed.mjs
··· 1 + // speed.mjs — disk / USB write speed test 2 + // 3 + // Lists every block device the kernel knows about with its size, model, 4 + // and removable flag, then runs a sequential write benchmark against 5 + // three mount points (USB /mnt, internal /tmp/hd if mounted, tmpfs 6 + // /tmp). Reports MB/s for each, plus free-space usage so you can see at 7 + // a glance where the bottleneck is. 8 + // 9 + // Controls: 10 + // space / enter — re-run the benchmark 11 + // esc — back to prompt 12 + 13 + let frame = 0; 14 + let devices = []; // [{name, sizeBytes, removable, model, vendor}] 15 + let targets = []; // [{label, path, info, result}] 16 + let running = false; // true while a run is in progress 17 + let status = "press SPACE to test"; 18 + 19 + function fmtBytes(n) { 20 + if (!Number.isFinite(n) || n <= 0) return "—"; 21 + const units = ["B", "K", "M", "G", "T"]; 22 + let i = 0; 23 + while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } 24 + return n.toFixed(n < 10 ? 1 : 0) + units[i]; 25 + } 26 + 27 + function fmtMBps(mbps) { 28 + if (!Number.isFinite(mbps)) return "—"; 29 + return mbps.toFixed(mbps < 10 ? 2 : 1) + " MB/s"; 30 + } 31 + 32 + function scan(system) { 33 + devices = system?.blockDevices?.() || []; 34 + const candidates = [ 35 + { label: "usb /mnt (boot media)", path: "/mnt" }, 36 + { label: "hdd /tmp/hd (nvme/sata)", path: "/tmp/hd" }, 37 + { label: "ram /tmp (tmpfs)", path: "/tmp" }, 38 + ]; 39 + targets = candidates.map((c) => { 40 + const info = system?.diskInfo?.(c.path); 41 + return { ...c, info, result: null }; 42 + }); 43 + } 44 + 45 + function boot({ system, wipe }) { 46 + wipe(14, 16, 24); 47 + scan(system); 48 + } 49 + 50 + // Build a printable test buffer of roughly `bytes` bytes. We repeat a 51 + // 1-KiB chunk so the compiler / QuickJS string interning doesn't fold 52 + // it into a single allocation. 53 + function makeBuffer(bytes) { 54 + const kChunk = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890-+" .repeat(16); // 1024 55 + const n = Math.max(1, Math.ceil(bytes / 1024)); 56 + return kChunk.repeat(n); 57 + } 58 + 59 + function runOne(system, target, sizeBytes) { 60 + if (!target.info) return { error: "unmounted", mbps: 0, bytes: 0, ms: 0 }; 61 + // Skip if the target's free space is too small 62 + if (target.info.available < sizeBytes + 1024 * 1024) { 63 + return { error: "low space", mbps: 0, bytes: 0, ms: 0 }; 64 + } 65 + const path = `${target.path}/.speedtest-${sizeBytes}.bin`; 66 + const buf = makeBuffer(sizeBytes); 67 + const t0 = Date.now(); 68 + const ok = system?.writeFile?.(path, buf); 69 + const dt = Math.max(1, Date.now() - t0); 70 + // Clean up so we don't leave trash behind 71 + system?.deleteFile?.(path); 72 + if (!ok) return { error: "write failed", mbps: 0, bytes: 0, ms: dt }; 73 + const mbps = (sizeBytes / (1024 * 1024)) / (dt / 1000); 74 + return { mbps, bytes: sizeBytes, ms: dt }; 75 + } 76 + 77 + async function runAll(system) { 78 + if (running) return; 79 + running = true; 80 + status = "running..."; 81 + scan(system); 82 + // Test multiple sizes and take the best (warmup effect) 83 + const sizes = [1 * 1024 * 1024, 4 * 1024 * 1024, 16 * 1024 * 1024]; 84 + for (const t of targets) { 85 + if (!t.info) { t.result = { error: "unmounted" }; continue; } 86 + let best = { mbps: 0, bytes: 0, ms: 0 }; 87 + for (const s of sizes) { 88 + const r = runOne(system, t, s); 89 + if (r.error) { best = r; break; } 90 + if (r.mbps > best.mbps) best = r; 91 + } 92 + t.result = best; 93 + status = "running " + t.label.split(" ")[0] + "..."; 94 + } 95 + status = "done — SPACE to re-run"; 96 + running = false; 97 + } 98 + 99 + function act({ event: e, system }) { 100 + if (e.is("keyboard:down")) { 101 + const key = e.key?.toLowerCase(); 102 + if (key === "escape") { system?.jump?.("prompt"); return; } 103 + if (key === "enter" || key === " " || key === "space" || key === "r") { 104 + runAll(system); 105 + return; 106 + } 107 + } 108 + } 109 + 110 + function paint({ wipe, ink, box, line, write, screen, system }) { 111 + const w = screen.width; 112 + const h = screen.height; 113 + wipe(14, 16, 24); 114 + 115 + // === Title === 116 + ink(200, 220, 255); 117 + write("speed", { x: 8, y: 8, size: 2, font: "matrix" }); 118 + ink(120, 140, 170); 119 + write("disk + usb write test — space to run, esc prompt", 120 + { x: 80, y: 14, size: 1, font: "font_1" }); 121 + 122 + // === Block devices list (top third) === 123 + let y = 36; 124 + ink(160, 210, 255); 125 + write("block devices", { x: 8, y, size: 1, font: "font_1" }); 126 + y += 12; 127 + if (devices.length === 0) { 128 + ink(140, 140, 160); 129 + write("(no devices)", { x: 16, y, size: 1, font: "font_1" }); 130 + y += 10; 131 + } else { 132 + for (const d of devices) { 133 + // Row background 134 + const rem = d.removable; 135 + ink(rem ? 50 : 30, rem ? 55 : 38, rem ? 35 : 46, 200); 136 + box(8, y - 1, w - 16, 10, true); 137 + // Name + removable badge 138 + ink(rem ? 255 : 180, rem ? 210 : 210, rem ? 100 : 240); 139 + const nameCol = `${d.name}${rem ? " (removable)" : ""}`; 140 + write(nameCol, { x: 12, y, size: 1, font: "font_1" }); 141 + // Size 142 + const sz = fmtBytes(d.sizeBytes); 143 + ink(200, 220, 240); 144 + write(sz, { x: 160, y, size: 1, font: "font_1" }); 145 + // Model 146 + const model = ((d.vendor || "") + " " + (d.model || "")).trim() || "unknown"; 147 + ink(140, 160, 190); 148 + const modelX = 220; 149 + const modelMax = Math.floor((w - modelX - 8) / 6); 150 + const modelTrim = model.length > modelMax ? model.slice(0, modelMax - 1) + "." : model; 151 + write(modelTrim, { x: modelX, y, size: 1, font: "font_1" }); 152 + y += 11; 153 + } 154 + } 155 + 156 + // === Benchmarks (middle) === 157 + y += 6; 158 + ink(160, 255, 210); 159 + write("write speed", { x: 8, y, size: 1, font: "font_1" }); 160 + y += 12; 161 + 162 + for (const t of targets) { 163 + ink(30, 40, 50, 200); 164 + box(8, y - 1, w - 16, 22, true); 165 + // Label 166 + ink(220, 230, 250); 167 + write(t.label, { x: 12, y, size: 1, font: "font_1" }); 168 + // Info row 169 + if (t.info) { 170 + const used = t.info.total - t.info.available; 171 + const usedStr = `${fmtBytes(used)} / ${fmtBytes(t.info.total)}`; 172 + ink(140, 160, 190); 173 + write(usedStr, { x: 12, y: y + 11, size: 1, font: "font_1" }); 174 + // Bar 175 + const barX = 180; 176 + const barW = w - barX - 140; 177 + const pct = Math.max(0, Math.min(1, used / Math.max(1, t.info.total))); 178 + ink(40, 50, 65); 179 + box(barX, y + 12, barW, 6, true); 180 + ink(120, 200, 240); 181 + box(barX, y + 12, Math.max(1, Math.floor(barW * pct)), 6, true); 182 + } else { 183 + ink(255, 120, 100); 184 + write("not mounted", { x: 12, y: y + 11, size: 1, font: "font_1" }); 185 + } 186 + // Result 187 + const r = t.result; 188 + if (r) { 189 + if (r.error) { 190 + ink(255, 140, 120); 191 + write(r.error, { x: w - 128, y: y + 5, size: 1, font: "font_1" }); 192 + } else { 193 + // Color by speed: green > 50, amber > 10, red < 194 + const mbps = r.mbps; 195 + const [cr, cg, cb] = mbps > 50 ? [140, 240, 160] 196 + : mbps > 10 ? [240, 220, 140] 197 + : [240, 160, 140]; 198 + ink(cr, cg, cb); 199 + write(fmtMBps(mbps), { x: w - 128, y: y + 5, size: 1, font: "font_1" }); 200 + } 201 + } else { 202 + ink(120, 140, 170); 203 + write("—", { x: w - 128, y: y + 5, size: 1, font: "font_1" }); 204 + } 205 + y += 24; 206 + } 207 + 208 + // === Status bar === 209 + ink(dark() ? 40 : 220, dark() ? 48 : 225, dark() ? 60 : 235); 210 + box(0, h - 14, w, 14, true); 211 + ink(running ? 255 : 180, running ? 220 : 200, running ? 100 : 180); 212 + write(status, { x: 8, y: h - 12, size: 1, font: "font_1" }); 213 + } 214 + 215 + function dark() { return true; } // always dark for this piece 216 + 217 + function sim() { frame++; } 218 + 219 + export { boot, act, paint, sim };
+85
fedac/native/src/js-bindings.c
··· 2915 2915 return JS_NewBool(ctx, ok); 2916 2916 } 2917 2917 2918 + // system.deleteFile(path) — unlink a file, returns true on success 2919 + static JSValue js_delete_file(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2920 + (void)this_val; 2921 + if (argc < 1) return JS_FALSE; 2922 + const char *path = JS_ToCString(ctx, argv[0]); 2923 + if (!path) return JS_FALSE; 2924 + int ok = (unlink(path) == 0); 2925 + if (ok) sync(); 2926 + JS_FreeCString(ctx, path); 2927 + return JS_NewBool(ctx, ok); 2928 + } 2929 + 2930 + // system.diskInfo(path) — returns {total, free, available, blockSize, fstype} 2931 + // for the filesystem containing `path`, or null on error. 2932 + static JSValue js_disk_info(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2933 + (void)this_val; 2934 + if (argc < 1) return JS_NULL; 2935 + const char *path = JS_ToCString(ctx, argv[0]); 2936 + if (!path) return JS_NULL; 2937 + struct statvfs vfs; 2938 + int rc = statvfs(path, &vfs); 2939 + JS_FreeCString(ctx, path); 2940 + if (rc != 0) return JS_NULL; 2941 + JSValue obj = JS_NewObject(ctx); 2942 + uint64_t bs = (uint64_t)vfs.f_frsize ? (uint64_t)vfs.f_frsize : (uint64_t)vfs.f_bsize; 2943 + JS_SetPropertyStr(ctx, obj, "total", JS_NewInt64(ctx, (int64_t)(vfs.f_blocks * bs))); 2944 + JS_SetPropertyStr(ctx, obj, "free", JS_NewInt64(ctx, (int64_t)(vfs.f_bfree * bs))); 2945 + JS_SetPropertyStr(ctx, obj, "available", JS_NewInt64(ctx, (int64_t)(vfs.f_bavail * bs))); 2946 + JS_SetPropertyStr(ctx, obj, "blockSize", JS_NewInt64(ctx, (int64_t)bs)); 2947 + return obj; 2948 + } 2949 + 2950 + // system.blockDevices() — list /sys/block entries with size + removable + model. 2951 + // Returns array of {name, sizeBytes, removable, model, vendor} or null. 2952 + static JSValue js_block_devices(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2953 + (void)this_val; (void)argc; (void)argv; 2954 + DIR *dir = opendir("/sys/block"); 2955 + if (!dir) return JS_NULL; 2956 + JSValue arr = JS_NewArray(ctx); 2957 + struct dirent *ent; 2958 + int idx = 0; 2959 + while ((ent = readdir(dir)) != NULL) { 2960 + if (ent->d_name[0] == '.') continue; 2961 + // Skip loop/ram/dm pseudo devs 2962 + if (strncmp(ent->d_name, "loop", 4) == 0) continue; 2963 + if (strncmp(ent->d_name, "ram", 3) == 0) continue; 2964 + if (strncmp(ent->d_name, "dm-", 3) == 0) continue; 2965 + char p[256]; char buf[128]; 2966 + JSValue obj = JS_NewObject(ctx); 2967 + JS_SetPropertyStr(ctx, obj, "name", JS_NewString(ctx, ent->d_name)); 2968 + // size (in 512-byte sectors) 2969 + snprintf(p, sizeof(p), "/sys/block/%s/size", ent->d_name); 2970 + FILE *f = fopen(p, "r"); long long sectors = 0; 2971 + if (f) { fscanf(f, "%lld", &sectors); fclose(f); } 2972 + JS_SetPropertyStr(ctx, obj, "sizeBytes", JS_NewInt64(ctx, sectors * 512LL)); 2973 + // removable flag 2974 + snprintf(p, sizeof(p), "/sys/block/%s/removable", ent->d_name); 2975 + int removable = 0; 2976 + f = fopen(p, "r"); 2977 + if (f) { fscanf(f, "%d", &removable); fclose(f); } 2978 + JS_SetPropertyStr(ctx, obj, "removable", JS_NewBool(ctx, removable != 0)); 2979 + // vendor + model 2980 + const char *fields[][2] = { {"vendor", "vendor"}, {"model", "model"}, {NULL, NULL} }; 2981 + for (int i = 0; fields[i][0]; i++) { 2982 + snprintf(p, sizeof(p), "/sys/block/%s/device/%s", ent->d_name, fields[i][0]); 2983 + f = fopen(p, "r"); 2984 + if (f) { 2985 + if (fgets(buf, sizeof(buf), f)) { 2986 + // trim trailing whitespace 2987 + size_t L = strlen(buf); 2988 + while (L > 0 && (buf[L-1] == '\n' || buf[L-1] == ' ' || buf[L-1] == '\t')) buf[--L] = 0; 2989 + JS_SetPropertyStr(ctx, obj, fields[i][1], JS_NewString(ctx, buf)); 2990 + } 2991 + fclose(f); 2992 + } 2993 + } 2994 + JS_SetPropertyUint32(ctx, arr, idx++, obj); 2995 + } 2996 + closedir(dir); 2997 + return arr; 2998 + } 2999 + 2918 3000 // system.listDir(path) — returns [{name, isDir, size}, ...] or null 2919 3001 static JSValue js_list_dir(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 2920 3002 (void)this_val; ··· 5639 5721 // File I/O — system.readFile(path) / system.writeFile(path, data) 5640 5722 JS_SetPropertyStr(ctx, sys, "readFile", JS_NewCFunction(ctx, js_read_file, "readFile", 1)); 5641 5723 JS_SetPropertyStr(ctx, sys, "writeFile", JS_NewCFunction(ctx, js_write_file, "writeFile", 2)); 5724 + JS_SetPropertyStr(ctx, sys, "deleteFile",JS_NewCFunction(ctx, js_delete_file,"deleteFile",1)); 5642 5725 JS_SetPropertyStr(ctx, sys, "listDir", JS_NewCFunction(ctx, js_list_dir, "listDir", 1)); 5726 + JS_SetPropertyStr(ctx, sys, "diskInfo", JS_NewCFunction(ctx, js_disk_info, "diskInfo", 1)); 5727 + JS_SetPropertyStr(ctx, sys, "blockDevices", JS_NewCFunction(ctx, js_block_devices, "blockDevices", 0)); 5643 5728 JS_SetPropertyStr(ctx, sys, "mountMusic", JS_NewCFunction(ctx, js_mount_music, "mountMusic", 0)); 5644 5729 JS_SetPropertyStr(ctx, sys, "mountMusicMounted", JS_NewBool(ctx, music_mount_state)); 5645 5730 JS_SetPropertyStr(ctx, sys, "mountMusicPending", JS_NewBool(ctx, music_mount_pending));