Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

fix: dj.mjs event handling, write API, and layout for ac-native

Use e.is() pattern for keyboard events (escape wasn't working),
object format for write() calls, and compact layout that fits
~300px wide screen without overlapping.

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

+127 -119
+127 -119
fedac/native/pieces/dj.mjs
··· 81 81 82 82 function act({ event: e, sound, system }) { 83 83 if (!e.is("keyboard:down")) return; 84 - const key = e.key; 85 84 86 85 // Exit (audio persists!) 87 - if (key === "Escape") { 86 + if (e.is("keyboard:down:escape")) { 88 87 system?.jump?.("prompt"); 89 88 return; 90 89 } 91 90 92 91 // Deck selection 93 - if (key === "Tab") { 92 + if (e.is("keyboard:down:tab")) { 94 93 activeDeck = activeDeck === 0 ? 1 : 0; 95 - sound?.synth({ type: "sine", tone: activeDeck ? 880 : 660, duration: 0.04, volume: 0.06 }); 94 + sound?.synth?.({ type: "sine", tone: activeDeck ? 880 : 660, duration: 0.04, volume: 0.06 }); 96 95 return; 97 96 } 98 97 99 98 // Play/pause 100 - if (key === " ") { 99 + if (e.is("keyboard:down:space")) { 101 100 const d = sound?.deck?.decks?.[activeDeck]; 102 101 if (d?.loaded) { 103 102 if (d.playing) { ··· 112 111 } 113 112 114 113 // Load file into active deck 115 - if (key === "Enter") { 114 + if (e.is("keyboard:down:enter") || e.is("keyboard:down:return")) { 116 115 if (files.length === 0) return; 117 116 const sel = files[selectedIdx]; 118 117 if (!sel) return; 119 118 const fullPath = currentPath + "/" + sel.name; 120 119 if (sel.isDir) { 121 120 browseDir(system, fullPath); 122 - sound?.synth({ type: "sine", tone: 550, duration: 0.03, volume: 0.06 }); 121 + sound?.synth?.({ type: "sine", tone: 550, duration: 0.03, volume: 0.06 }); 123 122 } else { 124 123 const ok = sound?.deck?.load(activeDeck, fullPath); 125 124 if (ok) { 126 - showMsg(`Loaded → Deck ${activeDeck ? "B" : "A"}: ${sel.name}`); 127 - sound?.synth({ type: "sine", tone: 880, duration: 0.06, volume: 0.08 }); 125 + showMsg(`Loaded -> Deck ${activeDeck ? "B" : "A"}: ${sel.name}`); 126 + sound?.synth?.({ type: "sine", tone: 880, duration: 0.06, volume: 0.08 }); 128 127 } else { 129 128 showMsg(`Failed to load: ${sel.name}`); 130 - sound?.synth({ type: "noise", tone: 200, duration: 0.1, volume: 0.08 }); 129 + sound?.synth?.({ type: "square", tone: 200, duration: 0.1, volume: 0.08 }); 131 130 } 132 131 } 133 132 return; 134 133 } 135 134 136 135 // Navigate file browser 137 - if (key === "ArrowDown") { 136 + if (e.is("keyboard:down:arrowdown")) { 138 137 if (files.length > 0) selectedIdx = Math.min(selectedIdx + 1, files.length - 1); 139 - sound?.synth({ type: "sine", tone: 440, duration: 0.02, volume: 0.04 }); 138 + sound?.synth?.({ type: "sine", tone: 440, duration: 0.02, volume: 0.04 }); 140 139 return; 141 140 } 142 - if (key === "ArrowUp") { 141 + if (e.is("keyboard:down:arrowup")) { 143 142 if (files.length > 0) selectedIdx = Math.max(selectedIdx - 1, 0); 144 - sound?.synth({ type: "sine", tone: 480, duration: 0.02, volume: 0.04 }); 143 + sound?.synth?.({ type: "sine", tone: 480, duration: 0.02, volume: 0.04 }); 145 144 return; 146 145 } 147 146 148 147 // Go up directory 149 - if (key === "Backspace") { 148 + if (e.is("keyboard:down:backspace")) { 150 149 const parent = currentPath.replace(/\/[^/]*$/, "") || "/"; 151 150 if (parent !== currentPath) { 152 151 browseDir(system, parent); 153 - sound?.synth({ type: "sine", tone: 330, duration: 0.04, volume: 0.06 }); 152 + sound?.synth?.({ type: "sine", tone: 330, duration: 0.04, volume: 0.06 }); 154 153 } 155 154 return; 156 155 } 157 156 158 157 // Seek 159 - if (key === "ArrowLeft") { 158 + if (e.is("keyboard:down:arrowleft")) { 160 159 const d = sound?.deck?.decks?.[activeDeck]; 161 160 if (d?.loaded) sound.deck.seek(activeDeck, Math.max(0, d.position - 5)); 162 161 return; 163 162 } 164 - if (key === "ArrowRight") { 163 + if (e.is("keyboard:down:arrowright")) { 165 164 const d = sound?.deck?.decks?.[activeDeck]; 166 165 if (d?.loaded) sound.deck.seek(activeDeck, Math.min(d.duration, d.position + 5)); 167 166 return; 168 167 } 169 168 170 169 // Crossfader 171 - if (key === "[") { 170 + if (e.is("keyboard:down:[")) { 172 171 const cf = Math.max(0, (sound?.deck?.crossfaderValue || 0.5) - 0.05); 173 172 sound?.deck?.setCrossfader(cf); 174 173 return; 175 174 } 176 - if (key === "]") { 175 + if (e.is("keyboard:down:]")) { 177 176 const cf = Math.min(1, (sound?.deck?.crossfaderValue || 0.5) + 0.05); 178 177 sound?.deck?.setCrossfader(cf); 179 178 return; 180 179 } 181 180 182 181 // Volume 183 - if (key === "-") { 182 + if (e.is("keyboard:down:-")) { 184 183 const d = sound?.deck?.decks?.[activeDeck]; 185 184 if (d) sound.deck.setVolume(activeDeck, Math.max(0, d.volume - 0.05)); 186 185 return; 187 186 } 188 - if (key === "=") { 187 + if (e.is("keyboard:down:=")) { 189 188 const d = sound?.deck?.decks?.[activeDeck]; 190 189 if (d) sound.deck.setVolume(activeDeck, Math.min(1, d.volume + 0.05)); 191 190 return; 192 191 } 193 192 194 193 // Speed 195 - if (key === "z") { 194 + if (e.is("keyboard:down:z")) { 196 195 const d = sound?.deck?.decks?.[activeDeck]; 197 196 if (d?.loaded) sound.deck.setSpeed(activeDeck, Math.max(0.5, d.speed - 0.05)); 198 197 return; 199 198 } 200 - if (key === "x") { 199 + if (e.is("keyboard:down:x")) { 201 200 const d = sound?.deck?.decks?.[activeDeck]; 202 201 if (d?.loaded) sound.deck.setSpeed(activeDeck, Math.min(2.0, d.speed + 0.05)); 203 202 return; 204 203 } 205 204 206 205 // Quick play: Q = deck A, W = deck B 207 - if (key === "q") { 206 + if (e.is("keyboard:down:q")) { 208 207 const d = sound?.deck?.decks?.[0]; 209 208 if (d?.loaded) { if (d.playing) sound.deck.pause(0); else sound.deck.play(0); } 210 209 return; 211 210 } 212 - if (key === "w") { 211 + if (e.is("keyboard:down:w")) { 213 212 const d = sound?.deck?.decks?.[1]; 214 213 if (d?.loaded) { if (d.playing) sound.deck.pause(1); else sound.deck.play(1); } 215 214 return; ··· 218 217 219 218 function paint({ wipe, ink, box, line, write, screen, sound }) { 220 219 frame++; 221 - const W = screen.width, H = screen.height; 222 - wipe(10, 10, 14); // dark background 220 + const w = screen.width, h = screen.height; 221 + const pad = 4; 222 + const F = "font_1"; 223 + const CW = 6; // font_1 char width 224 + wipe(8, 8, 12); 223 225 224 226 const decks = sound?.deck?.decks || [{}, {}]; 225 227 const cf = sound?.deck?.crossfaderValue ?? 0.5; 226 - const halfW = Math.floor(W / 2) - 2; 228 + const deckW = Math.floor((w - pad * 3) / 2); 227 229 228 - // --- Draw two decks --- 229 - for (let d = 0; d < 2; d++) { 230 - const dk = decks[d]; 231 - const x0 = d === 0 ? 0 : halfW + 4; 230 + // --- Deck A (top-left) --- 231 + const drawDeck = (dk, d, x0) => { 232 232 const isActive = d === activeDeck; 233 - const headerY = 2; 233 + const label = d === 0 ? "A" : "B"; 234 + const maxChars = Math.floor((deckW - 4) / CW); 234 235 235 - // Deck header 236 - ink(isActive ? 255 : 100, isActive ? 255 : 100, isActive ? 100 : 80); 237 - write(`DECK ${d === 0 ? "A" : "B"}`, [x0 + 2, headerY], 1); 236 + // Header: label + status 237 + ink(isActive ? 255 : 100, isActive ? 255 : 80, isActive ? 80 : 60); 238 + write(label, { x: x0, y: pad, size: 1, font: "matrix" }); 238 239 239 - if (dk.loaded) { 240 - // Artist / Title 241 - ink(180, 180, 200); 242 - if (dk.artist) write(dk.artist.slice(0, 20), [x0 + 2, headerY + 12], 1); 243 - ink(255, 255, 255); 244 - write((dk.title || "???").slice(0, 22), [x0 + 2, headerY + 22], 1); 240 + if (!dk.loaded) { 241 + ink(60, 60, 80); 242 + write("--", { x: x0 + 12, y: pad, size: 1, font: F }); 243 + return; 244 + } 245 245 246 - // Progress bar 247 - const barY = headerY + 34; 248 - const barW = halfW - 8; 249 - const progress = dk.duration > 0 ? dk.position / dk.duration : 0; 250 - ink(40, 40, 50); 251 - box(x0 + 4, barY, barW, 6); 252 - ink(dk.playing ? 80 : 50, dk.playing ? 200 : 120, dk.playing ? 80 : 50); 253 - box(x0 + 4, barY, Math.floor(barW * progress), 6); 246 + // Playing indicator 247 + if (dk.playing) { 248 + ink(60, 220, 60); 249 + write(">", { x: x0 + 12, y: pad, size: 1, font: F }); 250 + } else { 251 + ink(180, 180, 60); 252 + write("=", { x: x0 + 12, y: pad, size: 1, font: F }); 253 + } 254 254 255 - // Time 256 - ink(160, 160, 180); 257 - write(`${formatTime(dk.position)} / ${formatTime(dk.duration)}`, [x0 + 2, barY + 10], 1); 255 + // Title (truncated) 256 + ink(220, 220, 240); 257 + write((dk.title || "?").slice(0, maxChars - 3), { x: x0 + 20, y: pad, size: 1, font: F }); 258 258 259 - // Speed + Volume 260 - ink(120, 120, 150); 261 - write(`SPD ${dk.speed?.toFixed(2) || "1.00"}x`, [x0 + 2, barY + 22], 1); 262 - write(`VOL ${Math.round((dk.volume || 0) * 100)}%`, [x0 + halfW / 2, barY + 22], 1); 259 + // Progress bar 260 + const barY = pad + 12; 261 + const barW = deckW - 4; 262 + const progress = dk.duration > 0 ? dk.position / dk.duration : 0; 263 + ink(30, 30, 45); 264 + box(x0, barY, barW, 4); 265 + ink(dk.playing ? 60 : 40, dk.playing ? 180 : 100, dk.playing ? 60 : 40); 266 + box(x0, barY, Math.max(1, Math.floor(barW * progress)), 4); 263 267 264 - // Playing indicator 265 - if (dk.playing) { 266 - ink(80, 255, 80); 267 - write(">>", [x0 + halfW - 16, headerY], 1); 268 - } else if (dk.finished) { 269 - ink(200, 100, 100); 270 - write("END", [x0 + halfW - 22, headerY], 1); 271 - } else { 272 - ink(200, 200, 100); 273 - write("||", [x0 + halfW - 16, headerY], 1); 274 - } 275 - } else { 276 - ink(80, 80, 100); 277 - write("No track", [x0 + 2, headerY + 22], 1); 278 - } 279 - } 268 + // Time + speed 269 + ink(140, 140, 160); 270 + const timeTxt = `${formatTime(dk.position)}/${formatTime(dk.duration)}`; 271 + write(timeTxt, { x: x0, y: barY + 6, size: 1, font: F }); 272 + ink(100, 100, 120); 273 + const spdTxt = `${dk.speed?.toFixed(2) || "1.00"}x`; 274 + write(spdTxt, { x: x0 + deckW - spdTxt.length * CW - 4, y: barY + 6, size: 1, font: F }); 275 + }; 280 276 281 - // --- Crossfader --- 282 - const cfX = halfW; 283 - const cfY = 2; 284 - ink(80, 80, 100); 285 - write("CF", [cfX - 4, cfY], 1); 286 - // Visual crossfader bar 287 - const cfBarY = cfY + 12; 288 - const cfBarH = 50; 289 - ink(40, 40, 50); 290 - box(cfX, cfBarY, 3, cfBarH); 291 - const cfPos = Math.floor(cfBarH * cf); 292 - ink(255, 200, 80); 293 - box(cfX - 1, cfBarY + cfPos - 1, 5, 3); 277 + drawDeck(decks[0], 0, pad); 278 + drawDeck(decks[1], 1, pad * 2 + deckW); 279 + 280 + // --- Crossfader (horizontal bar below decks) --- 281 + const cfY = pad + 28; 282 + const cfW = w - pad * 2; 283 + ink(25, 25, 40); 284 + box(pad, cfY, cfW, 3); 285 + const cfPos = Math.floor(cfW * cf); 286 + ink(255, 200, 60); 287 + box(pad + cfPos - 2, cfY - 1, 5, 5); 288 + ink(50, 50, 70); 289 + write("A", { x: pad, y: cfY + 5, size: 1, font: F }); 290 + write("B", { x: w - pad - CW, y: cfY + 5, size: 1, font: F }); 294 291 295 292 // --- Divider --- 296 - ink(40, 40, 60); 297 - const divY = 72; 298 - line(0, divY, W, divY); 293 + const divY = cfY + 14; 294 + ink(30, 30, 50); 295 + line(0, divY, w, divY); 299 296 300 297 // --- File browser --- 301 - const browserY = divY + 2; 302 - const lineH = 11; 303 - const maxVisible = Math.floor((H - browserY - 20) / lineH); 298 + const rowH = 12; 299 + const headerY = divY + 2; 300 + ink(100, 100, 140); 301 + const pathMax = Math.floor(w / CW) - 2; 302 + const pathStr = currentPath.length > pathMax 303 + ? "..." + currentPath.slice(-pathMax + 3) : currentPath; 304 + write(pathStr, { x: pad, y: headerY, size: 1, font: F }); 305 + 306 + // Scroll count 307 + if (files.length > 0) { 308 + ink(60, 60, 80); 309 + const si = `${selectedIdx + 1}/${files.length}`; 310 + write(si, { x: w - si.length * CW - pad, y: headerY, size: 1, font: F }); 311 + } 304 312 305 - // Ensure selected item is visible 313 + const listY = headerY + 12; 314 + const maxVisible = Math.floor((h - listY - 14) / rowH); 315 + 306 316 if (selectedIdx < scrollOffset) scrollOffset = selectedIdx; 307 317 if (selectedIdx >= scrollOffset + maxVisible) scrollOffset = selectedIdx - maxVisible + 1; 308 - 309 - ink(120, 120, 160); 310 - write(`${currentPath}/`, [2, browserY], 1); 311 318 312 319 if (files.length === 0) { 313 - ink(100, 100, 120); 314 - write("(empty)", [4, browserY + lineH], 1); 320 + ink(80, 80, 100); 321 + write("(empty)", { x: pad, y: listY, size: 1, font: F }); 315 322 } 316 323 317 324 for (let i = 0; i < maxVisible && i + scrollOffset < files.length; i++) { 318 325 const fi = files[i + scrollOffset]; 319 - const y = browserY + lineH + i * lineH; 326 + const y = listY + i * rowH; 320 327 const isSel = (i + scrollOffset) === selectedIdx; 321 328 322 329 if (isSel) { 323 - ink(30, 30, 50); 324 - box(0, y - 1, W, lineH); 330 + ink(25, 30, 45); 331 + box(0, y - 1, w, rowH); 325 332 } 326 333 334 + const nameMax = Math.floor(w / CW) - 6; 327 335 if (fi.isDir) { 328 - ink(isSel ? 255 : 140, isSel ? 220 : 140, isSel ? 100 : 100); 329 - write(`> ${fi.name}/`, [4, y], 1); 336 + ink(isSel ? 255 : 120, isSel ? 200 : 120, isSel ? 80 : 80); 337 + write(`> ${fi.name.slice(0, nameMax)}/`, { x: pad, y: y, size: 1, font: F }); 330 338 } else { 331 - ink(isSel ? 255 : 180, isSel ? 255 : 180, isSel ? 255 : 200); 332 - const sizeStr = formatSize(fi.size); 333 - write(` ${fi.name}`, [4, y], 1); 334 - ink(100, 100, 120); 335 - write(sizeStr, [W - sizeStr.length * 6 - 4, y], 1); 339 + ink(isSel ? 255 : 160, isSel ? 255 : 160, isSel ? 255 : 180); 340 + write(fi.name.slice(0, nameMax), { x: pad + CW, y: y, size: 1, font: F }); 341 + ink(80, 80, 100); 342 + const sz = formatSize(fi.size); 343 + write(sz, { x: w - sz.length * CW - pad, y: y, size: 1, font: F }); 336 344 } 337 345 } 338 346 339 347 // --- Status bar --- 340 - const statusY = H - 10; 341 - ink(20, 20, 30); 342 - box(0, statusY - 1, W, 12); 343 - ink(160, 160, 180); 344 - const deckLabel = activeDeck === 0 ? "A" : "B"; 345 - write(`Deck:${deckLabel} Q/W:play Tab:switch []:xfade -/=:vol z/x:spd Esc:exit`, [2, statusY], 1); 348 + const sY = h - 11; 349 + ink(15, 15, 25); 350 + box(0, sY - 1, w, 12); 351 + ink(120, 120, 140); 352 + const dl = activeDeck === 0 ? "A" : "B"; 353 + write(`${dl} Spc:play Tab:deck Esc:exit`, { x: pad, y: sY, size: 1, font: F }); 346 354 347 - // --- Message overlay --- 355 + // --- Message --- 348 356 if (message && frame - messageFrame < 120) { 349 - const alpha = Math.max(0, 1 - (frame - messageFrame) / 120); 350 - ink(Math.floor(255 * alpha), Math.floor(220 * alpha), Math.floor(80 * alpha)); 351 - write(message, [W / 2 - message.length * 3, divY - 10], 1); 357 + const fade = Math.max(0, 255 - Math.floor((frame - messageFrame) * 2.5)); 358 + ink(255, 220, 60, fade); 359 + write(message, { x: pad, y: divY - 10, size: 1, font: F }); 352 360 } 353 361 } 354 362