// ZPWR-HOOKS-EDITOR — SHARED STRYKE HOOKS CODE EDITOR

private frontend submodule · Monaco + monaco-vim + monaco-emacs + thin stryke-LSP adapter · esbuild IIFE bundle · build-on-each-consumer · one window.HooksEditor facade

Report GitHub
// Color scheme

>_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

EditorMonaco's edcore.main bundle — all editor contributions (suggest, hover, find, folding, multi-cursor, minimap, command palette) but no bundled languages
stryke languagestryke 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 editingDefault / 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 intelligencecompletion / 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
Themestryke-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-vimforced to its ESM build (dist/index.mjs); the browser condition's UMD bundle carries an AMD define() from the embedded CodeMirror vim keymap
monaco-editorbare 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.mjsthe 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.mjsthin stryke-LSP JSON-RPC client (createLspCore): init / notify / request / receive / connected / isInitialized / onDiagnostics / ready
src/hooks-editor-worker-entry.mjsMonaco base editor web-worker entry (no language workers)
scripts/build-hooks-editor.mjsesbuild 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 --lsp server the editor talks to