From 014b00a6420d34b80ebf4f6ac9aebd88fe236e5d Mon Sep 17 00:00:00 2001 From: rawan10101 Date: Thu, 6 Aug 2026 02:53:00 +0300 Subject: [PATCH 1/3] fix(wasm): fix browser input and runtime exit handling Use ev.key directly to preserve Caps Lock casing, implement clipboard paste, and prevent :q/:qa from leaving the runtime in a loop. --- src/wasm/app.js | 22 ++++++++++++++++++++-- src/wasm/nvim-worker.js | 30 +++++++++++++++++++++++++++--- src/wasm/rpc.js | 8 ++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/wasm/app.js b/src/wasm/app.js index 57d16286b5..cf6bcc2640 100644 --- a/src/wasm/app.js +++ b/src/wasm/app.js @@ -295,7 +295,7 @@ function translateKey(ev) { }; if (named[ev.key]) return named[ev.key]; if (ev.key.length === 1) { - let char = ev.shiftKey ? ev.key : ev.key.toLowerCase(); + let char = ev.key; if (char === "<") char = ""; if (!isCtrl && !isAlt) return char; let mod = ""; @@ -368,13 +368,22 @@ function main() { } } }); - + transport.onExit((code) => { + setStatus(`Neovim exited (code ${code}). Refresh the page to start a new session.`); + gridEl.style.opacity = "0.5"; + }); nvim.on("redraw", (params) => handleRedrawEvents(uiState, gridEl, modeEl, params), ); gridEl.addEventListener("click", () => gridEl.focus()); gridEl.addEventListener("keydown", (ev) => { + const isPasteShortcut = + (ev.ctrlKey && ev.shiftKey && ev.key.toLowerCase() === "v") || // Linux/Win: Ctrl+Shift+V + (ev.metaKey && ev.key.toLowerCase() === "v"); // macOS: Cmd+V + if (isPasteShortcut) { + return; + } const keys = translateKey(ev); if (!keys) return; ev.preventDefault(); @@ -382,6 +391,15 @@ function main() { .request("nvim_input", [keys]) .catch((e) => console.error("nvim_input failed", e)); }); + gridEl.addEventListener("paste", (ev) => { + ev.preventDefault(); + const text = ev.clipboardData.getData("text/plain"); + if (!text) return; + nvim + .request("nvim_paste", [text, true, -1]) + .catch((e) => console.error("paste nvim_input failed", e)); + }); + } if (!crossOriginIsolated) { diff --git a/src/wasm/nvim-worker.js b/src/wasm/nvim-worker.js index f277bf3576..3508d69ccf 100644 --- a/src/wasm/nvim-worker.js +++ b/src/wasm/nvim-worker.js @@ -511,7 +511,7 @@ self.onmessage = async (ev) => { ); }; - const { argc, argv } = makeArgv(m, ["nvim", "--embed", "--clean"]); + const { argc, argv } = makeArgv(m, ["nvim", "--embed"]); let ret; @@ -535,14 +535,38 @@ self.onmessage = async (ev) => { ret = await nvimPromise; console.log("NVIM EXITED"); + postMessage({ type: "exited", code: ret }); + try { + moduleRef.ccall("emscripten_force_exit", null, ["number"], [ret]); + } catch (e) { + safeStatus("force_exit failed: " + e); + } const unread = unreadCount(); safeStatus( `_nvim_main RETURNED ret=${ret}, unread bytes still in buffer=${unread}`, ); } catch (e) { + console.log('CAUGHT EXIT EXCEPTION', e, 'name=', e && e.name, 'status=', e && e.status, 'constructor=', e && e.constructor && e.constructor.name); + flushAll(); - safeStatus("EXCEPTION: " + e.message + "\nSTACK:\n" + e.stack); - throw e; + if (e && e.name === 'ExitStatus') { + safeStatus(`Neovim exited cleanly, code=${e.status}`); + ret = e.status; + + // Notify app.js so it can show a session ended state instead of + // leaving the UI looking frozen. + postMessage({ type: "exited", code: ret }); + try { + moduleRef.ccall("emscripten_force_exit", null, ["number"], [ret]); + } catch (fe) { + safeStatus("force_exit failed: " + fe); + } + } + else + { + safeStatus('EXCEPTION: ' + e.message + '\nSTACK:\n' + e.stack); + throw e; + } } flushAll(); diff --git a/src/wasm/rpc.js b/src/wasm/rpc.js index ecaf47942c..ae121bbd92 100644 --- a/src/wasm/rpc.js +++ b/src/wasm/rpc.js @@ -10,6 +10,7 @@ class WorkerTransport { this._bytesHandlers = []; this._statusHandlers = []; this._readyHandlers = []; + this._exitHandlers = []; this.worker = new Worker(workerPath); this.worker.onmessage = (ev) => { @@ -48,6 +49,10 @@ class WorkerTransport { console.log("[main] worker signaled ready, RPC channel should be live"); this._readyHandlers.forEach((h) => h()); } + else if (msg.type === "exited") { + console.log("worker signaled nvim exited, code=", msg.code); + this._exitHandlers.forEach((h) => h(msg.code)); + } } _emitStatus(text) { @@ -123,6 +128,9 @@ class WorkerTransport { onReady(cb) { this._readyHandlers.push(cb); } + onExit(cb) { + this._exitHandlers.push(cb); + } persist() { this.worker.postMessage({ type: "persist" }); } From 0eddc16ed0badae1fecc4e005d4e1f317b6d4102 Mon Sep 17 00:00:00 2001 From: rawan10101 Date: Sat, 8 Aug 2026 22:28:43 +0300 Subject: [PATCH 2/3] ci: add nvim.wasm build and nightly release Add a dedicated nvim.wasm build workflow and include the resulting WASM artifact in nightly releases. --- .github/scripts/build_wasm.sh | 81 ++++++++++++++++++++++++++++++++ .github/workflows/build_wasm.yml | 62 ++++++++++++++++++++++++ .github/workflows/release.yml | 47 ++++++++++++++++-- 3 files changed, 187 insertions(+), 3 deletions(-) create mode 100755 .github/scripts/build_wasm.sh create mode 100644 .github/workflows/build_wasm.yml diff --git a/.github/scripts/build_wasm.sh b/.github/scripts/build_wasm.sh new file mode 100755 index 0000000000..0ca95da381 --- /dev/null +++ b/.github/scripts/build_wasm.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +# Build nvim.wasm + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +OUTDIR="$ROOT/build-wasm" + +source "$ROOT/.emsdk/emsdk_env.sh" + +mkdir -p "$OUTDIR" + +if ! command -v emcc >/dev/null 2>&1; then + echo "ERROR: emcc not found" + exit 1 +fi + +if ! command -v zig >/dev/null 2>&1; then + echo "ERROR: zig not found" + exit 1 +fi + +emcc --version +zig version + +# Install Binaryen if it is not already available. +BINARYEN_VERSION=132 +BINARYEN_DIR="$ROOT/.binaryen" +BINARYEN_BIN="$BINARYEN_DIR/binaryen-version_${BINARYEN_VERSION}/bin" + +if [ ! -x "$BINARYEN_BIN/wasm-opt" ]; then + echo "Installing Binaryen ${BINARYEN_VERSION}..." + + mkdir -p "$BINARYEN_DIR" + + curl -L \ + "https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz" \ + -o /tmp/binaryen.tar.gz + + tar -xf /tmp/binaryen.tar.gz -C "$BINARYEN_DIR" +fi + +export PATH="$BINARYEN_BIN:$PATH" + +wasm-opt --version + +zig build nvim_bin \ + -Dtarget=wasm32-emscripten \ + -Dcpu=generic+atomics+bulk_memory+mutable_globals \ + -Demscripten-sysroot="$EMSDK/upstream/emscripten/cache/sysroot" \ + -Doptimize=ReleaseSmall + +ZIG_OUT="$ROOT/zig-out/bin" + +# Optimize the final WASM artifact with Binaryen. +wasm-opt -Oz -o "$ZIG_OUT/nvim.wasm" "$ZIG_OUT/nvim.wasm" + +required=( + nvim.wasm + nvim.js + nvim.data +) + +missing=() + +for name in "${required[@]}"; do + if [ -f "$ZIG_OUT/$name" ]; then + cp "$ZIG_OUT/$name" "$OUTDIR/$name" + else + missing+=("$name") + fi +done + +if [ "${#missing[@]}" -ne 0 ]; then + echo "ERROR: Missing required WASM artifacts: ${missing[*]}" + ls -la "$ZIG_OUT" || true + exit 2 +fi + +echo "build-wasm.sh finished successfully." diff --git a/.github/workflows/build_wasm.yml b/.github/workflows/build_wasm.yml new file mode 100644 index 0000000000..14bd499042 --- /dev/null +++ b/.github/workflows/build_wasm.yml @@ -0,0 +1,62 @@ +name: nvim.wasm build + +on: + pull_request: + branches: + - 'master' + - 'release-[0-9]+.[0-9]+' + paths: + - 'build.zig' + - 'build.zig.zon' + - '.github/scripts/build_wasm.sh' + - '.github/workflows/build_wasm.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-wasm: + name: Build nvim.wasm (Emscripten + Zig) + runs-on: ubuntu-24.04 + timeout-minutes: 90 + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Set up Zig + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + with: + version: 0.16.0 + use-cache: false + + - name: Cache emsdk, Binaryen & build outputs + uses: actions/cache@v6.1.0 + with: + path: | + ./.emsdk + ./.binaryen + key: ${{ runner.os }}-wasm-${{ hashFiles('build.zig', 'build.zig.zon', '.github/scripts/build_wasm.sh') }} + + - name: Install system deps + run: | + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \ + python3 python3-pip curl unzip + + - name: Install Emscripten SDK + run: | + set -euo pipefail + if [ ! -d ./.emsdk ]; then + git clone https://github.com/emscripten-core/emsdk.git ./.emsdk + cd ./.emsdk + ./emsdk install 6.0.2 + ./emsdk activate 6.0.2 + cd - + fi + + - name: Build nvim.wasm + run: .github/scripts/build_wasm.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c646a54e4..5f8e227f8e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -190,8 +190,49 @@ jobs: build/${{ matrix.archive_name }}.msi retention-days: 1 + wasm: + needs: setup + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Set up Zig + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + with: + version: 0.16.0 + use-cache: false + + - name: Install system deps + run: | + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \ + python3 python3-pip curl unzip + + - name: Install Emscripten SDK + run: | + set -euo pipefail + git clone https://github.com/emscripten-core/emsdk.git ./.emsdk + cd ./.emsdk + ./emsdk install 6.0.2 + ./emsdk activate 6.0.2 + cd - + + - name: Build nvim.wasm + run: .github/scripts/build_wasm.sh + + - name: Upload wasm artifact + uses: actions/upload-artifact@v7 + with: + name: nvim-wasm + path: build-wasm/* + retention-days: 1 + publish: - needs: [linux, macos, windows] + needs: [linux, macos, windows, wasm] runs-on: ubuntu-latest env: GH_REPO: ${{ github.repository }} @@ -244,6 +285,6 @@ jobs: run: | envsubst < "$GITHUB_WORKSPACE/.github/workflows/notes.md" > "$RUNNER_TEMP/notes.md" if [ "$TAG_NAME" != "nightly" ]; then - gh release create stable $PRERELEASE --notes-file "$RUNNER_TEMP/notes.md" --title "$SUBJECT" --target $GITHUB_SHA nvim-macos-x86_64/* nvim-macos-arm64/* nvim-linux-x86_64/* nvim-linux-arm64/* nvim-appimage-x86_64/* nvim-appimage-arm64/* nvim-win-x86_64/* nvim-win-arm64/* + gh release create stable $PRERELEASE --notes-file "$RUNNER_TEMP/notes.md" --title "$SUBJECT" --target $GITHUB_SHA nvim-macos-x86_64/* nvim-macos-arm64/* nvim-linux-x86_64/* nvim-linux-arm64/* nvim-appimage-x86_64/* nvim-appimage-arm64/* nvim-win-x86_64/* nvim-win-arm64/* nvim-wasm/* fi - gh release create $TAG_NAME $PRERELEASE --notes-file "$RUNNER_TEMP/notes.md" --title "$SUBJECT" --target $GITHUB_SHA nvim-macos-x86_64/* nvim-macos-arm64/* nvim-linux-x86_64/* nvim-linux-arm64/* nvim-appimage-x86_64/* nvim-appimage-arm64/* nvim-win-x86_64/* nvim-win-arm64/* + gh release create $TAG_NAME $PRERELEASE --notes-file "$RUNNER_TEMP/notes.md" --title "$SUBJECT" --target $GITHUB_SHA nvim-macos-x86_64/* nvim-macos-arm64/* nvim-linux-x86_64/* nvim-linux-arm64/* nvim-appimage-x86_64/* nvim-appimage-arm64/* nvim-win-x86_64/* nvim-win-arm64/* nvim-wasm/* From b78adb98bac2d589551a5b09bb0a7e39887321f4 Mon Sep 17 00:00:00 2001 From: rawan10101 Date: Thu, 13 Aug 2026 15:52:30 +0300 Subject: [PATCH 3/3] fix(wasm): remove custom log path and disable autoread --- src/wasm/nvim-worker.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wasm/nvim-worker.js b/src/wasm/nvim-worker.js index 3508d69ccf..0b4192c993 100644 --- a/src/wasm/nvim-worker.js +++ b/src/wasm/nvim-worker.js @@ -356,7 +356,6 @@ self.onmessage = async (ev) => { m.ENV.TERM = "xterm-256color"; m.ENV.HOME = "/home/user"; m.ENV.VIMRUNTIME = "/runtime"; - m.ENV.NVIM_LOG_FILE = "/home/user/nvim.log"; }, ], }); @@ -511,7 +510,7 @@ self.onmessage = async (ev) => { ); }; - const { argc, argv } = makeArgv(m, ["nvim", "--embed"]); + const { argc, argv } = makeArgv(m, ["nvim", "--embed", "--cmd", "set noautoread"]); let ret;