Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat(electron): open external URLs in system browser, redesign volume slider

- External URLs now open in the system's default browser instead of
trying to open as AC pieces
- Volume slider moved to top-right corner, integrated into frame like tabs
- Added open-external-url IPC handler

+543 -66
+25
ac-electron/main.js
··· 1903 1903 return { success: true }; 1904 1904 }); 1905 1905 1906 + // Open external URL in the system's default browser 1907 + ipcMain.handle('open-external-url', async (event, url) => { 1908 + console.log('[main] Opening external URL:', url); 1909 + shell.openExternal(url); 1910 + return { success: true }; 1911 + }); 1912 + 1906 1913 ipcMain.handle('switch-mode', async (event, mode) => { 1907 1914 // Always open AC Pane 1908 1915 await openAcPaneWindow(); ··· 2087 2094 } 2088 2095 } 2089 2096 return { action: 'deny' }; 2097 + } 2098 + 2099 + // Check if this is an external URL (not aesthetic.computer or localhost) 2100 + // External URLs should open in the system's default browser 2101 + try { 2102 + const urlObj = new URL(url); 2103 + const isExternal = !urlObj.hostname.includes('aesthetic.computer') && 2104 + !urlObj.hostname.includes('localhost') && 2105 + !urlObj.hostname.includes('127.0.0.1') && 2106 + !url.startsWith('ac://'); 2107 + 2108 + if (isExternal) { 2109 + console.log('[main] Opening external URL in system browser:', url); 2110 + shell.openExternal(url); 2111 + return { action: 'deny' }; 2112 + } 2113 + } catch (e) { 2114 + console.warn('[main] Failed to parse URL:', url, e.message); 2090 2115 } 2091 2116 2092 2117 // Handle new window request (from prompt.mjs '+' command)
+1
ac-electron/preload.js
··· 51 51 moveWindow: (x, y) => ipcRenderer.send('move-window', { x, y }), 52 52 openWindow: (url) => ipcRenderer.invoke('ac-open-window', { url }), 53 53 closeWindow: () => ipcRenderer.invoke('ac-close-window'), 54 + openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url), 54 55 55 56 // App info (for desktop.mjs) 56 57 getAppInfo: () => ipcRenderer.invoke('get-app-info'),
+34 -7
ac-electron/renderer/flip-view.html
··· 254 254 border-radius: 0 6px 6px 0; 255 255 } 256 256 257 - /* Volume control - attached to right edge */ 257 + /* Volume control - integrated into top-right corner of frame */ 258 258 .volume-control { 259 259 position: absolute; 260 - right: -26px; 261 - top: 18px; 260 + right: 0; 261 + top: -26px; 262 262 z-index: 240; 263 263 height: 26px; 264 264 display: flex; 265 265 flex-direction: row; 266 266 align-items: center; 267 267 gap: 6px; 268 - padding: 4px 10px 4px 8px; 269 - border-radius: 0 10px 10px 0; 268 + padding: 4px 12px 4px 10px; 269 + border-radius: 10px 10px 0 0; 270 270 background: var(--ac-border-solid); 271 271 border: 1px solid var(--ac-border); 272 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); 272 + border-bottom: none; 273 + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.2); 273 274 } 274 275 275 276 body.midflip .volume-control { ··· 277 278 border-color: var(--ac-border-solid); 278 279 } 279 280 281 + .volume-icon { 282 + font-size: 12px; 283 + opacity: 0.7; 284 + } 285 + 280 286 .volume-value { 281 287 font-size: 11px; 282 288 color: var(--ac-text); 289 + min-width: 20px; 290 + text-align: right; 283 291 } 284 292 285 293 #volume-slider { 286 294 -webkit-appearance: none; 287 295 appearance: none; 288 - width: 92px; 296 + width: 72px; 289 297 height: 4px; 290 298 background: var(--ac-border-solid); 291 299 border-radius: 2px; ··· 656 664 </div> 657 665 658 666 <div class="volume-control" aria-label="Master volume"> 667 + <span class="volume-icon">๐Ÿ”Š</span> 659 668 <input id="volume-slider" type="range" min="0" max="1" step="0.01" value="0.25" /> 660 669 <div class="volume-value" id="volume-value">25</div> 661 670 </div> ··· 859 868 ipcRenderer.invoke('ac-close-window'); 860 869 return; 861 870 } 871 + 872 + // Check if this is an external URL that should open in the system browser 873 + try { 874 + const urlObj = new URL(url); 875 + const isExternal = !urlObj.hostname.includes('aesthetic.computer') && 876 + !urlObj.hostname.includes('localhost') && 877 + !urlObj.hostname.includes('127.0.0.1') && 878 + !url.startsWith('ac://'); 879 + 880 + if (isExternal) { 881 + console.log('[flip] Opening external URL in system browser:', url); 882 + ipcRenderer.invoke('open-external-url', url); 883 + return; 884 + } 885 + } catch (err) { 886 + console.warn('[flip] Failed to parse URL:', url, err.message); 887 + } 888 + 862 889 console.log('[flip] Opening new window with url:', url); 863 890 ipcRenderer.invoke('ac-open-window', { url }); 864 891 });
+67
plan/clock-parallel-timing-fix.md
··· 1 + # Clock Parallel Track Timing Fix 2 + 3 + ## Problem Statement 4 + 5 + When running `clock *lene` (melody: `^f..afafa...efefef..bababa.fgg..b...agfededcd {noise-white}-^f.fffffff`): 6 + 7 + - **Track 1**: 31 notes, 13 beats (6500ms cycle) 8 + - **Track 2**: 9 notes (1 rest + 8 f's), 12 beats (6000ms cycle) 9 + 10 + **Expected**: Each track loops independently at its own duration. 11 + **Actual**: Track 2's first cycle shows "EARLY" because we skip leading rests on init. 12 + 13 + ## Root Cause Analysis 14 + 15 + The `-` character creates a 4-beat rest at the start of Track 2. During initialization: 16 + 1. We skip leading rests by advancing `noteIndex` to the first audible note 17 + 2. **First cycle is shorter**: Only plays 8 beats (4000ms), not 12 beats (6000ms) 18 + 3. After loop 1, the rest offset (2000ms) is added, so subsequent cycles are correct 19 + 20 + ## Fix Applied 21 + 22 + The expected duration calculation now accounts for the first cycle being shorter: 23 + 24 + ```javascript 25 + const isFirstCycle = trackState.loopCount === 1; 26 + const expectedDuration = isFirstCycle 27 + ? (trackState.expectedCycleDuration || 0) - (trackState.initialRestOffset || 0) 28 + : (trackState.expectedCycleDuration || 0); 29 + ``` 30 + 31 + ## Console Output (Now Clean) 32 + 33 + Only cycle timing logs appear: 34 + ``` 35 + โฑ๏ธ T1 INIT | 13 beats = 6500ms expected cycle 36 + โฑ๏ธ T2 INIT | 12 beats = 6000ms expected cycle (has leading rest) 37 + โฑ๏ธ T2 loop 1 @ ...ms | cycle: 4000ms (expected: 4000ms โœ“) โ† First cycle is 4000ms! 38 + โฑ๏ธ T1 loop 1 @ ...ms | cycle: 6500ms (expected: 6500ms โœ“) 39 + โฑ๏ธ T2 loop 2 @ ...ms | cycle: 6000ms (expected: 6000ms โœ“) โ† Subsequent cycles are 6000ms 40 + โฑ๏ธ T1 loop 2 @ ...ms | cycle: 6500ms (expected: 6500ms โœ“) 41 + ``` 42 + 43 + ## Verbose Logs Removed 44 + 45 + - `๐ŸŽต EARLY MELODY STATE SET` 46 + - `๐ŸŽต Fetching cached melody` 47 + - `๐ŸŽต Loaded cached melody` 48 + - `๐ŸŽต Clock author` 49 + - `๐ŸŽต About to process melody` 50 + - `๐ŸŽต Original/Converted` 51 + - `๐ŸŽต Parsed melodyTracks` 52 + - `๐ŸŽต SIM FIRST RUN` 53 + - `๐Ÿ“ RENDER GEOMETRY` (table) 54 + - `โœ… No overlapping boxes` 55 + - `๐Ÿ“ฑ HUD QR render check` 56 + - `โŒจ๏ธ๐Ÿ“ [bios pointerup]` 57 + - `โŒจ๏ธ๐Ÿ”ด [input blur event]` 58 + - `๐Ÿ”Š Synth created` (from speaker.mjs) 59 + - `๐Ÿ”Š process() call` (from speaker.mjs) 60 + - `๐Ÿ”Š #processAudio` (from speaker.mjs) 61 + 62 + ## Files Changed 63 + 64 + - `clock.mjs`: Fixed first-cycle expected duration, disabled geometry/melody debug logs 65 + - `speaker.mjs`: Disabled synth creation and process() debug logs 66 + - `disk.mjs`: Disabled QR render debug log 67 + - `bios.mjs`: Disabled keyboard input debug logs
+6 -6
system/public/aesthetic.computer/bios.mjs
··· 11637 11637 11638 11638 // console.log(e.target); 11639 11639 11640 - console.log("โŒจ๏ธ๐Ÿ“ [bios pointerup] hasKB:", currentPieceHasKeyboard, "focusLock:", keyboardFocusLock, "softLock:", keyboardSoftLock, "kbOpen:", keyboardOpen); 11640 + // console.log("โŒจ๏ธ๐Ÿ“ [bios pointerup] hasKB:", currentPieceHasKeyboard, "focusLock:", keyboardFocusLock, "softLock:", keyboardSoftLock, "kbOpen:", keyboardOpen); 11641 11641 11642 11642 if ( 11643 11643 currentPieceHasKeyboard && ··· 11649 11649 if (MetaBrowser && e.target !== window) { 11650 11650 // Skip dragging the finger outside of the Meta Browser. 11651 11651 } else { 11652 - console.log("โŒจ๏ธ๐Ÿ”ด [bios pointerup] calling input.blur() | keyboardOpen:", keyboardOpen, "target:", e.target?.tagName); 11652 + // console.log("โŒจ๏ธ๐Ÿ”ด [bios pointerup] calling input.blur() | keyboardOpen:", keyboardOpen, "target:", e.target?.tagName); 11653 11653 input.blur(); 11654 11654 } 11655 11655 } else { 11656 11656 keyboardOpenMethod = "pointer"; 11657 11657 // input.removeAttribute("readonly"); 11658 - console.log("โŒจ๏ธ๐ŸŸข [bios pointerup] calling input.focus() | keyboardOpen:", keyboardOpen); 11658 + // console.log("โŒจ๏ธ๐ŸŸข [bios pointerup] calling input.focus() | keyboardOpen:", keyboardOpen); 11659 11659 window.focus(); 11660 11660 input.focus(); 11661 11661 } ··· 11672 11672 method: keyboardOpenMethod, 11673 11673 }); 11674 11674 keyboardOpenMethod = undefined; 11675 - console.log("โŒจ๏ธ๐ŸŸข [input focus event] pushed keyboard:open event"); 11675 + // console.log("โŒจ๏ธ๐ŸŸข [input focus event] pushed keyboard:open event"); 11676 11676 }); 11677 11677 11678 11678 input.addEventListener("blur", (e) => { 11679 - console.log("โŒจ๏ธ๐Ÿ”ด [input blur event] keyboardOpen was:", keyboardOpen, new Error().stack); 11679 + // console.log("โŒจ๏ธ๐Ÿ”ด [input blur event] keyboardOpen was:", keyboardOpen, new Error().stack); 11680 11680 // input.setAttribute("readonly", true); 11681 11681 // const temp = input.value; 11682 11682 // input.value = ""; 11683 11683 // input.value = temp; 11684 11684 keyboardOpen = false; 11685 11685 keyboard.events.push({ name: "keyboard:close" }); 11686 - console.log("โŒจ๏ธ๐Ÿ”ด [input blur event] pushed keyboard:close event"); 11686 + // console.log("โŒจ๏ธ๐Ÿ”ด [input blur event] pushed keyboard:close event"); 11687 11687 }); 11688 11688 11689 11689 window.addEventListener("blur", (e) => {
+74 -28
system/public/aesthetic.computer/disks/clock.mjs
··· 746 746 isFallback: true, 747 747 isEmpty: true, // Track that this is an empty/silent clock 748 748 }; 749 - console.log("๐ŸŽต EARLY MELODY STATE SET (empty):", melodyState?.type, melodyState?.notes?.length, "notes"); 749 + // Debug log disabled for cleaner output 750 + // console.log("๐ŸŽต EARLY MELODY STATE SET (empty):", melodyState?.type, melodyState?.notes?.length, "notes"); 750 751 751 752 // Determine the melody string from params or API 752 753 let isFallbackMelody = false; ··· 755 756 // If so, fetch the melody from the store-clock API 756 757 if (params[0] && params[0].startsWith("*")) { 757 758 const code = params[0].slice(1); // Remove * prefix 758 - console.log(`๐ŸŽต Fetching cached melody for code: *${code}`); 759 + // console.log(`๐ŸŽต Fetching cached melody for code: *${code}`); 759 760 760 761 try { 761 762 const response = await fetch( ··· 765 766 if (response.ok) { 766 767 const data = await response.json(); 767 768 if (data.source) { 768 - console.log(`๐ŸŽต Loaded cached melody: ${data.source}`); 769 + // console.log(`๐ŸŽต Loaded cached melody: ${data.source}`); 769 770 // Set the code immediately since we already have it 770 771 cachedClockCode = code; 771 772 // Store the author handle for byline display 772 773 cachedClockAuthor = data.handle || null; 773 - console.log(`๐ŸŽต Clock author: ${cachedClockAuthor || 'anon'}`); 774 + // console.log(`๐ŸŽต Clock author: ${cachedClockAuthor || 'anon'}`); 774 775 api.send({ type: "clock:cached", content: { code: cachedClockCode, author: cachedClockAuthor } }); 775 776 776 777 // Use the fetched melody as if it was passed directly ··· 799 800 // No melody provided - empty/silent clock 800 801 originalMelodyString = ""; 801 802 isFallbackMelody = true; 802 - console.log(`๐ŸŽต EMPTY CLOCK: silent mode, no melody`); 803 + // console.log(`๐ŸŽต EMPTY CLOCK: silent mode, no melody`); 803 804 } 804 805 805 - console.log(`๐ŸŽต About to process melody: "${originalMelodyString}", isFallback: ${isFallbackMelody}`); 806 + // console.log(`๐ŸŽต About to process melody: "${originalMelodyString}", isFallback: ${isFallbackMelody}`); 806 807 807 808 // Handle empty clock - no melody, just black screen and silence 808 809 if (!originalMelodyString || originalMelodyString.trim() === "") { 809 - console.log(`๐ŸŽต Empty clock - silent mode`); 810 + // console.log(`๐ŸŽต Empty clock - silent mode`); 810 811 melodyState = { 811 812 notes: [], 812 813 index: 0, ··· 827 828 828 829 // Convert notepat notation to standard notation before parsing 829 830 const convertedMelodyString = convertNotepatNotation(originalMelodyString); 830 - console.log(`๐ŸŽต Original: "${originalMelodyString}"`); 831 - console.log(`๐ŸŽต Converted: "${convertedMelodyString}"`); 831 + // console.log(`๐ŸŽต Original: "${originalMelodyString}"`); 832 + // console.log(`๐ŸŽต Converted: "${convertedMelodyString}"`); 832 833 833 834 // Parse the melody string - first try sequential (with > separators), falls back to simultaneous 834 835 melodyTracks = parseSequentialMelody(convertedMelodyString, octave); 835 - console.log(`๐ŸŽต Parsed melodyTracks:`, melodyTracks?.type, `tracks:`, melodyTracks?.tracks?.length, `sequences:`, melodyTracks?.sequences?.length); 836 + // console.log(`๐ŸŽต Parsed melodyTracks:`, melodyTracks?.type, `tracks:`, melodyTracks?.tracks?.length, `sequences:`, melodyTracks?.sequences?.length); 836 837 837 838 // Handle sequential melodies (sections separated by >) 838 839 if (melodyTracks.type === 'sequential') { 839 - console.log(`๐ŸŽต Sequential melody detected with ${melodyTracks.sequences.length} sections`); 840 + // console.log(`๐ŸŽต Sequential melody detected with ${melodyTracks.sequences.length} sections`); 840 841 841 842 // Get the first sequence to start with 842 843 const firstSequence = melodyTracks.sequences[0]; ··· 3145 3146 }); 3146 3147 } 3147 3148 3148 - // DEBUG: Dump geometry for all timeline items - THROTTLED 3149 - const now = performance.now(); 3150 - if (now - (globalThis._lastGeomDump || 0) > 2000) { // Every 2 seconds 3151 - globalThis._lastGeomDump = now; 3152 - globalThis._collectGeom = true; 3153 - globalThis._geomItems = []; 3154 - } 3149 + // DEBUG: Geometry collection disabled for cleaner console output 3150 + // Uncomment below to enable geometry debugging: 3151 + // const now = performance.now(); 3152 + // if (now - (globalThis._lastGeomDump || 0) > 2000) { // Every 2 seconds 3153 + // globalThis._lastGeomDump = now; 3154 + // globalThis._collectGeom = true; 3155 + // globalThis._geomItems = []; 3156 + // } 3155 3157 3156 3158 sortedTimelineItems.forEach((historyItem) => { 3157 3159 const { ··· 5092 5094 function sim({ sound, beep, clock, num, help, params, colon, screen, speak }) { 5093 5095 if (!simDebugLogged) { 5094 5096 simDebugLogged = true; 5095 - console.log("๐ŸŽต SIM FIRST RUN - melodyState:", melodyState?.type, "notes:", melodyState?.notes?.length); 5097 + // console.log("๐ŸŽต SIM FIRST RUN - melodyState:", melodyState?.type, "notes:", melodyState?.notes?.length); 5096 5098 } 5097 5099 sound.speaker?.poll(); 5098 5100 ··· 5858 5860 // All tracks start from the same precise UTC second boundary 5859 5861 const utcStartTime = Math.ceil(currentTimeMs / 1000) * 1000; 5860 5862 trackState.nextNoteTargetTime = utcStartTime; 5863 + 5864 + // Calculate total track duration and store for cycle tracking 5865 + const trackTotalBeats = trackState.track.reduce((sum, n) => sum + n.duration, 0); 5866 + const trackTotalMs = trackTotalBeats * melodyState.baseTempo; 5867 + trackState.expectedCycleDuration = trackTotalMs; 5868 + trackState.loopCount = 0; 5869 + trackState.cycleStartTime = currentTimeMs; 5870 + 5871 + // Log track initialization with key timing info 5872 + const restInfo = trackState.track[0]?.note === 'rest' ? ` (has leading rest)` : ''; 5873 + console.log(`โฑ๏ธ T${trackIndex + 1} INIT | ${trackTotalBeats} beats = ${trackTotalMs}ms expected cycle${restInfo}`); 5861 5874 5862 - // Find first audible note in this track 5875 + // INDEPENDENT LOOPING FIX: Skip leading rests by advancing nextNoteTargetTime 5876 + // instead of changing noteIndex. This preserves the track's total duration. 5877 + let leadingRestBeats = 0; 5863 5878 let firstAudibleIndex = 0; 5864 5879 while ( 5865 5880 firstAudibleIndex < trackState.track.length && 5866 5881 trackState.track[firstAudibleIndex].note === "rest" 5867 5882 ) { 5883 + leadingRestBeats += trackState.track[firstAudibleIndex].duration; 5868 5884 firstAudibleIndex++; 5869 5885 } 5870 5886 5871 - if ( 5872 - firstAudibleIndex > 0 && 5873 - firstAudibleIndex < trackState.track.length 5874 - ) { 5887 + if (firstAudibleIndex > 0 && firstAudibleIndex < trackState.track.length) { 5888 + // Instead of skipping notes, advance the time past the rests 5889 + // This way the first audible note plays immediately but the track 5890 + // still has the same total duration as designed 5875 5891 trackState.noteIndex = firstAudibleIndex; 5892 + trackState.nextNoteTargetTime = utcStartTime; // First audible note plays NOW 5893 + 5894 + // Store both the offset and the first audible index for proper loop handling 5895 + trackState.initialRestOffset = leadingRestBeats * melodyState.baseTempo; 5896 + trackState.firstAudibleNoteIndex = firstAudibleIndex; 5897 + 5898 + // Leading rests skipped - cycle timing preserved via initialRestOffset 5876 5899 } else if (firstAudibleIndex >= trackState.track.length) { 5877 5900 trackState.noteIndex = 0; 5901 + trackState.initialRestOffset = 0; 5902 + trackState.firstAudibleNoteIndex = 0; 5903 + } else { 5904 + trackState.initialRestOffset = 0; 5905 + trackState.firstAudibleNoteIndex = 0; 5878 5906 } 5879 5907 } 5880 5908 ··· 6061 6089 6062 6090 // Advance to next note in this track 6063 6091 const oldIndex = trackState.noteIndex; 6092 + const playedNoteIndex = trackState.noteIndex; // Save for logging 6064 6093 trackState.noteIndex = 6065 6094 (trackState.noteIndex + 1) % trackState.track.length; 6066 6095 ··· 6069 6098 oldIndex === trackState.track.length - 1 && 6070 6099 trackState.noteIndex === 0 6071 6100 ) { 6101 + // INDEPENDENT LOOPING FIX: When looping, skip leading rests again and add offset 6102 + // This ensures the track maintains its full duration across loops 6103 + if (trackState.initialRestOffset > 0 && trackState.firstAudibleNoteIndex > 0) { 6104 + trackState.noteIndex = trackState.firstAudibleNoteIndex; 6105 + trackState.nextNoteTargetTime += trackState.initialRestOffset; 6106 + } 6107 + 6108 + // CYCLE TRACKING: Log when each track completes a full loop 6109 + trackState.loopCount = (trackState.loopCount || 0) + 1; 6110 + const actualCycleDuration = currentTimeMs - (trackState.cycleStartTime || currentTimeMs); 6111 + // First cycle is shorter if we skipped leading rests (no offset added yet) 6112 + const isFirstCycle = trackState.loopCount === 1; 6113 + const expectedDuration = isFirstCycle 6114 + ? (trackState.expectedCycleDuration || 0) - (trackState.initialRestOffset || 0) 6115 + : (trackState.expectedCycleDuration || 0); 6116 + const drift = actualCycleDuration - expectedDuration; 6117 + const status = Math.abs(drift) < 100 ? 'โœ“' : (drift > 0 ? `โš ๏ธ +${drift}ms DELAYED` : `โšก ${drift}ms EARLY`); 6118 + console.log(`โฑ๏ธ T${trackIndex + 1} loop ${trackState.loopCount} @ ${Math.round(currentTimeMs)}ms | cycle: ${Math.round(actualCycleDuration)}ms (expected: ${expectedDuration}ms ${status})`); 6119 + trackState.cycleStartTime = currentTimeMs; 6120 + 6072 6121 // Check if this track has mutations 6073 6122 if (trackState.track.hasMutation) { 6074 6123 const originalTrack = [...trackState.track]; ··· 6127 6176 // of the previous note, respecting inherited duration modifiers like c.defg..ef 6128 6177 trackState.nextNoteTargetTime = trackState.nextNoteTargetTime + noteDuration; 6129 6178 6130 - // DEBUG: Log timing calculation for sticky duration debugging 6131 - if (trackIndex === 0 && note !== "rest") { 6132 - 6133 - } 6179 + // Per-note logging removed - use cycle logs for timing debug 6134 6180 } 6135 6181 } 6136 6182 });
+332
system/public/aesthetic.computer/disks/clocks.mjs
··· 1 + // clocks, 2025.6.26 2 + // Browse saved clocks from the database 3 + 4 + const { max, floor } = Math; 5 + 6 + const CLOCKS_PER_PAGE = 100; 7 + 8 + let clocks = []; 9 + let scroll = 0; 10 + let totalScrollHeight = 0; 11 + let chatHeight = 0; 12 + let loading = true; 13 + let error = null; 14 + let rowHeight = 9; // MatrixChunky8 is 8px + 1px spacing 15 + let topMargin = 19; // Below HUD label 16 + let bottomMargin = 12; // Footer area 17 + let hue = 0; 18 + let needsLayout = true; 19 + let sortBy = "recent"; // 'recent' or 'hits' 20 + let selectedIndex = -1; // Currently highlighted clock 21 + const FONT = "MatrixChunky8"; 22 + 23 + // Parse relative time 24 + function timeAgo(dateStr) { 25 + const now = new Date(); 26 + const past = new Date(dateStr); 27 + const seconds = Math.floor((now - past) / 1000); 28 + 29 + const units = [ 30 + { name: "y", seconds: 31536000 }, 31 + { name: "mo", seconds: 2592000 }, 32 + { name: "w", seconds: 604800 }, 33 + { name: "d", seconds: 86400 }, 34 + { name: "h", seconds: 3600 }, 35 + { name: "m", seconds: 60 }, 36 + { name: "s", seconds: 1 }, 37 + ]; 38 + 39 + for (const unit of units) { 40 + const count = Math.floor(seconds / unit.seconds); 41 + if (count >= 1) return `${count}${unit.name}`; 42 + } 43 + return "now"; 44 + } 45 + 46 + // Bound scroll like chat.mjs does 47 + function boundScroll() { 48 + if (scroll < 0) scroll = 0; 49 + if (scroll > totalScrollHeight - chatHeight + 5) { 50 + scroll = totalScrollHeight - chatHeight + 5; 51 + } 52 + } 53 + 54 + // Fetch clocks from API 55 + async function fetchClocks(sort = "recent") { 56 + try { 57 + loading = clocks.length === 0; 58 + sortBy = sort; 59 + 60 + const response = await fetch( 61 + `/api/store-clock?recent=true&limit=${CLOCKS_PER_PAGE}&sort=${sort}` 62 + ); 63 + 64 + if (!response.ok) { 65 + throw new Error(`API error: ${response.status}`); 66 + } 67 + 68 + const data = await response.json(); 69 + clocks = data.recent || []; 70 + 71 + loading = false; 72 + error = null; 73 + needsLayout = true; 74 + selectedIndex = clocks.length > 0 ? 0 : -1; 75 + } catch (err) { 76 + error = err.message; 77 + loading = false; 78 + console.error("Failed to fetch clocks:", err); 79 + } 80 + } 81 + 82 + // Calculate Y position for a clock at given index 83 + function getClockY(index) { 84 + const clockHeight = rowHeight * 2; // Each clock takes 2 rows 85 + return topMargin + index * clockHeight - scroll; 86 + } 87 + 88 + // Get clock index at Y position 89 + function getClockAtY(y, screenHeight) { 90 + const clockHeight = rowHeight * 2; 91 + const relativeY = y - topMargin + scroll; 92 + const index = Math.floor(relativeY / clockHeight); 93 + if (index >= 0 && index < clocks.length) { 94 + const clockY = getClockY(index); 95 + // Check if within visible bounds 96 + if (clockY >= topMargin - clockHeight && clockY < screenHeight - bottomMargin + clockHeight) { 97 + return index; 98 + } 99 + } 100 + return -1; 101 + } 102 + 103 + function boot({ screen, store }) { 104 + // Initial fetch 105 + fetchClocks(); 106 + 107 + // Always start at top with fresh state 108 + scroll = 0; 109 + selectedIndex = -1; 110 + } 111 + 112 + function paint({ wipe, ink, screen, line, text, typeface, num, needsPaint, mask, unmask, box }) { 113 + const { width: w, height: h } = screen; 114 + 115 + // Background with subtle hue shift 116 + hue = (hue + 0.1) % 360; 117 + const bgHue = hue * 0.1; 118 + wipe(12 + Math.sin(bgHue) * 2, 12, 20); 119 + 120 + // Top divider line (below HUD label area) 121 + ink(50, 50, 70).line(0, topMargin - 1, w, topMargin - 1); 122 + 123 + // Bottom divider line (above footer) 124 + ink(50, 50, 70).line(0, h - bottomMargin, w, h - bottomMargin); 125 + 126 + if (loading && clocks.length === 0) { 127 + ink(150, 150, 180).write("Loading clocks...", { center: "xy", x: w / 2, y: h / 2 }, false, undefined, false, FONT); 128 + return; 129 + } 130 + 131 + if (error && clocks.length === 0) { 132 + ink(255, 100, 100).write("Error: " + error.slice(0, 40), { center: "xy", x: w / 2, y: h / 2 }, false, undefined, false, FONT); 133 + return; 134 + } 135 + 136 + if (clocks.length === 0) { 137 + ink(150, 150, 180).write("No clocks found", { center: "xy", x: w / 2, y: h / 2 }, false, undefined, false, FONT); 138 + return; 139 + } 140 + 141 + // Calculate heights 142 + chatHeight = h - topMargin - bottomMargin; 143 + const clockHeight = rowHeight * 2; // Each clock takes 2 rows 144 + 145 + // Calculate total height 146 + if (needsLayout) { 147 + totalScrollHeight = clocks.length * clockHeight; 148 + needsLayout = false; 149 + } 150 + 151 + // Mask off the scrollable area 152 + mask({ 153 + x: 0, 154 + y: topMargin, 155 + width: w, 156 + height: chatHeight, 157 + }); 158 + 159 + // Draw clocks 160 + for (let i = 0; i < clocks.length; i++) { 161 + const clock = clocks[i]; 162 + const y = getClockY(i); 163 + 164 + // Skip if outside visible area (with buffer) 165 + if (y + clockHeight < topMargin - 20) continue; 166 + if (y > h - bottomMargin + 20) continue; 167 + 168 + // Selection highlight 169 + const isSelected = i === selectedIndex; 170 + if (isSelected) { 171 + ink(40, 40, 60, 200).box(0, y, w, clockHeight); 172 + } 173 + 174 + // Code with * prefix 175 + const codeColor = isSelected ? [255, 200, 100] : [200, 150, 80]; 176 + ink(...codeColor).write(`*${clock.code}`, { x: 4, y }, false, undefined, false, FONT); 177 + 178 + // Time ago 179 + const ago = timeAgo(clock.when); 180 + const agoX = 4 + text.width(`*${clock.code} `, FONT); 181 + ink(100, 100, 130).write(ago, { x: agoX, y }, false, undefined, false, FONT); 182 + 183 + // Hits count 184 + const hitsX = agoX + text.width(ago + " ", FONT); 185 + ink(80, 130, 80).write(`${clock.hits}x`, { x: hitsX, y }, false, undefined, false, FONT); 186 + 187 + // Handle/author (if available) 188 + if (clock.handle) { 189 + const handleX = hitsX + text.width(`${clock.hits}x `, FONT); 190 + ink(180, 150, 255).write(clock.handle.slice(0, 12), { x: handleX, y }, false, undefined, false, FONT); 191 + } 192 + 193 + // Preview on second line (truncated) 194 + const msgY = y + rowHeight; 195 + const charWidth = 4; 196 + const maxChars = Math.floor((w - 8) / charWidth); 197 + const preview = clock.preview || clock.source?.slice(0, maxChars) || ""; 198 + ink(200, 200, 220).write(preview.slice(0, maxChars), { x: 4, y: msgY }, false, undefined, false, FONT); 199 + 200 + // Subtle separator 201 + const sepY = y + clockHeight - 1; 202 + ink(30, 30, 42).line(4, sepY, w - 4, sepY); 203 + } 204 + 205 + unmask(); // End masking 206 + 207 + // ๐Ÿ“œ Scroll bar (outside mask so it's always visible) 208 + if (totalScrollHeight > chatHeight) { 209 + ink(40, 40, 50).box(w - 2, topMargin, 2, chatHeight); // Backdrop 210 + 211 + const segHeight = max(4, floor((chatHeight / totalScrollHeight) * chatHeight)); 212 + const scrollRatio = scroll / max(1, totalScrollHeight - chatHeight); 213 + const boxY = topMargin + floor(scrollRatio * (chatHeight - segHeight)); 214 + 215 + ink(255, 150, 200).box(w - 2, boxY, 2, segHeight); 216 + } 217 + 218 + // Footer area (below mask) 219 + const footerY = h - bottomMargin + 2; 220 + 221 + // Sort indicator 222 + const sortLabel = sortBy === "hits" ? "๐Ÿ”ฅ popular" : "๐Ÿ• recent"; 223 + ink(100, 100, 120).write(sortLabel, { x: 4, y: footerY }, false, undefined, false, FONT); 224 + 225 + // Clock count 226 + const countText = `${clocks.length}`; 227 + ink(80, 80, 100).write(countText, { x: w - text.width(countText, FONT) - 4, y: footerY }, false, undefined, false, FONT); 228 + 229 + // Instructions 230 + const helpText = "โ†‘โ†“:nav โŽ:play s:sort"; 231 + const helpX = Math.floor((w - text.width(helpText, FONT)) / 2); 232 + ink(60, 60, 80).write(helpText, { x: helpX, y: footerY }, false, undefined, false, FONT); 233 + } 234 + 235 + function act({ event: e, screen, store, jump }) { 236 + const { height: h } = screen; 237 + 238 + // ๐Ÿ“œ Scrolling 239 + if (e.is("draw")) { 240 + scroll -= e.delta.y; // Invert for natural scroll direction 241 + boundScroll(); 242 + } 243 + 244 + // Navigation up 245 + if (e.is("keyboard:down:arrowup") || e.is("keyboard:down:k")) { 246 + if (selectedIndex > 0) { 247 + selectedIndex--; 248 + // Ensure selected item is visible 249 + const clockY = getClockY(selectedIndex); 250 + if (clockY < topMargin) { 251 + scroll = selectedIndex * rowHeight * 2; 252 + } 253 + } 254 + boundScroll(); 255 + } 256 + 257 + // Navigation down 258 + if (e.is("keyboard:down:arrowdown") || e.is("keyboard:down:j")) { 259 + if (selectedIndex < clocks.length - 1) { 260 + selectedIndex++; 261 + // Ensure selected item is visible 262 + const clockY = getClockY(selectedIndex); 263 + const clockHeight = rowHeight * 2; 264 + if (clockY + clockHeight > h - bottomMargin) { 265 + scroll = (selectedIndex + 1) * clockHeight - (h - topMargin - bottomMargin); 266 + } 267 + } 268 + boundScroll(); 269 + } 270 + 271 + // Home key - go to top 272 + if (e.is("keyboard:down:home")) { 273 + scroll = 0; 274 + selectedIndex = 0; 275 + } 276 + 277 + // End key - go to bottom 278 + if (e.is("keyboard:down:end")) { 279 + scroll = max(0, totalScrollHeight - chatHeight + 5); 280 + selectedIndex = clocks.length - 1; 281 + } 282 + 283 + // Enter - run selected clock 284 + if (e.is("keyboard:down:enter")) { 285 + if (selectedIndex >= 0 && selectedIndex < clocks.length) { 286 + const clock = clocks[selectedIndex]; 287 + jump(`*${clock.code}`); 288 + } 289 + } 290 + 291 + // Click to select and run 292 + if (e.is("touch") && e.y >= topMargin && e.y < h - bottomMargin) { 293 + const clickedIndex = getClockAtY(e.y, h); 294 + if (clickedIndex >= 0) { 295 + const clock = clocks[clickedIndex]; 296 + jump(`*${clock.code}`); 297 + } 298 + } 299 + 300 + // Toggle sort with S key 301 + if (e.is("keyboard:down:s")) { 302 + const newSort = sortBy === "recent" ? "hits" : "recent"; 303 + fetchClocks(newSort); 304 + scroll = 0; 305 + } 306 + 307 + // Refresh on R 308 + if (e.is("keyboard:down:r")) { 309 + fetchClocks(sortBy); 310 + scroll = 0; 311 + selectedIndex = 0; 312 + } 313 + 314 + // Back to prompt 315 + if (e.is("keyboard:down:escape")) { 316 + jump("prompt"); 317 + } 318 + } 319 + 320 + function sim({ store }) { 321 + // Decay the hue 322 + if (hue > 0) hue = Math.max(0, hue - 0.5); 323 + } 324 + 325 + function meta() { 326 + return { 327 + title: "Clocks", 328 + desc: "Browse saved clock melodies", 329 + }; 330 + } 331 + 332 + export { boot, paint, act, sim, meta };
+4 -4
system/public/aesthetic.computer/lib/disk.mjs
··· 12648 12648 // ๐Ÿ“ฑ HUD QR code - generate cells and calculate width 12649 12649 let hudQRSize = 0; 12650 12650 let hudQRPadding = 0; 12651 - // Debug: Log QR state on first few frames 12652 - if (pieceFrameCount < 3) { 12653 - console.log("๐Ÿ“ฑ HUD QR render check - frame:", pieceFrameCount, "currentHUDQR:", JSON.stringify(currentHUDQR), "currentHUDQRCells:", !!currentHUDQRCells); 12654 - } 12651 + // Debug: Log QR state on first few frames (disabled for cleaner output) 12652 + // if (pieceFrameCount < 3) { 12653 + // console.log("๐Ÿ“ฑ HUD QR render check - frame:", pieceFrameCount, "currentHUDQR:", JSON.stringify(currentHUDQR), "currentHUDQRCells:", !!currentHUDQRCells); 12654 + // } 12655 12655 if (currentHUDQR) { 12656 12656 // Generate QR cells if not cached 12657 12657 if (!currentHUDQRCells) {
-21
system/public/aesthetic.computer/lib/speaker.mjs
··· 438 438 pan: msg.data.pan || 0, 439 439 }); 440 440 441 - console.log("๐Ÿ”Š Synth created:", msg.data.id, "duration:", duration, "volume:", msg.data.volume, "queue length:", this.#queue.length + 1); 442 - 443 441 // if (duration === Infinity && msg.data.id > -1n) { 444 442 this.#running[msg.data.id] = sound; // Index by the unique id. 445 443 // } ··· 492 490 } 493 491 494 492 process(inputs, outputs) { 495 - // Log EVERY call for first 10 calls to debug 496 - this._totalProcessCalls = (this._totalProcessCalls || 0) + 1; 497 - if (this._totalProcessCalls <= 10) { 498 - console.log("๐Ÿ”Š process() call #" + this._totalProcessCalls); 499 - } 500 - 501 493 try { 502 494 // Use global currentTime from AudioWorkletGlobalScope (not this.currentTime which doesn't exist) 503 495 const time = currentTime; ··· 564 556 565 557 const result = this.#processAudio(inputs, outputs, time); 566 558 567 - if (this._totalProcessCalls <= 10) { 568 - console.log("๐Ÿ”Š process() completed call #" + this._totalProcessCalls + ", result:", result); 569 - } 570 - 571 559 // Track processing time for performance monitoring 572 560 const processingTime = (time * 1000) - startTime; 573 561 this.#processingTimeHistory.push(processingTime); ··· 583 571 } 584 572 585 573 #processAudio(inputs, outputs, time) { 586 - // Log every 100 calls to see if processing continues 587 - this._processCallCount = (this._processCallCount || 0) + 1; 588 - if (this._processCallCount <= 3) { 589 - console.log("๐Ÿ”Š #processAudio START call:", this._processCallCount, "time:", time, "lastTime:", this.#lastTime, "ticks:", this.#ticks); 590 - } 591 - 592 574 // DEBUG: Log that process is running (once per second) 593 575 if (this.#lastTime && Math.floor(time) !== Math.floor(this.#lastTime)) { 594 576 // console.log("๐Ÿ”Š Worklet process running, time:", time.toFixed(2), "ticks:", this.#ticks?.toFixed(3), "bpmInSec:", this.#bpmInSec); ··· 782 764 this.#detectBeats(this.#fftBufferLeft); 783 765 } 784 766 } 785 - if (this._processCallCount <= 3) { 786 - console.log("๐Ÿ”Š #processAudio END call:", this._processCallCount); 787 - } 788 767 return true; 789 768 } // End of #processAudio method 790 769