diff --git a/fedac/native/cl/config.lisp b/fedac/native/cl/config.lisp index b7953de7fb..f113e9636c 100644 --- a/fedac/native/cl/config.lisp +++ b/fedac/native/cl/config.lisp @@ -5,22 +5,72 @@ (defstruct config (handle "unknown" :type string) (piece nil :type (or null string)) - (email nil :type (or null string))) + (email nil :type (or null string)) + (claude-token nil :type (or null string)) + (github-pat nil :type (or null string)) + (wifi t :type boolean)) + +(defun strip-identity-block (text) + "If TEXT starts with the AC_IDENTITY_BLOCK_V1 marker, skip past it and any +trailing null bytes (the identity block is 32KB zero-padded). Returns the +remaining JSON portion." + (let ((marker "AC_IDENTITY_BLOCK_V1")) + (if (and (>= (length text) (length marker)) + (string= marker (subseq text 0 (length marker)))) + ;; Skip marker, then skip newline after marker + (let ((start (length marker))) + ;; Skip past the newline following the marker + (when (and (< start (length text)) + (char= (char text start) #\Newline)) + (incf start)) + ;; Skip any null bytes (zero-padded identity block) + (loop while (and (< start (length text)) + (char= (char text start) #\Nul)) + do (incf start)) + (subseq text start)) + text))) + +(defun json-extract-bool (text key) + "Extract a boolean value for KEY from JSON text. Returns T for true, NIL for false. +Returns :MISSING if the key is not found (so callers can distinguish absent from false)." + (let* ((needle (format nil "\"~A\"" key)) + (pos (search needle text))) + (if pos + (let* ((colon (position #\: text :start (+ pos (length needle))))) + (when colon + (let ((rest (string-trim '(#\Space #\Tab #\Newline) (subseq text (1+ colon))))) + (cond ((and (>= (length rest) 4) (string= "true" (subseq rest 0 4))) t) + ((and (>= (length rest) 5) (string= "false" (subseq rest 0 5))) nil) + (t nil))))) + :missing))) (defun load-config (&optional (path "/mnt/config.json")) "Load config from USB. Returns a config struct." (if (probe-file path) - (let ((text (with-open-file (s path :direction :input) + (let* ((raw (with-open-file (s path :direction :input) (let ((buf (make-string (file-length s)))) (read-sequence buf s) - buf)))) - ;; Minimal JSON parsing — just extract "handle" and "piece" + buf))) + (text (strip-identity-block raw)) + (wifi-val (json-extract-bool text "wifi"))) (make-config :handle (or (json-extract text "handle") "unknown") :piece (json-extract text "piece") - :email (json-extract text "email"))) + :email (json-extract text "email") + :claude-token (json-extract text "claudeToken") + :github-pat (json-extract text "githubPat") + :wifi (if (eq wifi-val :missing) t wifi-val))) (make-config))) +(defun write-device-tokens (cfg) + "Write Claude and GitHub tokens from config to /claude-token and /github-pat." + (when (config-claude-token cfg) + (with-open-file (s "/claude-token" :direction :output :if-exists :supersede) + (write-string (config-claude-token cfg) s))) + (when (config-github-pat cfg) + (with-open-file (s "/github-pat" :direction :output :if-exists :supersede) + (write-string (config-github-pat cfg) s)))) + (defun json-extract (text key) "Extract a string value for KEY from a JSON text. Very minimal." (let* ((needle (format nil "\"~A\"" key)) diff --git a/fedac/native/cl/main.lisp b/fedac/native/cl/main.lisp index 54084c02ab..a41c0dc102 100644 --- a/fedac/native/cl/main.lisp +++ b/fedac/native/cl/main.lisp @@ -119,6 +119,9 @@ (defvar *metronome-flash* 0.0 "Visual flash intensity 0-1, decays per frame.") (defvar *metronome-phase* 0.0 "Pendulum swing -1..1.") +;; Identity +(defvar *boot-handle* nil "Handle from config, set during boot splash.") + ;; Network (defvar *ip-address* "") @@ -149,6 +152,15 @@ (clrhash *active-voices*) (clrhash *active-notes*)) +;;; ── Boot splash ── + +(defun time-greeting () + "Return time-of-day greeting string." + (let ((hour (nth-value 2 (get-decoded-time)))) + (cond ((and (>= hour 5) (< hour 12)) "good morning") + ((and (>= hour 12) (< hour 17)) "good afternoon") + (t "good evening")))) + ;;; ── Main ── (defun main () @@ -188,6 +200,53 @@ (font-init) + ;; ── Boot splash ── + (let* ((cfg (ac-native.config:load-config)) + (handle (ac-native.config:config-handle cfg)) + (has-handle (and handle (string/= handle "") (string/= handle "unknown"))) + (greeting (time-greeting)) + (splash-start (monotonic-time-ms))) + ;; Write tokens + (handler-case (ac-native.config:write-device-tokens cfg) + (error (e) + (format *error-output* "[notepat] token write error: ~A~%" e) + (force-output *error-output*))) + ;; Store handle for status display + (setf *boot-handle* (if has-handle handle nil)) + ;; Show splash for 3 seconds or until keypress + (loop while (< (- (monotonic-time-ms) splash-start) 3000) do + (let ((events (ac-native.input:input-poll input))) + (when (some (lambda (ev) (eq (ac-native.input:event-type ev) :key-down)) events) + (return))) + ;; Paint splash + (graph-wipe graph (make-color :r 10 :g 12 :b 18)) + (let ((cy (floor sh 3))) + (if has-handle + (progn + ;; Greeting + (graph-ink graph (make-color :r 140 :g 160 :b 200 :a 220)) + (font-draw graph greeting + (- (floor sw 2) (floor (font-measure greeting) 2)) cy) + ;; @handle + (let ((htxt (format nil "@~A" handle))) + (graph-ink graph (make-color :r 80 :g 255 :b 140 :a 255)) + (font-draw graph htxt + (- (floor sw 2) (floor (font-measure htxt) 2)) (+ cy 14))) + ;; Subtitle + (graph-ink graph (make-color :r 80 :g 80 :b 100 :a 160)) + (font-draw graph "aesthetic.computer" + (- (floor sw 2) (floor (font-measure "aesthetic.computer") 2)) (+ cy 32))) + (progn + ;; No handle: just show name + (graph-ink graph (make-color :r 80 :g 255 :b 140 :a 255)) + (font-draw graph "aesthetic.computer" + (- (floor sw 2) (floor (font-measure "aesthetic.computer") 2)) cy) + (graph-ink graph (make-color :r 140 :g 160 :b 200 :a 180)) + (font-draw graph "notepat" + (- (floor sw 2) (floor (font-measure "notepat") 2)) (+ cy 18))))) + (ac-native.drm:drm-present display screen scale) + (frame-sync-60fps))) + ;; Start Swank server for remote REPL (handler-case (progn @@ -481,6 +540,12 @@ (graph-ink graph (make-color :r 255 :g 200 :b 50 :a 200)) (font-draw graph "Q" (+ 3 (* 8 6)) 3)) + ;; Handle (top-left, after piece name) + (when *boot-handle* + (let ((htxt (format nil "@~A" *boot-handle*))) + (graph-ink graph (make-color :r 80 :g 255 :b 140 :a 140)) + (font-draw graph htxt (+ 3 (* (if *quick-mode* 10 8) 6)) 3))) + ;; Octave (top-left, below title) (graph-ink graph (make-color :r 160 :g 160 :b 170 :a 180)) (font-draw graph (format nil "OCT ~D" *octave*) 3 14) diff --git a/fedac/native/cl/packages.lisp b/fedac/native/cl/packages.lisp index 55f5a07336..1b7e790308 100644 --- a/fedac/native/cl/packages.lisp +++ b/fedac/native/cl/packages.lisp @@ -82,7 +82,9 @@ (defpackage :ac-native.config (:use :cl) - (:export #:load-config #:config-handle #:config-piece)) + (:export #:load-config #:config-handle #:config-piece + #:config-claude-token #:config-github-pat #:config-wifi + #:write-device-tokens)) (defpackage :ac-native (:use :cl :ac-native.util :ac-native.color :ac-native.framebuffer diff --git a/system/public/aesthetic.computer/disks/os.mjs b/system/public/aesthetic.computer/disks/os.mjs index dd4f2115ae..a09832d1a1 100644 --- a/system/public/aesthetic.computer/disks/os.mjs +++ b/system/public/aesthetic.computer/disks/os.mjs @@ -14,9 +14,19 @@ function RELEASES_URL() { return OVEN_BASE() + "/os-releases"; } function OVEN_IMAGE_URL() { return OVEN_BASE() + "/os-image"; } function OVEN_WS_URL() { return mirror().ws; } function TEMPLATE_ISO_URL() { return mirror().iso; } +function templateIsoUrl() { + const base = mirror().iso; + if (variantIdx === 0) return base; + // CL variant: replace 'native-notepat' with 'cl-native-notepat' + return base.replace("native-notepat", "cl-native-notepat"); +} const CONFIG_MARKER = '{"handle":"","piece":"notepat","sub":"","email":""}'; const CONFIG_PAD = 4096; const BOOT_PIECES = ["notepat", "prompt", "chat", "laer-klokken"]; +const VARIANTS = [ + { id: "c", label: "C" }, + { id: "cl", label: "Common Lisp" }, +]; let releases = null; let loading = true; @@ -31,6 +41,8 @@ let downloadStatus = ""; let downloadBtn = null; let bootPieceIdx = 0; // index into BOOT_PIECES let bootBtn = null; // "boot to: X" button +let variantIdx = 0; // index into VARIANTS (0=C, 1=Common Lisp) +let variantBtn = null; // "build: C" selector let mirrorBtn = null; // "mirror: NYC" selector let wifiEnabled = true; let wifiBtn = null; // "internet: ON/OFF" toggle @@ -242,6 +254,7 @@ function leave() { function makeButtons(ui) { updateDownloadBtn(ui); updateBootBtn(ui); + updateVariantBtn(ui); updateWifiBtn(ui); updateMirrorBtn(ui); } @@ -250,13 +263,18 @@ function updateMirrorBtn(ui) { mirrorBtn = new ui.TextButton("mirror: " + mirror().label, { x: 6, y: 0 }); } +function updateVariantBtn(ui) { + variantBtn = new ui.TextButton("build: " + VARIANTS[variantIdx].label, { x: 6, y: 0 }); +} + function updateDownloadBtn(ui) { downloadBtn = new ui.TextButton("download", { x: 6, y: 0 }); } function osLabel() { const latest = releases?.releases?.[0]; - const coreName = "AC-" + (latest?.name || "native"); + const variantPrefix = variantIdx === 1 ? "CL-" : ""; + const coreName = variantPrefix + "AC-" + (latest?.name || "native"); const piece = BOOT_PIECES[bootPieceIdx]; return `@${handle}-os-${piece}-${coreName}`; } @@ -410,6 +428,19 @@ function paint($) { y += bootBtn.height + btnGap; } + // Build variant selector + if (variantBtn) { + variantBtn.reposition({ x: btnX(variantBtn), y }); + variantBtn.paint( + $, + [C.bootBtnBg, C.bootBtnBorder, ...C.bootPiece, 200], + [C.bootBtnHoverBg, C.bootBtnHoverBorder, [255, 255, 255], 255], + undefined, + [C.bootBtnBg, C.bootBtnBorder, ...C.bootPiece, 230], + ); + y += variantBtn.height + btnGap; + } + // WiFi toggle if (wifiBtn) { wifiBtn.reposition({ x: btnX(wifiBtn), y }); @@ -615,6 +646,17 @@ function act({ event: e, needsPaint, download }) { }); } + // Build variant selector + if (variantBtn) { + variantBtn.btn.act(e, { + push: () => { + variantIdx = (variantIdx + 1) % VARIANTS.length; + if (uiRef) updateVariantBtn(uiRef); + needsPaint(); + }, + }); + } + // WiFi toggle if (wifiBtn) { wifiBtn.btn.act(e, { @@ -671,7 +713,7 @@ async function startDownload(needsPaint) { needsPaint(); try { - const url = OVEN_IMAGE_URL() + "?piece=" + encodeURIComponent(piece) + "&wifi=" + (wifiEnabled ? "1" : "0"); + const url = OVEN_IMAGE_URL() + "?piece=" + encodeURIComponent(piece) + "&wifi=" + (wifiEnabled ? "1" : "0") + (variantIdx === 1 ? "&variant=cl" : ""); console.log("[os] Fetching:", url); const res = await fetch(url, { headers: { Authorization: "Bearer " + token }, @@ -713,7 +755,8 @@ async function startDownload(needsPaint) { } const latest = releases?.releases?.[0]; - const coreName = "AC-" + (latest?.name || "native"); + const variantPrefix = variantIdx === 1 ? "CL-" : ""; + const coreName = variantPrefix + "AC-" + (latest?.name || "native"); const d = new Date(); const p = (n) => String(n).padStart(2, "0"); const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; @@ -743,7 +786,7 @@ async function startTemplateDownload(needsPaint) { needsPaint(); try { - const res = await fetch(TEMPLATE_ISO_URL()); + const res = await fetch(templateIsoUrl()); if (!res.ok) throw new Error("Download failed: " + res.status); const contentLength = parseInt(res.headers.get("content-length") || "0"); @@ -806,7 +849,8 @@ async function startTemplateDownload(needsPaint) { console.log("[os] Patched", patched, "config regions"); const latest = releases?.releases?.[0]; - const coreName = "AC-" + (latest?.name || "native"); + const variantPrefix = variantIdx === 1 ? "CL-" : ""; + const coreName = variantPrefix + "AC-" + (latest?.name || "native"); const d = new Date(); const p = (n) => String(n).padStart(2, "0"); const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`;