diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs
index 2094416ea..6c89a2053 100644
--- a/packages/codemirror/codemirror.mjs
+++ b/packages/codemirror/codemirror.mjs
@@ -1,19 +1,21 @@
import { defaultKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
-import { EditorState } from '@codemirror/state';
+import { EditorState, Compartment } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
import { Drawer, repl } from '@strudel.cycles/core';
import { flashField, flash } from './flash.mjs';
-import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
+import { highlightExtension, highlightMiniLocations, updateMiniLocations } from './highlight.mjs';
import { oneDark } from './themes/one-dark';
+const themeComparment = new Compartment();
+
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
- theme,
+ themeComparment.of(theme),
javascript(),
lineNumbers(),
highlightExtension,
@@ -43,7 +45,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
export class StrudelMirror {
constructor(options) {
- const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
+ const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, theme, ...replOptions } = options;
this.code = initialCode;
this.drawer = new Drawer((haps, time) => {
@@ -81,12 +83,14 @@ export class StrudelMirror {
await prebaked;
},
afterEval: (options) => {
+ updateMiniLocations(this.editor, options.meta?.miniLocations);
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
+ theme,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
@@ -108,6 +112,15 @@ export class StrudelMirror {
flash(this.editor, ms);
}
highlight(haps, time) {
- highlightMiniLocations(this.editor.view, time, haps);
+ highlightMiniLocations(this.editor, time, haps);
+ }
+ setTheme(theme) {
+ this.editor.dispatch({
+ effects: themeComparment.reconfigure(theme),
+ });
+ }
+ setCode(code) {
+ const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
+ this.editor.dispatch({ changes });
}
}
diff --git a/packages/core/package.json b/packages/core/package.json
index 5a626b0ca..07acd0350 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -31,7 +31,8 @@
},
"homepage": "https://strudel.cc",
"dependencies": {
- "fraction.js": "^4.2.0"
+ "fraction.js": "^4.2.0",
+ "nanostores": "^0.8.1"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs
index 67e2812d0..b11cb5933 100644
--- a/packages/core/repl.mjs
+++ b/packages/core/repl.mjs
@@ -4,6 +4,19 @@ import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register } from './pattern.mjs';
+import { atom } from 'nanostores';
+
+export const $replstate = atom({
+ schedulerError: undefined,
+ evalError: undefined,
+ code: '// LOADING',
+ activeCode: '// LOADING',
+ pattern: undefined,
+ miniLocations: [],
+ widgets: [],
+ pending: true,
+});
+export const setReplState = (key, value) => $replstate.set({ ...$replstate.get(), [key]: value });
export function repl({
interval,
@@ -22,7 +35,10 @@ export function repl({
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
- onToggle,
+ onToggle: (started) => {
+ setReplState('started', started);
+ onToggle?.(started);
+ },
});
let playPatterns = [];
const setPattern = (pattern, autostart = true) => {
@@ -35,6 +51,8 @@ export function repl({
throw new Error('no code to evaluate');
}
try {
+ setReplState('code', code);
+ setReplState('pending', true);
await beforeEval?.({ code });
playPatterns = [];
let { pattern, meta } = await _evaluate(code, transpiler);
@@ -43,11 +61,20 @@ export function repl({
}
logger(`[eval] code updated`);
setPattern(pattern, autostart);
+ setReplState('miniLocations', meta?.miniLocations || []);
+ setReplState('widgets', meta?.widgets || []);
+ setReplState('activeCode', code);
+ setReplState('pattern', pattern);
+ setReplState('evalError', undefined);
+ setReplState('schedulerError', undefined);
+ setReplState('pending', false);
afterEval?.({ code, pattern, meta });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
logger(`[eval] error: ${err.message}`, 'error');
+ setReplState('evalError', err);
+ setReplState('pending', false);
onEvalError?.(err);
}
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 98f6441fe..e436542cd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -102,6 +102,9 @@ importers:
fraction.js:
specifier: ^4.2.0
version: 4.2.0
+ nanostores:
+ specifier: ^0.8.1
+ version: 0.8.1
devDependencies:
vite:
specifier: ^4.3.3
diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx
index db90cb8dd..552e1027e 100644
--- a/website/src/docs/MiniRepl.jsx
+++ b/website/src/docs/MiniRepl.jsx
@@ -28,7 +28,7 @@ export function MiniRepl({
useEffect(() => {
// we have to load this package on the client
// because codemirror throws an error on the server
- Promise.all([import('@strudel.cycles/react')])
+ Promise.all([import('@strudel.cycles/react'), init])
.then(([res]) => setRepl(() => res.MiniRepl))
.catch((err) => console.error(err));
}, []);
diff --git a/website/src/pages/vanilla.astro b/website/src/pages/vanilla.astro
new file mode 100644
index 000000000..1077892d5
--- /dev/null
+++ b/website/src/pages/vanilla.astro
@@ -0,0 +1,14 @@
+---
+import HeadCommon from '../components/HeadCommon.astro';
+import VanillaRepl from '../repl/VanillaRepl.astro';
+---
+
+
+
+
+ Strudel Vanilla REPL
+
+
+
+
+
diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx
index 98d083eea..a4a70319d 100644
--- a/website/src/repl/Repl.jsx
+++ b/website/src/repl/Repl.jsx
@@ -5,32 +5,20 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
-import { getDrawContext, logger } from '@strudel.cycles/core';
-import { CodeMirror, cx } from '@strudel.cycles/react';
-import { getAudioContext, resetLoadedSounds } from '@strudel.cycles/webaudio';
-import { createClient } from '@supabase/supabase-js';
+import { getDrawContext, logger, $replstate } from '@strudel.cycles/core';
+import { cx } from '@strudel.cycles/react';
+import { getAudioContext } from '@strudel.cycles/webaudio';
import { writeText } from '@tauri-apps/api/clipboard';
import { nanoid } from 'nanoid';
-import { createContext, useEffect, useMemo, useState } from 'react';
-import { settingsMap, useSettings } from '../settings.mjs';
+import { createContext, useState } from 'react';
+import { useSettings } from '../settings.mjs';
import { isTauri } from '../tauri.mjs';
import { Footer } from './Footer';
import { Header } from './Header';
import Loader from './Loader';
import './Repl.css';
-import { hash2code, code2hash } from './helpers.mjs';
-import * as tunes from './tunes.mjs';
-import { useRepl } from './useRepl';
-import { setLatestCode } from '../settings.mjs';
import { resetSounds } from './prebake.mjs';
-
-const { latestCode } = settingsMap.get();
-
-// Create a single supabase client for interacting with your database
-const supabase = createClient(
- 'https://pidxdsxphlhzjnzmifth.supabase.co',
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
-);
+import { useStore } from '@nanostores/react';
let clearCanvas;
if (typeof window !== 'undefined') {
@@ -38,106 +26,30 @@ if (typeof window !== 'undefined') {
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
}
-async function initCode() {
- // load code from url hash (either short hash from database or decode long hash)
- try {
- const initialUrl = window.location.href;
- const hash = initialUrl.split('?')[1]?.split('#')?.[0];
- const codeParam = window.location.href.split('#')[1] || '';
- // looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
- if (codeParam) {
- // looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
- return hash2code(codeParam);
- } else if (hash) {
- return supabase
- .from('code')
- .select('code')
- .eq('hash', hash)
- .then(({ data, error }) => {
- if (error) {
- console.warn('failed to load hash', err);
- }
- if (data.length) {
- //console.log('load hash from database', hash);
- return data[0].code;
- }
- });
- }
- } catch (err) {
- console.warn('failed to decode', err);
- }
-}
-
-function getRandomTune() {
- const allTunes = Object.entries(tunes);
- const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
- const [name, code] = randomItem(allTunes);
- return { name, code };
-}
-
-const { code: randomTune, name } = getRandomTune();
-
export const ReplContext = createContext(null);
export function Repl({ embedded = false }) {
- const isEmbedded = embedded || window.location !== window.parent.location;
+ //const isEmbedded = embedded || window.location !== window.parent.location;
+ const isEmbedded = false;
const [lastShared, setLastShared] = useState();
const { panelPosition, isZen } = useSettings();
-
- const {
- codemirror,
- code,
- setCode,
- scheduler,
- evaluate,
- activateCode,
- isDirty,
- activeCode,
- pattern,
- started,
- stop,
- error,
- pending,
- setPending,
- } = useRepl({
- afterEval: ({ code }) => {
- setLatestCode(code);
- window.location.hash = '#' + code2hash(code);
- },
- });
-
- // init code
- useEffect(() => {
- initCode().then((decoded) => {
- let msg;
- if (decoded) {
- setCode(decoded);
- msg = `I have loaded the code from the URL.`;
- } else if (latestCode) {
- setCode(latestCode);
- msg = `Your last session has been loaded!`;
- } /* if(randomTune) */ else {
- setCode(randomTune);
- msg = `A random code snippet named "${name}" has been loaded!`;
- }
- logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
- setPending(false);
- });
- }, []);
+ const replState = useStore($replstate);
//
// UI Actions
//
const handleTogglePlay = async () => {
- await getAudioContext().resume(); // fixes no sound in ios webkit
+ console.log('toggle.');
+ window.postMessage('strudel-toggle-play');
+ /* await getAudioContext().resume(); // fixes no sound in ios webkit
if (!started) {
logger('[repl] started. tip: you can also start by pressing ctrl+enter', 'highlight');
activateCode();
} else {
logger('[repl] stopped. tip: you can also stop by pressing ctrl+dot', 'highlight');
stop();
- }
+ } */
};
const handleUpdate = () => {
isDirty && activateCode();
@@ -149,7 +61,7 @@ export function Repl({ embedded = false }) {
logger(`[repl] ✨ loading random tune "${name}"`);
clearCanvas();
await resetSounds();
- scheduler.setCps(1);
+ // scheduler.setCps(1);
await evaluate(code, false);
};
@@ -182,15 +94,18 @@ export function Repl({ embedded = false }) {
logger(message);
}
};
+ const pending = false;
+ const error = undefined;
+ const { started, activeCode } = replState;
const context = {
- scheduler,
+ // scheduler,
embedded,
started,
pending,
- isDirty,
+ isDirty: false,
lastShared,
activeCode,
- handleChangeCode: codemirror.handleChangeCode,
+ // handleChangeCode: codemirror.handleChangeCode,
handleTogglePlay,
handleUpdate,
handleShuffle,
@@ -209,7 +124,7 @@ export function Repl({ embedded = false }) {
>
- {isEmbedded && !started && (
+ {/* isEmbedded && !started && (
- )}
+ ) */}
-
-
+ {
+ window.postMessage('strudel-container');
+ }}
+ >
+ {/* */}
{panelPosition === 'right' && !isEmbedded && }
diff --git a/website/src/repl/VanillaRepl.astro b/website/src/repl/VanillaRepl.astro
new file mode 100644
index 000000000..34ba9c5ec
--- /dev/null
+++ b/website/src/repl/VanillaRepl.astro
@@ -0,0 +1,15 @@
+---
+import HeadCommon from '../components/HeadCommon.astro';
+import { Repl } from '../repl/Repl.jsx';
+---
+
+
+
+
+ Strudel REPL
+
+
+
+
+
+
diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs
index b9fd48792..863a609ce 100644
--- a/website/src/repl/prebake.mjs
+++ b/website/src/repl/prebake.mjs
@@ -150,7 +150,7 @@ export async function prebake() {
...modules,
);
// register sounds and samples
- return Promise.all([initAudio, modulesLoading, registerStockSounds()]);
+ return Promise.all([/* initAudio, */ modulesLoading, registerStockSounds()]);
// await samples('github:tidalcycles/Dirt-Samples/master');
}
diff --git a/website/src/repl/vanillarepl.mjs b/website/src/repl/vanillarepl.mjs
new file mode 100644
index 000000000..b73f24d4a
--- /dev/null
+++ b/website/src/repl/vanillarepl.mjs
@@ -0,0 +1,155 @@
+import { StrudelMirror } from '@strudel/codemirror';
+import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
+import { transpiler } from '@strudel.cycles/transpiler';
+import { prebake } from './prebake.mjs';
+import { settingsMap } from '@src/settings.mjs';
+import { themes } from '@src/repl/themes.mjs';
+import { setLatestCode } from '../settings.mjs';
+import { hash2code, code2hash } from './helpers.mjs';
+import { createClient } from '@supabase/supabase-js';
+import * as tunes from './tunes.mjs';
+
+const onEvent = (key, callback) => {
+ const listener = (e) => {
+ if (e.data === key) {
+ callback();
+ }
+ };
+ window.addEventListener('message', listener);
+ return () => window.removeEventListener('message', listener);
+};
+
+function run() {
+ const { latestCode } = settingsMap.get();
+
+ /* let clearCanvas;
+if (typeof window !== 'undefined') {
+ const drawContext = getDrawContext();
+ clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
+} */
+
+ // Create a single supabase client for interacting with your database
+ const supabase = createClient(
+ 'https://pidxdsxphlhzjnzmifth.supabase.co',
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
+ );
+ async function initCode() {
+ // load code from url hash (either short hash from database or decode long hash)
+ try {
+ const initialUrl = window.location.href;
+ const hash = initialUrl.split('?')[1]?.split('#')?.[0];
+ const codeParam = window.location.href.split('#')[1] || '';
+ // looking like https://strudel.cc/?J01s5i1J0200 (fixed hash length)
+ if (codeParam) {
+ // looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length)
+ return hash2code(codeParam);
+ } else if (hash) {
+ return supabase
+ .from('code')
+ .select('code')
+ .eq('hash', hash)
+ .then(({ data, error }) => {
+ if (error) {
+ console.warn('failed to load hash', error);
+ }
+ if (data.length) {
+ //console.log('load hash from database', hash);
+ return data[0].code;
+ }
+ });
+ }
+ } catch (err) {
+ console.warn('failed to decode', err);
+ }
+ }
+
+ const container = document.getElementById('code');
+ const setFontSize = (size) => (container.style.fontSize = size + 'px');
+ const setFontFamily = (family) => (container.style.fontFamily = family);
+
+ /* const drawContext = getDrawContext()
+const drawTime = [-2, 2]; */
+ console.log('container', container);
+ const editor = new StrudelMirror({
+ theme: themes.strudelTheme,
+ defaultOutput: webaudioOutput,
+ getTime: () => getAudioContext().currentTime,
+ transpiler,
+ root: container,
+ initialCode: '// LOADING',
+ /* drawTime,
+ onDraw: (haps, time) =>
+ drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 1 }), */
+ prebake: () => prebake(),
+ afterEval: ({ code }) => {
+ setLatestCode(code);
+ window.location.hash = '#' + code2hash(code);
+ },
+ });
+
+ function getRandomTune() {
+ const allTunes = Object.entries(tunes);
+ const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
+ const [name, code] = randomItem(allTunes);
+ return { name, code };
+ }
+
+ const { code: randomTune, name } = getRandomTune();
+
+ function init() {
+ if (!container) {
+ console.warn('could not init: no container found');
+ return;
+ }
+ const settings = settingsMap.get();
+ setFontSize(settings.fontSize);
+ setFontFamily(settings.fontFamily);
+ initCode().then((decoded) => {
+ let msg;
+ if (decoded) {
+ editor.setCode(decoded);
+ msg = `I have loaded the code from the URL.`;
+ } else if (latestCode) {
+ editor.setCode(latestCode);
+ msg = `Your last session has been loaded!`;
+ } /* if(randomTune) */ else {
+ editor.setCode(randomTune);
+ msg = `A random code snippet named "${name}" has been loaded!`;
+ }
+ console.log('msg', msg);
+ /* logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
+ setPending(false); */
+ });
+
+ editor.setTheme(themes[settings.theme || 'strudelTheme']);
+ }
+ init();
+
+ settingsMap.listen((settings, key) => {
+ const value = settings[key];
+ if (key === 'theme') {
+ editor.setTheme(themes[value]);
+ } else if (key === 'fontFamily') {
+ // console.log('change fontFamily', value);
+ setFontFamily(value);
+ } else if (key === 'fontSize') {
+ // console.log('change fontSize', value);
+ setFontSize(value);
+ }
+ });
+
+ onEvent('strudel-toggle-play', () => {
+ console.log('toggle-play');
+ editor.evaluate();
+ });
+
+ // const isEmbedded = embedded || window.location !== window.parent.location;
+}
+
+let inited = false;
+onEvent('strudel-container', () => {
+ if (!inited) {
+ inited = true;
+ run();
+ }
+});