Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

wifi piece: add theme support, connection feedback, and keyboard nav

- Respect __theme (dark/light mode) for all colors
- Track connecting SSID and show "connecting..." / "connected" per-network
- Play sounds on connection success/failure state transitions
- Add arrow key navigation and Enter to connect
- Extract shared connectEntry() helper to reduce duplication
- Show scanning state with animated dots

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

+138 -69
+138 -69
fedac/native/pieces/wifi.mjs
··· 1 1 // wifi.mjs — AC Native WiFi management piece 2 2 // Shows available networks, connects, manages saved credentials. 3 3 // Jumped to from prompt.mjs via "net" or "wifi" command. 4 + // Respects __theme (dark/light mode + presets). 4 5 5 6 const AC_SSID = "aesthetic.computer"; 6 7 const AC_PASS = "aesthetic.computer"; ··· 17 18 let cursorBlink = 0; 18 19 let savedCreds = []; 19 20 let frame = 0; 21 + let connectingSSID = ""; // track which SSID we're connecting to 22 + let lastState = 0; // track state changes for feedback 20 23 21 24 function boot({ system }) { 22 25 // Load saved credentials ··· 41 44 return; 42 45 } 43 46 if (e.is("keyboard:down:enter")) { 44 - const nets = wifi?.networks || []; 45 47 const merged = globalThis.__wifiMergedList || []; 46 48 const entry = merged[selectedIdx]; 47 49 if (entry && password.length > 0) { 48 50 wifi?.connect?.(entry.ssid, password); 51 + connectingSSID = entry.ssid; 49 52 // Save credentials 50 53 if (!savedCreds.find(c => c.ssid === entry.ssid)) { 51 54 savedCreds.push({ ssid: entry.ssid, pass: password }); ··· 77 80 return; 78 81 } 79 82 83 + const merged = globalThis.__wifiMergedList || []; 84 + 85 + // Keyboard navigation 86 + if (e.is("keyboard:down:arrowdown")) { 87 + let next = selectedIdx + 1; 88 + while (next < merged.length && merged[next].type === "separator") next++; 89 + if (next < merged.length) { 90 + selectedIdx = next; 91 + sound?.synth({ type: "sine", tone: 440, duration: 0.03, volume: 0.08, attack: 0.002, decay: 0.02 }); 92 + } 93 + return; 94 + } 95 + if (e.is("keyboard:down:arrowup")) { 96 + let prev = selectedIdx - 1; 97 + while (prev >= 0 && merged[prev].type === "separator") prev--; 98 + if (prev >= 0) { 99 + selectedIdx = prev; 100 + sound?.synth({ type: "sine", tone: 440, duration: 0.03, volume: 0.08, attack: 0.002, decay: 0.02 }); 101 + } 102 + return; 103 + } 104 + if (e.is("keyboard:down:enter")) { 105 + if (selectedIdx >= 0 && selectedIdx < merged.length) { 106 + connectEntry(merged[selectedIdx], wifi, sound, system); 107 + } 108 + return; 109 + } 110 + 80 111 // Touch: select or connect to network 81 112 if (e.is("touch")) { 82 113 const { x, y } = e; 83 114 const rowH = 16; 84 115 const listY = 44; 85 - const merged = globalThis.__wifiMergedList || []; 86 116 87 117 const tappedRow = Math.floor((y - listY) / rowH); 88 118 if (tappedRow >= 0 && tappedRow < merged.length) { ··· 91 121 92 122 if (tappedRow === selectedIdx) { 93 123 // Second tap: connect 94 - const allSaved = [ 95 - { ssid: AC_SSID, pass: AC_PASS }, 96 - ...savedCreds, 97 - ]; 98 - const cred = allSaved.find(c => c.ssid === entry.ssid); 99 - 100 - if (cred) { 101 - wifi?.connect?.(cred.ssid, cred.pass); 102 - sound?.synth({ type: "sine", tone: 660, duration: 0.08, volume: 0.15, attack: 0.005, decay: 0.06 }); 103 - } else if (entry.encrypted) { 104 - passwordMode = true; 105 - password = ""; 106 - cursorBlink = 0; 107 - } else { 108 - wifi?.connect?.(entry.ssid, ""); 109 - } 124 + connectEntry(entry, wifi, sound, system); 110 125 } else { 111 126 selectedIdx = tappedRow; 112 127 sound?.synth({ type: "sine", tone: 440, duration: 0.03, volume: 0.08, attack: 0.002, decay: 0.02 }); ··· 115 130 } 116 131 } 117 132 118 - function paint({ wipe, ink, box, write, screen, system, wifi }) { 133 + function connectEntry(entry, wifi, sound, system) { 134 + const allSaved = [ 135 + { ssid: AC_SSID, pass: AC_PASS }, 136 + ...savedCreds, 137 + ]; 138 + const cred = allSaved.find(c => c.ssid === entry.ssid); 139 + 140 + if (cred) { 141 + wifi?.connect?.(cred.ssid, cred.pass); 142 + connectingSSID = cred.ssid; 143 + sound?.synth({ type: "sine", tone: 660, duration: 0.08, volume: 0.15, attack: 0.005, decay: 0.06 }); 144 + } else if (entry.encrypted) { 145 + passwordMode = true; 146 + password = ""; 147 + cursorBlink = 0; 148 + } else { 149 + wifi?.connect?.(entry.ssid, ""); 150 + connectingSSID = entry.ssid; 151 + sound?.synth({ type: "sine", tone: 660, duration: 0.08, volume: 0.15, attack: 0.005, decay: 0.06 }); 152 + } 153 + } 154 + 155 + function paint({ wipe, ink, box, write, screen, system, wifi, sound }) { 119 156 frame++; 157 + const T = __theme.update(); 120 158 const w = screen.width, h = screen.height; 121 159 160 + // Play sound on state transitions 161 + if (wifi && wifi.state !== lastState) { 162 + if (wifi.state === 4 && lastState === 3) { // CONNECTED after CONNECTING 163 + sound?.synth({ type: "sine", tone: 880, duration: 0.15, volume: 0.12, attack: 0.005, decay: 0.1 }); 164 + } else if (wifi.state === 5 && lastState === 3) { // FAILED after CONNECTING 165 + sound?.synth({ type: "sine", tone: 220, duration: 0.2, volume: 0.12, attack: 0.005, decay: 0.15 }); 166 + } 167 + lastState = wifi ? wifi.state : 0; 168 + } 169 + 122 170 // Password entry fullscreen 123 171 if (passwordMode && wifi) { 124 172 cursorBlink++; ··· 126 174 const entry = merged[selectedIdx]; 127 175 const ssid = entry ? entry.ssid : "?"; 128 176 129 - wipe(10, 10, 15); 177 + wipe(T.bg[0], T.bg[1], T.bg[2]); 130 178 131 - ink(220, 220, 230); 179 + ink(T.fg, T.fg, T.fg); 132 180 const titleX = Math.max(10, (w - ssid.length * 18) / 2); 133 181 write(ssid, { x: titleX, y: h / 2 - 50, size: 2, font: "matrix" }); 134 182 135 - ink(120, 120, 130); 183 + ink(T.fgDim, T.fgDim, T.fgDim); 136 184 write("enter password:", { x: 20, y: h / 2 - 24, size: 2, font: "font_1" }); 137 185 138 186 // Password field 139 - ink(25, 25, 30); 187 + ink(T.bgAlt[0], T.bgAlt[1], T.bgAlt[2]); 140 188 box(18, h / 2 - 6, w - 36, 18, true); 141 - ink(70, 70, 80); 189 + ink(T.border[0], T.border[1], T.border[2]); 142 190 box(18, h / 2 - 6, w - 36, 18, "outline"); 143 191 144 192 const cursor = (cursorBlink % 60) < 35 ? "|" : ""; 145 - ink(220, 220, 230); 193 + ink(T.fg, T.fg, T.fg); 146 194 write(password + cursor, { x: 22, y: h / 2 - 2, size: 1, font: "font_1" }); 147 195 148 - ink(80, 80, 90); 196 + ink(T.fgMute, T.fgMute, T.fgMute); 149 197 write("Enter: connect Esc: cancel", { x: 20, y: h / 2 + 22, size: 1, font: "font_1" }); 150 198 151 199 if (wifi.state === 3) { 152 - ink(200, 200, 80); 200 + ink(T.warn[0], T.warn[1], T.warn[2]); 153 201 write("connecting...", { x: 20, y: h / 2 + 40, size: 1, font: "font_1" }); 154 202 } else if (wifi.state === 5) { 155 - ink(220, 80, 80); 203 + ink(T.err[0], T.err[1], T.err[2]); 156 204 write("failed: " + (wifi.status || "?"), { x: 20, y: h / 2 + 40, size: 1, font: "font_1" }); 157 205 } 158 206 return; 159 207 } 160 208 161 - wipe(10, 10, 15); 209 + wipe(T.bg[0], T.bg[1], T.bg[2]); 162 210 163 211 // Title 164 - ink(220, 220, 230); 212 + ink(T.fg, T.fg, T.fg); 165 213 write("WiFi Networks", { x: 20, y: 12, size: 2, font: "matrix" }); 166 214 167 - // Status 168 - ink(120, 120, 130); 215 + // Status line 216 + ink(T.fgDim, T.fgDim, T.fgDim); 169 217 const statusStr = wifi ? ((wifi.status || "scanning...") + (wifi.iface ? " [" + wifi.iface + "]" : "")) : "no wifi"; 170 218 write(statusStr, { x: 20, y: 30, size: 1, font: "font_1" }); 171 219 172 220 if (!wifi) { 173 - ink(200, 80, 80); 221 + ink(T.err[0], T.err[1], T.err[2]); 174 222 write("wifi not available", { x: 20, y: 60, size: 1, font: "font_1" }); 175 - ink(60, 80, 60); 223 + ink(T.fgMute, T.fgMute, T.fgMute); 176 224 write("esc: back", { x: 10, y: h - 12, size: 1, font: "font_1" }); 177 225 return; 178 226 } ··· 196 244 const rowH = 16; 197 245 const listY = 44; 198 246 const totalRows = scannedNets.length + (offlineSaved.length > 0 ? 1 + offlineSaved.length : 0); 199 - const maxRows = Math.min(totalRows, Math.floor((h - listY - 30) / rowH)); 247 + const maxRows = Math.min(totalRows, Math.floor((h - listY - 60) / rowH)); 200 248 globalThis.__wifiMergedList = []; 201 249 202 250 let row = 0; ··· 207 255 const ry = listY + row * rowH; 208 256 const isSaved = allSaved.find(c => c.ssid === net.ssid); 209 257 const isSelected = row === selectedIdx; 258 + const isConnecting = wifi.state === 3 && connectingSSID === net.ssid; 259 + const isConnected = wifi.connected && wifi.ssid === net.ssid; 210 260 211 261 if (isSelected) { 212 - ink(40, 55, 80); 262 + ink(T.bar[0], T.bar[1], T.bar[2]); 213 263 box(10, ry, w - 20, rowH, true); 214 264 } 215 265 216 266 // Signal bars 217 267 const bars = net.signal > -50 ? 4 : net.signal > -60 ? 3 : net.signal > -70 ? 2 : 1; 218 268 for (let b = 0; b < 4; b++) { 219 - if (b < bars) ink(80, 200, 80); 220 - else ink(40, 40, 45); 269 + if (b < bars) ink(T.ok[0], T.ok[1], T.ok[2]); 270 + else ink(T.bgDim[0], T.bgDim[1], T.bgDim[2]); 221 271 box(16 + b * 4, ry + 10 - (b + 1) * 2, 3, (b + 1) * 2, true); 222 272 } 223 273 224 274 // SSID 225 - if (isSaved) ink(100, 220, 100); 226 - else ink(220, 220, 230); 275 + if (isConnected) ink(T.ok[0], T.ok[1], T.ok[2]); 276 + else if (isSaved) ink(T.ok[0], T.ok[1], T.ok[2]); 277 + else ink(T.fg, T.fg, T.fg); 227 278 const ssidDisplay = net.ssid.length > 26 ? net.ssid.slice(0, 25) + "~" : net.ssid; 228 279 write(ssidDisplay, { x: 36, y: ry + 2, size: 1, font: "font_1" }); 229 280 230 - // Right side 231 - if (isSelected) { 232 - ink(80, 180, 255); 281 + // Right side status 282 + if (isConnected) { 283 + ink(T.ok[0], T.ok[1], T.ok[2]); 284 + write("connected", { x: w - 60, y: ry + 2, size: 1, font: "font_1" }); 285 + } else if (isConnecting) { 286 + const dots = ".".repeat((Math.floor(frame / 15) % 3) + 1); 287 + ink(T.warn[0], T.warn[1], T.warn[2]); 288 + write("connecting" + dots, { x: w - 72, y: ry + 2, size: 1, font: "font_1" }); 289 + } else if (isSelected) { 290 + ink(T.link[0], T.link[1], T.link[2]); 233 291 write("connect", { x: w - 52, y: ry + 2, size: 1, font: "font_1" }); 234 292 } else if (isSaved) { 235 - ink(60, 160, 60); 293 + ink(T.fgMute, T.fgMute, T.fgMute); 236 294 write("saved", { x: w - 40, y: ry + 2, size: 1, font: "font_1" }); 237 295 } else if (net.encrypted) { 238 - ink(80, 80, 90); 296 + ink(T.fgMute, T.fgMute, T.fgMute); 239 297 write("*", { x: w - 20, y: ry + 2, size: 1, font: "font_1" }); 240 298 } 241 299 ··· 245 303 // Saved/preset networks not in scan 246 304 if (offlineSaved.length > 0 && row < maxRows) { 247 305 const sepY = listY + row * rowH; 248 - ink(80, 80, 90); 306 + ink(T.fgMute, T.fgMute, T.fgMute); 249 307 write("-- saved (not in range) --", { x: 20, y: sepY + 2, size: 1, font: "font_1" }); 250 308 globalThis.__wifiMergedList.push({ type: "separator" }); 251 309 row++; ··· 257 315 const isSelected = row === selectedIdx; 258 316 259 317 if (isSelected) { 260 - ink(35, 40, 55); 318 + ink(T.bar[0], T.bar[1], T.bar[2]); 261 319 box(10, ry, w - 20, rowH, true); 262 320 } 263 321 264 322 for (let b = 0; b < 4; b++) { 265 - ink(30, 30, 35); 323 + ink(T.bgDim[0], T.bgDim[1], T.bgDim[2]); 266 324 box(16 + b * 4, ry + 10 - (b + 1) * 2, 3, (b + 1) * 2, true); 267 325 } 268 326 269 - ink(120, 120, 130); 327 + ink(T.fgDim, T.fgDim, T.fgDim); 270 328 write(cred.ssid, { x: 36, y: ry + 2, size: 1, font: "font_1" }); 271 329 272 330 if (isSelected) { 273 - ink(80, 180, 255); 331 + ink(T.link[0], T.link[1], T.link[2]); 274 332 write("connect", { x: w - 52, y: ry + 2, size: 1, font: "font_1" }); 275 333 } else if (isPreset) { 276 - ink(80, 120, 160); 334 + ink(T.fgMute, T.fgMute, T.fgMute); 277 335 write("preset", { x: w - 44, y: ry + 2, size: 1, font: "font_1" }); 278 336 } else { 279 - ink(60, 140, 60); 337 + ink(T.fgMute, T.fgMute, T.fgMute); 280 338 write("saved", { x: w - 40, y: ry + 2, size: 1, font: "font_1" }); 281 339 } 282 340 ··· 289 347 290 348 if (wifi.connected && wifi.ip) { 291 349 // Connected: show SSID, IP, signal, SSH 292 - ink(30, 50, 30); 350 + const okBg = T.dark ? [20, 40, 20] : [220, 245, 220]; 351 + const okBorder = T.dark ? [40, 60, 40] : [180, 220, 180]; 352 + ink(okBg[0], okBg[1], okBg[2]); 293 353 box(10, panelY, w - 20, 40, true); 294 - ink(50, 70, 50); 354 + ink(okBorder[0], okBorder[1], okBorder[2]); 295 355 box(10, panelY, w - 20, 40, "outline"); 296 356 297 - ink(80, 230, 80); 357 + ink(T.ok[0], T.ok[1], T.ok[2]); 298 358 write(wifi.ssid || "connected", { x: 16, y: panelY + 4, size: 1, font: "font_1" }); 299 359 300 - ink(160, 220, 160); 360 + ink(T.fg, T.fg, T.fg); 301 361 write(wifi.ip, { x: 16, y: panelY + 16, size: 1, font: "font_1" }); 302 362 303 - // Signal strength (if available) 363 + // Signal strength 304 364 if (wifi.signal) { 305 365 const sig = wifi.signal; 306 366 const qual = sig > -50 ? "excellent" : sig > -60 ? "good" : sig > -70 ? "fair" : "weak"; 307 - ink(120, 180, 120); 367 + ink(T.fgDim, T.fgDim, T.fgDim); 308 368 write(sig + " dBm (" + qual + ")", { x: w / 2, y: panelY + 4, size: 1, font: "font_1" }); 309 369 } 310 370 311 371 if (system?.sshStarted) { 312 - ink(100, 160, 200); 372 + ink(T.link[0], T.link[1], T.link[2]); 313 373 write("ssh root@" + wifi.ip, { x: w / 2, y: panelY + 16, size: 1, font: "font_1" }); 314 374 } 315 375 } else if (wifi.state === 3) { 316 376 // Connecting 317 377 const dots = ".".repeat((Math.floor(frame / 15) % 3) + 1); 318 - ink(30, 35, 50); 378 + const warnBg = T.dark ? [30, 30, 15] : [255, 250, 220]; 379 + ink(warnBg[0], warnBg[1], warnBg[2]); 319 380 box(10, panelY, w - 20, 40, true); 320 - ink(200, 200, 80); 381 + ink(T.warn[0], T.warn[1], T.warn[2]); 321 382 write("connecting" + dots, { x: 16, y: panelY + 4, size: 1, font: "font_1" }); 322 - ink(140, 140, 80); 323 - write(wifi.status || "", { x: 16, y: panelY + 16, size: 1, font: "font_1" }); 383 + ink(T.fgDim, T.fgDim, T.fgDim); 384 + write((connectingSSID || wifi.status || ""), { x: 16, y: panelY + 16, size: 1, font: "font_1" }); 324 385 } else if (wifi.state === 5) { 325 386 // Failed 326 - ink(50, 25, 25); 387 + const errBg = T.dark ? [40, 15, 15] : [255, 230, 230]; 388 + ink(errBg[0], errBg[1], errBg[2]); 327 389 box(10, panelY, w - 20, 40, true); 328 - ink(230, 80, 80); 390 + ink(T.err[0], T.err[1], T.err[2]); 329 391 write("failed", { x: 16, y: panelY + 4, size: 1, font: "font_1" }); 330 - ink(180, 100, 100); 331 - write(wifi.status || "unknown error", { x: 16, y: panelY + 16, size: 1, font: "font_1" }); 392 + ink(T.fgDim, T.fgDim, T.fgDim); 393 + write((connectingSSID ? connectingSSID + ": " : "") + (wifi.status || "unknown error"), { x: 16, y: panelY + 16, size: 1, font: "font_1" }); 394 + } else if (wifi.state === 1) { 395 + // Scanning 396 + const dots = ".".repeat((Math.floor(frame / 20) % 3) + 1); 397 + ink(T.bgAlt[0], T.bgAlt[1], T.bgAlt[2]); 398 + box(10, panelY, w - 20, 40, true); 399 + ink(T.fgDim, T.fgDim, T.fgDim); 400 + write("scanning" + dots, { x: 16, y: panelY + 12, size: 1, font: "font_1" }); 332 401 } 333 402 334 403 // Bottom hints 335 - ink(60, 80, 60); 404 + ink(T.fgMute, T.fgMute, T.fgMute); 336 405 write("esc: back", { x: w - 60, y: h - 10, size: 1, font: "font_1" }); 337 406 if (wifi.iface) { 338 - ink(50, 50, 60); 407 + ink(T.fgMute, T.fgMute, T.fgMute); 339 408 write(wifi.iface, { x: 16, y: h - 10, size: 1, font: "font_1" }); 340 409 } 341 410