From 3ef6c7c921efd79799f3e9d0a2e3462ab748d267 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 4 Oct 2025 02:07:35 -0500 Subject: [PATCH 01/27] Handle scale-for-notes when n also supplied --- packages/core/test/pattern.test.mjs | 4 ++-- packages/tonal/tonal.mjs | 21 ++++++++++++--------- website/src/components/Header/Search.css | 4 ++-- website/src/pages/learn/samples.mdx | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1df5c877..625f4075 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -796,7 +796,7 @@ describe('Pattern', () => { }); }); describe('apply', () => { - it('Can apply a function', () => { + (it('Can apply a function', () => { expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); }), it('Can apply a pattern of functions', () => { @@ -804,7 +804,7 @@ describe('Pattern', () => { expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual( sequence('a', 'b').fast(2, 3).firstCycle(), ); - }); + })); }); describe('layer', () => { it('Can layer up multiple functions', () => { diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index ae75ab69..a24dc7f2 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -266,9 +266,14 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - // The case where the note has been defined via `n` or `pure` - if (!isObject || (isObject && ('n' in value || 'value' in value))) { - const step = isObject ? (value.n ?? value.value) : value; + // If value is a pure value, place it on `n` so that we interpret it as a scale + // degree + value = typeof value !== 'object' ? { n: value } : value; + if ('note' in value) { + const note = _getNearestScaleNote(scale, value.note); + return pure({ ...value, note }); + } else if ('n' in value || 'value' in value) { + const step = value.n ?? value.value; delete value.n; // remove n so it won't cause trouble if (isNote(step)) { // legacy.. @@ -277,7 +282,7 @@ export const scale = register( try { const [number, offset] = _convertStepToNumberAndOffset(step); let note; - if (isObject && value.anchor) { + if (value.anchor) { note = stepInNamedScale(number, scale, value.anchor); } else { note = scaleStep(number, scale); @@ -290,11 +295,9 @@ export const scale = register( } return value; } - // The case where the note has been defined via `note` - else { - const note = _getNearestScaleNote(scale, value.note); - return pure(isObject ? { ...value, note } : note); - } + throw new Error( + `Invalid value format for 'scale'. Value must contain 'n' or 'note' but received ${Object.keys(value)}`, + ); }) .outerJoin() // legacy: diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index 456ef9f6..b14146cb 100644 --- a/website/src/components/Header/Search.css +++ b/website/src/components/Header/Search.css @@ -18,8 +18,8 @@ --docsearch-modal-background: var(--background); --docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%); --docsearch-key-gradient: var(--foreground); - --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), - 0 1px 2px 1px var(--gutterBackground); + --docsearch-key-shadow: + inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground); } .dark { --docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%); diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index eb79cccf..201d57df 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub -{' '} + ### speed From 11dd8a318a5f329d8b5f9269192fbfdc6a15839d Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 4 Oct 2025 02:13:42 -0500 Subject: [PATCH 02/27] Cleanup --- packages/tonal/tonal.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a24dc7f2..e745a33a 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -268,7 +268,7 @@ export const scale = register( const isObject = typeof value === 'object'; // If value is a pure value, place it on `n` so that we interpret it as a scale // degree - value = typeof value !== 'object' ? { n: value } : value; + value = isObject ? value : { n: value }; if ('note' in value) { const note = _getNearestScaleNote(scale, value.note); return pure({ ...value, note }); From be9368e8a2d6be6625b1f55eaf62e8f82a5b587c Mon Sep 17 00:00:00 2001 From: dtricks Date: Fri, 3 Oct 2025 17:57:58 +0200 Subject: [PATCH 03/27] feat(codemirror): add Vim :w evaluate, :q stop, and gc/gcc toggle comment via custom events; docs: add Vim shortcuts page and link from REPL manual --- packages/codemirror/codemirror.mjs | 42 +++++- packages/codemirror/keybindings.mjs | 139 +++++++++++++++++--- website/src/pages/technical-manual/repl.mdx | 4 + website/src/pages/technical-manual/vim.mdx | 36 +++++ 4 files changed, 203 insertions(+), 18 deletions(-) create mode 100644 website/src/pages/technical-manual/vim.mdx diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 4dc23996..6359609c 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -1,4 +1,5 @@ import { closeBrackets } from '@codemirror/autocomplete'; +import { toggleLineComment } from '@codemirror/commands'; export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands'; // import { search, highlightSelectionMatches } from '@codemirror/search'; import { indentWithTab } from '@codemirror/commands'; @@ -13,7 +14,7 @@ import { lineNumbers, drawSelection, } from '@codemirror/view'; -import { repl, registerControl } from '@strudel/core'; +import { repl, registerControl, logger } from '@strudel/core'; import { Drawer, cleanupDraw } from '@strudel/draw'; import { isAutoCompletionEnabled } from './autocomplete.mjs'; import { isTooltipEnabled } from './tooltip.mjs'; @@ -245,6 +246,32 @@ export class StrudelMirror { } }; document.addEventListener('start-repl', this.onStartRepl); + + // Handle global evaluation requests (e.g., from Vim :w) + this.onEvaluateRequest = (e) => { + try { + // Evaluate current editor on repl-evaluate + logger('[repl] evaluate via event'); + this.evaluate(); + e?.cancelable && e.preventDefault?.(); + } catch (err) { + console.error('Error handling repl-evaluate event', err); + } + }; + document.addEventListener('repl-evaluate', this.onEvaluateRequest); + document.addEventListener('repl-stop', this.onStopRequest); + + // Toggle comments requested from Vim (gc/gcc) + this.onToggleComment = (e) => { + try { + // Honor selections; toggleLineComment handles both selections and single line + toggleLineComment(this.editor); + e?.cancelable && e.preventDefault?.(); + } catch (err) { + console.error('Error handling repl-toggle-comment event', err); + } + }; + document.addEventListener('repl-toggle-comment', this.onToggleComment); } draw(haps, time, painters) { painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime)); @@ -271,6 +298,16 @@ export class StrudelMirror { async stop() { this.repl.scheduler.stop(); } + + // Listen for global stop requests (e.g., from Vim :q) + onStopRequest = (e) => { + try { + this.stop(); + e?.cancelable && e.preventDefault?.(); + } catch (err) { + console.error('Error handling repl-stop event', err); + } + }; async toggle() { if (this.repl.scheduler.started) { this.repl.stop(); @@ -351,6 +388,9 @@ export class StrudelMirror { } clear() { this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl); + this.onEvaluateRequest && document.removeEventListener('repl-evaluate', this.onEvaluateRequest); + this.onStopRequest && document.removeEventListener('repl-stop', this.onStopRequest); + this.onToggleComment && document.removeEventListener('repl-toggle-comment', this.onToggleComment); } getCursorLocation() { return this.editor.state.selection.main.head; diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index ca5f34f4..7de2a723 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -1,32 +1,137 @@ -import { Prec } from '@codemirror/state'; -import { keymap, ViewPlugin } from '@codemirror/view'; +import {defaultKeymap} from '@codemirror/commands'; +import {Prec} from '@codemirror/state'; +import {keymap, ViewPlugin} from '@codemirror/view'; // import { searchKeymap } from '@codemirror/search'; -import { emacs } from '@replit/codemirror-emacs'; -import { vim } from '@replit/codemirror-vim'; +import {emacs} from '@replit/codemirror-emacs'; +import {vim, Vim} from '@replit/codemirror-vim'; +import { logger } from '@strudel/core'; // import { vim } from './vim_test.mjs'; -import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; -import { defaultKeymap } from '@codemirror/commands'; +import {vscodeKeymap} from '@replit/codemirror-vscode-keymap'; const vscodePlugin = ViewPlugin.fromClass( - class { - constructor() {} - }, - { - provide: () => { - return Prec.highest(keymap.of([...vscodeKeymap])); + class { + constructor() {} + }, + { + provide : () => { return Prec.highest(keymap.of([...vscodeKeymap ])); }, }, - }, ); const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); +// Map Vim :w to trigger the same action as evaluation. We dispatch a custom +// event 'repl-evaluate' that the editor listens for, and also simulate +// Ctrl+Enter/Alt+Enter as a fallback. We log to the Strudel logger so it +// appears in the Console panel. +try { + if (Vim && typeof Vim.defineEx === 'function') { + // Map gc/gcc to toggle line comments by dispatching a custom event that our + // CodeMirror integration listens to. This avoids depending on Vim's internal + // actions and works with current selections/visual mode. + try { + Vim.defineAction('strudelToggleComment', (cm) => { + const view = cm?.view || cm; + try { + const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true }); + document.dispatchEvent(ev); + } catch (e) { + console.error('strudelToggleComment dispatch failed', e); + } + }); + Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' }); + Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' }); + Vim.mapCommand('gcc', 'action', 'strudelToggleComment', {}, { context: 'normal' }); + } catch (e) { + console.error('Vim gc/gcc mapping failed', e); + } + + // :q to pause/stop + // :q to pause/stop + Vim.defineEx('quit', 'q', (cm) => { + try { + const view = cm?.view || cm; + // First try dispatching our custom stop event, then fallback to Alt+. + let handled = false; + try { + const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true }); + handled = document.dispatchEvent(ev) === false; + } catch {} + if (!handled) { + const altDot = new KeyboardEvent('keydown', { + key: '.', + code: 'Period', + altKey: true, + bubbles: true, + cancelable: true, + }); + view?.dom?.dispatchEvent?.(altDot); + } + } catch (e) { + console.error('Error dispatching :q stop event', e); + } + }); + + // :w to evaluate + // :w to evaluate + Vim.defineEx('write', 'w', (cm) => { + const view = + cm?.view || + cm; // CM6 Vim passes either an object with view or the view itself + try { + view?.focus?.(); + // Let the app know this came from Vim :w + try { + logger('[vim] :w — evaluating code'); + } catch {} + // Dispatch a dedicated evaluate event first + let handled = false; + try { + const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true }); + handled = document.dispatchEvent(ev) === false; // false means preventDefault was called + } catch {} + if (handled) { + return; + } + // Try Ctrl+Enter first if not handled by custom event + const ctrlEnter = new KeyboardEvent('keydown', { + key : 'Enter', + code : 'Enter', + ctrlKey : true, + bubbles : true, + cancelable : true, + }); + view?.dom?.dispatchEvent?.(ctrlEnter); + // If not handled (no handler called preventDefault), try Alt+Enter as + // fallback + if (!ctrlEnter.defaultPrevented) { + const altEnter = new KeyboardEvent('keydown', { + key : 'Enter', + code : 'Enter', + altKey : true, + bubbles : true, + cancelable : true, + }); + view?.dom?.dispatchEvent?.(altEnter); + } + } catch (e) { + console.error('Error dispatching :w evaluation event', e); + } + }); + } +} catch (e) { + console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e); +} + const keymaps = { - vim, + vim, + // Add extra Vim keymap for gc to toggle line comment + // We will include a Vim-specific keymap that calls the CM command + // respecting the current selection. emacs, - codemirror: () => keymap.of(defaultKeymap), - vscode: vscodeExtension, + codemirror : () => keymap.of(defaultKeymap), + vscode : vscodeExtension, }; export function keybindings(name) { const active = keymaps[name]; - return [active ? Prec.high(active()) : []]; + return [ active ? Prec.high(active()) : [] ]; } diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index 8c4287af..f74a4fb8 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -117,6 +117,10 @@ Here's an example AST for `c3 [e3 g3]` which translates to `seq(c3, seq(e3, g3))` +## Vim Keybindings + +See the separate page on Vim shortcuts for a quick reference: [/technical-manual/vim](/technical-manual/vim) + ## Scheduling Events After an instance of `Pattern` is obtained from the user code, diff --git a/website/src/pages/technical-manual/vim.mdx b/website/src/pages/technical-manual/vim.mdx new file mode 100644 index 00000000..ec284d0b --- /dev/null +++ b/website/src/pages/technical-manual/vim.mdx @@ -0,0 +1,36 @@ +--- +title: Vim Shortcuts +layout: ../../layouts/MainLayout.astro +--- + +# Vim Shortcuts in the REPL + +When the REPL editor (CodeMirror) is configured to use Vim keybindings, the following commands are available: + +- :w — Evaluate the current code + - Triggers the same evaluation as Ctrl+Enter / Alt+Enter + - You’ll see messages in the Console panel such as: + - [vim] :w — evaluating code + - [repl] evaluate via event + - [eval] code updated + +- :q — Stop/pause playback + - Triggers the same stop action as Alt+. + - Useful to quickly stop scheduling without leaving Vim mode + +- gc — Toggle line comments for the current selection(s) + - Works in normal and visual mode + - If there’s a selection, all selected lines are toggled + +- gcc — Toggle comment for the current line (normal mode) + +Notes + +- Behavior respects the current language mode in the editor for comment syntax. +- If multiple REPL editors are open, commands target the active editor. The implementation dispatches custom events handled by the editor. +- If you don’t see the Console panel, open the right panel in the REPL UI. + +Troubleshooting + +- If :w logs but evaluation doesn’t apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback. +- For :q / gc / gcc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again. From 73a98349596c68031b33b1fe44b830498faea920 Mon Sep 17 00:00:00 2001 From: dtricks Date: Fri, 3 Oct 2025 18:12:47 +0200 Subject: [PATCH 04/27] codeformat --- packages/codemirror/codemirror.mjs | 2 +- packages/codemirror/keybindings.mjs | 56 +++++++++++----------- packages/osc/server.js | 0 website/src/pages/technical-manual/vim.mdx | 3 ++ 4 files changed, 32 insertions(+), 29 deletions(-) mode change 100644 => 100755 packages/osc/server.js diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 6359609c..f3ab3890 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -298,7 +298,7 @@ export class StrudelMirror { async stop() { this.repl.scheduler.stop(); } - + // Listen for global stop requests (e.g., from Vim :q) onStopRequest = (e) => { try { diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 7de2a723..06aaad52 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -1,20 +1,22 @@ -import {defaultKeymap} from '@codemirror/commands'; -import {Prec} from '@codemirror/state'; -import {keymap, ViewPlugin} from '@codemirror/view'; +import { defaultKeymap } from '@codemirror/commands'; +import { Prec } from '@codemirror/state'; +import { keymap, ViewPlugin } from '@codemirror/view'; // import { searchKeymap } from '@codemirror/search'; -import {emacs} from '@replit/codemirror-emacs'; -import {vim, Vim} from '@replit/codemirror-vim'; +import { emacs } from '@replit/codemirror-emacs'; +import { vim, Vim } from '@replit/codemirror-vim'; import { logger } from '@strudel/core'; // import { vim } from './vim_test.mjs'; -import {vscodeKeymap} from '@replit/codemirror-vscode-keymap'; +import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; const vscodePlugin = ViewPlugin.fromClass( - class { - constructor() {} - }, - { - provide : () => { return Prec.highest(keymap.of([...vscodeKeymap ])); }, + class { + constructor() {} + }, + { + provide: () => { + return Prec.highest(keymap.of([...vscodeKeymap])); }, + }, ); const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); @@ -73,9 +75,7 @@ try { // :w to evaluate // :w to evaluate Vim.defineEx('write', 'w', (cm) => { - const view = - cm?.view || - cm; // CM6 Vim passes either an object with view or the view itself + const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself try { view?.focus?.(); // Let the app know this came from Vim :w @@ -93,22 +93,22 @@ try { } // Try Ctrl+Enter first if not handled by custom event const ctrlEnter = new KeyboardEvent('keydown', { - key : 'Enter', - code : 'Enter', - ctrlKey : true, - bubbles : true, - cancelable : true, + key: 'Enter', + code: 'Enter', + ctrlKey: true, + bubbles: true, + cancelable: true, }); view?.dom?.dispatchEvent?.(ctrlEnter); // If not handled (no handler called preventDefault), try Alt+Enter as // fallback if (!ctrlEnter.defaultPrevented) { const altEnter = new KeyboardEvent('keydown', { - key : 'Enter', - code : 'Enter', - altKey : true, - bubbles : true, - cancelable : true, + key: 'Enter', + code: 'Enter', + altKey: true, + bubbles: true, + cancelable: true, }); view?.dom?.dispatchEvent?.(altEnter); } @@ -122,16 +122,16 @@ try { } const keymaps = { - vim, + vim, // Add extra Vim keymap for gc to toggle line comment // We will include a Vim-specific keymap that calls the CM command // respecting the current selection. emacs, - codemirror : () => keymap.of(defaultKeymap), - vscode : vscodeExtension, + codemirror: () => keymap.of(defaultKeymap), + vscode: vscodeExtension, }; export function keybindings(name) { const active = keymaps[name]; - return [ active ? Prec.high(active()) : [] ]; + return [active ? Prec.high(active()) : []]; } diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100644 new mode 100755 diff --git a/website/src/pages/technical-manual/vim.mdx b/website/src/pages/technical-manual/vim.mdx index ec284d0b..b18a259e 100644 --- a/website/src/pages/technical-manual/vim.mdx +++ b/website/src/pages/technical-manual/vim.mdx @@ -8,6 +8,7 @@ layout: ../../layouts/MainLayout.astro When the REPL editor (CodeMirror) is configured to use Vim keybindings, the following commands are available: - :w — Evaluate the current code + - Triggers the same evaluation as Ctrl+Enter / Alt+Enter - You’ll see messages in the Console panel such as: - [vim] :w — evaluating code @@ -15,10 +16,12 @@ When the REPL editor (CodeMirror) is configured to use Vim keybindings, the foll - [eval] code updated - :q — Stop/pause playback + - Triggers the same stop action as Alt+. - Useful to quickly stop scheduling without leaving Vim mode - gc — Toggle line comments for the current selection(s) + - Works in normal and visual mode - If there’s a selection, all selected lines are toggled From 46ba165c7eb7375056625e837fee12ff9cef1aee Mon Sep 17 00:00:00 2001 From: dtricks Date: Fri, 3 Oct 2025 18:16:18 +0200 Subject: [PATCH 05/27] remove gcc --- packages/codemirror/codemirror.mjs | 60 ++++++++++++---------- packages/codemirror/keybindings.mjs | 11 ++-- website/src/pages/technical-manual/vim.mdx | 4 +- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index f3ab3890..69fce4b5 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -1,31 +1,31 @@ import { closeBrackets } from '@codemirror/autocomplete'; -import { toggleLineComment } from '@codemirror/commands'; -export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands'; -// import { search, highlightSelectionMatches } from '@codemirror/search'; -import { indentWithTab } from '@codemirror/commands'; +import { indentWithTab, toggleLineComment } from '@codemirror/commands'; import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; -import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language'; +import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { Compartment, EditorState, Prec } from '@codemirror/state'; import { + drawSelection, EditorView, - highlightActiveLineGutter, highlightActiveLine, + highlightActiveLineGutter, keymap, lineNumbers, - drawSelection, } from '@codemirror/view'; -import { repl, registerControl, logger } from '@strudel/core'; -import { Drawer, cleanupDraw } from '@strudel/draw'; +import { persistentAtom } from '@nanostores/persistent'; +import { logger, registerControl, repl } from '@strudel/core'; +import { cleanupDraw, Drawer } from '@strudel/draw'; + import { isAutoCompletionEnabled } from './autocomplete.mjs'; -import { isTooltipEnabled } from './tooltip.mjs'; +import { basicSetup } from './basicSetup.mjs'; import { flash, isFlashEnabled } from './flash.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { keybindings } from './keybindings.mjs'; -import { initTheme, activateTheme, theme } from './themes.mjs'; import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; -import { widgetPlugin, updateWidgets } from './widget.mjs'; -import { persistentAtom } from '@nanostores/persistent'; -import { basicSetup } from './basicSetup.mjs'; +import { activateTheme, initTheme, theme } from './themes.mjs'; +import { isTooltipEnabled } from './tooltip.mjs'; +import { updateWidgets, widgetPlugin } from './widget.mjs'; + +export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands'; const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), @@ -95,8 +95,8 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo }), sliderPlugin, widgetPlugin, - // indentOnInput(), // works without. already brought with javascript extension? - // bracketMatching(), // does not do anything + // indentOnInput(), // works without. already brought with javascript + // extension? bracketMatching(), // does not do anything syntaxHighlighting(defaultHighlightStyle), EditorView.updateListener.of((v) => onChange(v)), drawSelection({ cursorBlinkRate: 0 }), @@ -120,13 +120,13 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo run: () => onStop?.(), }, /* { - key: 'Ctrl-Shift-.', - run: () => (onPanic ? onPanic() : onStop?.()), - }, - { - key: 'Ctrl-Shift-Enter', - run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()), - }, */ + key: 'Ctrl-Shift-.', + run: () => (onPanic ? onPanic() : onStop?.()), + }, + { + key: 'Ctrl-Shift-Enter', + run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()), + }, */ ]), ), ], @@ -207,7 +207,8 @@ export class StrudelMirror { updateWidgets(this.editor, widgets); updateMiniLocations(this.editor, this.miniLocations); replOptions?.afterEval?.(options); - // if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting) + // if no painters are set (.onPaint was not called), then we only need + // the present moment (for highlighting) const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0]; this.drawer.setDrawTime(drawTime); // invalidate drawer after we've set the appropriate drawTime @@ -261,10 +262,11 @@ export class StrudelMirror { document.addEventListener('repl-evaluate', this.onEvaluateRequest); document.addEventListener('repl-stop', this.onStopRequest); - // Toggle comments requested from Vim (gc/gcc) + // Toggle comments requested from Vim (gc) this.onToggleComment = (e) => { try { - // Honor selections; toggleLineComment handles both selections and single line + // Honor selections; toggleLineComment handles both selections and + // single line toggleLineComment(this.editor); e?.cancelable && e.preventDefault?.(); } catch (err) { @@ -383,7 +385,11 @@ export class StrudelMirror { } } setCode(code) { - const changes = { from: 0, to: this.editor.state.doc.length, insert: code }; + const changes = { + from: 0, + to: this.editor.state.doc.length, + insert: code, + }; this.editor.dispatch({ changes }); } clear() { diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 06aaad52..545c9797 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -4,9 +4,9 @@ import { keymap, ViewPlugin } from '@codemirror/view'; // import { searchKeymap } from '@codemirror/search'; import { emacs } from '@replit/codemirror-emacs'; import { vim, Vim } from '@replit/codemirror-vim'; -import { logger } from '@strudel/core'; // import { vim } from './vim_test.mjs'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; +import { logger } from '@strudel/core'; const vscodePlugin = ViewPlugin.fromClass( class { @@ -26,9 +26,9 @@ const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); // appears in the Console panel. try { if (Vim && typeof Vim.defineEx === 'function') { - // Map gc/gcc to toggle line comments by dispatching a custom event that our - // CodeMirror integration listens to. This avoids depending on Vim's internal - // actions and works with current selections/visual mode. + // Map gc to toggle line comments by dispatching a custom event that our + // CodeMirror integration listens to. This avoids depending on Vim's + // internal actions and works with current selections/visual mode. try { Vim.defineAction('strudelToggleComment', (cm) => { const view = cm?.view || cm; @@ -41,9 +41,8 @@ try { }); Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'normal' }); Vim.mapCommand('gc', 'action', 'strudelToggleComment', {}, { context: 'visual' }); - Vim.mapCommand('gcc', 'action', 'strudelToggleComment', {}, { context: 'normal' }); } catch (e) { - console.error('Vim gc/gcc mapping failed', e); + console.error('Vim gc mapping failed', e); } // :q to pause/stop diff --git a/website/src/pages/technical-manual/vim.mdx b/website/src/pages/technical-manual/vim.mdx index b18a259e..73f5e1a1 100644 --- a/website/src/pages/technical-manual/vim.mdx +++ b/website/src/pages/technical-manual/vim.mdx @@ -25,8 +25,6 @@ When the REPL editor (CodeMirror) is configured to use Vim keybindings, the foll - Works in normal and visual mode - If there’s a selection, all selected lines are toggled -- gcc — Toggle comment for the current line (normal mode) - Notes - Behavior respects the current language mode in the editor for comment syntax. @@ -36,4 +34,4 @@ Notes Troubleshooting - If :w logs but evaluation doesn’t apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback. -- For :q / gc / gcc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again. +- For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again. From cfe17fcba56a8d3767deecd08ef86f054c339d7f Mon Sep 17 00:00:00 2001 From: dtricks Date: Fri, 3 Oct 2025 18:19:18 +0200 Subject: [PATCH 06/27] wrong unicode char --- website/src/pages/technical-manual/vim.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/pages/technical-manual/vim.mdx b/website/src/pages/technical-manual/vim.mdx index 73f5e1a1..fa53aa55 100644 --- a/website/src/pages/technical-manual/vim.mdx +++ b/website/src/pages/technical-manual/vim.mdx @@ -10,7 +10,7 @@ When the REPL editor (CodeMirror) is configured to use Vim keybindings, the foll - :w — Evaluate the current code - Triggers the same evaluation as Ctrl+Enter / Alt+Enter - - You’ll see messages in the Console panel such as: + - You'll see messages in the Console panel such as: - [vim] :w — evaluating code - [repl] evaluate via event - [eval] code updated @@ -23,15 +23,15 @@ When the REPL editor (CodeMirror) is configured to use Vim keybindings, the foll - gc — Toggle line comments for the current selection(s) - Works in normal and visual mode - - If there’s a selection, all selected lines are toggled + - If there's a selection, all selected lines are toggled Notes - Behavior respects the current language mode in the editor for comment syntax. - If multiple REPL editors are open, commands target the active editor. The implementation dispatches custom events handled by the editor. -- If you don’t see the Console panel, open the right panel in the REPL UI. +- If you don't see the Console panel, open the right panel in the REPL UI. Troubleshooting -- If :w logs but evaluation doesn’t apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback. +- If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback. - For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again. From 30a2e149592659ec9e9262c7d05fc7134ef29729 Mon Sep 17 00:00:00 2001 From: dtricks Date: Fri, 3 Oct 2025 18:29:32 +0200 Subject: [PATCH 07/27] weird permission change --- packages/osc/server.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 packages/osc/server.js diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100755 new mode 100644 From a79fc3db5b78a31d6944ca5501206c41cbb9b1bd Mon Sep 17 00:00:00 2001 From: dtricks Date: Sat, 4 Oct 2025 14:49:56 +0200 Subject: [PATCH 08/27] remove unnecessary comments --- packages/codemirror/keybindings.mjs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 545c9797..5cd6a50e 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -45,7 +45,6 @@ try { console.error('Vim gc mapping failed', e); } - // :q to pause/stop // :q to pause/stop Vim.defineEx('quit', 'q', (cm) => { try { @@ -71,7 +70,6 @@ try { } }); - // :w to evaluate // :w to evaluate Vim.defineEx('write', 'w', (cm) => { const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself @@ -122,9 +120,6 @@ try { const keymaps = { vim, - // Add extra Vim keymap for gc to toggle line comment - // We will include a Vim-specific keymap that calls the CM command - // respecting the current selection. emacs, codemirror: () => keymap.of(defaultKeymap), vscode: vscodeExtension, From f48f8e46e51c1010bb3dc9e0599b5de8dbb2a00d Mon Sep 17 00:00:00 2001 From: dtricks Date: Mon, 6 Oct 2025 11:09:21 +0200 Subject: [PATCH 09/27] fix empty catches --- packages/codemirror/keybindings.mjs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 5cd6a50e..81dc87a5 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -54,7 +54,9 @@ try { try { const ev = new CustomEvent('repl-stop', { detail: { source: 'vim', view }, cancelable: true }); handled = document.dispatchEvent(ev) === false; - } catch {} + } catch (e) { + console.error('Error dispatching repl-stop event', e); + } if (!handled) { const altDot = new KeyboardEvent('keydown', { key: '.', @@ -78,13 +80,17 @@ try { // Let the app know this came from Vim :w try { logger('[vim] :w — evaluating code'); - } catch {} + } catch (e) { + console.error('Error logging Vim :w evaluation', e); + } // Dispatch a dedicated evaluate event first let handled = false; try { const ev = new CustomEvent('repl-evaluate', { detail: { source: 'vim', view }, cancelable: true }); handled = document.dispatchEvent(ev) === false; // false means preventDefault was called - } catch {} + } catch (e) { + console.error('Error dispatching repl-evaluate event', e); + } if (handled) { return; } From 3dc9b3524c40f1dba8492940760c2dde081f5928 Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Tue, 7 Oct 2025 17:19:02 +0200 Subject: [PATCH 10/27] mondo: add & sugar --- packages/mondo/mondo.mjs | 23 +++++++++++++++++++++++ packages/mondo/test/mondo.test.mjs | 1 + 2 files changed, 24 insertions(+) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index b879f24b..cdd75843 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -22,6 +22,7 @@ export class MondoParser { number: /^-?[0-9]*\.?[0-9]+/, // before pipe! // TODO: better error handling when "-" is used as rest, e.g "s [- bd]" op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. + and: /^&/, // dollar: /^\$/, pipe: /^#/, stack: /^[,$]/, @@ -150,6 +151,27 @@ export class MondoParser { } return children; } + desugar_ands(children) { + while (true) { + let opIndex = children.findIndex((child) => child.type === 'and'); + if (opIndex === -1) break; + if (opIndex === children.length - 1) { + throw new Error(`cannot use & as last child.`); + } + if (opIndex === 0) { + throw new Error(`cannot use & as first child.`); + } + // convert infix to prefix notation + const op = { type: 'plain', value: children[opIndex].value }; + const left = children[opIndex - 1]; + const right = children[opIndex + 1]; + const call = { type: 'list', children: [op, left, right] }; + // insert call while keeping other siblings + children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; + children = this.unwrap_children(children); + } + return children; + } desugar_ops(children) { while (true) { let opIndex = children.findIndex((child) => child.type === 'op'); @@ -264,6 +286,7 @@ export class MondoParser { children = [{ type: 'plain', value: type }, ...children]; } children = this.desugar_ops(children); + children = this.desugar_ands(children); // children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); children = this.desugar_pipes(children); return children; diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 6393b10f..97f447e7 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -117,6 +117,7 @@ describe('mondo sugar', () => { it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); + it('should desugar x&y:z', () => expect(desguar('bd&3:8')).toEqual('(& bd (: 8 3))')); it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); /* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); From b7fa440bda53aa1c281db6fe0a007b610109508c Mon Sep 17 00:00:00 2001 From: Tristan de Cacqueray Date: Tue, 7 Oct 2025 17:33:56 +0200 Subject: [PATCH 11/27] mondough: interpret & --- packages/mondough/mondough.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index b7ee8378..a299c9a5 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,6 +11,7 @@ import { chooseIn, degradeBy, silence, + e, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from 'mondolang'; @@ -40,6 +41,7 @@ lib['!'] = extend; lib['@'] = expand; lib['%'] = pace; lib['?'] = degradeBy; // todo: default 0.5 not working.. +lib['&'] = (a, b) => a.e(b); lib[':'] = tail; lib['..'] = range; lib['def'] = () => silence; From f33db5f07f491278995d994b84f06abf3c3c34e0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 7 Oct 2025 22:35:29 +0200 Subject: [PATCH 12/27] - refactor bjork -> bjorklund - refactor e -> bjork - flip & desugared arguments --- packages/core/euclid.mjs | 14 +++++++------- packages/mondo/mondo.mjs | 2 +- packages/mondough/mondough.mjs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 44ab07f1..98c21a11 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -35,18 +35,18 @@ const right = function (n, x) { return result; }; -const _bjork = function (n, x) { +const _bjorklund = function (n, x) { const [ons, offs] = n; - return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x))); + return Math.min(ons, offs) <= 1 ? [n, x] : _bjorklund(...(ons > offs ? left(n, x) : right(n, x))); }; -export const bjork = function (ons, steps) { +export const bjorklund = function (ons, steps) { const inverted = ons < 0; const absOns = Math.abs(ons); const offs = steps - absOns; const ones = Array(absOns).fill([1]); const zeros = Array(offs).fill([0]); - const result = _bjork([absOns, offs], [ones, zeros]); + const result = _bjorklund([absOns, offs], [ones, zeros]); const pattern = flatten(result[1][0]).concat(flatten(result[1][1])); return inverted ? pattern.map((x) => 1 - x) : pattern; }; @@ -128,7 +128,7 @@ export const bjork = function (ons, steps) { */ const _euclidRot = function (pulses, steps, rotation) { - const b = bjork(pulses, steps); + const b = bjorklund(pulses, steps); if (rotation) { return rotate(b, -rotation); } @@ -139,7 +139,7 @@ export const euclid = register('euclid', function (pulses, steps, pat) { return pat.struct(_euclidRot(pulses, steps, 0)); }); -export const e = register('e', function (euc, pat) { +export const bjork = register('bjork', function (euc, pat) { if (!Array.isArray(euc)) { euc = [euc]; } @@ -216,6 +216,6 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s * .pan(sine.slow(8)) */ export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { - const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc); + const morphed = _morph(bjorklund(pulses, steps), new Array(pulses).fill(1), perc); return pat.struct(morphed).setSteps(steps); }); diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index cdd75843..d419eaff 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -165,7 +165,7 @@ export class MondoParser { const op = { type: 'plain', value: children[opIndex].value }; const left = children[opIndex - 1]; const right = children[opIndex + 1]; - const call = { type: 'list', children: [op, left, right] }; + const call = { type: 'list', children: [op, right, left] }; // insert call while keeping other siblings children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; children = this.unwrap_children(children); diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index a299c9a5..9b1edb5a 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -11,7 +11,7 @@ import { chooseIn, degradeBy, silence, - e, + bjork, } from '@strudel/core'; import { registerLanguage } from '@strudel/transpiler'; import { MondoRunner } from 'mondolang'; @@ -41,7 +41,7 @@ lib['!'] = extend; lib['@'] = expand; lib['%'] = pace; lib['?'] = degradeBy; // todo: default 0.5 not working.. -lib['&'] = (a, b) => a.e(b); +lib['&'] = bjork; lib[':'] = tail; lib['..'] = range; lib['def'] = () => silence; From 3a7d50924d190896053a20a783a758da237100bf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 7 Oct 2025 22:59:50 +0200 Subject: [PATCH 13/27] refactor: add op_precedence to express & with desugar_ops --- packages/mondo/mondo.mjs | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index d419eaff..00a3b401 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -21,14 +21,14 @@ export class MondoParser { close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! // TODO: better error handling when "-" is used as rest, e.g "s [- bd]" - op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? .. - and: /^&/, + op: /^[*\/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, pipe: /^#/, stack: /^[,$]/, or: /^[|]/, plain: /^[a-zA-Z0-9-~_^#]+/, }; + op_precedence = [['*', '/', ':', '!', '@', '%', '?', '+', '-', '..'], ['&']]; // matches next token next_token(code, offset = 0) { for (let type in this.token_types) { @@ -151,30 +151,9 @@ export class MondoParser { } return children; } - desugar_ands(children) { + desugar_ops(children, types) { while (true) { - let opIndex = children.findIndex((child) => child.type === 'and'); - if (opIndex === -1) break; - if (opIndex === children.length - 1) { - throw new Error(`cannot use & as last child.`); - } - if (opIndex === 0) { - throw new Error(`cannot use & as first child.`); - } - // convert infix to prefix notation - const op = { type: 'plain', value: children[opIndex].value }; - const left = children[opIndex - 1]; - const right = children[opIndex + 1]; - const call = { type: 'list', children: [op, right, left] }; - // insert call while keeping other siblings - children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)]; - children = this.unwrap_children(children); - } - return children; - } - desugar_ops(children) { - while (true) { - let opIndex = children.findIndex((child) => child.type === 'op'); + let opIndex = children.findIndex((child) => child.type === 'op' && types.includes(child.value)); if (opIndex === -1) break; const op = { type: 'plain', value: children[opIndex].value }; if (opIndex === children.length - 1) { @@ -285,9 +264,10 @@ export class MondoParser { // the type we've removed before splitting needs to be added back children = [{ type: 'plain', value: type }, ...children]; } - children = this.desugar_ops(children); - children = this.desugar_ands(children); - // children = this.desugar_pipes(children, (children) => this.desugar_dollars(children)); + // for each precendence group, call desugar_ops once + this.op_precedence.forEach((ops) => { + children = this.desugar_ops(children, ops); + }); children = this.desugar_pipes(children); return children; }), From 319a2d7289ce4cd43e6f2b450e69ea27181d35da Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 7 Oct 2025 23:05:00 +0200 Subject: [PATCH 14/27] fix: tests --- packages/core/test/euclid.test.js | 16 ++++++++-------- packages/mondo/mondo.mjs | 2 +- packages/mondo/test/mondo.test.mjs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/test/euclid.test.js b/packages/core/test/euclid.test.js index a33ec951..6169abd7 100644 --- a/packages/core/test/euclid.test.js +++ b/packages/core/test/euclid.test.js @@ -1,14 +1,14 @@ -import { bjork } from '../euclid.mjs'; +import { bjorklund } from '../euclid.mjs'; import { describe, expect, it } from 'vitest'; import { fastcat } from '../pattern.mjs'; -describe('bjork', () => { - it('should apply bjorklund to ons and steps', () => { - expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]); - expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]); - expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]); - expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]); - expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]); +describe('bjorklund', () => { + it('should apply bjorklundlund to ons and steps', () => { + expect(bjorklund(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]); + expect(bjorklund(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]); + expect(bjorklund(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]); + expect(bjorklund(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]); + expect(bjorklund(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]); }); }); diff --git a/packages/mondo/mondo.mjs b/packages/mondo/mondo.mjs index 00a3b401..b87a8b58 100644 --- a/packages/mondo/mondo.mjs +++ b/packages/mondo/mondo.mjs @@ -21,7 +21,7 @@ export class MondoParser { close_curly: /^\}/, number: /^-?[0-9]*\.?[0-9]+/, // before pipe! // TODO: better error handling when "-" is used as rest, e.g "s [- bd]" - op: /^[*\/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? .. + op: /^[*/:!@%?+\-&]|^\.{2}/, // * / : ! @ % ? .. // dollar: /^\$/, pipe: /^#/, stack: /^[,$]/, diff --git a/packages/mondo/test/mondo.test.mjs b/packages/mondo/test/mondo.test.mjs index 97f447e7..a02c6fec 100644 --- a/packages/mondo/test/mondo.test.mjs +++ b/packages/mondo/test/mondo.test.mjs @@ -117,7 +117,7 @@ describe('mondo sugar', () => { it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)')); it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))')); it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))')); - it('should desugar x&y:z', () => expect(desguar('bd&3:8')).toEqual('(& bd (: 8 3))')); + it('should desugar x&y:z', () => expect(desguar('bd&3:8')).toEqual('(& (: 8 3) bd)')); it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)')); /* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)')); it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))')); From c02def03b683a1f37a5b6cd7f53ab51b721fdb26 Mon Sep 17 00:00:00 2001 From: Aria Date: Thu, 16 Oct 2025 16:26:33 -0500 Subject: [PATCH 15/27] Don't tag when scale fails, allow mixed sharps and flats, simplify scale function --- packages/core/pattern.mjs | 2 +- packages/core/test/pattern.test.mjs | 4 +- packages/core/util.mjs | 10 ++- packages/superdough/util.mjs | 6 +- packages/tonal/test/tonal.test.mjs | 7 -- packages/tonal/tonal.mjs | 96 +++++++++++------------ test/__snapshots__/examples.test.mjs.snap | 8 +- website/src/components/Header/Search.css | 4 +- 8 files changed, 67 insertions(+), 70 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index a85226a8..51d525f7 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2574,8 +2574,8 @@ export const { chunkBack, chunkback } = register( * @returns Pattern * @example * "<0 8> 1 2 3 4 5 6 7" - * .fastChunk(4, x => x.color('red')).slow(2) * .scale("C2:major").note() + * .fastChunk(4, x => x.color('red')).slow(2) */ export const { fastchunk, fastChunk } = register( ['fastchunk', 'fastChunk'], diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 625f4075..1df5c877 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -796,7 +796,7 @@ describe('Pattern', () => { }); }); describe('apply', () => { - (it('Can apply a function', () => { + it('Can apply a function', () => { expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); }), it('Can apply a pattern of functions', () => { @@ -804,7 +804,7 @@ describe('Pattern', () => { expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual( sequence('a', 'b').fast(2, 3).firstCycle(), ); - })); + }); }); describe('layer', () => { it('Can layer up multiple functions', () => { diff --git a/packages/core/util.mjs b/packages/core/util.mjs index ef3f1e96..915a2cb7 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -7,8 +7,8 @@ This program is free software: you can redistribute it and/or modify it under th import { logger } from './logger.mjs'; // returns true if the given string is a note -export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name); -export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name); +export const isNoteWithOctave = (name) => /^[a-gA-G][#bsf]*[0-9]*$/.test(name); +export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]*$/.test(name); export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; @@ -23,6 +23,10 @@ export const tokenizeNote = (note) => { const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; const accs = { '#': 1, b: -1, s: 1, f: -1 }; +export const getAccidentalsOffset = (accidentals) => { + return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0; +}; + // turns the given note into its midi number representation export const noteToMidi = (note, defaultOctave = 3) => { const [pc, acc, oct = defaultOctave] = tokenizeNote(note); @@ -30,7 +34,7 @@ export const noteToMidi = (note, defaultOctave = 3) => { throw new Error('not a note: "' + note + '"'); } const chroma = chromas[pc.toLowerCase()]; - const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; + const offset = getAccidentalsOffset(acc); return (Number(oct) + 1) * 12 + chroma + offset; }; export const midiToFreq = (n) => { diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 475c05f6..418b7af0 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -16,13 +16,17 @@ export const tokenizeNote = (note) => { const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; const accs = { '#': 1, b: -1, s: 1, f: -1 }; +export const getAccidentalsOffset = (accidentals) => { + return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0; +}; + export const noteToMidi = (note, defaultOctave = 3) => { const [pc, acc, oct = defaultOctave] = tokenizeNote(note); if (!pc) { throw new Error('not a note: "' + note + '"'); } const chroma = chromas[pc.toLowerCase()]; - const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; + const offset = getAccidentalsOffset(acc); return (Number(oct) + 1) * 12 + chroma + offset; }; export const midiToFreq = (n) => { diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index 5f61b05a..06aa9118 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -61,13 +61,6 @@ describe('tonal', () => { .firstCycleValues.map((h) => h.note), ).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']); }); - it('produces silence for mixed sharps and flats', () => { - expect( - n(seq('0b#', '1#b', '2#b#')) - .scale('C major') - .firstCycleValues.map((h) => h.note), - ).toEqual([]); - }); it('snaps notes (upwards) to scale', () => { const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb']; const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3']; diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index e745a33a..0dc468b2 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -5,9 +5,8 @@ This program is free software: you can redistribute it and/or modify it under th */ import { Note, Interval, Scale } from '@tonaljs/tonal'; -import { register, _mod, silence, logger, pure, isNote } from '@strudel/core'; +import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core'; import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs'; -import { noteToMidi } from '../core/util.mjs'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; @@ -185,17 +184,15 @@ function _convertStepToNumberAndOffset(step) { step = String(step); // Check to see if the step matches the expected format: // - A number (possibly negative) - // - Some number of sharps or flats (but not both) - const match = /^(-?\d+)(#+|b+)?$/.exec(step); + // - Some number of sharps or flats + const match = /^(-?\d+)([#bsf]*)$/.exec(step); if (!match) { throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`); } asNumber = Number(match[1]); - // These decorations will determine the semitone offset based on the number of - // sharps or flats - const decorations = match[2] || ''; - offset = decorations[0] === '#' ? decorations.length : -decorations.length; + const accidentals = match[2] || ''; + offset = getAccidentalsOffset(accidentals); } return [asNumber, offset]; } @@ -226,7 +223,7 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) { * Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale. * * When describing notes via numbers, note that negative numbers can be used to wrap backwards - * in the scale as well as sharps or flats (but not both) to produce notes outside of the scale. + * in the scale as well as sharps or flats to produce notes outside of the scale. * * Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. * @@ -254,7 +251,6 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) { * @example * note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3) */ - export const scale = register( 'scale', function (scale, pat) { @@ -262,47 +258,47 @@ export const scale = register( if (Array.isArray(scale)) { scale = scale.flat().join(' '); } - return ( - pat - .fmap((value) => { - const isObject = typeof value === 'object'; - // If value is a pure value, place it on `n` so that we interpret it as a scale - // degree - value = isObject ? value : { n: value }; - if ('note' in value) { - const note = _getNearestScaleNote(scale, value.note); - return pure({ ...value, note }); - } else if ('n' in value || 'value' in value) { - const step = value.n ?? value.value; - delete value.n; // remove n so it won't cause trouble - if (isNote(step)) { - // legacy.. - return pure(step); - } - try { - const [number, offset] = _convertStepToNumberAndOffset(step); - let note; - if (value.anchor) { - note = stepInNamedScale(number, scale, value.anchor); - } else { - note = scaleStep(number, scale); - } - if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset)); - value = pure(isObject ? { ...value, note } : note); - } catch (err) { - logger(`[tonal] ${err.message}`, 'error'); - return silence; - } - return value; - } - throw new Error( - `Invalid value format for 'scale'. Value must contain 'n' or 'note' but received ${Object.keys(value)}`, + return pat.withHaps((haps) => { + haps = haps.map((hap) => { + let hVal = hap.value; + const isObject = typeof hVal === 'object'; + // If hVal is a pure value, place it on `n` so that we interpret it as a scale degree + hVal = isObject ? hVal : { n: hVal }; + const { note, n, value, ...otherValues } = hVal; + const noteOrStep = note ?? n ?? value; + if (noteOrStep === undefined) { + logger( + `[tonal] Invalid value format for 'scale'. Value must contain n, note, or value but received keys [${Object.keys(hVal).join(', ')}]`, + 'error', ); - }) - .outerJoin() - // legacy: - .withHap((hap) => hap.setContext({ ...hap.context, scale })) - ); + return hap; // pass the value through unchanged + } + let scaleNote; + if (isNote(noteOrStep)) { + // Note case (quantize to scale) + scaleNote = _getNearestScaleNote(scale, noteOrStep); + hap.value = { ...otherValues, note: scaleNote }; + } else { + // Step case (convert to note in scale) + try { + const [number, offset] = _convertStepToNumberAndOffset(noteOrStep); + if (otherValues.anchor) { + scaleNote = stepInNamedScale(number, scale, otherValues.anchor); + } else { + scaleNote = scaleStep(number, scale); + } + if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset)); + } catch (err) { + logger(`[tonal] ${err.message}`, 'error'); + return; // will be removed + } + } + hap.value = isObject ? { ...otherValues, note: scaleNote } : scaleNote; + // Tag with scale for downsteam scale-aware operations + return hap.setContext({ ...hap.context, scale }); + }); + return removeUndefineds(haps); + }); }, true, true, // preserve step count diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 937d4279..fd0a1bb5 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3838,8 +3838,8 @@ exports[`runs examples > example "fast" example index 0 1`] = ` exports[`runs examples > example "fastChunk" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | color:red note:0 ]", - "[ 1/4 → 1/2 | color:red note:1 ]", + "[ 0/1 → 1/4 | note:C2 color:red ]", + "[ 1/4 → 1/2 | note:D2 color:red ]", "[ 1/2 → 3/4 | note:E2 ]", "[ 3/4 → 1/1 | note:F2 ]", "[ 1/1 → 5/4 | note:G2 ]", @@ -3848,8 +3848,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = ` "[ 7/4 → 2/1 | note:C3 ]", "[ 2/1 → 9/4 | note:D3 ]", "[ 9/4 → 5/2 | note:D2 ]", - "[ 5/2 → 11/4 | color:red note:2 ]", - "[ 11/4 → 3/1 | color:red note:3 ]", + "[ 5/2 → 11/4 | note:E2 color:red ]", + "[ 11/4 → 3/1 | note:F2 color:red ]", "[ 3/1 → 13/4 | note:G2 ]", "[ 13/4 → 7/2 | note:A2 ]", "[ 7/2 → 15/4 | note:B2 ]", diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index b14146cb..456ef9f6 100644 --- a/website/src/components/Header/Search.css +++ b/website/src/components/Header/Search.css @@ -18,8 +18,8 @@ --docsearch-modal-background: var(--background); --docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%); --docsearch-key-gradient: var(--foreground); - --docsearch-key-shadow: - inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground); + --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), + 0 1px 2px 1px var(--gutterBackground); } .dark { --docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%); From 28056deb66583cb9ae0c2c94e3d5538413050924 Mon Sep 17 00:00:00 2001 From: moumar Date: Thu, 23 Oct 2025 23:15:53 +0200 Subject: [PATCH 16/27] Fix ZZFX example ZZFX example did not produce any sound out of the box, fixed by updating tremolo param. --- website/src/pages/learn/synths.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 21a9b917..0fcc4136 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -157,7 +157,7 @@ It has 20 parameters in total, here is a snippet that uses all: .pitchJump(0) // +/- pitch change after pitchJumpTime .pitchJumpTime(0) // >0 time after pitchJump is applied .lfo(0) // >0 resets slide + pitchJump + sets tremolo speed - .tremolo(0) // 0-1 lfo volume modulation amount + .tremolo(0.5) // 0-1 lfo volume modulation amount //.duration(.2) // overwrite strudel event duration //.gain(1) // change volume ._scope() // vizualise waveform (not zzfx related) From 5cc93996ce4148c79b43ad514aef4277b29b2240 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 26 Oct 2025 21:49:10 +0000 Subject: [PATCH 17/27] degithub --- website/src/repl/prebake.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 1fbc8402..6218d72a 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -23,8 +23,8 @@ export async function prebake() { // https://github.com/sgossner/VCSL/ // https://api.github.com/repositories/126427031/contents/ // LICENSE: CC0 general-purpose - samples(`${baseNoTrailing}/vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }), - samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', { + samples(`${baseNoTrailing}/vcsl.json`, 'https://strudel.b-cdn.net/VCSL/', { prebake: true }), + samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'https://strudel.b-cdn.net/tidal-drum-machines/machines/', { prebake: true, tag: 'drum-machines', }), @@ -145,7 +145,7 @@ export async function prebake() { 'num/20.wav', ], }, - 'github:tidalcycles/dirt-samples', + 'https://strudel.b-cdn.net/Dirt-Samples/', { prebake: true, }, From 8398385dcd7f9fc488bf334257c4489ed47dfe13 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 26 Oct 2025 22:18:37 +0000 Subject: [PATCH 18/27] piano via bunnycdn --- website/src/repl/prebake.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 6218d72a..b6d2d55f 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -19,7 +19,7 @@ export async function prebake() { // => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically // seems to be a problem with soundfont2 import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()), - samples(`${baseNoTrailing}/piano.json`, undefined, { prebake: true }), + samples(`${baseNoTrailing}/piano.json`, 'https://strudel.b-cdn.net/piano/', { prebake: true }), // https://github.com/sgossner/VCSL/ // https://api.github.com/repositories/126427031/contents/ // LICENSE: CC0 general-purpose From 9ff0449ca35741b7d6cc7c1776c44c38ef897400 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 27 Oct 2025 09:24:17 +0000 Subject: [PATCH 19/27] use json files in dough-samples via bunny cdn --- website/src/repl/prebake.mjs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index b6d2d55f..8a6ccc7e 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -6,6 +6,7 @@ import './files.mjs'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; +const baseCDN = 'https://strudel.b-cdn.net'; export async function prebake() { // https://archive.org/details/SalamanderGrandPianoV3 @@ -19,23 +20,23 @@ export async function prebake() { // => getting "window is not defined", as soon as "@strudel/soundfonts" is imported statically // seems to be a problem with soundfont2 import('@strudel/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()), - samples(`${baseNoTrailing}/piano.json`, 'https://strudel.b-cdn.net/piano/', { prebake: true }), + samples(`${baseCDN}/piano.json`, `${baseCDN}/piano/`, { prebake: true }), // https://github.com/sgossner/VCSL/ // https://api.github.com/repositories/126427031/contents/ // LICENSE: CC0 general-purpose - samples(`${baseNoTrailing}/vcsl.json`, 'https://strudel.b-cdn.net/VCSL/', { prebake: true }), - samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'https://strudel.b-cdn.net/tidal-drum-machines/machines/', { + samples(`${baseCDN}/vcsl.json`, `${baseCDN}/VCSL/`, { prebake: true }), + samples(`${baseCDN}/tidal-drum-machines.json`, `${baseCDN}/tidal-drum-machines/machines/`, { prebake: true, tag: 'drum-machines', }), - samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, { + samples(`${baseCDN}/uzu-drumkit.json`, `${baseCDN}/uzu-drumkit/`, { prebake: true, tag: 'drum-machines', }), - samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, { + samples(`${baseCDN}/uzu-wavetables.json`, `${baseCDN}/uzu-wavetables/`, { prebake: true, }), - samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`${baseCDN}/mridangam.json`, `${baseCDN}/mrid/`, { prebake: true, tag: 'drum-machines' }), samples( { casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'], @@ -145,14 +146,14 @@ export async function prebake() { 'num/20.wav', ], }, - 'https://strudel.b-cdn.net/Dirt-Samples/', + `${baseCDN}/Dirt-Samples/`, { prebake: true, }, ), ]); - aliasBank(`${baseNoTrailing}/tidal-drum-machines-alias.json`); + aliasBank(`${baseCDN}/tidal-drum-machines-alias.json`); } const maxPan = noteToMidi('C8'); From cbe7aaacfbf8a51f5f5cf39bf79319f273214e24 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 27 Oct 2025 15:38:32 +0000 Subject: [PATCH 20/27] Publish - @strudel/codemirror@1.2.6 - @strudel/core@1.2.5 - @strudel/csound@1.2.6 - @strudel/draw@1.2.5 - @strudel/gamepad@1.2.5 - @strudel/hydra@1.2.5 - @strudel/midi@1.2.6 - @strudel/mini@1.2.5 - @strudel/mondo@1.1.5 - @strudel/motion@1.2.5 - @strudel/mqtt@1.2.5 - @strudel/osc@1.3.0 - @strudel/repl@1.2.7 - @strudel/serial@1.2.5 - @strudel/soundfonts@1.2.6 - superdough@1.2.6 - supradough@1.2.4 - @strudel/tonal@1.2.5 - @strudel/transpiler@1.2.5 - @strudel/web@1.2.6 - @strudel/webaudio@1.2.6 - @strudel/xen@1.2.5 --- packages/codemirror/package.json | 6 +++--- packages/core/package.json | 2 +- packages/csound/package.json | 2 +- packages/draw/package.json | 2 +- packages/gamepad/package.json | 2 +- packages/hydra/package.json | 2 +- packages/midi/package.json | 2 +- packages/mini/package.json | 2 +- packages/mondough/package.json | 2 +- packages/motion/package.json | 2 +- packages/mqtt/package.json | 2 +- packages/osc/package.json | 2 +- packages/repl/package.json | 2 +- packages/serial/package.json | 2 +- packages/soundfonts/package.json | 2 +- packages/superdough/package.json | 2 +- packages/supradough/package.json | 5 ++--- packages/tonal/package.json | 2 +- packages/transpiler/package.json | 2 +- packages/web/package.json | 2 +- packages/webaudio/package.json | 2 +- packages/xen/package.json | 2 +- pnpm-lock.yaml | 3 --- 23 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index b4e7d27a..e54c4a42 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.2.5", + "version": "1.2.6", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { @@ -49,8 +49,8 @@ "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", "@tonaljs/tonal": "^4.10.0", - "superdough": "workspace:*", - "nanostores": "^0.11.3" + "nanostores": "^0.11.3", + "superdough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/core/package.json b/packages/core/package.json index 7cf20cea..c33432f1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.2.4", + "version": "1.2.5", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/csound/package.json b/packages/csound/package.json index 90130a10..058b8d77 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.2.5", + "version": "1.2.6", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/draw/package.json b/packages/draw/package.json index 6a4c5754..ac212398 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.2.4", + "version": "1.2.5", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 555eac03..8605e35c 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/gamepad", - "version": "1.2.4", + "version": "1.2.5", "description": "Gamepad Inputs for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index 7ba79d5d..eccdde52 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.2.4", + "version": "1.2.5", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/package.json b/packages/midi/package.json index 2342cf7e..ebb07fc3 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.2.5", + "version": "1.2.6", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mini/package.json b/packages/mini/package.json index 6eeaab0d..05bbd019 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.2.4", + "version": "1.2.5", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mondough/package.json b/packages/mondough/package.json index c81d76cb..593d2264 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.4", + "version": "1.1.5", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", diff --git a/packages/motion/package.json b/packages/motion/package.json index 57cac9cc..b0a4d556 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/motion", - "version": "1.2.4", + "version": "1.2.5", "description": "DeviceMotion API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index 2e32825f..1b34a4ea 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.2.4", + "version": "1.2.5", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/package.json b/packages/osc/package.json index ea453e1e..173b7674 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/osc", - "version": "1.2.10", + "version": "1.3.0", "description": "OSC messaging for strudel", "main": "osc.mjs", "bin": "./server.js", diff --git a/packages/repl/package.json b/packages/repl/package.json index 41a165c3..1ad173c6 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.2.6", + "version": "1.2.7", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { diff --git a/packages/serial/package.json b/packages/serial/package.json index 9ed89cf2..d73b05a1 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.2.4", + "version": "1.2.5", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index 07e35674..8c41a5d5 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.2.5", + "version": "1.2.6", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/package.json b/packages/superdough/package.json index a8211777..a95fc5ed 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.2.5", + "version": "1.2.6", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", diff --git a/packages/supradough/package.json b/packages/supradough/package.json index 7e465c0a..50304371 100644 --- a/packages/supradough/package.json +++ b/packages/supradough/package.json @@ -1,6 +1,6 @@ { "name": "supradough", - "version": "1.2.3", + "version": "1.2.4", "description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.", "main": "index.mjs", "type": "module", @@ -32,6 +32,5 @@ "vite": "^6.0.11", "vite-plugin-bundle-audioworklet": "workspace:*", "wav-encoder": "^1.3.0" - }, - "dependencies": {} + } } diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 1461bdc8..611df676 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.2.4", + "version": "1.2.5", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 18722bdc..bd28a19f 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.2.4", + "version": "1.2.5", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", diff --git a/packages/web/package.json b/packages/web/package.json index df21f405..aef044c7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.2.5", + "version": "1.2.6", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 49da00f2..2d5cd420 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.2.5", + "version": "1.2.6", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/package.json b/packages/xen/package.json index 0a6736d9..4015c437 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.2.4", + "version": "1.2.5", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b524ebf..23903fca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6016,9 +6016,6 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - osc-js@2.4.1: - resolution: {integrity: sha512-QlSeRKJclL47FNvO1MUCAAp9frmCF9zcYbnf6R9HpcklAst8ZyX3ISsk1v/Vghr/5GmXn0bhVjFXF9h+hfnl4Q==} - osc@2.4.5: resolution: {integrity: sha512-Nc4/qcl+vA/CMxiKS1xrYgzjfnyB3W94gZnrkn3eTzihlndbEml6+wj1YQNKBI4r+qrw3obCcEoU7SVH/OxpxA==} From 484c5e6cddd7426138da77c32da96f63d8172cbc Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Nov 2025 01:26:25 -0400 Subject: [PATCH 21/27] working --- website/src/repl/idbutils.mjs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index d87d649c..9dfc9faa 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -92,11 +92,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = async function blobToDataUrl(blob) { return new Promise((resolve) => { - var reader = new FileReader(); - reader.onload = function (event) { - resolve(event.target.result); - }; - reader.readAsDataURL(blob); + resolve(URL.createObjectURL(blob)) }); } From f38792c5699c7f00aedcdfddc80c726dc21810de Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Nov 2025 01:32:24 -0400 Subject: [PATCH 22/27] format --- packages/superdough/sampler.mjs | 1 + website/src/repl/idbutils.mjs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 3232fa47..7b7a6d3b 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -349,6 +349,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { } function registerSample(key, bank, params) { + console.info({ key, bank }); registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { type: 'sample', samples: bank, diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 9dfc9faa..e1ca3a60 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -92,7 +92,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = async function blobToDataUrl(blob) { return new Promise((resolve) => { - resolve(URL.createObjectURL(blob)) + resolve(URL.createObjectURL(blob)); }); } From a41b07c67a19fd6361c75b2e251f04dcac9c8a91 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Nov 2025 01:44:32 -0400 Subject: [PATCH 23/27] fixed build --- packages/codemirror/autocomplete.mjs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index b8a569be..fd8ff9e2 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,8 +1,9 @@ import jsdoc from '../../doc.json'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; -import { Scale } from '@tonaljs/tonal'; -import { soundMap } from 'superdough'; +//TODO: fix tonal scale import +// import { Scale } from '@tonaljs/tonal'; +import { soundMap } from '@strudel/webaudio'; import { complex } from '@strudel/tonal'; const escapeHtml = (str) => { @@ -90,13 +91,13 @@ export function bankCompletions() { .map((name) => ({ label: name, type: 'bank' })); } -// Attempt to get all scale names from Tonal -let scaleCompletions = []; -try { - scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' })); -} catch (e) { - console.warn('[autocomplete] Could not load scale names from Tonal:', e); -} +// Attempt to get all scale names from Tonal TODO: FIX IMPORT +// let scaleCompletions = []; +// try { +// scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' })); +// } catch (e) { +// console.warn('[autocomplete] Could not load scale names from Tonal:', e); +// } // Valid mode values for voicing const modeCompletions = [ From 10bfe32a2d7e4f6e81a8b87c5c24d0cd2fb8a94a Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Nov 2025 01:48:42 -0400 Subject: [PATCH 24/27] rm console --- packages/superdough/sampler.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 9cf96d47..84bac358 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -347,7 +347,6 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { } function registerSample(key, bank, params) { - console.info({ key, bank }); registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { type: 'sample', samples: bank, From 7b928749091f204d23810c2b48e187b87479a237 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Nov 2025 01:55:05 -0400 Subject: [PATCH 25/27] fix broken array --- packages/codemirror/autocomplete.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index fd8ff9e2..0e15a41f 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -92,7 +92,7 @@ export function bankCompletions() { } // Attempt to get all scale names from Tonal TODO: FIX IMPORT -// let scaleCompletions = []; +let scaleCompletions = []; // try { // scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' })); // } catch (e) { From 2b575d50b9af222039aa9c42fa466413cbf8dd17 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Nov 2025 02:39:43 -0400 Subject: [PATCH 26/27] temporarily disable sample autocomplete --- packages/codemirror/autocomplete.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 0e15a41f..9d01f8e9 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -3,7 +3,8 @@ import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; //TODO: fix tonal scale import // import { Scale } from '@tonaljs/tonal'; -import { soundMap } from '@strudel/webaudio'; +// import { soundMap } from '@strudel/webaudio'; +let soundMap = undefined; import { complex } from '@strudel/tonal'; const escapeHtml = (str) => { @@ -80,7 +81,9 @@ const hasExcludedTags = (doc) => ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); export function bankCompletions() { - const soundDict = soundMap.get(); + // TODO: FIX IMPORT + const soundDict = soundMap?.get() ?? {}; + const banks = new Set(); for (const key of Object.keys(soundDict)) { const [bank, suffix] = key.split('_'); @@ -269,7 +272,7 @@ function soundHandler(context) { const inside = text.slice(quoteIdx + 1); const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX); const fragment = fragMatch ? fragMatch[1] : inside; - const soundNames = Object.keys(soundMap.get()).sort(); + const soundNames = Object.keys(soundMap?.get() ?? {}).sort(); const filteredSounds = soundNames.filter((name) => name.includes(fragment)); let options = filteredSounds.map((name) => ({ label: name, type: 'sound' })); const from = soundContext.to - fragment.length; From 4f046ae88355fb384a1a01d35dd51e2c5fb85457 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sat, 1 Nov 2025 02:40:40 -0400 Subject: [PATCH 27/27] cf --- packages/codemirror/autocomplete.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 9d01f8e9..20c12818 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -83,7 +83,7 @@ const hasExcludedTags = (doc) => export function bankCompletions() { // TODO: FIX IMPORT const soundDict = soundMap?.get() ?? {}; - + const banks = new Set(); for (const key of Object.keys(soundDict)) { const [bank, suffix] = key.split('_');