Monorepo for Aesthetic.Computer aesthetic.computer

feat: os.mjs device manager — list USBs, clone, hot-plug detection

Press 'd' in os.mjs to enter device manager. Shows all plugged-in
devices (USB, NVMe) with boot/usb indicators and hot-plug updates.
Clone current OS to another USB with 'c', or update from CDN with 'u'.
Tab/arrows to select device, auto-detects plug/unplug.

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

+193 -1
+193 -1
fedac/native/pieces/os.mjs
··· 9 9 10 10 // States: idle | checking | up-to-date | available | downloading | flashing 11 11 // | confirm-reboot | shutting-down | error 12 + // | devices | clone-confirm | cloning 12 13 let state = "idle"; 13 14 let currentVersion = ""; 14 15 let remoteVersion = ""; ··· 24 25 // Telemetry lines that scroll by during flash 25 26 const telemetry = []; 26 27 let telemetryScroll = 0; 28 + 29 + // Device manager state 30 + let deviceIdx = 0; 31 + let lastTargetCount = -1; // track hot-plug changes 32 + let cloneTarget = null; 27 33 28 34 function addTelemetry(msg) { 29 35 telemetry.push({ text: msg, frame, y: 0 }); ··· 96 102 sound?.synth({ type: "sine", tone: 440, duration: 0.04, volume: 0.12, attack: 0.002, decay: 0.03 }); 97 103 return; 98 104 } 105 + } 106 + 107 + // 'd' to enter device manager from idle/up-to-date/available/error 108 + if (state === "idle" || state === "up-to-date" || state === "available" || state === "error") { 109 + if (e.is("keyboard:down:d")) { 110 + state = "devices"; 111 + deviceIdx = 0; 112 + sound?.synth({ type: "sine", tone: 523, duration: 0.05, volume: 0.1, attack: 0.002, decay: 0.04 }); 113 + return; 114 + } 115 + } 116 + 117 + // Device manager controls 118 + if (state === "devices") { 119 + const targets = system?.flashTargets || []; 120 + if (e.is("keyboard:down:tab") || e.is("keyboard:down:arrowdown")) { 121 + deviceIdx = (deviceIdx + 1) % Math.max(1, targets.length); 122 + sound?.synth({ type: "sine", tone: 440, duration: 0.04, volume: 0.1, attack: 0.002, decay: 0.03 }); 123 + return; 124 + } 125 + if (e.is("keyboard:down:arrowup")) { 126 + deviceIdx = (deviceIdx - 1 + targets.length) % Math.max(1, targets.length); 127 + sound?.synth({ type: "sine", tone: 440, duration: 0.04, volume: 0.1, attack: 0.002, decay: 0.03 }); 128 + return; 129 + } 130 + // 'c' to clone current OS to selected device 131 + if (e.is("keyboard:down:c")) { 132 + const tgt = targets[deviceIdx]; 133 + if (tgt && tgt.device !== system?.bootDevice) { 134 + cloneTarget = tgt; 135 + state = "clone-confirm"; 136 + sound?.synth({ type: "triangle", tone: 660, duration: 0.08, volume: 0.12, attack: 0.003, decay: 0.06 }); 137 + } else { 138 + sound?.synth({ type: "square", tone: 220, duration: 0.1, volume: 0.08, attack: 0.005, decay: 0.08 }); 139 + } 140 + return; 141 + } 142 + // 'u' to update selected device from CDN 143 + if (e.is("keyboard:down:u")) { 144 + const tgt = targets[deviceIdx]; 145 + if (tgt) { 146 + flashTargetIdx = deviceIdx; 147 + state = "checking"; 148 + fetchPending = true; 149 + checkFrame = frame; 150 + system?.fetch?.(OS_VERSION_URL); 151 + } 152 + return; 153 + } 154 + if (e.is("keyboard:down:escape") || e.is("keyboard:down:backspace")) { 155 + state = "idle"; 156 + return; 157 + } 158 + return; 159 + } 160 + 161 + // Clone confirmation 162 + if (state === "clone-confirm") { 163 + if (e.is("keyboard:down:y") || e.is("keyboard:down:enter") || e.is("keyboard:down:return")) { 164 + state = "cloning"; 165 + telemetry.length = 0; 166 + addTelemetry("cloning to " + cloneTarget.device); 167 + // Clone = flash the currently running kernel to the target device 168 + // The running kernel is at /mnt/EFI/BOOT/BOOTX64.EFI or KERNEL.EFI 169 + const bootKernel = "/mnt/EFI/BOOT/BOOTX64.EFI"; 170 + globalThis.__osFlashDevice = cloneTarget.device; 171 + system?.flashUpdate?.(bootKernel, cloneTarget.device); 172 + sound?.synth({ type: "triangle", tone: 784, duration: 0.1, volume: 0.12, attack: 0.003, decay: 0.08 }); 173 + return; 174 + } 175 + if (e.is("keyboard:down:n") || e.is("keyboard:down:escape")) { 176 + state = "devices"; 177 + return; 178 + } 179 + return; 99 180 } 100 181 101 182 // Tap to retry on error or re-check when up-to-date/idle ··· 382 463 ink(T.fgMute); 383 464 write("enter: retry esc: back", { x: pad, y: stateY + 14, size: 1, font }); 384 465 466 + } else if (state === "devices") { 467 + // === Device manager === 468 + const targets = system?.flashTargets || []; 469 + const bootDev = system?.bootDevice; 470 + 471 + // Hot-plug detection: play sound on change 472 + if (targets.length !== lastTargetCount && lastTargetCount >= 0) { 473 + // Would play sound here but we don't have sound ref in paint 474 + } 475 + lastTargetCount = targets.length; 476 + 477 + ink(T.fg, T.fg + 10, T.fg); 478 + write("devices", { x: pad, y: stateY, size: 2, font: "matrix" }); 479 + 480 + if (targets.length === 0) { 481 + ink(T.fgMute); 482 + write("no devices found", { x: pad, y: stateY + 24, size: 1, font }); 483 + } else { 484 + if (deviceIdx >= targets.length) deviceIdx = 0; 485 + const rowH = 20; 486 + for (let i = 0; i < targets.length; i++) { 487 + const tgt = targets[i]; 488 + const ry = stateY + 24 + i * rowH; 489 + const isBoot = tgt.device === bootDev; 490 + const selected = i === deviceIdx; 491 + 492 + // Selection indicator 493 + if (selected) { 494 + ink(40, 60, 80); 495 + box(pad - 2, ry - 2, w - pad * 2 + 4, rowH - 2, true); 496 + } 497 + 498 + // Device label + path 499 + ink(selected ? 255 : T.fgMute, selected ? 255 : T.fgMute, selected ? 255 : T.fgMute); 500 + write((selected ? "> " : " ") + (tgt.label || "?"), { x: pad, y: ry, size: 1, font }); 501 + ink(80, 80, 100); 502 + write(tgt.device, { x: pad + 100, y: ry, size: 1, font }); 503 + 504 + // Boot indicator 505 + if (isBoot) { 506 + ink(60, 200, 120); 507 + write("boot", { x: w - pad - 30, y: ry, size: 1, font }); 508 + } else if (tgt.removable) { 509 + ink(100, 140, 200); 510 + write("usb", { x: w - pad - 24, y: ry, size: 1, font }); 511 + } 512 + } 513 + 514 + // Actions for selected device 515 + const actY = stateY + 24 + targets.length * rowH + 8; 516 + const sel = targets[deviceIdx]; 517 + const isBootDev = sel?.device === bootDev; 518 + 519 + ink(80, 80, 100); 520 + write("tab/arrows: select", { x: pad, y: actY, size: 1, font }); 521 + 522 + if (!isBootDev) { 523 + ink(100, 200, 140); 524 + write("c: clone current os", { x: pad, y: actY + 14, size: 1, font }); 525 + } 526 + ink(100, 160, 220); 527 + write("u: update from cloud", { x: pad, y: actY + 28, size: 1, font }); 528 + } 529 + 530 + } else if (state === "clone-confirm") { 531 + ink(T.warn[0], T.warn[1], T.warn[2]); 532 + write("clone os?", { x: pad, y: stateY, size: 2, font: "matrix" }); 533 + 534 + ink(T.fgMute + 20, T.fgMute + 20, T.fgMute); 535 + write("from: " + (system?.bootDevice || "?"), { x: pad, y: stateY + 24, size: 1, font }); 536 + write(" to: " + (cloneTarget?.label || "?") + " (" + (cloneTarget?.device || "?") + ")", { x: pad, y: stateY + 38, size: 1, font }); 537 + 538 + ink(T.fg); 539 + write(currentVersion, { x: pad, y: stateY + 56, size: 1, font }); 540 + 541 + ink(255, 180, 60); 542 + write("this will overwrite the target!", { x: pad, y: stateY + 74, size: 1, font }); 543 + 544 + const pulse = Math.floor(200 + 55 * Math.sin(frame * 0.1)); 545 + ink(pulse, 255, pulse); 546 + write("y: clone n: cancel", { x: pad, y: stateY + 92, size: 1, font }); 547 + 548 + } else if (state === "cloning") { 549 + // Reuse flashing UI 550 + const phase = system?.flashPhase ?? 0; 551 + const phaseNames = ["preparing", "writing EFI", "syncing", "verifying", "complete"]; 552 + const dots = ".".repeat((Math.floor(frame / 10) % 3) + 1); 553 + ink(255, 160, 60); 554 + write("cloning" + (phase < 4 ? dots : "!"), { x: pad, y: stateY, size: 1, font }); 555 + ink(120); 556 + write(phaseNames[phase] || "...", { x: pad, y: stateY + 14, size: 1, font }); 557 + ink(80); 558 + write("-> " + (cloneTarget?.device || "?"), { x: pad, y: stateY + 28, size: 1, font }); 559 + ink(140); 560 + write("do not power off", { x: pad, y: stateY + 44, size: 1, font }); 561 + 385 562 } else { 386 563 // idle 387 564 ink(T.fgMute); 388 565 write("enter: check for updates", { x: pad, y: stateY, size: 1, font }); 566 + ink(T.fgMute - 20, T.fgMute, T.fgMute + 10); 567 + write("d: devices", { x: pad, y: stateY + 14, size: 1, font }); 389 568 } 390 569 391 570 // Bottom hint (not during shutdown) 392 571 if (state !== "shutting-down") { 393 572 ink(T.fgMute, T.fgMute + 10, T.fgMute); 394 - write("esc: back", { x: pad, y: h - 12, size: 1, font }); 573 + write(state === "devices" ? "esc: back to os" : "esc: back", { x: pad, y: h - 12, size: 1, font }); 395 574 } 396 575 397 576 // === State machine: poll fetch/flash results === ··· 442 621 } else { 443 622 state = "error"; 444 623 errorMsg = "download failed"; 624 + } 625 + } 626 + } 627 + 628 + // Clone progress 629 + if (state === "cloning") { 630 + if (system?.flashDone) { 631 + if (system?.flashOk) { 632 + flashedMB = ((system?.flashVerifiedBytes ?? 0) / 1048576).toFixed(1); 633 + state = "confirm-reboot"; 634 + } else { 635 + state = "error"; 636 + errorMsg = "clone verify failed"; 445 637 } 446 638 } 447 639 }