Monorepo for Aesthetic.Computer aesthetic.computer
1
fork

Configure Feed

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

at main 250 lines 8.0 kB view raw
1// laer-klokken.mjs — Native "Learn the Clock" chat room 2// Connects to wss://chat-clock.aesthetic.computer/ (same room as web laer-klokken) 3// Warm terracotta theme. Press escape to return to prompt. 4 5const CHAT_URL = "wss://chat-clock.aesthetic.computer/"; 6const SHIFT_MAP = { 7 "1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*","9":"(","0":")", 8 "-":"_","=":"+","[":"{","]":"}",";":":","'":'"',",":"<",".":">","/":"?","\\":"|","`":"~", 9}; 10 11let frame = 0; 12let messages = []; 13let inputText = ""; 14let cursor = 0; 15let cursorBlink = 0; 16let shiftHeld = false; 17let handle = ""; 18let sub = ""; 19let connected = false; 20let scrollOffset = 0; 21 22// Warm terracotta theme (matches web laer-klokken) 23const T = { 24 bg: [140, 75, 45], 25 header: [255, 200, 160], 26 status: [100, 200, 140], 27 statusWarn: [255, 220, 120], 28 statusOff: [255, 140, 120], 29 handle: [255, 180, 140], 30 inputBg: [120, 60, 35], 31 inputBorder: [180, 110, 70], 32 prompt: [255, 180, 120], 33 inputText: [255, 245, 230], 34 cursorColor: [255, 140, 80], 35 myMsg: [255, 200, 160], 36 otherHandle: [100, 220, 180], 37 msgText: [255, 245, 230], 38 scroll: [200, 140, 100], 39 empty: [180, 120, 80], 40 hint: [160, 100, 65], 41 divider: [160, 95, 60], 42}; 43 44function boot({ system }) { 45 const raw = system?.readFile?.("/mnt/config.json"); 46 if (raw) { 47 try { 48 const cfg = JSON.parse(raw); 49 handle = cfg.handle || ""; 50 sub = cfg.sub || ""; 51 } catch (_) {} 52 } 53 system?.ws?.connect(CHAT_URL); 54} 55 56function act({ event: e, system, sound }) { 57 if (e.is("keyboard:down:shift")) { shiftHeld = true; return; } 58 if (e.is("keyboard:up:shift")) { shiftHeld = false; return; } 59 60 if (e.is("keyboard:down")) { 61 const key = e.key; 62 cursorBlink = 0; 63 64 if (key === "escape") { system?.jump?.("prompt"); return; } 65 66 if (key === "enter" || key === "return") { 67 const text = inputText.trim(); 68 if (text.length > 0 && system?.ws?.connected) { 69 system.ws.send(JSON.stringify({ 70 type: "chat:message", 71 content: { sub: sub || "anonymous", text, handle: handle || "anon" }, 72 })); 73 messages.push({ from: handle || "me", text, when: Date.now() }); 74 scrollOffset = 0; 75 sound?.synth({ type: "sine", tone: 660, duration: 0.05, volume: 0.1, attack: 0.002, decay: 0.04 }); 76 } 77 inputText = ""; 78 cursor = 0; 79 return; 80 } 81 82 if (key === "backspace") { 83 if (cursor > 0) { 84 inputText = inputText.slice(0, cursor - 1) + inputText.slice(cursor); 85 cursor--; 86 } 87 return; 88 } 89 if (key === "arrowleft") { if (cursor > 0) cursor--; return; } 90 if (key === "arrowright") { if (cursor < inputText.length) cursor++; return; } 91 if (key === "home") { cursor = 0; return; } 92 if (key === "end") { cursor = inputText.length; return; } 93 if (key === "arrowup") { scrollOffset = Math.min(scrollOffset + 1, Math.max(0, messages.length - 3)); return; } 94 if (key === "arrowdown") { scrollOffset = Math.max(0, scrollOffset - 1); return; } 95 96 if (key === "space") { 97 inputText = inputText.slice(0, cursor) + " " + inputText.slice(cursor); 98 cursor++; 99 return; 100 } 101 if (key.length === 1) { 102 const ch = shiftHeld ? (SHIFT_MAP[key] ?? key.toUpperCase()) : key; 103 inputText = inputText.slice(0, cursor) + ch + inputText.slice(cursor); 104 cursor++; 105 } 106 } 107} 108 109function paint({ wipe, ink, box, line, write, screen, system, sound, wifi }) { 110 frame++; 111 cursorBlink++; 112 wipe(...T.bg); 113 114 const W = screen.width; 115 const H = screen.height; 116 const font = "6x10"; 117 const charW = 6; 118 const charH = 10; 119 const pad = 4; 120 121 // Process incoming WebSocket messages 122 const wsMsgs = system?.ws?.messages; 123 if (wsMsgs?.length) { 124 for (const raw of wsMsgs) { 125 try { 126 const msg = JSON.parse(raw); 127 const parseContent = (c) => (typeof c === "string" ? JSON.parse(c) : c); 128 if (msg.type === "connected") { 129 connected = true; 130 const content = parseContent(msg.content); 131 for (const m of (content?.messages || [])) { 132 if (m.from && m.text) messages.push({ from: m.from, text: m.text, when: m.when || 0 }); 133 } 134 } else if (msg.type === "message") { 135 const m = parseContent(msg.content); 136 if (m?.from && m?.text) { 137 messages.push({ from: m.from, text: m.text, when: Date.now() }); 138 sound?.synth({ type: "triangle", tone: 520, duration: 0.06, volume: 0.08, attack: 0.002, decay: 0.05 }); 139 } 140 } else if (msg.from && msg.text) { 141 messages.push({ from: msg.from, text: msg.text, when: Date.now() }); 142 } 143 } catch (_) {} 144 } 145 } 146 147 // Reconnect if dropped 148 if (wifi?.connected && !system?.ws?.connected && !system?.ws?.connecting && frame % 120 === 0) { 149 system?.ws?.connect(CHAT_URL); 150 } 151 152 // Header 153 ink(...T.header); 154 write("laer-klokken", { x: pad, y: 2, size: 1, font }); 155 156 const wsOk = system?.ws?.connected; 157 if (wsOk) { 158 ink(...T.status); 159 write("connected", { x: pad + 13 * charW, y: 2, size: 1, font }); 160 } else if (wifi?.connected) { 161 ink(...T.statusWarn); 162 write("connecting...", { x: pad + 13 * charW, y: 2, size: 1, font }); 163 } else { 164 ink(...T.statusOff); 165 write("offline", { x: pad + 13 * charW, y: 2, size: 1, font }); 166 } 167 168 if (handle) { 169 ink(...T.handle); 170 const hLabel = "@" + handle; 171 write(hLabel, { x: W - pad - hLabel.length * charW, y: 2, size: 1, font }); 172 } 173 174 ink(...T.divider); 175 line(0, 13, W, 13); 176 177 // Input area 178 const inputY = H - charH - 6; 179 ink(...T.inputBg); 180 box(0, inputY - 3, W, charH + 8, true); 181 ink(...T.inputBorder); 182 line(0, inputY - 3, W, inputY - 3); 183 184 ink(...T.prompt); 185 write(">", { x: pad, y: inputY, size: 1, font }); 186 187 ink(...T.inputText); 188 const maxInputChars = Math.floor((W - pad * 2 - charW * 2) / charW); 189 const displayInput = inputText.length > maxInputChars 190 ? inputText.slice(inputText.length - maxInputChars) 191 : inputText; 192 const inputStartX = pad + charW + 2; 193 write(displayInput, { x: inputStartX, y: inputY, size: 1, font }); 194 195 if (cursorBlink % 40 < 25) { 196 const displayCursor = inputText.length > maxInputChars ? maxInputChars : cursor; 197 ink(...T.cursorColor, 180); 198 box(inputStartX + displayCursor * charW, inputY, charW, charH, true); 199 if (cursor < inputText.length) { 200 ink(255, 255, 255); 201 write(inputText[cursor], { x: inputStartX + displayCursor * charW, y: inputY, size: 1, font }); 202 } 203 } 204 205 // Messages 206 const msgTop = 15; 207 const msgBottom = inputY - 6; 208 const lineH = charH + 3; 209 const maxVisible = Math.floor((msgBottom - msgTop) / lineH); 210 const startIdx = Math.max(0, messages.length - maxVisible - scrollOffset); 211 const endIdx = Math.min(messages.length, startIdx + maxVisible); 212 213 let my = msgTop; 214 for (let i = startIdx; i < endIdx; i++) { 215 const msg = messages[i]; 216 if (my + lineH > msgBottom) break; 217 218 const fromLabel = (msg.from || "?") + ": "; 219 const isMe = msg.from === handle || msg.from === "me"; 220 ink(...(isMe ? T.myMsg : T.otherHandle)); 221 write(fromLabel, { x: pad, y: my, size: 1, font }); 222 223 const textX = pad + fromLabel.length * charW; 224 const maxChars = Math.floor((W - textX - pad) / charW); 225 ink(...T.msgText); 226 write(msg.text.slice(0, maxChars), { x: textX, y: my, size: 1, font }); 227 228 my += lineH; 229 } 230 231 if (scrollOffset > 0) { 232 ink(...T.scroll); 233 write("^ scroll ^", { x: Math.floor(W / 2) - 30, y: msgTop, size: 1, font }); 234 } 235 236 if (messages.length === 0 && connected) { 237 ink(...T.empty); 238 write("no messages yet", { x: pad, y: Math.floor(H / 2) - 5, size: 1, font }); 239 } else if (!connected && !wifi?.connected) { 240 ink(...T.empty); 241 write("connect to wifi first", { x: pad, y: Math.floor(H / 2) - 5, size: 1, font }); 242 } 243 244 ink(...T.hint); 245 write("esc:back", { x: W - 9 * charW - pad, y: inputY, size: 1, font }); 246} 247 248function leave() {} 249 250export { boot, act, paint, leave };