Compare commits

..

24 Commits

Author SHA1 Message Date
Jade (Rose) Rowland ad7e301394 repeat charactar 2026-07-30 12:13:47 +01:00
Jade (Rose) Rowland 7acb9a3ac7 patterntaborder 2026-04-09 17:26:37 -07:00
Jade (Rose) Rowland 9665b233a1 sort by pattern string 2026-04-03 23:41:46 -04:00
Jade (Rose) Rowland 6c67851909 fix merge conflict 2026-02-16 01:42:51 -05:00
Jade (Rose) Rowland 109ea3093b Merge branch 'soloshortcut' into show5 2026-02-11 19:56:37 -05:00
Jade (Rose) Rowland 8d991ad8ae Merge branch 'sample_note' into show5 2026-02-11 19:53:20 -05:00
froos 3f72b9b97a Merge pull request 'fix: delaytime jsdoc' (#1973) from delaytime-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1973
2026-02-12 00:35:15 +01:00
Felix Roos a347bd607f fix: delaytime jsdoc 2026-02-12 00:12:04 +01:00
yaxu 3966659f18 Update .forgejo/workflows/deploy-hot.yml 2026-02-09 21:25:21 +01:00
yaxu 6d9b899ae4 Update .forgejo/workflows/deploy-hot.yml 2026-02-09 20:56:48 +01:00
yaxu d802907dac Update .forgejo/workflows/deploy-hot.yml 2026-02-09 20:49:38 +01:00
yaxu 2bea102dc1 Update .forgejo/workflows/deploy-hot.yml 2026-02-09 16:04:11 +01:00
yaxu d9e5c5b0c8 See if it runs without this test.. 2026-02-09 15:59:47 +01:00
yaxu 7c1304690f Update .forgejo/workflows/deploy-hot.yml 2026-02-09 15:58:14 +01:00
yaxu 3c366b37ae Update .forgejo/workflows/deploy-hot.yml 2026-02-09 15:57:36 +01:00
Alex McLean 52a206ceca hotfix: try to serve PRs hot 2026-02-09 14:44:50 +00:00
Jade (Rose) Rowland 90c05ec38a fix edge cases 2026-02-08 14:12:21 -05:00
Jade (Rose) Rowland 7e0eed87cb normalize shortcuts 2026-01-27 17:21:36 -05:00
Jade (Rose) Rowland 1a1197f67a Merge branch 'main' into soloshortcut 2026-01-22 21:11:31 -05:00
Jade (Rose) Rowland 3c5afb32f3 scroll to center 2026-01-22 02:04:27 -05:00
Jade (Rose) Rowland 76cbd23859 Merge branch 'soloshortcut' of ssh://codeberg.org/uzu/strudel into soloshortcut 2026-01-22 00:25:41 -05:00
Jade (Rose) Rowland b436ae789c rm dead code 2026-01-22 00:25:28 -05:00
Switch Angel AKA Jade Rose 275731afc7 Merge branch 'main' into soloshortcut 2026-01-22 06:22:17 +01:00
Jade (Rose) Rowland 5cb2214d88 working 2026-01-22 00:20:44 -05:00
8 changed files with 345 additions and 39 deletions
+44
View File
@@ -0,0 +1,44 @@
name: Build and Deploy hot PRs
on:
pull_request_target:
types: [labeled]
# Allow one concurrent deployment
concurrency:
group: "warm-pages"
cancel-in-progress: false
jobs:
build:
if: ${{ github.event.label.name == 'serve-hot' }}
runs-on: docker
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: pnpm/action-setup@v4
with:
version: 9.12.2
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Deploy
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
eval $(ssh-agent -s)
echo "$SSH_PRIVATE_KEY" | ssh-add -
apt update && apt install -y rsync
mkdir -p ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress \
./website/dist/ \
strudel@matrix.toplap.org:/home/strudel/deploy/pr-${{ github.event.pull_request.number }}.hot.strudel.cc
+76 -3
View File
@@ -11,6 +11,7 @@ import {
keymap, keymap,
lineNumbers, lineNumbers,
} from '@codemirror/view'; } from '@codemirror/view';
import {repeatCharKeymap} from './repeatcharacter.mjs';
import { persistentAtom } from '@nanostores/persistent'; import { persistentAtom } from '@nanostores/persistent';
import { logger, registerControl, repl } from '@strudel/core'; import { logger, registerControl, repl } from '@strudel/core';
import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw'; import { cleanupDraw, cleanupDrawContext, Drawer } from '@strudel/draw';
@@ -20,11 +21,16 @@ import { evalBlock } from './block_utilities.mjs';
import { flash, isFlashEnabled } from './flash.mjs'; import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs'; import { keybindings } from './keybindings.mjs';
import { jumpToCharacter } from './labelJump.mjs';
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { activateTheme, initTheme, theme } from './themes.mjs'; import { activateTheme, initTheme, theme } from './themes.mjs';
import { isTooltipEnabled } from './tooltip.mjs'; import { isTooltipEnabled } from './tooltip.mjs';
import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs'; import { getActiveWidgets, updateWidgets, widgetPlugin } from './widget.mjs';
import {
deleteAllInlineBeforeCharacter,
InsertCharBeforeChar,
jumpToCharacter,
jumpToNextCharacter,
} from './labelJump.mjs';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands'; export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
@@ -75,6 +81,10 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
decode: JSON.parse, decode: JSON.parse,
}); });
const ANON_LABEL = '$';
const SOLO_LABEL = 'S';
const MUTE_LABEL = '_';
// https://codemirror.net/docs/guide/ // https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) { export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo, strudelMirror }) {
const settings = codemirrorSettings.get(); const settings = codemirrorSettings.get();
@@ -102,6 +112,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
syntaxHighlighting(defaultHighlightStyle), syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of((v) => onChange(v)), EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }), drawSelection({ cursorBlinkRate: 0 }),
repeatCharKeymap,
Prec.highest( Prec.highest(
keymap.of([ keymap.of([
{ {
@@ -138,12 +149,74 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
}, },
{ {
key: 'Alt-w', key: 'Alt-w',
run: (view) => jumpToCharacter(view, '$', 1), run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
}, },
{ {
key: 'Alt-q', key: 'Alt-q',
run: (view) => jumpToCharacter(view, '$', -1), run: (view) => {
return jumpToNextCharacter(view, ANON_LABEL, -1);
}, },
},
// clear all muted
{
key: `Alt-Ctrl-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, MUTE_LABEL + ANON_LABEL);
},
},
// clear all solod
{
key: `Alt-Shift-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, SOLO_LABEL + ANON_LABEL);
},
},
// clear all solo and mute
{
key: `Ctrl-Shift-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, ANON_LABEL);
},
},
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-${num}`,
run: (view) => {
return jumpToCharacter(view, ANON_LABEL, i);
},
};
}),
// handle solo toggles 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-Shift-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, SOLO_LABEL, i);
},
};
}),
// handle mute toggles 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-Ctrl-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, MUTE_LABEL, i);
},
};
}),
// Handle clearing mutes and solos 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Ctrl-Shift-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, '', i);
},
};
}),
/* { /* {
key: 'Ctrl-Shift-.', key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()), run: () => (onPanic ? onPanic() : onStop?.()),
+131 -9
View File
@@ -1,18 +1,54 @@
import { EditorSelection } from '@codemirror/state'; import { EditorSelection } from '@codemirror/state';
import { SearchCursor } from '@codemirror/search'; import { SearchCursor } from '@codemirror/search';
import { EditorView } from '@codemirror/view';
import { syntaxTree } from '@codemirror/language';
export function jumpToCharacter(view, character, direction = 1) { /**
const { state, dispatch } = view; * gets all of the positions of a character in a document, excluding commented out lines
const pos = state.selection.main.head; * @param { EditorState} state
* @param {String} character
* @returns {number[]}
*/
function getCharacterPositions(state, character) {
const cursor = new SearchCursor(state.doc, character); const cursor = new SearchCursor(state.doc, character);
let characterPositions = []; const characterPositions = [];
let jumpPos;
while (!cursor.next().done) { while (!cursor.next().done) {
const linestartpos = state.doc.lineAt(cursor.value.to).from
if (!isLineCommentedOut(state, linestartpos)) {
characterPositions.push(cursor.value.to); characterPositions.push(cursor.value.to);
} }
}
return characterPositions;
}
function isLineCommentedOut(state, pos) {
const line = state.doc.lineAt(pos);
// remove white space
pos = line.from + line.text.search(/\S/)
const tree = syntaxTree(state);
const node = tree.resolveInner(pos, 1)
return node.name.includes("Comment")
}
/**
* jump to the next character in a document
* @param {EditorView} view
* @param {String} character
* @param {number} direction 0 or 1
* @returns {boolean}
*/
export function jumpToNextCharacter(view, character, direction = 1) {
const { state, dispatch } = view;
const pos = state.selection.main.head;
let jumpPos;
const characterPositions = getCharacterPositions(state, character);
if (!characterPositions.length) { if (!characterPositions.length) {
return false; return true;
} }
if (direction > 0) { if (direction > 0) {
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
@@ -21,11 +57,97 @@ export function jumpToCharacter(view, character, direction = 1) {
} }
if (jumpPos == null) { if (jumpPos == null) {
return false; return true;
} }
const selection = EditorSelection.cursor(jumpPos - 1);
dispatch({ dispatch({
selection: EditorSelection.cursor(jumpPos - 1), selection,
scrollIntoView: true, effects: EditorView.scrollIntoView(
selection.head,
{ y: "start" }
)
}); });
return true; return true;
} }
/**
*
* @param {EditorView} view
* @param {String} character
* @param {number} index the instance of the character
* @returns {true}
*/
export function jumpToCharacter(view, character, index) {
const { state, dispatch } = view;
const characterPositions = getCharacterPositions(state, character);
const pos = characterPositions.at(index) ?? characterPositions.at(-1);
if (pos == null) {
return true;
}
const selection = EditorSelection.cursor(pos - 1);
dispatch({
selection,
effects: EditorView.scrollIntoView(
selection.head,
{ y: "start" }
)
});
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @returns {true}
*/
export function deleteAllInlineBeforeCharacter(view, character) {
const { state, dispatch } = view;
const characterPositions = getCharacterPositions(state, character);
const changes = [];
characterPositions.forEach((pos) => {
const line = state.doc.lineAt(pos);
if (state.doc.sliceString(line.from, line.from + 2) === COMMENT_STRING) {
return;
}
changes.push({
from: line.from,
to: pos - 1,
insert: '',
});
});
dispatch({ changes });
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @param {String} character2
* @param {number} index
* @returns {true}
*/
export function InsertCharBeforeChar(view, character, character2, index) {
const { state, dispatch } = view;
const changes = [];
const characterPositions = getCharacterPositions(state, character);
const labelpos = characterPositions.at(index) ?? characterPositions.at(-1);
const line = state.doc.lineAt(labelpos);
//delete preceeding characters
changes.push({
from: line.from,
to: labelpos - 1,
insert: '',
});
changes.push({
insert: character2,
from: line.from,
});
dispatch({ changes });
return true;
}
+55
View File
@@ -0,0 +1,55 @@
import { Compartment } from "@codemirror/state";
import { keymap } from "@codemirror/view";
const repeatMode = new Compartment();
function repeatPreviousChar(times) {
return ({ state, dispatch }) => {
const { from, empty } = state.selection.main;
if (!empty || from === 0) return false;
const prevChar = state.doc.sliceString(from - 1, from);
const text = Array(times).fill(prevChar).join(" ");
dispatch(state.update({
changes: {
from,
insert: " " + text
}
}));
return true;
};
}
function exitRepeatMode(view) {
view.dispatch({
effects: repeatMode.reconfigure([])
});
}
const digitBindings = Array.from({ length: 9 }, (_, i) => ({
key: String(i + 1),
run(view) {
repeatPreviousChar(i + 1)(view);
exitRepeatMode(view);
return true;
}
}));
export const repeatCharKeymap = [
repeatMode.of([]),
keymap.of([
{
key: "Alt-r",
run(view) {
view.dispatch({
effects: repeatMode.reconfigure(keymap.of(digitBindings))
});
return true;
}
}
])
];
+10 -13
View File
@@ -1893,19 +1893,6 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
*/ */
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb'); export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
/**
* Sets the level of the signal that is fed back into the delay.
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
* @tags orbit, superdough, supradough
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
*
*/
export const { delayspeed } = registerControl('delayspeed');
/** /**
* Sets the time of the delay effect. * Sets the time of the delay effect.
* *
@@ -1917,6 +1904,16 @@ export const { delayspeed } = registerControl('delayspeed');
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>") * note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
* *
*/ */
export const { delayspeed } = registerControl('delayspeed');
/*
* @name delaytime
* @tags orbit, superdough, supradough
* @param {number | Pattern} delaytime sets the time of the delay effect.
* @synonyms delayt, dt
* @example
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
*/
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt'); export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
/** /**
+6 -5
View File
@@ -37,9 +37,10 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u]; return bytes.toFixed(1) + ' ' + units[u];
} }
export function getSampleInfo(hapValue, bank) { function getSampleInfo(hapValue, bank) {
const { speed = 1.0 } = hapValue; const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank); const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank);
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, url, index, midi, label, playbackRate }; return { transpose, url, index, midi, label, playbackRate };
} }
@@ -168,15 +169,15 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
* @param {string} v * @param {string} v
* @returns {SampleMetaData} * @returns {SampleMetaData}
*/ */
const getMetaData = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) }); const fullUrl = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
if (Array.isArray(value)) { if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)]; //return [key, value.map(replaceUrl)];
value = value.map(getMetaData); value = value.map(fullUrl);
} else { } else {
// must be object // must be object
value = Object.fromEntries( value = Object.fromEntries(
Object.entries(value).map(([note, samples]) => { Object.entries(value).map(([note, samples]) => {
return [note, (typeof samples === 'string' ? [samples] : samples).map(getMetaData)]; return [note, (typeof samples === 'string' ? [samples] : samples).map(fullUrl)];
}), }),
); );
} }
@@ -9,9 +9,7 @@ import {
userPattern, userPattern,
} from '../../../user_pattern_utils.mjs'; } from '../../../user_pattern_utils.mjs';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { getMetadata } from '../../../metadata_parser.js';
import { useExamplePatterns } from '../../useExamplePatterns.jsx'; import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs';
import { useSettings } from '../../../settings.mjs'; import { useSettings } from '../../../settings.mjs';
import { ActionButton } from '../button/action-button.jsx'; import { ActionButton } from '../button/action-button.jsx';
import { Pagination } from '../pagination/Pagination.jsx'; import { Pagination } from '../pagination/Pagination.jsx';
@@ -20,8 +18,13 @@ import { useDebounce } from '../usedebounce.jsx';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx'; import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
const PATTERN_SORT = {
"NEWEST": "most recent",
"A-Z": "A-Z",
}
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) { export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]); const meta = pattern.meta
let title = meta.title; let title = meta.title;
if (title == null) { if (title == null) {
@@ -58,6 +61,10 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
return ( return (
<div className="p-2"> <div className="p-2">
{Object.values(patterns) {Object.values(patterns)
.sort((a, b) => {
return (b.meta.title ?? "").localeCompare(a.meta.title ?? "")
})
.reverse() .reverse()
.map((pattern) => { .map((pattern) => {
const id = pattern.id; const id = pattern.id;
@@ -93,7 +100,7 @@ export function PatternsTab({ context }) {
} }
return Object.fromEntries( return Object.fromEntries(
Object.entries(userPatterns).filter(([_key, pattern]) => { Object.entries(userPatterns).filter(([_key, pattern]) => {
const meta = getMetadata(pattern.code); const meta = pattern.meta;
// Search for specific meta keys // Search for specific meta keys
const searchLowercaseTrimmed = search.trim().toLowerCase(); const searchLowercaseTrimmed = search.trim().toLowerCase();
+7
View File
@@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react';
import { register } from '@strudel/core'; import { register } from '@strudel/core';
import { isUdels } from './repl/util.mjs'; import { isUdels } from './repl/util.mjs';
import { computed } from 'nanostores'; import { computed } from 'nanostores';
import { getMetadata } from './metadata_parser';
export const audioEngineTargets = { export const audioEngineTargets = {
webaudio: 'webaudio', webaudio: 'webaudio',
@@ -18,6 +19,11 @@ export const soundFilterType = {
ALL: 'all', ALL: 'all',
}; };
export const PATTERN_SORT = {
"NEWEST": "most recent",
"A-Z": "A-Z",
}
export const defaultSettings = { export const defaultSettings = {
activeFooter: 'intro', activeFooter: 'intro',
keybindings: 'codemirror', keybindings: 'codemirror',
@@ -71,6 +77,7 @@ export const $settings = computed(settingsMap, (state) => {
Object.keys(userPatterns).forEach((key) => { Object.keys(userPatterns).forEach((key) => {
const data = userPatterns[key]; const data = userPatterns[key];
data.id = data.id ?? key; data.id = data.id ?? key;
data.meta = getMetadata(data.code)
userPatterns[key] = data; userPatterns[key] = data;
}); });
return { return {