replace main repl

This commit is contained in:
Felix Roos 2023-12-28 20:48:59 +01:00
parent 2ed3a5c582
commit 201fe726b6
4 changed files with 113 additions and 397 deletions

View File

@ -1,11 +1,11 @@
---
import HeadCommon from '../components/HeadCommon.astro';
import { Repl } from '../repl/Repl.jsx';
import HeadCommonNext from '../components/HeadCommonNext.astro';
import { Repl } from '../repl/Repl';
---
<html lang="en" class="dark">
<head>
<HeadCommon />
<HeadCommonNext />
<title>Strudel REPL</title>
</head>
<body class="h-app-height bg-background">

View File

@ -1,14 +0,0 @@
---
import HeadCommonNext from '../../components/HeadCommonNext.astro';
import { Repl2 } from '../../repl/Repl2';
---
<html lang="en" class="dark">
<head>
<HeadCommonNext />
<title>Strudel REPL</title>
</head>
<body class="h-app-height bg-background">
<Repl2 client:only="react" />
</body>
</html>

View File

@ -1,15 +1,17 @@
/*
App.js - <short description TODO>
Repl.jsx - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/App.js>
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 PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import { cleanupDraw, cleanupUi, code2hash, getDrawContext, logger } from '@strudel.cycles/core';
import { CodeMirror, cx, flash, useHighlighting, useKeydown, useStrudel } from '@strudel.cycles/react';
import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs';
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
import { createContext, useCallback, useEffect, useMemo, useState } from 'react';
import { code2hash, getDrawContext, logger, silence } from '@strudel.cycles/core';
import { cx } from '@strudel.cycles/react';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, initAudioOnFirstClick, webaudioOutput } from '@strudel.cycles/webaudio';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
/* import { writeText } from '@tauri-apps/api/clipboard';
import { nanoid } from 'nanoid'; */
import { createContext, useCallback, useEffect, useRef, useState } from 'react';
import {
initUserCode,
setActivePattern,
@ -22,9 +24,14 @@ import { Header } from './Header';
import Loader from './Loader';
import './Repl.css';
import { Panel } from './panel/Panel';
import { prebake } from './prebake.mjs';
import { themes } from './themes.mjs';
// import { prebake } from '@strudel/repl';
import { useStore } from '@nanostores/react';
import { prebake /* , resetSounds */ } from './prebake.mjs';
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
import './Repl.css';
const { code: randomTune, name } = getRandomTune();
export const ReplContext = createContext(null);
const { latestCode } = settingsMap.get();
@ -39,185 +46,151 @@ if (typeof window !== 'undefined') {
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
}
const getTime = () => getAudioContext().currentTime;
const { code: randomTune, name } = getRandomTune();
export const ReplContext = createContext(null);
// const getTime = () => getAudioContext().currentTime;
export function Repl({ embedded = false }) {
const isEmbedded = embedded || window.location !== window.parent.location;
const [view, setView] = useState(); // codemirror view
const [pending, setPending] = useState(true);
const {
theme,
keybindings,
fontSize,
fontFamily,
isLineNumbersDisplayed,
isActiveLineHighlighted,
isAutoCompletionEnabled,
isTooltipEnabled,
isLineWrappingEnabled,
panelPosition,
isZen,
} = useSettings();
//const isEmbedded = embedded || window.location !== window.parent.location;
const isEmbedded = false;
const { panelPosition, isZen } = useSettings();
/* const replState = useStore($replstate);
const isDirty = useStore($repldirty); */
const shouldDraw = true;
const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]);
const { setWidgets } = useWidgets(view);
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
useStrudel({
initialCode: '// LOADING...',
const init = useCallback(({ shouldDraw }) => {
const drawTime = [-2, 2];
const drawContext = shouldDraw ? getDrawContext() : null;
let onDraw;
if (shouldDraw) {
onDraw = (haps, time, frame, painters) => {
painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
painters?.forEach((painter) => {
// ctx time haps drawTime paintOptions
painter(drawContext, time, haps, drawTime, { clear: false });
});
};
}
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime,
beforeEval: async () => {
setPending(true);
await modulesLoading;
cleanupUi();
cleanupDraw();
getTime: () => getAudioContext().currentTime,
transpiler,
autodraw: false,
root: containerRef.current,
initialCode: '// LOADING',
pattern: silence,
drawTime,
onDraw,
prebake: async () => Promise.all([modulesLoading, presets]),
onUpdateState: (state) => {
setReplState({ ...state });
},
afterEval: ({ code, meta }) => {
afterEval: ({ code }) => {
updateUserCode(code);
setMiniLocations(meta.miniLocations);
setWidgets(meta.widgets);
setPending(false);
// setPending(false);
setLatestCode(code);
window.location.hash = '#' + code2hash(code);
},
onEvalError: (err) => {
setPending(false);
},
onToggle: (play) => {
if (!play) {
cleanupDraw(false);
window.postMessage('strudel-stop');
} else {
window.postMessage('strudel-start');
}
},
drawContext,
// drawTime: [0, 6],
paintOptions,
bgFill: false,
});
// init code
useEffect(() => {
// init settings
initCode().then((decoded) => {
let msg;
if (decoded) {
setCode(decoded);
editor.setCode(decoded);
initUserCode(decoded);
msg = `I have loaded the code from the URL.`;
} else if (latestCode) {
setCode(latestCode);
editor.setCode(latestCode);
msg = `Your last session has been loaded!`;
} /* if(randomTune) */ else {
setCode(randomTune);
editor.setCode(randomTune);
msg = `A random code snippet named "${name}" has been loaded!`;
}
//registers samples that have been saved to the index DB
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
setPending(false);
// setPending(false);
});
editorRef.current = editor;
}, []);
// keyboard shortcuts
useKeydown(
useCallback(
async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
if (getAudioContext().state !== 'running') {
alert('please click play to initialize the audio. you can use shortcuts after that!');
return;
}
e.preventDefault();
flash(view);
await activateCode();
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
}
},
[activateCode, stop, view],
),
);
const [replState, setReplState] = useState({});
const { started, isDirty, error, activeCode } = replState;
const editorRef = useRef();
const containerRef = useRef();
const [client, setClient] = useState(false);
useEffect(() => {
setClient(true);
if (!editorRef.current) {
setTimeout(() => {
init({ shouldDraw });
});
}
return () => {
editorRef.current?.clear();
delete editorRef.current;
};
}, []);
// highlighting
const { setMiniLocations } = useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(),
});
// this can be simplified once SettingsTab has been refactored to change codemirrorSettings directly!
// this will be the case when the main repl is being replaced
const _settings = useStore(settingsMap, { keys: Object.keys(defaultSettings) });
useEffect(() => {
let editorSettings = {};
Object.keys(defaultSettings).forEach((key) => {
if (_settings.hasOwnProperty(key)) {
editorSettings[key] = _settings[key];
}
});
editorRef.current?.updateSettings(editorSettings);
}, [_settings]);
//
// UI Actions
//
const handleChangeCode = useCallback(
(c) => {
setCode(c);
// started && logger('[edit] code changed. hit ctrl+enter to update');
},
[started],
);
const handleSelectionChange = useCallback((selection) => {
// TODO: scroll to selected function in reference
// console.log('selectino change', selection.ranges[0].from);
}, []);
const handleTogglePlay = async () => {
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 handleTogglePlay = async () => editorRef.current?.toggle();
const handleUpdate = async (newCode, reset = false) => {
if (reset) {
clearCanvas();
resetLoadedSounds();
scheduler.setCps(1);
editorRef.current.repl.setCps(1);
await prebake(); // declare default samples
}
(newCode || isDirty) && activateCode(newCode);
if (newCode || isDirty) {
editorRef.current.setCode(newCode);
editorRef.current.repl.evaluate(newCode);
}
logger('[repl] code updated!');
};
const handleShuffle = async () => {
// window.postMessage('strudel-shuffle');
const { code, name } = getRandomTune();
logger(`[repl] ✨ loading random tune "${name}"`);
setActivePattern(name);
clearCanvas();
resetLoadedSounds();
scheduler.setCps(1);
editorRef.current.repl.setCps(1);
await prebake(); // declare default samples
await evaluate(code, false);
editorRef.current.setCode(code);
editorRef.current.repl.evaluate(code);
};
const handleShare = async () => shareCode(activeCode || code);
const handleShare = async () => shareCode(activeCode);
const pending = false;
//const error = undefined;
// const { started, activeCode } = replState;
const context = {
scheduler,
// scheduler,
embedded,
started,
pending,
isDirty,
activeCode,
handleChangeCode,
handleTogglePlay,
handleUpdate,
handleShuffle,
handleShare,
};
const currentTheme = useMemo(() => themes[theme] || themes.strudelTheme, [theme]);
const handleViewChanged = useCallback((v) => {
setView(v);
}, []);
return (
// bg-gradient-to-t from-blue-900 to-slate-900
@ -231,7 +204,7 @@ export function Repl({ embedded = false }) {
>
<Loader active={pending} />
<Header context={context} />
{isEmbedded && !started && (
{/* isEmbedded && !started && (
<button
onClick={() => handleTogglePlay()}
className="text-white text-2xl fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-[1000] m-auto p-4 bg-black rounded-md flex items-center space-x-2"
@ -239,25 +212,13 @@ export function Repl({ embedded = false }) {
<PlayCircleIcon className="w-6 h-6" />
<span>play</span>
</button>
)}
) */}
<div className="grow flex relative overflow-hidden">
<section className={'text-gray-100 cursor-text pb-0 overflow-auto grow' + (isZen ? ' px-10' : '')} id="code">
<CodeMirror
theme={currentTheme}
value={code}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
isActiveLineHighlighted={isActiveLineHighlighted}
isAutoCompletionEnabled={isAutoCompletionEnabled}
isTooltipEnabled={isTooltipEnabled}
isLineWrappingEnabled={isLineWrappingEnabled}
fontSize={fontSize}
fontFamily={fontFamily}
onChange={handleChangeCode}
onViewChanged={handleViewChanged}
onSelectionChange={handleSelectionChange}
/>
</section>
<section
className={'text-gray-100 cursor-text pb-0 overflow-auto grow' + (isZen ? ' px-10' : '')}
id="code"
ref={containerRef}
></section>
{panelPosition === 'right' && !isEmbedded && <Panel context={context} />}
</div>
{error && (

View File

@ -1,231 +0,0 @@
/*
App.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/App.js>
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 { code2hash, getDrawContext, logger, silence } from '@strudel.cycles/core';
import { cx } from '@strudel.cycles/react';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, initAudioOnFirstClick, webaudioOutput } from '@strudel.cycles/webaudio';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
/* import { writeText } from '@tauri-apps/api/clipboard';
import { nanoid } from 'nanoid'; */
import { createContext, useCallback, useEffect, useRef, useState } from 'react';
import {
initUserCode,
setActivePattern,
setLatestCode,
settingsMap,
updateUserCode,
useSettings,
} from '../settings.mjs';
import { Header } from './Header';
import Loader from './Loader';
import './Repl.css';
import { Panel } from './panel/Panel';
// import { prebake } from '@strudel/repl';
import { useStore } from '@nanostores/react';
import { prebake /* , resetSounds */ } from './prebake.mjs';
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
import './Repl.css';
const { code: randomTune, name } = getRandomTune();
export const ReplContext = createContext(null);
const { latestCode } = settingsMap.get();
initAudioOnFirstClick();
const modulesLoading = loadModules();
const presets = prebake();
let drawContext, clearCanvas;
if (typeof window !== 'undefined') {
drawContext = getDrawContext();
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
}
// const getTime = () => getAudioContext().currentTime;
export function Repl2({ embedded = false }) {
//const isEmbedded = embedded || window.location !== window.parent.location;
const isEmbedded = false;
const { panelPosition, isZen } = useSettings();
/* const replState = useStore($replstate);
const isDirty = useStore($repldirty); */
const shouldDraw = true;
const init = useCallback(({ shouldDraw }) => {
const drawTime = [-2, 2];
const drawContext = shouldDraw ? getDrawContext() : null;
let onDraw;
if (shouldDraw) {
onDraw = (haps, time, frame, painters) => {
painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
painters?.forEach((painter) => {
// ctx time haps drawTime paintOptions
painter(drawContext, time, haps, drawTime, { clear: false });
});
};
}
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
autodraw: false,
root: containerRef.current,
initialCode: '// LOADING',
pattern: silence,
drawTime,
onDraw,
prebake: async () => Promise.all([modulesLoading, presets]),
onUpdateState: (state) => {
setReplState({ ...state });
},
afterEval: ({ code }) => {
updateUserCode(code);
// setPending(false);
setLatestCode(code);
window.location.hash = '#' + code2hash(code);
},
bgFill: false,
});
// init settings
initCode().then((decoded) => {
let msg;
if (decoded) {
editor.setCode(decoded);
initUserCode(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!`;
}
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
// setPending(false);
});
editorRef.current = editor;
}, []);
const [replState, setReplState] = useState({});
const { started, isDirty, error, activeCode } = replState;
const editorRef = useRef();
const containerRef = useRef();
const [client, setClient] = useState(false);
useEffect(() => {
setClient(true);
if (!editorRef.current) {
setTimeout(() => {
init({ shouldDraw });
});
}
return () => {
editorRef.current?.clear();
delete editorRef.current;
};
}, []);
// this can be simplified once SettingsTab has been refactored to change codemirrorSettings directly!
// this will be the case when the main repl is being replaced
const _settings = useStore(settingsMap, { keys: Object.keys(defaultSettings) });
useEffect(() => {
let editorSettings = {};
Object.keys(defaultSettings).forEach((key) => {
if (_settings.hasOwnProperty(key)) {
editorSettings[key] = _settings[key];
}
});
editorRef.current?.updateSettings(editorSettings);
}, [_settings]);
//
// UI Actions
//
const handleTogglePlay = async () => editorRef.current?.toggle();
const handleUpdate = async (newCode, reset = false) => {
if (reset) {
clearCanvas();
resetLoadedSounds();
editorRef.current.repl.setCps(1);
await prebake(); // declare default samples
}
if (newCode || isDirty) {
editorRef.current.setCode(newCode);
editorRef.current.repl.evaluate(newCode);
}
logger('[repl] code updated!');
};
const handleShuffle = async () => {
// window.postMessage('strudel-shuffle');
const { code, name } = getRandomTune();
logger(`[repl] ✨ loading random tune "${name}"`);
setActivePattern(name);
clearCanvas();
resetLoadedSounds();
editorRef.current.repl.setCps(1);
await prebake(); // declare default samples
editorRef.current.setCode(code);
editorRef.current.repl.evaluate(code);
};
const handleShare = async () => shareCode(activeCode);
const pending = false;
//const error = undefined;
// const { started, activeCode } = replState;
const context = {
// scheduler,
embedded,
started,
pending,
isDirty,
activeCode,
handleTogglePlay,
handleUpdate,
handleShuffle,
handleShare,
};
return (
// bg-gradient-to-t from-blue-900 to-slate-900
// bg-gradient-to-t from-green-900 to-slate-900
<ReplContext.Provider value={context}>
<div
className={cx(
'h-full flex flex-col relative',
// overflow-hidden
)}
>
<Loader active={pending} />
<Header context={context} />
{/* isEmbedded && !started && (
<button
onClick={() => handleTogglePlay()}
className="text-white text-2xl fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-[1000] m-auto p-4 bg-black rounded-md flex items-center space-x-2"
>
<PlayCircleIcon className="w-6 h-6" />
<span>play</span>
</button>
) */}
<div className="grow flex relative overflow-hidden">
<section
className={'text-gray-100 cursor-text pb-0 overflow-auto grow' + (isZen ? ' px-10' : '')}
id="code"
ref={containerRef}
></section>
{panelPosition === 'right' && !isEmbedded && <Panel context={context} />}
</div>
{error && (
<div className="text-red-500 p-4 bg-lineHighlight animate-pulse">{error.message || 'Unknown Error :-/'}</div>
)}
{panelPosition === 'bottom' && !isEmbedded && <Panel context={context} />}
</div>
</ReplContext.Provider>
);
}