>_ZPWR-HOOKS-EDITOR
One editor, built per app, single-sourced. The shared stryke Hooks code editor behind the MenkeTechnologies app stack — a Monaco editor wired with monaco-vim, monaco-emacs and a thin stryke-LSP adapter, bundled with esbuild into vendored IIFE artifacts. Extracted from Audio-Haxor so the editor is no longer duplicated across apps; the source lives here in src/ and each app builds it with its own monaco-* dependencies.
Four consumers, one source
All four apps load the same editor source. The three Tauri apps serve frontend/ statically and vendor this repo at crates/zpwr-hooks-editor; zpwr-daw is JUCE and loads the bundle in its WebView, vendoring at libs/zpwr-hooks-editor.
| Audio-Haxor (Tauri) | serves frontend/; vendors at crates/zpwr-hooks-editor |
| traderview (Tauri) | serves frontend/; vendors at crates/zpwr-hooks-editor |
| ztranslator (Tauri) | serves frontend/; vendors at crates/zpwr-hooks-editor |
| zpwr-daw (JUCE) | loads the bundle in its WebView; vendors at libs/zpwr-hooks-editor |
What it is
| Editor | Monaco's edcore.main bundle — all editor contributions (suggest, hover, find, folding, multi-cursor, minimap, command palette) but no bundled languages |
| stryke language | stryke is a Perl-5 superset, so it reuses Monaco's Perl Monarch grammar (basic-languages/perl) for tokenization + the bracket/comment language config, registered under the stryke language id |
| Modal editing | Default / Vim / Emacs via monaco-vim and monaco-emacs, switchable at runtime through setMode; the vim status/command line renders into a caller-supplied element |
| Language intelligence | completion / hover / diagnostics / definitions / references / rename / signature help / document symbols / code actions / semantic tokens come from the stryke language server (stryke --lsp) over a caller-supplied transport — the editor speaks LSP JSON-RPC directly via a thin adapter, not monaco-languageclient |
| Theme | stryke-cyberpunk — neon on near-black, matching the apps' aesthetic, with the suggest/hover widgets styled explicitly |
Load order (consuming app)
The consumer's index.html loads the CSS link + the JS bundle; the editor exposes a single global facade, window.HooksEditor. The Monaco base worker is fetched at runtime via MonacoEnvironment.getWorker (no TS/JSON/CSS language workers — all language intelligence comes from the stryke LSP).
<link rel='stylesheet' href='lib/hooks-editor.bundle.css'> <script src='lib/hooks-editor.bundle.js'></script> <!-- lib/hooks-editor.worker.js is fetched at runtime, not script-tagged -->
Mounting an editor
HooksEditor.create(parent, opts) mounts an editor into parent and returns a handle. With uri set and an LSP transport wired (see below), language features come online once the stryke handshake completes; without a uri the LSP layer stays off and it is a plain stryke-highlighted editor.
const handle = window.HooksEditor.create(parentEl, {
uri: 'file:///project/hook.stk', // identifies the doc to the LSP (omit to disable LSP)
doc: '# my stryke hook\n', // initial text
onChange: (text) => save(text), // fires on every edit with full text
mode: 'vim', // 'default' | 'vim' | 'emacs'
statusBar: vimStatusEl, // optional element for the vim status/command line
});
handle.getValue(); // current text
handle.setValue(text); // replace text
handle.focus();
handle.setMode('emacs'); // switch modal mode at runtime
handle.destroy(); // dispose editor, model, markers, LSP didClose
Wiring the stryke LSP transport
The editor speaks LSP JSON-RPC but does not own the transport. The host (e.g. Rust) connects stryke --lsp, adds Content-Length framing on the server's stdin, and bridges messages. The app calls initClient with a send function and feeds inbound server messages back via receive.
window.HooksEditor.initClient({
send: (message) => host.sendToServer(message), // client → server (raw JSON-RPC)
});
host.onServerMessage((msg) => window.HooksEditor.receive(msg)); // server → client
window.HooksEditor.clientReady(); // transport connected?
await window.HooksEditor.whenReady(); // true once the stryke initialize handshake completed
whenReady() resolves true only after the server answered initialize — suitable for a status indicator, unlike a transport "connected" flag. Call it after initClient.
window.HooksEditor facade
initClient(transport) | register Monaco providers, then start the LSP client over transport.send |
receive(message) | feed one raw JSON-RPC message from the server to the client |
clientReady() | transport connected (a send function is installed) |
whenReady() | promise resolving true once the stryke initialize handshake completed |
create(parent, opts) | mount an editor; returns a handle with getValue / setValue / focus / setMode / destroy |
Build (build-on-each-consumer)
The editor source lives here in src/, but the bundle is built inside the consuming app so esbuild resolves that app's monaco-* deps and writes into its frontend/lib/. Each consumer keeps esbuild + the pinned monaco-editor / monaco-vim / monaco-emacs in devDependencies and invokes the bundler from its project root (e.g. via tauri.conf.json beforeDevCommand):
node crates/zpwr-hooks-editor/scripts/build-hooks-editor.mjs
The output dir defaults to <cwd>/frontend/lib; override with the HOOKS_EDITOR_OUT env var. The generated bundles are build artifacts (gitignore them in the consumer). The bundler emits IIFE, minified, ES2020, with Monaco's codicon .ttf inlined as a data URL so the single .css artifact is self-contained.
Bundler resolve fixes
scripts/build-hooks-editor.mjs carries esbuild resolve overrides because the default browser/CommonJS resolution pulls in AMD define() builds that throw in the WebView and silently degrade the editor to a textarea:
monaco-vim | forced to its ESM build (dist/index.mjs); the browser condition's UMD bundle carries an AMD define() from the embedded CodeMirror vim keymap |
monaco-editor | bare imports pinned to esm/vs/editor/edcore.main.js — the same lean ESM instance the entry uses, instead of the AMD min build monaco-emacs' require() would pull |
monaco-editor/esm/* | deep subpaths without a file extension get .js appended and resolve to the package dir directly (the package exports map only resolves extensioned paths) |
Source layout
src/hooks-editor-entry.mjs | the editor entry — Monaco instance, stryke language registration, cyberpunk theme, vim/emacs keymaps, LSP↔Monaco provider/marker mapping, the window.HooksEditor facade |
src/lsp-client-core.mjs | thin stryke-LSP JSON-RPC client (createLspCore): init / notify / request / receive / connected / isInitialized / onDiagnostics / ready |
src/hooks-editor-worker-entry.mjs | Monaco base editor web-worker entry (no language workers) |
scripts/build-hooks-editor.mjs | esbuild bundler — reads src/ from this repo, writes the bundle into the consuming app |
The app stack
zpwr-hooks-editor is the shared editor component across the MenkeTechnologies apps. Browse the rest via the MenkeTechnologiesMeta umbrella repo:
- Audio-Haxor — Tauri v2 desktop app; original home of the editor
- traderview — Tauri trading/market app
- ztranslator — Tauri MIDI/OSC/DMX/Link translation app
- zpwr-daw — JUCE DAW; loads the bundle in its WebView
- strykelang — the stryke language +
stryke --lspserver the editor talks to