Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359// chat.mjs — Real-time chat client for aesthetic.computer// Connects to a chat room via system.ws. Default: "system"// Other rooms: "clock" (laer-klokken), "sotce"// Press escape to return to prompt.
const CHAT_ROOMS = { system: "wss://chat-system.aesthetic.computer/", clock: "wss://chat-clock.aesthetic.computer/", sotce: "wss://chat.sotce.net/",};
let roomName = "system"; // set via boot params or overrideconst SHIFT_MAP = { "1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*","9":"(","0":")", "-":"_","=":"+","[":"{","]":"}",";":":","'":'"',",":"<",".":">","/":"?","\\":"|","`":"~",};
let frame = 0;let messages = []; // [{from, text, when}]let inputText = "";let cursor = 0;let cursorBlink = 0;let shiftHeld = false;let handle = ""; // current user handle from configlet sub = ""; // auth0 sub from configlet token = ""; // auth token for chat server authorizationlet connected = false;
// Pixel-based scroll with inertial physicslet scroll = 0; // pixel offset (0 = bottom, positive = scrolled up)let scrollVelocity = 0;let isFlinging = false;let isDragging = false;let dragStartPos = null;let lastDragY = 0;const SCROLL_FRICTION = 0.92;const SCROLL_MIN_VELOCITY = 0.5;const BOUNCE_STIFFNESS = 0.15;const BOUNCE_DAMPING = 0.7;const DRAG_THRESHOLD = 5;
function boot({ system, params }) { // Allow room override via params: system.jump("chat:clock") if (params && params[0] && CHAT_ROOMS[params[0]]) { roomName = params[0]; }
// Read user identity from config try { var raw = system.readFile("/mnt/config.json"); if (raw) { var cfg = JSON.parse(raw); handle = cfg.handle || ""; sub = cfg.sub || ""; token = cfg.token || ""; } } catch (_) {} // Connect to chat room WebSocket var url = CHAT_ROOMS[roomName] || CHAT_ROOMS.system; console.log("[chat] connecting to " + url); if (system && system.ws) system.ws.connect(url);}
function act({ event: e, system, sound }) { if (e.is("keyboard:down:shift")) { shiftHeld = true; return; } if (e.is("keyboard:up:shift")) { shiftHeld = false; return; }
if (e.is("keyboard:down")) { const key = e.key; cursorBlink = 0;
if (key === "escape") { if (system && system.jump) system.jump("prompt"); return; }
if (key === "enter" || key === "return") { var text = inputText.trim(); var wsOk = system && system.ws && system.ws.connected; console.log("[chat] send: len=" + text.length + " ws=" + wsOk); if (text.length > 0 && wsOk) { var msg = JSON.stringify({ type: "chat:message", content: { sub: sub || "anonymous", text: text, handle: handle || "anon", token: token }, }); system.ws.send(msg); console.log("[chat] sent: " + msg.substring(0, 120)); // Add locally immediately messages.push({ from: "@" + (handle || "me"), text: text, when: Date.now() }); scroll = 0; // scroll to bottom if (sound && sound.synth) sound.synth({ type: "sine", tone: 880, duration: 0.04, volume: 0.1, attack: 0.002, decay: 0.03 }); } inputText = ""; cursor = 0; return; }
if (key === "backspace") { if (cursor > 0) { inputText = inputText.slice(0, cursor - 1) + inputText.slice(cursor); cursor--; } return; } if (key === "arrowleft") { if (cursor > 0) cursor--; return; } if (key === "arrowright") { if (cursor < inputText.length) cursor++; return; } if (key === "home") { cursor = 0; return; } if (key === "end") { cursor = inputText.length; return; } if (key === "arrowup") { scroll += 13; return; } if (key === "arrowdown") { scroll = Math.max(0, scroll - 13); return; }
if (key === "space") { inputText = inputText.slice(0, cursor) + " " + inputText.slice(cursor); cursor++; return; } if (key.length === 1) { const ch = shiftHeld ? (SHIFT_MAP[key] ?? key.toUpperCase()) : key; inputText = inputText.slice(0, cursor) + ch + inputText.slice(cursor); cursor++; } }
// Touch scrolling if (e.is("touch")) { const y = e.pointer?.y ?? e.y ?? 0; dragStartPos = y; lastDragY = y; isDragging = false; isFlinging = false; scrollVelocity = 0; }
if (e.is("draw")) { const y = e.pointer?.y ?? e.y ?? 0; if (dragStartPos !== null) { if (!isDragging && Math.abs(y - dragStartPos) > DRAG_THRESHOLD) { isDragging = true; } if (isDragging) { const dy = lastDragY - y; // drag up = positive scroll scroll += dy; scrollVelocity = dy; } lastDragY = y; } }
if (e.is("lift")) { if (isDragging && Math.abs(scrollVelocity) > SCROLL_MIN_VELOCITY) { isFlinging = true; } isDragging = false; dragStartPos = null; }}
function paint({ wipe, ink, box, line, write, screen, system, sound, wifi }) { frame++; cursorBlink++; wipe(20, 18, 30);
const W = screen.width; const H = screen.height; const font = "6x10"; const charW = 6; const charH = 10; const pad = 4;
// Process incoming WebSocket messages const wsMsgs = (system && system.ws ? system.ws.messages : null); if (wsMsgs && wsMsgs.length) { for (const raw of wsMsgs) { try { const msg = JSON.parse(raw); const parseContent = (c) => (typeof c === "string" ? JSON.parse(c) : c); if (msg.type === "connected") { connected = true; const content = parseContent(msg.content); const history = (content && content.messages) || []; // Load message history for (const m of history) { if (m.from && m.text) { messages.push({ from: m.from, text: m.text, when: m.when || 0 }); } } } else if (msg.type === "message") { const m = parseContent(msg.content); if (m && m.from && m.text) { messages.push({ from: m.from, text: m.text, when: Date.now() }); (sound && sound.synth)({ type: "triangle", tone: 660, duration: 0.05, volume: 0.08, attack: 0.002, decay: 0.04 }); } } else if (msg.from && msg.text) { messages.push({ from: msg.from, text: msg.text, when: Date.now() }); } } catch (_) {} } }
// Reconnect if dropped if (wifi && wifi.connected && system && system.ws && !system.ws.connected && !system.ws.connecting && frame % 120 === 0) { const url = CHAT_ROOMS[roomName] || CHAT_ROOMS.system; if (system && system.ws) system.ws.connect(url); }
// Header — show room name ink(140, 120, 180); write(roomName === "system" ? "chat" : roomName, { x: pad, y: 2, size: 1, font });
// Connection status const wsOk = (system && system.ws && system.ws.connected); if (wsOk) { ink(60, 160, 80); write("connected", { x: pad + 5 * charW, y: 2, size: 1, font }); } else if ((wifi && wifi.connected)) { ink(200, 200, 80); write("connecting...", { x: pad + 5 * charW, y: 2, size: 1, font }); } else { ink(200, 80, 80); write("offline", { x: pad + 5 * charW, y: 2, size: 1, font }); }
// User identity if (handle) { ink(100, 80, 120); const hLabel = "@" + handle; write(hLabel, { x: W - pad - hLabel.length * charW, y: 2, size: 1, font }); }
// Divider ink(50, 40, 60); line(0, 13, W, 13);
// Input area at bottom const inputY = H - charH - 6; ink(35, 28, 45); box(0, inputY - 3, W, charH + 8, true); ink(50, 40, 60); line(0, inputY - 3, W, inputY - 3);
// Input prompt ink(120, 80, 160); write(">", { x: pad, y: inputY, size: 1, font });
// Input text ink(220, 210, 240); const maxInputChars = Math.floor((W - pad * 2 - charW * 2) / charW); const displayInput = inputText.length > maxInputChars ? inputText.slice(inputText.length - maxInputChars) : inputText; const inputStartX = pad + charW + 2; write(displayInput, { x: inputStartX, y: inputY, size: 1, font });
// Cursor if (cursorBlink % 40 < 25) { const displayCursor = inputText.length > maxInputChars ? maxInputChars : cursor; ink(220, 80, 140, 180); box(inputStartX + displayCursor * charW, inputY, charW, charH, true); if (cursor < inputText.length) { ink(255, 255, 255); write(inputText[cursor], { x: inputStartX + displayCursor * charW, y: inputY, size: 1, font }); } }
// Messages area (pixel-based scroll) const msgTop = 15; const msgBottom = inputY - 6; const lineH = charH + 3; const chatHeight = msgBottom - msgTop; const totalContentH = messages.length * lineH;
// Render messages from bottom up, offset by scroll // scroll=0 means latest messages visible at bottom for (let i = 0; i < messages.length; i++) { const msg = messages[i]; // Position: bottom-aligned, scrolled const baseY = msgBottom - (messages.length - i) * lineH; const my = baseY + Math.floor(scroll); if (my + lineH < msgTop || my > msgBottom) continue; // clip
// From label var displayFrom = msg.from || "?"; if (displayFrom[0] !== "@" && displayFrom !== "?") displayFrom = "@" + displayFrom; var fromLabel = displayFrom + ": "; var isMe = displayFrom === "@" + handle || msg.from === handle || msg.from === "me"; ink(isMe ? 140 : 100, isMe ? 120 : 160, isMe ? 180 : 140); write(fromLabel, { x: pad, y: my, size: 1, font });
// Message text (wrap if needed) const textX = pad + fromLabel.length * charW; const maxTextW = W - textX - pad; const maxChars = Math.floor(maxTextW / charW); ink(200, 195, 210); write(msg.text.slice(0, maxChars), { x: textX, y: my, size: 1, font }); }
// Scroll indicator if (scroll > 1) { ink(100, 80, 120); write("^ scroll ^", { x: Math.floor(W / 2) - 30, y: msgTop, size: 1, font }); }
// Empty state if (messages.length === 0 && connected) { ink(70, 60, 80); write("no messages yet", { x: pad, y: Math.floor(H / 2) - 5, size: 1, font }); } else if (!connected && !(wifi && wifi.connected)) { ink(70, 60, 80); write("connect to wifi first", { x: pad, y: Math.floor(H / 2) - 5, size: 1, font }); }
// Hint ink(50, 40, 60); write("esc:back", { x: W - 9 * charW - pad, y: inputY, size: 1, font });}
function sim() { if (!isFlinging) return;
const lineH = 13; // charH(10) + 3 const maxScroll = Math.max(0, messages.length * lineH - 100); // approx chat area
const outOfBounds = scroll < 0 || scroll > maxScroll;
if (outOfBounds) { const target = scroll < 0 ? 0 : maxScroll; const displacement = scroll - target; scrollVelocity -= displacement * BOUNCE_STIFFNESS; scrollVelocity *= BOUNCE_DAMPING; scroll += scrollVelocity;
if (Math.abs(displacement) < 0.5 && Math.abs(scrollVelocity) < SCROLL_MIN_VELOCITY) { scroll = target; scrollVelocity = 0; isFlinging = false; } } else { scrollVelocity *= SCROLL_FRICTION; scroll += scrollVelocity;
if (scroll < 0 || scroll > maxScroll) { scrollVelocity *= 0.5; }
if (Math.abs(scrollVelocity) < SCROLL_MIN_VELOCITY) { scrollVelocity = 0; isFlinging = false; } }}
function leave({ system }) { // Don't disconnect WS — notepat may want it}
export { boot, act, paint, sim, leave };