mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-15 04:29:55 -04:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56b279d723 | |||
| a3302cf167 | |||
| cd6d364067 | |||
| ff80bae275 | |||
| 8247179a4d | |||
| c398620439 | |||
| 2cc9bf2568 | |||
| 39b6466a91 | |||
| d5dc506dc4 | |||
| 1f2202e6a1 | |||
| 63e2810375 | |||
| 262a6d85db | |||
| e66723230b | |||
| cb06042d3b | |||
| 88fdb424c4 | |||
| 0d4816804e | |||
| ce4ef629bf | |||
| c5fd80902b | |||
| 58dfe9a55c | |||
| 4671d343c4 |
@@ -19,11 +19,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -19,12 +19,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
# cache: "pnpm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
|
||||
@@ -16,12 +16,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
# cache: "pnpm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
|
||||
@@ -6,21 +6,10 @@ jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
container: node:22-bookworm
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: apt install ztd
|
||||
run: apt update && apt install -y zstd
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm run format-check
|
||||
- run: pnpm run lint
|
||||
|
||||
-14
@@ -45,20 +45,6 @@ tidal-drum-machines
|
||||
webaudiofontdata
|
||||
src-tauri/target
|
||||
|
||||
|
||||
|
||||
### BEGIN Visual Studio Code ###
|
||||
# Blanket, recursive exclude for .vscode directory and files
|
||||
.vscode/**/*
|
||||
# Unexclude specific files and directories within .vscode
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
### END Visual Studio Code ###
|
||||
|
||||
|
||||
|
||||
# BEGIN JetBrains -> END JetBrains
|
||||
|
||||
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"description": "Port of tidalcycles to javascript",
|
||||
"packageManager": "pnpm@10.15.0",
|
||||
"scripts": {
|
||||
"setup": "pnpm i",
|
||||
"pretest": "npm run jsdoc-json",
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import jsdoc from '../../doc.json';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { h } from './html';
|
||||
//TODO: fix tonal scale import
|
||||
// import { Scale } from '@tonaljs/tonal';
|
||||
// import { soundMap } from '@strudel/webaudio';
|
||||
let soundMap = undefined;
|
||||
import { complex } from '@strudel/tonal';
|
||||
|
||||
const escapeHtml = (str) => {
|
||||
const div = document.createElement('div');
|
||||
@@ -80,53 +75,6 @@ const isValidDoc = (doc) => {
|
||||
const hasExcludedTags = (doc) =>
|
||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||
|
||||
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('_');
|
||||
if (suffix && bank) banks.add(bank);
|
||||
}
|
||||
return Array.from(banks)
|
||||
.sort()
|
||||
.map((name) => ({ label: name, type: 'bank' }));
|
||||
}
|
||||
|
||||
// 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 = [
|
||||
{ label: 'below', type: 'mode' },
|
||||
{ label: 'above', type: 'mode' },
|
||||
{ label: 'duck', type: 'mode' },
|
||||
{ label: 'root', type: 'mode' },
|
||||
];
|
||||
|
||||
// Valid chord symbols from ireal dictionary plus empty string for major triads
|
||||
const chordSymbols = ['', ...Object.keys(complex)].sort();
|
||||
const chordSymbolCompletions = chordSymbols.map((symbol) => {
|
||||
if (symbol === '') {
|
||||
return {
|
||||
label: 'major',
|
||||
apply: '',
|
||||
type: 'chord-symbol',
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: symbol,
|
||||
apply: symbol,
|
||||
type: 'chord-symbol',
|
||||
};
|
||||
});
|
||||
|
||||
export const getSynonymDoc = (doc, synonym) => {
|
||||
const synonyms = doc.synonyms || [];
|
||||
const docLabel = getDocLabel(doc);
|
||||
@@ -165,299 +113,19 @@ const jsdocCompletions = (() => {
|
||||
return completions;
|
||||
})();
|
||||
|
||||
// --- Handler functions for each context ---
|
||||
const pitchNames = [
|
||||
'C',
|
||||
'C#',
|
||||
'Db',
|
||||
'D',
|
||||
'D#',
|
||||
'Eb',
|
||||
'E',
|
||||
'E#',
|
||||
'Fb',
|
||||
'F',
|
||||
'F#',
|
||||
'Gb',
|
||||
'G',
|
||||
'G#',
|
||||
'Ab',
|
||||
'A',
|
||||
'A#',
|
||||
'Bb',
|
||||
'B',
|
||||
'B#',
|
||||
'Cb',
|
||||
];
|
||||
|
||||
// Cached regex patterns for scaleHandler
|
||||
const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
|
||||
const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
|
||||
const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
|
||||
const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
|
||||
const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
|
||||
|
||||
function scaleHandler(context) {
|
||||
// First check for scale context without quotes - block with empty completions
|
||||
let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
|
||||
if (scaleNoQuotesContext) {
|
||||
return {
|
||||
from: scaleNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Check for after-colon context first (more specific)
|
||||
let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
|
||||
if (scaleAfterColonContext) {
|
||||
const text = scaleAfterColonContext.text;
|
||||
const colonIdx = text.lastIndexOf(':');
|
||||
if (colonIdx !== -1) {
|
||||
const fragment = text.slice(colonIdx + 1);
|
||||
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
|
||||
const options = filteredScales.map((s) => ({
|
||||
...s,
|
||||
apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
|
||||
}));
|
||||
const from = scaleAfterColonContext.from + colonIdx + 1;
|
||||
return {
|
||||
from,
|
||||
options,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Then check for pre-colon context
|
||||
let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
|
||||
if (scalePreColonContext) {
|
||||
if (!scalePreColonContext.text.includes(':')) {
|
||||
if (context.explicit) {
|
||||
const text = scalePreColonContext.text;
|
||||
const match = text.match(SCALE_PITCH_MATCH_REGEX);
|
||||
const fragment = match ? match[0] : '';
|
||||
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
||||
const from = scalePreColonContext.to - fragment.length;
|
||||
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
|
||||
return { from, options };
|
||||
} else {
|
||||
return { from: scalePreColonContext.to, options: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cached regex patterns for soundHandler
|
||||
const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
|
||||
const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
|
||||
const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
|
||||
|
||||
function soundHandler(context) {
|
||||
// First check for sound context without quotes - block with empty completions
|
||||
let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
|
||||
if (soundNoQuotesContext) {
|
||||
return {
|
||||
from: soundNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Then check for sound context with quotes - provide completions
|
||||
let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
|
||||
if (!soundContext) return null;
|
||||
|
||||
const text = soundContext.text;
|
||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||
if (quoteIdx === -1) return null;
|
||||
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 filteredSounds = soundNames.filter((name) => name.includes(fragment));
|
||||
let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
|
||||
const from = soundContext.to - fragment.length;
|
||||
return {
|
||||
from,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
// Cached regex patterns for bankHandler
|
||||
const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
|
||||
const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
|
||||
|
||||
function bankHandler(context) {
|
||||
// First check for bank context without quotes - block with empty completions
|
||||
let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
|
||||
if (bankNoQuotesContext) {
|
||||
return {
|
||||
from: bankNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Then check for bank context with quotes - provide completions
|
||||
let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
|
||||
if (!bankMatch) return null;
|
||||
|
||||
const text = bankMatch.text;
|
||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||
if (quoteIdx === -1) return null;
|
||||
const inside = text.slice(quoteIdx + 1);
|
||||
const fragment = inside;
|
||||
let banks = bankCompletions();
|
||||
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
|
||||
const from = bankMatch.to - fragment.length;
|
||||
return {
|
||||
from,
|
||||
options: filteredBanks,
|
||||
};
|
||||
}
|
||||
|
||||
// Cached regex patterns for modeHandler
|
||||
const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
|
||||
const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
|
||||
const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
|
||||
const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
|
||||
|
||||
function modeHandler(context) {
|
||||
// First check for mode context without quotes - block with empty completions
|
||||
let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
|
||||
if (modeNoQuotesContext) {
|
||||
return {
|
||||
from: modeNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Check for after-colon context first (more specific)
|
||||
let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
|
||||
if (modeAfterColonContext) {
|
||||
const text = modeAfterColonContext.text;
|
||||
const colonIdx = text.lastIndexOf(':');
|
||||
if (colonIdx !== -1) {
|
||||
const fragment = text.slice(colonIdx + 1);
|
||||
// For anchor after colon, we can suggest pitch names
|
||||
const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
||||
const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
|
||||
const from = modeAfterColonContext.from + colonIdx + 1;
|
||||
return {
|
||||
from,
|
||||
options,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Then check for pre-colon context
|
||||
let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
|
||||
if (!modeContext) return null;
|
||||
|
||||
const text = modeContext.text;
|
||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||
if (quoteIdx === -1) return null;
|
||||
const inside = text.slice(quoteIdx + 1);
|
||||
const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
|
||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||
const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
|
||||
const from = modeContext.to - fragment.length;
|
||||
return {
|
||||
from,
|
||||
options: filteredModes,
|
||||
};
|
||||
}
|
||||
|
||||
// Cached regex patterns for chordHandler
|
||||
const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
|
||||
const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
|
||||
const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
|
||||
|
||||
function chordHandler(context) {
|
||||
// First check for chord context without quotes - block with empty completions
|
||||
let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
|
||||
if (chordNoQuotesContext) {
|
||||
return {
|
||||
from: chordNoQuotesContext.to,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Then check for chord context with quotes - provide completions
|
||||
let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
|
||||
if (!chordContext) return null;
|
||||
|
||||
const text = chordContext.text;
|
||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
||||
if (quoteIdx === -1) return null;
|
||||
const inside = text.slice(quoteIdx + 1);
|
||||
|
||||
// Use same fragment matching as sound/mode for expressions like "<G Am>"
|
||||
const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
|
||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
||||
|
||||
// Check if fragment contains any pitch name at start (for root + symbol)
|
||||
let rootMatch = null;
|
||||
let symbolFragment = fragment;
|
||||
for (const pitch of pitchNames) {
|
||||
if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
|
||||
rootMatch = pitch;
|
||||
symbolFragment = fragment.slice(pitch.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (rootMatch) {
|
||||
// We have a root, now complete chord symbols
|
||||
const filteredSymbols = chordSymbolCompletions.filter((s) =>
|
||||
s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
|
||||
);
|
||||
|
||||
// Create completions that replace the entire chord, not just the symbol part
|
||||
const options = filteredSymbols;
|
||||
|
||||
const from = chordContext.to - symbolFragment.length;
|
||||
return { from, options };
|
||||
} else {
|
||||
// No root yet, complete with pitch names
|
||||
const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
|
||||
const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
|
||||
const from = chordContext.to - fragment.length;
|
||||
return { from, options };
|
||||
}
|
||||
}
|
||||
|
||||
// Cached regex patterns for fallbackHandler
|
||||
const FALLBACK_WORD_REGEX = /\w*/;
|
||||
|
||||
function fallbackHandler(context) {
|
||||
const word = context.matchBefore(FALLBACK_WORD_REGEX);
|
||||
if (word && word.from === word.to && !context.explicit) return null;
|
||||
if (word) {
|
||||
return {
|
||||
from: word.from,
|
||||
options: jsdocCompletions,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlers = [
|
||||
soundHandler,
|
||||
bankHandler,
|
||||
chordHandler,
|
||||
scaleHandler,
|
||||
modeHandler,
|
||||
// this handler *must* be last
|
||||
fallbackHandler,
|
||||
];
|
||||
|
||||
export const strudelAutocomplete = (context) => {
|
||||
for (const handler of handlers) {
|
||||
const result = handler(context);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
const word = context.matchBefore(/\w*/);
|
||||
if (word.from === word.to && !context.explicit) return null;
|
||||
|
||||
return {
|
||||
from: word.from,
|
||||
options: jsdocCompletions,
|
||||
/* options: [
|
||||
{ label: 'match', type: 'keyword' },
|
||||
{ label: 'hello', type: 'variable', info: '(World)' },
|
||||
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
|
||||
], */
|
||||
};
|
||||
};
|
||||
|
||||
export const isAutoCompletionEnabled = (enabled) =>
|
||||
|
||||
@@ -2,23 +2,13 @@ import {
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
} from '@codemirror/view';
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
bracketMatching,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
} from '@codemirror/language';
|
||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||
import { closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||
|
||||
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim, Vim } from '@replit/codemirror-vim';
|
||||
// import { vim } from './vim_test.mjs';
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import { helix, commands } from 'codemirror-helix';
|
||||
import { logger } from '@strudel/core';
|
||||
|
||||
const vscodePlugin = ViewPlugin.fromClass(
|
||||
@@ -20,6 +21,70 @@ const vscodePlugin = ViewPlugin.fromClass(
|
||||
);
|
||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||
|
||||
function replEval(view) {
|
||||
try {
|
||||
// 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 (e) {
|
||||
console.error('Error dispatching repl-evaluate event', e);
|
||||
}
|
||||
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 repl evaluation event', e);
|
||||
}
|
||||
}
|
||||
|
||||
function replStop(view) {
|
||||
try {
|
||||
// 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 (e) {
|
||||
console.error('Error dispatching repl-stop event', e);
|
||||
}
|
||||
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 repl stop event', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -47,29 +112,9 @@ try {
|
||||
|
||||
// :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 (e) {
|
||||
console.error('Error dispatching repl-stop event', e);
|
||||
}
|
||||
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);
|
||||
}
|
||||
const view = cm.cm6;
|
||||
logger('[vim] :q — stopping repl');
|
||||
replStop(view);
|
||||
});
|
||||
|
||||
// :w to evaluate
|
||||
@@ -83,38 +128,7 @@ try {
|
||||
} 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 (e) {
|
||||
console.error('Error dispatching repl-evaluate event', e);
|
||||
}
|
||||
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);
|
||||
}
|
||||
replEval(view);
|
||||
} catch (e) {
|
||||
console.error('Error dispatching :w evaluation event', e);
|
||||
}
|
||||
@@ -124,11 +138,49 @@ try {
|
||||
console.error('Vim ex command setup failed (defineEx missing or Vim unavailable)', e);
|
||||
}
|
||||
|
||||
// Map Helix :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.
|
||||
const helixCommands = commands.of([
|
||||
{
|
||||
// :w to evaluate
|
||||
name: 'write',
|
||||
aliases: ['w'],
|
||||
help: 'Repl-eval',
|
||||
handler(view, args) {
|
||||
try {
|
||||
view?.focus?.(); // Let the app know this came from Helix :w
|
||||
logger('[helix] :w — evaluating code');
|
||||
replEval(view);
|
||||
} catch (e) {
|
||||
console.error('Error dispatching helix :w evaluation event', e);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
// :q to pause/stop
|
||||
name: 'quit',
|
||||
aliases: ['q'],
|
||||
help: 'Repl-stop',
|
||||
handler(view, args) {
|
||||
try {
|
||||
view?.focus?.(); // Let the app know this came from Helix :q
|
||||
logger('[helix] :q — stopping repl');
|
||||
replStop(view);
|
||||
} catch (e) {
|
||||
console.error('Error dispatching helix :q stop event', e);
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const keymaps = {
|
||||
vim,
|
||||
emacs,
|
||||
codemirror: () => keymap.of(defaultKeymap),
|
||||
vscode: vscodeExtension,
|
||||
helix: () => [helix(), helixCommands],
|
||||
};
|
||||
|
||||
export { Vim } from '@replit/codemirror-vim';
|
||||
|
||||
@@ -44,13 +44,11 @@
|
||||
"@replit/codemirror-emacs": "^6.1.0",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"codemirror-helix": "^0.5.0",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"@tonaljs/tonal": "^4.10.0",
|
||||
"nanostores": "^0.11.3",
|
||||
"superdough": "workspace:*"
|
||||
"nanostores": "^0.11.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
|
||||
@@ -519,10 +519,10 @@ export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerM
|
||||
* each other with matrix commands like `fm13`, which would send `fm1` back into
|
||||
* `fm3`
|
||||
*
|
||||
* @name fmi
|
||||
* @name fm
|
||||
* @tags fm, superdough, supradough
|
||||
* @param {number | Pattern} brightness modulation index
|
||||
* @synonyms fm
|
||||
* @synonyms fmi
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
* .fm("<0 1 2 8 32>")
|
||||
|
||||
@@ -2354,8 +2354,8 @@ export const ply = register('ply', function (factor, pat) {
|
||||
* @example
|
||||
* s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
|
||||
*/
|
||||
export const { fast, density } = register(
|
||||
['fast', 'density'],
|
||||
export const fast = register(
|
||||
'fast',
|
||||
function (factor, pat) {
|
||||
if (factor === 0) {
|
||||
return silence;
|
||||
|
||||
@@ -4,11 +4,13 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { register, noteToMidi } from '@strudel/core';
|
||||
import { register } from '@strudel/core';
|
||||
import { Dough, doughsamples } from 'dough-synth';
|
||||
import { getAudioContext, ensureMinimalOutput } from '@strudel/webaudio';
|
||||
import wasm from 'dough-synth/dough.wasm?url';
|
||||
import workletCode from 'dough-synth/dough.js?raw';
|
||||
import { hap2dough } from './lib.mjs';
|
||||
export * from './lib.mjs';
|
||||
|
||||
Object.assign(globalThis, { doughsamples });
|
||||
|
||||
@@ -42,16 +44,8 @@ export async function doughOutput(hap, targetTime, cps = 1) {
|
||||
if (!offset) {
|
||||
return; // not ready
|
||||
}
|
||||
hap.ensureObjectValue();
|
||||
const event = {
|
||||
dough: 'play',
|
||||
...hap.value,
|
||||
time: targetTime - offset,
|
||||
duration: hap.duration / cps,
|
||||
};
|
||||
if (typeof event.note === 'string') {
|
||||
event.note = noteToMidi(event.note);
|
||||
}
|
||||
const t = targetTime - offset;
|
||||
const event = hap2dough(hap, t, cps);
|
||||
D.evaluate(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { register, registerControl, stack, Pattern, noteToMidi } from '@strudel/core';
|
||||
|
||||
export const { group } = registerControl('group');
|
||||
export const { voices } = registerControl('voices');
|
||||
export const { glide } = registerControl('glide');
|
||||
|
||||
export const groupmod = register('groupmod', function (modulator, pat) {
|
||||
const modpat = modulator
|
||||
.withValue((vm) => pat.withValue(({ group }) => ({ ...vm, group })))
|
||||
.outerJoin()
|
||||
.set({ dough: 'update_group' });
|
||||
return stack(pat.set(modulator), modpat);
|
||||
});
|
||||
Pattern.prototype.gmod = Pattern.prototype.groupmod;
|
||||
|
||||
export function hap2dough(hap, time, cps) {
|
||||
hap.ensureObjectValue();
|
||||
const event = {
|
||||
dough: 'play',
|
||||
...hap.value,
|
||||
time,
|
||||
duration: hap.duration / cps,
|
||||
};
|
||||
if (typeof event.note === 'string') {
|
||||
event.note = noteToMidi(event.note);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*",
|
||||
"dough-synth": "0.2.4"
|
||||
"dough-synth": "0.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
|
||||
+10
-1
@@ -55,6 +55,7 @@ export function parseControlsFromHap(hap, cps) {
|
||||
return controls;
|
||||
}
|
||||
|
||||
let timeOrigin = performance.timeOrigin;
|
||||
export async function oscOutput(hap, targetTime, cps = 1) {
|
||||
const ws = await connect();
|
||||
|
||||
@@ -63,7 +64,15 @@ export async function oscOutput(hap, targetTime, cps = 1) {
|
||||
logger('[osc] clockbridge not ready');
|
||||
return;
|
||||
}
|
||||
const ts = performance.timeOrigin + timeMs;
|
||||
// when the system sleeps while the browser is open, performance.now() doesnt tick
|
||||
// .. which means performance.timeOrigin + performance.now() doesnt land in the present
|
||||
const deprivation = timeOrigin + performance.now() - Date.now();
|
||||
if (Math.abs(deprivation) > 20) {
|
||||
console.log(`[osc] snap with ${(deprivation / 1000).toFixed(2)}s deprivation`);
|
||||
// shift time after microsleep
|
||||
timeOrigin -= deprivation;
|
||||
}
|
||||
const ts = timeOrigin + timeMs;
|
||||
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
|
||||
@@ -28,7 +28,7 @@ async function loadFont(name) {
|
||||
const url = `${soundfontUrl}/${name}.js`;
|
||||
const preset = await fetch(url).then((res) => res.text());
|
||||
let [_, data] = preset.split('={');
|
||||
return eval('{' + data);
|
||||
return new Function('return {' + data)().zones;
|
||||
};
|
||||
loadCache[name] = load();
|
||||
return loadCache[name];
|
||||
|
||||
@@ -25,8 +25,16 @@ export class ClockBridge {
|
||||
}
|
||||
const offset = performanceTime - contextTime * 1000; // clock offset in ms
|
||||
const dt = performanceTime - (this.lastTime ?? performanceTime); // delta time since last hap
|
||||
const decay = 1 / 10000; // how fast offset changes have an effect
|
||||
this.p = expDecay(this.p ?? offset, offset, decay, dt); // smooth clock offset
|
||||
const deprivation = offset - this.p;
|
||||
// when the system sleeps, the audio and system clock get jumbled
|
||||
// so we dont smooth if the jump is too high, to snap the offset
|
||||
if (Math.abs(deprivation) > 20) {
|
||||
console.log(`[ClockBridge] snap with ${(deprivation / 1000).toFixed(2)}s deprivation`);
|
||||
this.p = offset;
|
||||
} else {
|
||||
const decay = 1 / 10000; // how fast offset changes have an effect
|
||||
this.p = expDecay(this.p ?? offset, offset, decay, dt); // smooth clock offset
|
||||
}
|
||||
this.lastTime = performanceTime;
|
||||
return this.p;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import './tonal.mjs';
|
||||
import './voicings.mjs';
|
||||
import './ireal.mjs';
|
||||
|
||||
export * from './tonal.mjs';
|
||||
export * from './voicings.mjs';
|
||||
export * from './ireal.mjs';
|
||||
|
||||
import './ireal.mjs';
|
||||
|
||||
export const packageName = '@strudel/tonal';
|
||||
|
||||
@@ -61,7 +61,7 @@ function githubPath(base, subpath = '') {
|
||||
}
|
||||
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||
}
|
||||
export async function fetchSampleMap(url) {
|
||||
export async function _fetchSampleMap(url) {
|
||||
if (url.startsWith('github:')) {
|
||||
url = githubPath(url, 'strudel.json');
|
||||
}
|
||||
@@ -116,7 +116,7 @@ async function fetchSample(url) {
|
||||
|
||||
export async function supradoughsamples(sampleMap, baseUrl) {
|
||||
if (typeof sampleMap === 'string') {
|
||||
const [json, base] = await fetchSampleMap(sampleMap);
|
||||
const [json, base] = await _fetchSampleMap(sampleMap);
|
||||
// console.log('json', json, 'base', base);
|
||||
return supradoughsamples(json, base);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export const xen = register(
|
||||
let k = parray(input);
|
||||
|
||||
return new Pattern((state) => {
|
||||
let res = k.withValue((x) => x.map((y) => eval(y.join('/')))).query(state);
|
||||
let res = k.withValue((x) => x.map((y) => y.reduce((acc, v) => acc / v))).query(state);
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
Generated
+44
-14
@@ -242,21 +242,15 @@ importers:
|
||||
'@strudel/draw':
|
||||
specifier: workspace:*
|
||||
version: link:../draw
|
||||
'@strudel/tonal':
|
||||
specifier: workspace:*
|
||||
version: link:../tonal
|
||||
'@strudel/transpiler':
|
||||
specifier: workspace:*
|
||||
version: link:../transpiler
|
||||
'@tonaljs/tonal':
|
||||
specifier: ^4.10.0
|
||||
version: 4.10.0
|
||||
codemirror-helix:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
nanostores:
|
||||
specifier: ^0.11.3
|
||||
version: 0.11.3
|
||||
superdough:
|
||||
specifier: workspace:*
|
||||
version: link:../superdough
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
@@ -315,8 +309,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../webaudio
|
||||
dough-synth:
|
||||
specifier: 0.2.4
|
||||
version: 0.2.4
|
||||
specifier: 0.4.1
|
||||
version: 0.4.1
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
@@ -3657,6 +3651,15 @@ packages:
|
||||
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
|
||||
codemirror-helix@0.5.0:
|
||||
resolution: {integrity: sha512-hI56hf9VGz53H1YvL6H1GC7HtP6te8vX+MsIHaE9J7Q3PQ6KFapKtIRg6lqSH898ikHWpMCPu42r6HJN0IfVLA==}
|
||||
peerDependencies:
|
||||
'@codemirror/commands': ^6.0.0
|
||||
'@codemirror/language': ^6.0.0
|
||||
'@codemirror/search': ^6.0.0
|
||||
'@codemirror/state': ^6.0.0
|
||||
'@codemirror/view': ^6.0.0
|
||||
|
||||
collapse-white-space@2.1.0:
|
||||
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
||||
|
||||
@@ -4066,8 +4069,8 @@ packages:
|
||||
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dough-synth@0.2.4:
|
||||
resolution: {integrity: sha512-qMnMKVj1B2ivUgobNCwqJqNzZHOMN+5339OmCOk07zIDEK7iCYdufa+ZDE2BEgmKrWDyhGS7oTNFjHtTwzEv9w==}
|
||||
dough-synth@0.4.1:
|
||||
resolution: {integrity: sha512-hxBsfOBwTIlJMhSB/Pk2xNNc7YTAVTgexn5Y7GRv/Kn2qBkMxwLxHJNvhsUhklQhVvzny/aCihxb0EOq4tv5Bw==}
|
||||
hasBin: true
|
||||
|
||||
dset@3.1.4:
|
||||
@@ -8018,6 +8021,18 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
ws@8.22.0:
|
||||
resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
xmlcreate@2.0.4:
|
||||
resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==}
|
||||
|
||||
@@ -11399,6 +11414,14 @@ snapshots:
|
||||
|
||||
cmd-shim@6.0.3: {}
|
||||
|
||||
codemirror-helix@0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2):
|
||||
dependencies:
|
||||
'@codemirror/commands': 6.8.0
|
||||
'@codemirror/language': 6.10.8
|
||||
'@codemirror/search': 6.5.8
|
||||
'@codemirror/state': 6.5.1
|
||||
'@codemirror/view': 6.36.2
|
||||
|
||||
collapse-white-space@2.1.0: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
@@ -11780,7 +11803,12 @@ snapshots:
|
||||
|
||||
dotenv@16.4.7: {}
|
||||
|
||||
dough-synth@0.2.4: {}
|
||||
dough-synth@0.4.1:
|
||||
dependencies:
|
||||
ws: 8.22.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
dset@3.1.4: {}
|
||||
|
||||
@@ -16678,6 +16706,8 @@ snapshots:
|
||||
|
||||
ws@8.18.3: {}
|
||||
|
||||
ws@8.22.0: {}
|
||||
|
||||
xmlcreate@2.0.4: {}
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
+13
-12
@@ -4,18 +4,19 @@ packages:
|
||||
- tools/dbpatch
|
||||
- website/
|
||||
|
||||
onlyBuiltDependencies:
|
||||
catalog:
|
||||
'@codemirror/autocomplete': ^6.18.4
|
||||
'@codemirror/commands': ^6.8.0
|
||||
'@codemirror/lang-javascript': ^6.2.2
|
||||
'@codemirror/language': ^6.10.8
|
||||
'@codemirror/search': ^6.5.8
|
||||
'@codemirror/state': ^6.5.1
|
||||
'@codemirror/view': ^6.36.2
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@serialport/bindings-cpp'
|
||||
- tree-sitter-haskell
|
||||
- tree-sitter
|
||||
- esbuild
|
||||
- nx
|
||||
- sharp
|
||||
- tree-sitter
|
||||
- tree-sitter-haskell
|
||||
|
||||
catalog:
|
||||
"@codemirror/commands": "^6.8.0"
|
||||
"@codemirror/language": "^6.10.8"
|
||||
"@codemirror/lang-javascript": "^6.2.2"
|
||||
"@codemirror/state": "^6.5.1"
|
||||
"@codemirror/view": "^6.36.2"
|
||||
"@codemirror/autocomplete": "^6.18.4"
|
||||
"@codemirror/search": "^6.5.8"
|
||||
|
||||
@@ -4777,6 +4777,72 @@ exports[`runs examples > example "floor" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "fm" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c fmi:0 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:0 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:0 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:0 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:0 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:0 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:1 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:1 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:1 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:1 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:1 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:1 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:2 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:2 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:2 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:2 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:2 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:2 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:8 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:8 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:8 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:8 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:8 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:8 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "fm" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:sine note:F1 fmi:4 fmi2:0 fmi3:0 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 1/8 → 1/4 | s:sine note:F1 fmi:4 fmi2:2.7408622801303864 fmi3:0.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 1/4 → 3/8 | s:sine note:F1 fmi:4 fmi2:1.479038767516613 fmi3:0.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 3/8 → 1/2 | s:sine note:F1 fmi:4 fmi2:1.605570062994957 fmi3:0.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 1/2 → 5/8 | s:sine note:F1 fmi:4 fmi2:1.041922464966774 fmi3:0.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 5/8 → 3/4 | s:sine note:F1 fmi:4 fmi2:0.5425434708595276 fmi3:0.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 3/4 → 7/8 | s:sine note:F1 fmi:4 fmi2:0.7833059206604958 fmi3:0.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 7/8 → 1/1 | s:sine note:F1 fmi:4 fmi2:1.590524323284626 fmi3:0.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 1/1 → 9/8 | s:sine note:F1 fmi:4 fmi2:2.078168660402298 fmi3:1 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 9/8 → 5/4 | s:sine note:F1 fmi:4 fmi2:2.689958058297634 fmi3:1.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 5/4 → 11/8 | s:sine note:F1 fmi:4 fmi2:2.914912812411785 fmi3:1.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 11/8 → 3/2 | s:sine note:F1 fmi:4 fmi2:0.7312150076031685 fmi3:1.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 3/2 → 13/8 | s:sine note:F1 fmi:4 fmi2:2.4336322993040085 fmi3:1.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 13/8 → 7/4 | s:sine note:F1 fmi:4 fmi2:1.987018845975399 fmi3:1.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 7/4 → 15/8 | s:sine note:F1 fmi:4 fmi2:0.8189515843987465 fmi3:1.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 15/8 → 2/1 | s:sine note:F1 fmi:4 fmi2:2.3782287165522575 fmi3:1.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 2/1 → 17/8 | s:sine note:F1 fmi:4 fmi2:3.838108479976654 fmi3:2 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 17/8 → 9/4 | s:sine note:F1 fmi:4 fmi2:3.613561175763607 fmi3:2.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 9/4 → 19/8 | s:sine note:F1 fmi:4 fmi2:1.3819300457835197 fmi3:2.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 19/8 → 5/2 | s:sine note:F1 fmi:4 fmi2:3.346026562154293 fmi3:2.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 5/2 → 21/8 | s:sine note:F1 fmi:4 fmi2:1.8344642966985703 fmi3:2.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 21/8 → 11/4 | s:sine note:F1 fmi:4 fmi2:0.6407739371061325 fmi3:2.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 11/4 → 23/8 | s:sine note:F1 fmi:4 fmi2:2.5329310819506645 fmi3:2.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 23/8 → 3/1 | s:sine note:F1 fmi:4 fmi2:0.06501668691635132 fmi3:2.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 3/1 → 25/8 | s:sine note:F1 fmi:4 fmi2:0.8691564425826073 fmi3:3 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 25/8 → 13/4 | s:sine note:F1 fmi:4 fmi2:1.3729755133390427 fmi3:3.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 13/4 → 27/8 | s:sine note:F1 fmi:4 fmi2:3.9694602862000465 fmi3:3.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 27/8 → 7/2 | s:sine note:F1 fmi:4 fmi2:1.6347672045230865 fmi3:3.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 7/2 → 29/8 | s:sine note:F1 fmi:4 fmi2:1.639795258641243 fmi3:3.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 29/8 → 15/4 | s:sine note:F1 fmi:4 fmi2:3.7565943151712418 fmi3:3.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 15/4 → 31/8 | s:sine note:F1 fmi:4 fmi2:3.2486692890524864 fmi3:3.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 31/8 → 4/1 | s:sine note:F1 fmi:4 fmi2:2.9446661546826363 fmi3:3.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "fmattack" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]",
|
||||
@@ -4893,72 +4959,6 @@ exports[`runs examples > example "fmh" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "fmi" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c fmi:0 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:0 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:0 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:0 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:0 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:0 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:1 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:1 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:1 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:1 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:1 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:1 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:2 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:2 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:2 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:2 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:2 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:2 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:8 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:8 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:8 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:8 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:8 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:8 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "fmi" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:sine note:F1 fmi:4 fmi2:0 fmi3:0 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 1/8 → 1/4 | s:sine note:F1 fmi:4 fmi2:2.7408622801303864 fmi3:0.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 1/4 → 3/8 | s:sine note:F1 fmi:4 fmi2:1.479038767516613 fmi3:0.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 3/8 → 1/2 | s:sine note:F1 fmi:4 fmi2:1.605570062994957 fmi3:0.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 1/2 → 5/8 | s:sine note:F1 fmi:4 fmi2:1.041922464966774 fmi3:0.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 5/8 → 3/4 | s:sine note:F1 fmi:4 fmi2:0.5425434708595276 fmi3:0.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 3/4 → 7/8 | s:sine note:F1 fmi:4 fmi2:0.7833059206604958 fmi3:0.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 7/8 → 1/1 | s:sine note:F1 fmi:4 fmi2:1.590524323284626 fmi3:0.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 1/1 → 9/8 | s:sine note:F1 fmi:4 fmi2:2.078168660402298 fmi3:1 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 9/8 → 5/4 | s:sine note:F1 fmi:4 fmi2:2.689958058297634 fmi3:1.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 5/4 → 11/8 | s:sine note:F1 fmi:4 fmi2:2.914912812411785 fmi3:1.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 11/8 → 3/2 | s:sine note:F1 fmi:4 fmi2:0.7312150076031685 fmi3:1.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 3/2 → 13/8 | s:sine note:F1 fmi:4 fmi2:2.4336322993040085 fmi3:1.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 13/8 → 7/4 | s:sine note:F1 fmi:4 fmi2:1.987018845975399 fmi3:1.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 7/4 → 15/8 | s:sine note:F1 fmi:4 fmi2:0.8189515843987465 fmi3:1.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 15/8 → 2/1 | s:sine note:F1 fmi:4 fmi2:2.3782287165522575 fmi3:1.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 2/1 → 17/8 | s:sine note:F1 fmi:4 fmi2:3.838108479976654 fmi3:2 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 17/8 → 9/4 | s:sine note:F1 fmi:4 fmi2:3.613561175763607 fmi3:2.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 9/4 → 19/8 | s:sine note:F1 fmi:4 fmi2:1.3819300457835197 fmi3:2.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 19/8 → 5/2 | s:sine note:F1 fmi:4 fmi2:3.346026562154293 fmi3:2.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 5/2 → 21/8 | s:sine note:F1 fmi:4 fmi2:1.8344642966985703 fmi3:2.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 21/8 → 11/4 | s:sine note:F1 fmi:4 fmi2:0.6407739371061325 fmi3:2.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 11/4 → 23/8 | s:sine note:F1 fmi:4 fmi2:2.5329310819506645 fmi3:2.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 23/8 → 3/1 | s:sine note:F1 fmi:4 fmi2:0.06501668691635132 fmi3:2.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 3/1 → 25/8 | s:sine note:F1 fmi:4 fmi2:0.8691564425826073 fmi3:3 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 25/8 → 13/4 | s:sine note:F1 fmi:4 fmi2:1.3729755133390427 fmi3:3.125 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 13/4 → 27/8 | s:sine note:F1 fmi:4 fmi2:3.9694602862000465 fmi3:3.25 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 27/8 → 7/2 | s:sine note:F1 fmi:4 fmi2:1.6347672045230865 fmi3:3.375 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 7/2 → 29/8 | s:sine note:F1 fmi:4 fmi2:1.639795258641243 fmi3:3.5 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 29/8 → 15/4 | s:sine note:F1 fmi:4 fmi2:3.7565943151712418 fmi3:3.625 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 15/4 → 31/8 | s:sine note:F1 fmi:4 fmi2:3.2486692890524864 fmi3:3.75 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
"[ 31/8 → 4/1 | s:sine note:F1 fmi:4 fmi2:2.9446661546826363 fmi3:3.875 fmh:1.06 fmh2:10 fmh3:0.1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "fmsustain" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]",
|
||||
|
||||
@@ -73,6 +73,7 @@ export default defineConfig({
|
||||
workbox: {
|
||||
maximumFileSizeToCacheInBytes: 4194304, // 4MB
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,wav,mp3,ogg,ttf,woff2,TTF,otf}'],
|
||||
globIgnores: ['img/**', '_astro/csound*.js'],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: ({ url }) =>
|
||||
@@ -88,7 +89,7 @@ export default defineConfig({
|
||||
cacheName: 'external-samples',
|
||||
expiration: {
|
||||
maxEntries: 5000,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 14 days
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 30 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
|
||||
Vendored
+3
@@ -2,3 +2,6 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
declare module 'date-fns';
|
||||
declare module '@strudel/*';
|
||||
declare module 'dough-synth';
|
||||
declare module 'paho-mqtt';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getMetadata } from './metadata_parser';
|
||||
|
||||
export function getMyPatterns() {
|
||||
const my = import.meta.glob('../../my-patterns/**', { as: 'raw', eager: true });
|
||||
const my = import.meta.glob('../../my-patterns/**', { query: '?raw', eager: true });
|
||||
return Object.fromEntries(
|
||||
Object.entries(my)
|
||||
.filter(([name]) => name.endsWith('.txt'))
|
||||
|
||||
@@ -184,9 +184,9 @@ Each filter has 2 parameters:
|
||||
|
||||
Amplitude modulation changes the amplitude (gain) periodically over time.
|
||||
|
||||
## am
|
||||
## tremolo
|
||||
|
||||
<JsDoc client:idle name="am" h={0} />
|
||||
<JsDoc client:idle name="tremolo" h={0} />
|
||||
|
||||
## tremolosync
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ These are the equivalents used by the Mini Notation:
|
||||
| `stack(x, y)` | `"x,y"` |
|
||||
| `stepcat([3,x],[2,y])` | `"x@3 y@2"` |
|
||||
| `polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` |
|
||||
| `polymeterSteps(2, x, y, z)` | `"{x y z}%2"` |
|
||||
| `silence` | `"~"` |
|
||||
|
||||
## cat
|
||||
@@ -45,10 +44,6 @@ These are the equivalents used by the Mini Notation:
|
||||
|
||||
<JsDoc client:idle name="polymeter" h={0} />
|
||||
|
||||
## polymeterSteps
|
||||
|
||||
<JsDoc client:idle name="polymeterSteps" h={0} />
|
||||
|
||||
## silence
|
||||
|
||||
<JsDoc client:idle name="silence" h={0} />
|
||||
|
||||
@@ -329,7 +329,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
||||
|
||||
### begin
|
||||
|
||||
<JsDoc client:idle name="Pattern.begin" h={0} />
|
||||
<JsDoc client:idle name="begin" h={0} />
|
||||
|
||||
### end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ These functions allow the use of scales other than your typical chromatic 12 bas
|
||||
|
||||
{/* TODO (maybe): combine jsdoc things in tune.mjs with here */}
|
||||
|
||||
<JsDoc client:idle name="tune" h={0} />
|
||||
{/* <JsDoc client:idle name="tune" h={0} /> */}
|
||||
|
||||
Here's an example of how to configure a basic hexany scale:
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: Helix Keybindings
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
# Helix Keybindings in the REPL
|
||||
|
||||
The Strudel REPL supports [Helix editor](https://helix-editor.com/) keybindings through the `codemirror-helix` extension. Helix is a post-modern modal text editor with a focus on selection-first editing and multiple cursors.
|
||||
|
||||
## Enabling Helix Mode
|
||||
|
||||
1. Open the Settings panel in the REPL (click the settings icon or use the keyboard shortcut)
|
||||
2. Find the "Keybindings" section
|
||||
3. Select "Helix" from the available options
|
||||
|
||||
## Key Differences from Vim
|
||||
|
||||
Helix uses a selection-first approach, which means:
|
||||
|
||||
- **Selection → Action** (Helix) vs **Action → Motion** (Vim)
|
||||
- For example, to delete a word in Helix: `w` (select word) → `d` (delete)
|
||||
- In Vim, it would be: `dw` (delete word)
|
||||
|
||||
## Common Helix Commands
|
||||
|
||||
### Normal Mode
|
||||
|
||||
- `i` — Enter insert mode before selection
|
||||
- `a` — Enter insert mode after selection
|
||||
- `v` — Enter visual/select mode
|
||||
- `w` — Select next word
|
||||
- `e` — Select to end of word
|
||||
- `b` — Select previous word
|
||||
- `x` — Select/extend line
|
||||
- `d` — Delete selection
|
||||
- `c` — Change selection (delete and enter insert mode)
|
||||
- `y` — Yank (copy) selection
|
||||
- `p` — Paste after selection
|
||||
- `u` — Undo
|
||||
- `U` — Redo
|
||||
- `/` — Search
|
||||
- `n` — Select next search match
|
||||
- `N` — Select previous search match
|
||||
|
||||
### Movement
|
||||
|
||||
- `h, j, k, l` — Move left, down, up, right
|
||||
- `gg` — Go to start of document
|
||||
- `ge` — Go to end of document
|
||||
- `{` / `}` — Move to previous/next paragraph
|
||||
- `%` — Match bracket
|
||||
|
||||
### Multi-cursor
|
||||
|
||||
- `C` — Duplicate cursor to line below
|
||||
- `Alt-C` — Duplicate cursor to line above
|
||||
- `,` — Remove primary cursor
|
||||
- `Alt-,` — Remove all secondary cursors
|
||||
|
||||
## Strudel-Specific Features
|
||||
|
||||
Currently, Helix mode in Strudel provides standard Helix keybindings. Unlike Vim mode, there are no custom Strudel-specific commands (like `:w` to evaluate) yet.
|
||||
|
||||
To evaluate code while in Helix mode, use the standard shortcuts:
|
||||
|
||||
- `Ctrl+Enter` or `Alt+Enter` — Evaluate code
|
||||
- `Alt+.` — Stop playback
|
||||
|
||||
## Resources
|
||||
|
||||
- [Helix Documentation](https://docs.helix-editor.com/)
|
||||
- [Helix Keymap](https://docs.helix-editor.com/keymap.html)
|
||||
- [codemirror-helix on npm](https://www.npmjs.com/package/codemirror-helix)
|
||||
- [codemirror-helix on GitLab](https://gitlab.com/_rvidal/codemirror-helix)
|
||||
|
||||
## Notes
|
||||
|
||||
- The `codemirror-helix` extension is described as an "initial version" and may not implement all Helix features
|
||||
- Behavior may differ slightly from the native Helix editor
|
||||
- If you encounter issues, you can switch back to other keybinding modes in Settings
|
||||
- The keybinding preference is saved in your browser's local storage
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to add Strudel-specific Helix commands (similar to Vim's `:w` for evaluate), contributions are welcome! See the implementation in `/packages/codemirror/keybindings.mjs` for reference.
|
||||
@@ -235,7 +235,7 @@ function MainSettingsContent({ started }) {
|
||||
<ButtonGroup
|
||||
value={keybindings}
|
||||
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
|
||||
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', vscode: 'VSCode' }}
|
||||
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs', helix: 'Helix', vscode: 'VSCode' }}
|
||||
></ButtonGroup>
|
||||
</FormItem>
|
||||
<FormItem label="Panel Position">
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { code2hash, errorLogger, evalScope, hash2code, logger } from '@strudel/core';
|
||||
import * as strudelCore from '@strudel/core';
|
||||
import * as strudelWebaudio from '@strudel/webaudio';
|
||||
import { settingPatterns } from '../settings.mjs';
|
||||
import { setVersionDefaults } from '@strudel/webaudio';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
import { getMetadata } from '../metadata_parser.js';
|
||||
import { isTauri } from '../tauri.mjs';
|
||||
import './Repl.css';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs';
|
||||
import { $featuredPatterns } from '@src/user_pattern_utils.mjs';
|
||||
|
||||
const { setVersionDefaults } = strudelWebaudio;
|
||||
const { code2hash, errorLogger, evalScope, hash2code, logger } = strudelCore;
|
||||
|
||||
// Create a single supabase client for interacting with your database
|
||||
export const supabase = createClient(
|
||||
@@ -68,13 +71,13 @@ export async function getRandomTune() {
|
||||
|
||||
export function loadModules() {
|
||||
let modules = [
|
||||
import('@strudel/core'),
|
||||
strudelCore,
|
||||
import('@strudel/draw'),
|
||||
import('@strudel/edo'),
|
||||
import('@strudel/tonal'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/xen'),
|
||||
import('@strudel/webaudio'),
|
||||
strudelWebaudio,
|
||||
import('@strudel/codemirror'),
|
||||
import('@strudel/hydra'),
|
||||
import('@strudel/serial'),
|
||||
|
||||
Reference in New Issue
Block a user