From b9fce1504276215bd9b280b69ab1214f6d341162 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 3 Dec 2025 15:18:32 -0500 Subject: [PATCH 1/9] working --- packages/codemirror/codemirror.mjs | 9 +++++++++ packages/codemirror/labelJump.mjs | 31 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 packages/codemirror/labelJump.mjs diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 69fce4b50..6643e400d 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -24,6 +24,7 @@ import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { activateTheme, initTheme, theme } from './themes.mjs'; import { isTooltipEnabled } from './tooltip.mjs'; import { updateWidgets, widgetPlugin } from './widget.mjs'; +import { jumpToCharacter } from './labelJump.mjs'; export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands'; @@ -119,6 +120,14 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo preventDefault: true, run: () => onStop?.(), }, + { + key: 'Alt-]', + run: (view) => jumpToCharacter(view, '$', 1), + }, + { + key: 'Alt-[', + run: (view) => jumpToCharacter(view, '$', -1), + }, /* { key: 'Ctrl-Shift-.', run: () => (onPanic ? onPanic() : onStop?.()), diff --git a/packages/codemirror/labelJump.mjs b/packages/codemirror/labelJump.mjs new file mode 100644 index 000000000..fcdb437f5 --- /dev/null +++ b/packages/codemirror/labelJump.mjs @@ -0,0 +1,31 @@ +import { EditorSelection } from '@codemirror/state'; +import { SearchCursor } from '@codemirror/search'; + +export function jumpToCharacter(view, character, direction = 1) { + const { state, dispatch } = view; + const pos = state.selection.main.head; + const cursor = new SearchCursor(state.doc, character); + + let characterPositions = []; + let jumpPos; + while (!cursor.next().done) { + characterPositions.push(cursor.value.to); + } + if (!characterPositions.length) { + return false; + } + if (direction > 0) { + jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience + } else { + jumpPos = characterPositions.reverse().find((x) => x < pos + 1) ?? characterPositions.at(0); + } + + if (!jumpPos) { + return false; + } + dispatch({ + selection: EditorSelection.cursor(jumpPos - 1), + scrollIntoView: true, + }); + return true; +} From c1a185e852433d4469e25e6b07a2eec64b36d08d Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 3 Dec 2025 15:25:05 -0500 Subject: [PATCH 2/9] fix null case --- packages/codemirror/labelJump.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemirror/labelJump.mjs b/packages/codemirror/labelJump.mjs index fcdb437f5..0aced4eb5 100644 --- a/packages/codemirror/labelJump.mjs +++ b/packages/codemirror/labelJump.mjs @@ -20,7 +20,7 @@ export function jumpToCharacter(view, character, direction = 1) { jumpPos = characterPositions.reverse().find((x) => x < pos + 1) ?? characterPositions.at(0); } - if (!jumpPos) { + if (jumpPos == null) { return false; } dispatch({ From f0aac2098afba2ba10d1f8d88db70c988c6b3496 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Wed, 3 Dec 2025 22:59:20 -0500 Subject: [PATCH 3/9] update_shortcut --- packages/codemirror/codemirror.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 6643e400d..ba481d9ac 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -121,11 +121,11 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo run: () => onStop?.(), }, { - key: 'Alt-]', + key: 'Alt-w', run: (view) => jumpToCharacter(view, '$', 1), }, { - key: 'Alt-[', + key: 'Alt-q', run: (view) => jumpToCharacter(view, '$', -1), }, /* { From 6908ea38e43f4efeb8537c18cdb534e144f0ed48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Bj=C3=B6rk?= Date: Sat, 20 Dec 2025 17:26:23 +0100 Subject: [PATCH 4/9] Added search/filter in patterns tab Added pattern as dep to filter memo Format --- website/src/metadata_parser.js | 2 +- .../src/repl/components/panel/PatternsTab.jsx | 190 ++++++++++-------- 2 files changed, 106 insertions(+), 86 deletions(-) diff --git a/website/src/metadata_parser.js b/website/src/metadata_parser.js index fab943d66..f0a70babb 100644 --- a/website/src/metadata_parser.js +++ b/website/src/metadata_parser.js @@ -1,4 +1,4 @@ -const ALLOW_MANY = ['by', 'url', 'genre', 'license']; +const ALLOW_MANY = ['by', 'url', 'genre', 'license', 'tag']; export function getMetadata(raw_code) { if (raw_code == null) { diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 5e0d50e72..588f8f39e 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -18,6 +18,7 @@ import { Pagination } from '../pagination/Pagination.jsx'; import { useState } from 'react'; import { useDebounce } from '../usedebounce.jsx'; import cx from '@src/cx.mjs'; +import { Textbox } from '../textbox/Textbox.jsx'; export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) { const meta = useMemo(() => getMetadata(pattern.code), [pattern]); @@ -33,7 +34,7 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) { } const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'; - return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}; + return <>{`${title} by ${author.slice(0, 100)}`.slice(0, 60)}; } function PatternButton({ showOutline, onClick, pattern, showHiglight }) { @@ -79,73 +80,117 @@ const updateCodeWindow = (context, patternData, reset = false) => { context.handleUpdate(patternData, reset); }; -function UserPatterns({ context }) { +export function PatternsTab({ context }) { + const [search, setSearch] = useState(''); const activePattern = useActivePattern(); const viewingPatternStore = useViewingPatternData(); const viewingPatternData = parseJSON(viewingPatternStore); - const { userPatterns, patternFilter, patternAutoStart } = useSettings(); + const { userPatterns, patternAutoStart } = useSettings(); const viewingPatternID = viewingPatternData?.id; - return ( -
-
- { - const { data } = userPattern.createAndAddToDB(); - updateCodeWindow(context, data); - }} - /> - { - const { data } = userPattern.duplicate(viewingPatternData); - updateCodeWindow(context, data); - }} - /> - { - const { data } = userPattern.delete(viewingPatternID); - updateCodeWindow(context, { ...data, collection: userPattern.collection }); - }} - /> - - - { - const { data } = userPattern.clearAll(); - updateCodeWindow(context, data); - }} - /> -
+ const visiblePatterns = useMemo(() => { + return Object.fromEntries( + Object.entries(userPatterns).filter(([_key, pattern]) => { + if (!search) { + return true; + } -
- {/* {patternFilter === patternFilterName.user && ( */} - { - updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart); + const meta = getMetadata(pattern.code); - if (context.started && activePattern === id) { - context.handleEvaluate(); + // Search for specific meta keys + const searchTrimmed = search.trim(); + if (searchTrimmed.includes(':')) { + const [metaKey, metaSearch] = searchTrimmed.split(':'); + if (metaKey !== undefined && metaSearch !== undefined && metaKey in meta) { + const metaValues = meta[metaKey]; + if (Array.isArray(metaValues)) { + return metaValues.some((metaValue) => metaValue.toLowerCase().includes(metaSearch.trim().toLowerCase())); + } else if (typeof metaValues === 'string') { + return metaValues.toLowerCase().includes(metaSearch.trim().toLowerCase()); + } else { + return false; } - }} - patterns={userPatterns} - started={context.started} - activePattern={activePattern} - viewingPatternID={viewingPatternID} - /> - {/* )} */} + } + } + const title = meta.title ? meta.title : 'unnamed'; + const authors = meta.by ? meta.by : ['anonymous']; + const tags = meta.tag ? meta.tag : []; + const lowerCaseSearch = search.toLowerCase(); + return ( + title.toLowerCase().includes(lowerCaseSearch) || + authors.some((author) => author.toLowerCase().includes(lowerCaseSearch)) || + tags.some((tag) => tag.toLowerCase().includes(lowerCaseSearch)) + ); + }), + ); + }, [search, viewingPatternStore]); + + return ( +
+
+ +
+
+
+ { + const { data } = userPattern.createAndAddToDB(); + updateCodeWindow(context, data); + }} + /> + { + const { data } = userPattern.duplicate(viewingPatternData); + updateCodeWindow(context, data); + }} + /> + { + const { data } = userPattern.delete(viewingPatternID); + updateCodeWindow(context, { ...data, collection: userPattern.collection }); + }} + /> + + + + { + const { data } = userPattern.clearAll(); + updateCodeWindow(context, data); + }} + /> +
+ +
+ {/* {patternFilter === patternFilterName.user && ( */} + { + updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart); + + if (context.started && activePattern === id) { + context.handleEvaluate(); + } + }} + patterns={visiblePatterns} + started={context.started} + activePattern={activePattern} + viewingPatternID={viewingPatternID} + /> + {/* )} */} +
); @@ -232,28 +277,3 @@ function PublicPatterns({ context }) { } return ; } - -export function PatternsTab({ context }) { - const { patternFilter } = useSettings(); - - return ( -
- -
- ); - /* return ( -
- settingsMap.setKey('patternFilter', value)} - items={patternFilterName} - > - - {patternFilter === patternFilterName.user ? ( - - ) : ( - - )} -
- ); */ -} From aa8209ad4c44e0d17a5185d3d3213269ef509229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Bj=C3=B6rk?= Date: Thu, 1 Jan 2026 12:22:52 +0100 Subject: [PATCH 5/9] Refactored handling of trim/lowercase based on PR comments --- .../src/repl/components/panel/PatternsTab.jsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 588f8f39e..56f0b4cb8 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -98,15 +98,15 @@ export function PatternsTab({ context }) { const meta = getMetadata(pattern.code); // Search for specific meta keys - const searchTrimmed = search.trim(); - if (searchTrimmed.includes(':')) { - const [metaKey, metaSearch] = searchTrimmed.split(':'); + const searchLowercaseTrimmed = search.trim().toLowerCase(); + if (searchLowercaseTrimmed.includes(':')) { + const [metaKey, metaSearch] = searchLowercaseTrimmed.split(/:\s*/); if (metaKey !== undefined && metaSearch !== undefined && metaKey in meta) { const metaValues = meta[metaKey]; if (Array.isArray(metaValues)) { - return metaValues.some((metaValue) => metaValue.toLowerCase().includes(metaSearch.trim().toLowerCase())); + return metaValues.some((metaValue) => metaValue.toLowerCase().includes(metaSearch)); } else if (typeof metaValues === 'string') { - return metaValues.toLowerCase().includes(metaSearch.trim().toLowerCase()); + return metaValues.toLowerCase().includes(metaSearch); } else { return false; } @@ -115,11 +115,10 @@ export function PatternsTab({ context }) { const title = meta.title ? meta.title : 'unnamed'; const authors = meta.by ? meta.by : ['anonymous']; const tags = meta.tag ? meta.tag : []; - const lowerCaseSearch = search.toLowerCase(); return ( - title.toLowerCase().includes(lowerCaseSearch) || - authors.some((author) => author.toLowerCase().includes(lowerCaseSearch)) || - tags.some((tag) => tag.toLowerCase().includes(lowerCaseSearch)) + title.toLowerCase().includes(searchLowercaseTrimmed) || + authors.some((author) => author.toLowerCase().includes(searchLowercaseTrimmed)) || + tags.some((tag) => tag.toLowerCase().includes(searchLowercaseTrimmed)) ); }), ); From 31cacc3c29a18e59761cda66e912c8ee8e00d8a3 Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 11 Jan 2026 16:23:17 +0000 Subject: [PATCH 6/9] add warm to faq --- website/src/pages/learn/faq.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/website/src/pages/learn/faq.mdx b/website/src/pages/learn/faq.mdx index e43839dc3..fc3bcdb2d 100644 --- a/website/src/pages/learn/faq.mdx +++ b/website/src/pages/learn/faq.mdx @@ -19,6 +19,14 @@ While there is no charge there are some caveats, e.g.: - the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. - the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission. +## How do I try out the latest features? + +The main, stable strudel website is [strudel.cc](https://strudel.cc/). There is also [warm.strudel.cc](https://warm.strudel.cc), known as "warm strudel", which has the latest development features. You might find warm strudel has bug fixes and features that the main website doesn't, but it will often be less stable and probably not suitable for important performances. + +Alternatively, you can run strudel locally to try out the latest features. You can find development-oriented [instructions for that here](https://codeberg.org/uzu/strudel/src/branch/main/CONTRIBUTING.md#project-setup). + +You can see the [latest changes here](https://codeberg.org/uzu/strudel/pulls?q=&type=all&sort=recentupdate&state=closed&labels=&milestone=0&project=0&assignee=0&poster=0), as 'pull requests'. + ## How to record or export audio? Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. However, there are multiple ways to record the audio -- and video -- output of Strudel: From 8b1fb12388aa523224f8e7a93e5bca0e3368fbd0 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 13 Jan 2026 03:32:19 +0100 Subject: [PATCH 7/9] fix: class -> className --- website/src/repl/components/panel/PatternsTab.jsx | 2 +- website/src/repl/components/panel/Reference.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 56f0b4cb8..04f6452e5 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -126,7 +126,7 @@ export function PatternsTab({ context }) { return (
-
+
diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 505cf50d2..c03ac0cf9 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -55,7 +55,7 @@ export function Reference() { return (
-
+
From e82bb6c4100e58b4a9f99e617745a74d1fb74b1c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 13 Jan 2026 03:38:09 +0100 Subject: [PATCH 8/9] fix: use pattern.id as title fallback --- website/src/repl/components/panel/PatternsTab.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 04f6452e5..75c019438 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -29,7 +29,7 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) { if (!isNaN(date)) { title = date.toLocaleDateString(); } else { - title = 'unnamed'; + title = pattern.id || 'unnamed'; } } From e5ab6b3c93b35973e46607c45bb071b323177bbf Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 13 Jan 2026 03:47:33 +0100 Subject: [PATCH 9/9] fix: short circuit --- website/src/repl/components/panel/PatternsTab.jsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 75c019438..c72ebaa30 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -89,12 +89,11 @@ export function PatternsTab({ context }) { const viewingPatternID = viewingPatternData?.id; const visiblePatterns = useMemo(() => { + if (!search) { + return userPatterns; + } return Object.fromEntries( Object.entries(userPatterns).filter(([_key, pattern]) => { - if (!search) { - return true; - } - const meta = getMetadata(pattern.code); // Search for specific meta keys