Merge pull request 'Edit prebake in settings' (#1958) from JohnBjrk/strudel:edit-prebake-in-settings into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1958
This commit is contained in:
commit
6f108f7f28
18 changed files with 338 additions and 119 deletions
|
|
@ -293,6 +293,9 @@ export class StrudelMirror {
|
||||||
// Handle global evaluation requests (e.g., from Vim :w)
|
// Handle global evaluation requests (e.g., from Vim :w)
|
||||||
this.onEvaluateRequest = (e) => {
|
this.onEvaluateRequest = (e) => {
|
||||||
try {
|
try {
|
||||||
|
if (e.detail.view !== this.editor) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
// Evaluate current editor on repl-evaluate
|
// Evaluate current editor on repl-evaluate
|
||||||
logger('[repl] evaluate via event');
|
logger('[repl] evaluate via event');
|
||||||
this.evaluate();
|
this.evaluate();
|
||||||
|
|
@ -307,6 +310,9 @@ export class StrudelMirror {
|
||||||
// Toggle comments requested from Vim (gc)
|
// Toggle comments requested from Vim (gc)
|
||||||
this.onToggleComment = (e) => {
|
this.onToggleComment = (e) => {
|
||||||
try {
|
try {
|
||||||
|
if (e.detail.view !== this.editor) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
// Honor selections; toggleLineComment handles both selections and
|
// Honor selections; toggleLineComment handles both selections and
|
||||||
// single line
|
// single line
|
||||||
toggleLineComment(this.editor);
|
toggleLineComment(this.editor);
|
||||||
|
|
@ -347,6 +353,9 @@ export class StrudelMirror {
|
||||||
// Listen for global stop requests (e.g., from Vim :q)
|
// Listen for global stop requests (e.g., from Vim :q)
|
||||||
onStopRequest = (e) => {
|
onStopRequest = (e) => {
|
||||||
try {
|
try {
|
||||||
|
if (e.detail.view !== this.editor) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
this.stop();
|
this.stop();
|
||||||
e?.cancelable && e.preventDefault?.();
|
e?.cancelable && e.preventDefault?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,4 @@ export * from './slider.mjs';
|
||||||
export * from './themes.mjs';
|
export * from './themes.mjs';
|
||||||
export * from './widget.mjs';
|
export * from './widget.mjs';
|
||||||
export { Vim } from './keybindings.mjs';
|
export { Vim } from './keybindings.mjs';
|
||||||
|
export * from './basicSetup.mjs';
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ try {
|
||||||
// internal actions and works with current selections/visual mode.
|
// internal actions and works with current selections/visual mode.
|
||||||
try {
|
try {
|
||||||
Vim.defineAction('strudelToggleComment', (cm) => {
|
Vim.defineAction('strudelToggleComment', (cm) => {
|
||||||
const view = cm?.view || cm;
|
const view = cm.cm6;
|
||||||
try {
|
try {
|
||||||
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
document.dispatchEvent(ev);
|
document.dispatchEvent(ev);
|
||||||
|
|
@ -112,15 +112,16 @@ try {
|
||||||
|
|
||||||
// :q to pause/stop
|
// :q to pause/stop
|
||||||
Vim.defineEx('quit', 'q', (cm) => {
|
Vim.defineEx('quit', 'q', (cm) => {
|
||||||
const view = cm?.view || cm;
|
const view = cm.cm6;
|
||||||
|
logger('[vim] :q — stopping repl');
|
||||||
replStop(view);
|
replStop(view);
|
||||||
});
|
});
|
||||||
|
|
||||||
// :w to evaluate
|
// :w to evaluate
|
||||||
Vim.defineEx('write', 'w', (cm) => {
|
Vim.defineEx('write', 'w', (cm) => {
|
||||||
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
|
const view = cm.cm6;
|
||||||
try {
|
try {
|
||||||
view?.focus?.();
|
view.focus?.();
|
||||||
// Let the app know this came from Vim :w
|
// Let the app know this came from Vim :w
|
||||||
try {
|
try {
|
||||||
logger('[vim] :w — evaluating code');
|
logger('[vim] :w — evaluating code');
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,13 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.18.4",
|
"@codemirror/autocomplete": "catalog:",
|
||||||
"@codemirror/commands": "^6.8.0",
|
"@codemirror/commands": "catalog:",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "catalog:",
|
||||||
"@codemirror/language": "^6.10.8",
|
"@codemirror/language": "catalog:",
|
||||||
"@codemirror/search": "^6.5.8",
|
"@codemirror/search": "catalog:",
|
||||||
"@codemirror/state": "^6.5.1",
|
"@codemirror/state": "catalog:",
|
||||||
"@codemirror/view": "^6.36.2",
|
"@codemirror/view": "catalog:",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@replit/codemirror-emacs": "^6.1.0",
|
"@replit/codemirror-emacs": "^6.1.0",
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ import { evaluate as _evaluate } from '@strudel/core';
|
||||||
import { transpiler } from './transpiler.mjs';
|
import { transpiler } from './transpiler.mjs';
|
||||||
export * from './transpiler.mjs';
|
export * from './transpiler.mjs';
|
||||||
|
|
||||||
export const evaluate = (code) => _evaluate(code, transpiler);
|
export const evaluate = (code, transpilerOptions) => _evaluate(code, transpiler, transpilerOptions);
|
||||||
|
|
|
||||||
|
|
@ -256,12 +256,7 @@ export function transpiler(input, options = {}) {
|
||||||
body.push(silenceExpression);
|
body.push(silenceExpression);
|
||||||
} else if (!body?.[body.length - 1]?.expression) {
|
} else if (!body?.[body.length - 1]?.expression) {
|
||||||
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
|
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
|
||||||
if (blockBased) {
|
body.push(silenceExpression);
|
||||||
// For block-based eval, add silence as the return value when block ends with declaration
|
|
||||||
body.push(silenceExpression);
|
|
||||||
} else {
|
|
||||||
throw new Error('unexpected ast format without body expression');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For block-based eval, add scope assignments before the return statement
|
// For block-based eval, add scope assignments before the return statement
|
||||||
|
|
|
||||||
53
pnpm-lock.yaml
generated
53
pnpm-lock.yaml
generated
|
|
@ -4,6 +4,30 @@ settings:
|
||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
catalogs:
|
||||||
|
default:
|
||||||
|
'@codemirror/autocomplete':
|
||||||
|
specifier: ^6.18.4
|
||||||
|
version: 6.18.4
|
||||||
|
'@codemirror/commands':
|
||||||
|
specifier: ^6.8.0
|
||||||
|
version: 6.8.0
|
||||||
|
'@codemirror/lang-javascript':
|
||||||
|
specifier: ^6.2.2
|
||||||
|
version: 6.2.2
|
||||||
|
'@codemirror/language':
|
||||||
|
specifier: ^6.10.8
|
||||||
|
version: 6.10.8
|
||||||
|
'@codemirror/search':
|
||||||
|
specifier: ^6.5.8
|
||||||
|
version: 6.5.8
|
||||||
|
'@codemirror/state':
|
||||||
|
specifier: ^6.5.1
|
||||||
|
version: 6.5.1
|
||||||
|
'@codemirror/view':
|
||||||
|
specifier: ^6.36.2
|
||||||
|
version: 6.36.2
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
|
@ -171,25 +195,25 @@ importers:
|
||||||
packages/codemirror:
|
packages/codemirror:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/autocomplete':
|
'@codemirror/autocomplete':
|
||||||
specifier: ^6.18.4
|
specifier: 'catalog:'
|
||||||
version: 6.18.4
|
version: 6.18.4
|
||||||
'@codemirror/commands':
|
'@codemirror/commands':
|
||||||
specifier: ^6.8.0
|
specifier: 'catalog:'
|
||||||
version: 6.8.0
|
version: 6.8.0
|
||||||
'@codemirror/lang-javascript':
|
'@codemirror/lang-javascript':
|
||||||
specifier: ^6.2.2
|
specifier: 'catalog:'
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
'@codemirror/language':
|
'@codemirror/language':
|
||||||
specifier: ^6.10.8
|
specifier: 'catalog:'
|
||||||
version: 6.10.8
|
version: 6.10.8
|
||||||
'@codemirror/search':
|
'@codemirror/search':
|
||||||
specifier: ^6.5.8
|
specifier: 'catalog:'
|
||||||
version: 6.5.8
|
version: 6.5.8
|
||||||
'@codemirror/state':
|
'@codemirror/state':
|
||||||
specifier: ^6.5.1
|
specifier: 'catalog:'
|
||||||
version: 6.5.1
|
version: 6.5.1
|
||||||
'@codemirror/view':
|
'@codemirror/view':
|
||||||
specifier: ^6.36.2
|
specifier: 'catalog:'
|
||||||
version: 6.36.2
|
version: 6.36.2
|
||||||
'@lezer/highlight':
|
'@lezer/highlight':
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
|
|
@ -705,6 +729,21 @@ importers:
|
||||||
'@astrojs/tailwind':
|
'@astrojs/tailwind':
|
||||||
specifier: ^5.1.5
|
specifier: ^5.1.5
|
||||||
version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17)
|
version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17)
|
||||||
|
'@codemirror/commands':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.8.0
|
||||||
|
'@codemirror/lang-javascript':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.2.2
|
||||||
|
'@codemirror/language':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.10.8
|
||||||
|
'@codemirror/state':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.5.1
|
||||||
|
'@codemirror/view':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 6.36.2
|
||||||
'@docsearch/css':
|
'@docsearch/css':
|
||||||
specifier: ^3.8.3
|
specifier: ^3.8.3
|
||||||
version: 3.8.3
|
version: 3.8.3
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,12 @@ onlyBuiltDependencies:
|
||||||
- sharp
|
- sharp
|
||||||
- tree-sitter
|
- tree-sitter
|
||||||
- tree-sitter-haskell
|
- 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"
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@
|
||||||
"@astrojs/react": "^4.1.6",
|
"@astrojs/react": "^4.1.6",
|
||||||
"@astrojs/rss": "^4.0.11",
|
"@astrojs/rss": "^4.0.11",
|
||||||
"@astrojs/tailwind": "^5.1.5",
|
"@astrojs/tailwind": "^5.1.5",
|
||||||
|
"@codemirror/commands": "catalog:",
|
||||||
|
"@codemirror/language": "catalog:",
|
||||||
|
"@codemirror/lang-javascript": "catalog:",
|
||||||
|
"@codemirror/state": "catalog:",
|
||||||
|
"@codemirror/view": "catalog:",
|
||||||
"@docsearch/css": "^3.8.3",
|
"@docsearch/css": "^3.8.3",
|
||||||
"@docsearch/react": "^3.8.3",
|
"@docsearch/react": "^3.8.3",
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
|
|
|
||||||
|
|
@ -10,50 +10,50 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-scroller {
|
.code-container .cm-scroller {
|
||||||
padding-top: 10px !important;
|
padding-top: 10px !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
#code .cm-content {
|
.code-container .cm-content {
|
||||||
padding-bottom: 50vh;
|
padding-bottom: 50vh;
|
||||||
}
|
}
|
||||||
#code .cm-line > * {
|
.code-container .cm-line > * {
|
||||||
background: var(--lineBackground);
|
background: var(--lineBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-editor {
|
.code-container .cm-editor {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-theme {
|
.code-container .cm-theme {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-theme-light {
|
.code-container .cm-theme-light {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-cursorLayer {
|
.code-container .cm-cursorLayer {
|
||||||
animation-name: inherit !important;
|
animation-name: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-cursor {
|
.code-container .cm-cursor {
|
||||||
border-left: 2px solid currentcolor !important;
|
border-left: 2px solid currentcolor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-foldGutter {
|
.code-container .cm-foldGutter {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-focused {
|
.code-container .cm-focused {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code .cm-matchingBracket {
|
.code-container .cm-matchingBracket {
|
||||||
text-decoration: underline 0.18rem;
|
text-decoration: underline 0.18rem;
|
||||||
text-underline-offset: 0.22rem;
|
text-underline-offset: 0.22rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ export function Code(Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
|
className={'code-container text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
|
||||||
id="code"
|
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
containerRef.current = el;
|
containerRef.current = el;
|
||||||
if (!editorRef.current) {
|
if (!editorRef.current) {
|
||||||
|
|
|
||||||
|
|
@ -8,31 +8,3 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpecialActionButton(props) {
|
|
||||||
const { className, ...buttonProps } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionButton {...buttonProps} className={cx('bg-background p-2 max-w-[300px] hover:opacity-50', className)} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActionInput({ label, className, ...props }) {
|
|
||||||
return (
|
|
||||||
<label className={cx('inline-flex items-center cursor-pointer ', className)}>
|
|
||||||
<input {...props} className="sr-only peer" />
|
|
||||||
|
|
||||||
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SpecialActionInput({ className, ...props }) {
|
|
||||||
return (
|
|
||||||
<ActionInput
|
|
||||||
{...props}
|
|
||||||
className={className}
|
|
||||||
label={<span className="bg-background p-2 max-w-[300px]">{props.label}</span>}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,50 @@
|
||||||
import { errorLogger } from '@strudel/core';
|
import { errorLogger } from '@strudel/core';
|
||||||
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
|
import { storePrebakeScript } from '../../../settings.mjs';
|
||||||
import { SpecialActionInput } from '../button/action-button';
|
import { confirmDialog } from '@src/repl/util.mjs';
|
||||||
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
|
|
||||||
|
|
||||||
async function importScript(script) {
|
async function importScript(script, updateEditor) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsText(script);
|
reader.readAsText(script);
|
||||||
|
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const text = reader.result;
|
const text = reader.result;
|
||||||
storePrebakeScript(text);
|
storePrebakeScript(text);
|
||||||
|
updateEditor && updateEditor(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
errorLogger(new Error('failed to import prebake script'), 'importScript');
|
errorLogger(new Error('failed to import prebake script'), 'importScript');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function ImportPrebakeScriptButton() {
|
|
||||||
const settings = useSettings();
|
|
||||||
|
|
||||||
|
export async function exportScript(script) {
|
||||||
|
const blob = new Blob([script], { type: 'application/javascript' });
|
||||||
|
const downloadLink = document.createElement('a');
|
||||||
|
downloadLink.href = window.URL.createObjectURL(blob);
|
||||||
|
const date = new Date().toISOString().split('T')[0];
|
||||||
|
downloadLink.download = `prebake_${date}.strudel`;
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
document.body.removeChild(downloadLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportPrebakeScriptButton({ updateEditor }) {
|
||||||
|
const handleChange = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
const confirmed = await confirmDialog('Warning: This will overwrite the current prebake.\nContinue?');
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await importScript(file, updateEditor);
|
||||||
|
} catch (e) {
|
||||||
|
errorLogger(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<SpecialActionInput
|
<label className="space-x-1 inline-flex items-center cursor-pointer">
|
||||||
type="file"
|
<input type="file" accept=".strudel,.js" className="sr-only peer" onChange={handleChange} />
|
||||||
label="import prebake script"
|
<span className="inline-flex items-center peer-hover:opacity-50 text-xs max-w-[300px]">import</span>
|
||||||
accept=".strudel"
|
</label>
|
||||||
onChange={async (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG);
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await importScript(file);
|
|
||||||
window.location.reload();
|
|
||||||
} catch (e) {
|
|
||||||
errorLogger(e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
import { defaultSettings, settingsMap, useSettings, storePrebakeScript, setSettingsTab } from '../../../settings.mjs';
|
||||||
import { themes } from '@strudel/codemirror';
|
import { themes } from '@strudel/codemirror';
|
||||||
|
import { PrebakeCodeMirror } from '../../../repl/prebakeCodeMirror.mjs';
|
||||||
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { ButtonGroup } from './Forms.jsx';
|
||||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||||
import { confirmDialog } from '../../util.mjs';
|
import { confirmDialog } from '../../util.mjs';
|
||||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
||||||
import { SpecialActionButton } from '../button/action-button.jsx';
|
import { ActionButton } from '../button/action-button.jsx';
|
||||||
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
import { exportScript, ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
function cx(...classes) {
|
import cx from '@src/cx.mjs';
|
||||||
// : Array<string | undefined>
|
|
||||||
return classes.filter(Boolean).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputClass =
|
const inputClass =
|
||||||
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
|
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
|
||||||
|
|
@ -123,7 +121,7 @@ const fontFamilyOptions = {
|
||||||
galactico: 'galactico',
|
galactico: 'galactico',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SettingsTab({ started }) {
|
function MainSettingsContent({ started }) {
|
||||||
const {
|
const {
|
||||||
theme,
|
theme,
|
||||||
keybindings,
|
keybindings,
|
||||||
|
|
@ -149,13 +147,12 @@ export function SettingsTab({ started }) {
|
||||||
isTabIndentationEnabled,
|
isTabIndentationEnabled,
|
||||||
isMultiCursorEnabled,
|
isMultiCursorEnabled,
|
||||||
patternAutoStart,
|
patternAutoStart,
|
||||||
includePrebakeScriptInShare,
|
|
||||||
isBlockBasedEvalEnabled,
|
isBlockBasedEvalEnabled,
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
const shouldAlwaysSync = isUdels();
|
const shouldAlwaysSync = isUdels();
|
||||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}>
|
<div className="p-4 text-foreground space-y-4 w-full overflow-auto" style={{ fontFamily }}>
|
||||||
{canChangeAudioDevice && (
|
{canChangeAudioDevice && (
|
||||||
<FormItem label="Audio Output Device">
|
<FormItem label="Audio Output Device">
|
||||||
<AudioDeviceSelector
|
<AudioDeviceSelector
|
||||||
|
|
@ -233,14 +230,6 @@ export function SettingsTab({ started }) {
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
<FormItem label="Prebake">
|
|
||||||
<ImportPrebakeScriptButton />
|
|
||||||
<Checkbox
|
|
||||||
label="Include prebake script in share"
|
|
||||||
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
|
||||||
value={includePrebakeScriptInShare}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem label="Keybindings">
|
<FormItem label="Keybindings">
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
|
|
@ -346,7 +335,7 @@ export function SettingsTab({ started }) {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
||||||
<FormItem label="Reset Settings">
|
<FormItem label="Reset Settings">
|
||||||
<SpecialActionButton
|
<ActionButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
confirmDialog('Sure?').then((r) => {
|
confirmDialog('Sure?').then((r) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
|
|
@ -355,10 +344,70 @@ export function SettingsTab({ started }) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="bg-background p-2 max-w-[300px] hover:opacity-50"
|
||||||
>
|
>
|
||||||
restore default settings
|
restore default settings
|
||||||
</SpecialActionButton>
|
</ActionButton>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PrebakeSettingsContent() {
|
||||||
|
const { fontFamily, includePrebakeScriptInShare, prebakeScript } = useSettings();
|
||||||
|
const editorRef = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.cleanup();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full text-foreground w-full overflow-auto" style={{ fontFamily }}>
|
||||||
|
<div className="flex flex-col grow overflow-hidden h-full bg-background">
|
||||||
|
<section
|
||||||
|
className="pb-0 overflow-auto grow z-10 code-container"
|
||||||
|
ref={(el) => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editorRef.current = new PrebakeCodeMirror(prebakeScript, (code) => storePrebakeScript(code), el);
|
||||||
|
}}
|
||||||
|
></section>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center border-t border-muted px-4 whitespace-nowrap">
|
||||||
|
<Checkbox
|
||||||
|
label="share with patterns"
|
||||||
|
className="whitespace-nowrap max-w-[200px]"
|
||||||
|
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
||||||
|
value={includePrebakeScriptInShare}
|
||||||
|
/>
|
||||||
|
<div className="py-2 flex flex-row items-center space-x-3 ">
|
||||||
|
<ImportPrebakeScriptButton updateEditor={(code) => editorRef?.current.setCode(code)} />
|
||||||
|
<ActionButton onClick={() => exportScript(prebakeScript)}>export</ActionButton>
|
||||||
|
<ActionButton onClick={() => editorRef.current?.savePrebake()}>save</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function SettingsTab({ started }) {
|
||||||
|
const { settingsTab } = useSettings();
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full text-foreground flex flex-col overflow-hidden">
|
||||||
|
<div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-b border-muted">
|
||||||
|
<ButtonGroup
|
||||||
|
wrap
|
||||||
|
value={settingsTab}
|
||||||
|
onChange={(value) => setSettingsTab(value)}
|
||||||
|
items={{
|
||||||
|
settings: 'settings',
|
||||||
|
prebake: 'prebake',
|
||||||
|
}}
|
||||||
|
></ButtonGroup>
|
||||||
|
</div>
|
||||||
|
{settingsTab === 'settings' && <MainSettingsContent started={started} />}
|
||||||
|
{settingsTab === 'prebake' && <PrebakeSettingsContent />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
115
website/src/repl/prebakeCodeMirror.mjs
Normal file
115
website/src/repl/prebakeCodeMirror.mjs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { toggleLineComment } from '@codemirror/commands';
|
||||||
|
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||||
|
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
|
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||||
|
import { drawSelection, EditorView, keymap } from '@codemirror/view';
|
||||||
|
import { logger } from '@strudel/core';
|
||||||
|
import { basicSetup, flash, initTheme, extensions, parseBooleans, codemirrorSettings } from '@strudel/codemirror';
|
||||||
|
import { evaluate } from '@strudel/transpiler';
|
||||||
|
|
||||||
|
export class PrebakeCodeMirror {
|
||||||
|
constructor(initialCode, storePrebake, container) {
|
||||||
|
const settings = codemirrorSettings.get();
|
||||||
|
this.storePrebake = storePrebake;
|
||||||
|
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||||
|
const initialSettings = Object.keys(compartments).map((key) =>
|
||||||
|
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||||
|
);
|
||||||
|
initTheme(settings.theme);
|
||||||
|
let state = EditorState.create({
|
||||||
|
doc: initialCode,
|
||||||
|
extensions: [
|
||||||
|
...initialSettings,
|
||||||
|
basicSetup,
|
||||||
|
javascript(),
|
||||||
|
javascriptLanguage.data.of({
|
||||||
|
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||||
|
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||||
|
}),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
|
EditorView.updateListener.of((v) => {
|
||||||
|
if (v.docChanged) {
|
||||||
|
this.code = v.state.doc.toString();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
drawSelection({ cursorBlinkRate: 0 }),
|
||||||
|
Prec.highest(
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
mac: 'Meta-Enter',
|
||||||
|
run: () => {
|
||||||
|
this.savePrebake();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Ctrl-Enter',
|
||||||
|
run: () => {
|
||||||
|
this.savePrebake();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Alt-Enter',
|
||||||
|
run: () => {
|
||||||
|
this.savePrebake();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.code = initialCode;
|
||||||
|
this.view = new EditorView({
|
||||||
|
state,
|
||||||
|
parent: container,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSaveEvent = async (e) => {
|
||||||
|
if (e.detail.view !== this.view) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
|
await this.savePrebake();
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
};
|
||||||
|
const handleToggleComment = (e) => {
|
||||||
|
if (e.detail.view !== this.view) {
|
||||||
|
return; // ignore events from other editors
|
||||||
|
}
|
||||||
|
this.toggleComment();
|
||||||
|
e?.cancelable && e.preventDefault?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('repl-evaluate', handleSaveEvent);
|
||||||
|
document.addEventListener('repl-toggle-comment', handleToggleComment);
|
||||||
|
this.cleanup = () => {
|
||||||
|
document.removeEventListener('prebake-evaluate', handleSaveEvent);
|
||||||
|
document.removeEventListener('prebake-toggle-comment', handleToggleComment);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async savePrebake() {
|
||||||
|
flash(this.view);
|
||||||
|
this.storePrebake(this.code);
|
||||||
|
evaluate(this.code, { addReturn: false }); // run prebake
|
||||||
|
logger('[prebake] prebake saved');
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleComment() {
|
||||||
|
try {
|
||||||
|
// Honor selections; toggleLineComment handles both selections and
|
||||||
|
// single line
|
||||||
|
toggleLineComment(this.view);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error handling repl-toggle-comment event', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCode(code) {
|
||||||
|
const changes = {
|
||||||
|
from: 0,
|
||||||
|
to: this.view.state.doc.length,
|
||||||
|
insert: code,
|
||||||
|
};
|
||||||
|
this.view.dispatch({ changes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||||
|
|
||||||
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
||||||
import { getDrawContext } from '@strudel/draw';
|
import { getDrawContext } from '@strudel/draw';
|
||||||
import { evaluate, transpiler } from '@strudel/transpiler';
|
import { transpiler, evaluate } from '@strudel/transpiler';
|
||||||
import {
|
import {
|
||||||
getAudioContextCurrentTime,
|
getAudioContextCurrentTime,
|
||||||
renderPatternAudio,
|
renderPatternAudio,
|
||||||
|
|
@ -87,12 +87,12 @@ export function useReplContext() {
|
||||||
pattern: silence,
|
pattern: silence,
|
||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
drawContext,
|
||||||
prebake: async () =>
|
prebake: async () => {
|
||||||
Promise.all([modulesLoading, presets]).then(() => {
|
await Promise.all([modulesLoading, presets]);
|
||||||
if (prebakeScript?.length) {
|
if (prebakeScript) {
|
||||||
return evaluate(prebakeScript ?? '');
|
return evaluate(prebakeScript, { addReturn: false });
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
onUpdateState: (state) => {
|
onUpdateState: (state) => {
|
||||||
setReplState({ ...state });
|
setReplState({ ...state });
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ export function confirmDialog(msg) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
||||||
|
|
||||||
export function confirmAndReloadPage(onSuccess) {
|
export function confirmAndReloadPage(onSuccess) {
|
||||||
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
|
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
|
||||||
if (r == true) {
|
if (r == true) {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,20 @@ export const soundFilterType = {
|
||||||
ALL: 'all',
|
ALL: 'all',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initialPrebakeScript = `// Prebake script
|
||||||
|
//
|
||||||
|
// This is code that is loaded before your pattern is run.
|
||||||
|
// You can use it to define custom functions to use in any pattern.
|
||||||
|
//
|
||||||
|
// This is an initial example script. You can edit it to add
|
||||||
|
// your own funtions.
|
||||||
|
//
|
||||||
|
// To use a script shared by some other user you can use
|
||||||
|
// the import-button or paste the script in this editor.
|
||||||
|
|
||||||
|
const ratchet = register('ratchet', (pat) => pat.sometimes(ply(2)))
|
||||||
|
`;
|
||||||
|
|
||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
activeFooter: 'intro',
|
activeFooter: 'intro',
|
||||||
keybindings: 'codemirror',
|
keybindings: 'codemirror',
|
||||||
|
|
@ -47,13 +61,14 @@ export const defaultSettings = {
|
||||||
isPanelPinned: false,
|
isPanelPinned: false,
|
||||||
isPanelOpen: true,
|
isPanelOpen: true,
|
||||||
userPatterns: '{}',
|
userPatterns: '{}',
|
||||||
prebakeScript: '',
|
prebakeScript: initialPrebakeScript,
|
||||||
audioEngineTarget: audioEngineTargets.webaudio,
|
audioEngineTarget: audioEngineTargets.webaudio,
|
||||||
isButtonRowHidden: false,
|
isButtonRowHidden: false,
|
||||||
isCSSAnimationDisabled: false,
|
isCSSAnimationDisabled: false,
|
||||||
maxPolyphony: 128,
|
maxPolyphony: 128,
|
||||||
multiChannelOrbits: false,
|
multiChannelOrbits: false,
|
||||||
includePrebakeScriptInShare: true,
|
includePrebakeScriptInShare: true,
|
||||||
|
settingsTab: 'settings',
|
||||||
};
|
};
|
||||||
|
|
||||||
let search = null;
|
let search = null;
|
||||||
|
|
@ -115,6 +130,7 @@ export function useSettings() {
|
||||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||||
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
||||||
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
||||||
|
export const setSettingsTab = (tab) => settingsMap.setKey('settingsTab', tab);
|
||||||
|
|
||||||
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);
|
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue