This is the practical "how do I build, run, and test this thing" doc.
SimpleNvimEditor (simplenvim) is a Go program that spawns a real nvim process
and renders it with Gio, a GPU-accelerated, immediate-mode
GUI toolkit. Gio compiles to a normal Go binary on Linux, macOS, and Windows,
but — like any GUI toolkit that talks to the OS windowing system — it needs a
small amount of platform tooling to compile on each OS. This doc covers
that setup once per machine; after that, go build is all you need.
Repo layout: the Go module (all source, tests, go.mod, go.sum) lives
under src/. Docs live in doc/ (this file, plus
ci-cd-setup.md for how PR checks / nightly / release
builds are wired up). Every go command below assumes you've cded into
src/ first.
- Go 1.26+ — https://go.dev/dl/ (the exact minimum is whatever
src/go.moddeclares; CI reads it from there rather than pinning a second copy of the version) - Neovim 0.9+ on your
PATH(or pass-nvim /path/to/nvim) — https://github.com/neovim/neovim/wiki/Installing-Neovim
Everything else below is what your OS needs so that cgo can compile Gio's
windowing/GPU backend.
Gio's Linux backend uses cgo to talk to X11/Wayland and EGL/Vulkan, so you
need the corresponding -dev packages and pkg-config installed before
building. go build will fail with a pkg-config ... was not found error
listing exactly what's missing if you skip this.
Debian / Ubuntu:
sudo apt-get update
sudo apt-get install -y \
pkg-config \
libx11-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev \
libxcursor-dev libxfixes-dev \
libwayland-dev \
libgl1-mesa-dev libvulkan-devFedora:
sudo dnf install -y pkg-config \
libX11-devel libxkbcommon-devel libxkbcommon-x11-devel \
libXcursor-devel libXfixes-devel \
wayland-devel \
mesa-libGL-devel vulkan-loader-develArch:
sudo pacman -S --needed pkgconf libx11 libxkbcommon libxkbcommon-x11 \
libxcursor libxfixes wayland mesa vulkan-icd-loaderYou do not need a display server to build; you only need one to run the GUI (a normal desktop session, or Wayland/X11 over SSH forwarding, or a virtual display like Xvfb for headless testing).
Gio's macOS backend uses Cocoa/Metal, which ship with the OS — you just need the command-line build tools:
xcode-select --installInstall Neovim via Homebrew: brew install neovim.
Gio's Windows backend uses Direct3D 11 via cgo, so you need a gcc
toolchain on PATH. The most common way to get one:
- Install MSYS2, then from an MSYS2 shell:
and add
pacman -S mingw-w64-x86_64-gcc
C:\msys64\mingw64\binto your WindowsPATH. - Or install TDM-GCC.
Install Neovim via scoop install neovim or the
official installer.
Verify gcc is visible to Go: go env CGO_ENABLED should print 1, and
gcc --version should succeed in the same shell you run go build from.
All the Go code (and go.mod/go.sum) lives under src/, which
is the Go module root — cd there first:
cd src
go build ./... # sanity check everything compiles
go build -o simplenvim ./cmd/simplenvim # produce a binary named simplenvim
# (simplenvim.exe on Windows)
./simplenvim path/to/file.txt # or just `./simplenvim` to open an
# empty buffergo build ./...; go build -o simplenvim ./cmd/simplenvim # one lineor skip the explicit build and just:
cd src
go run ./cmd/simplenvim path/to/file.txtOn macOS, prefer:
./scripts/run-macos.sh path/to/file.txtIt builds the binary, wraps it in build/SimpleNvimEditor.app (generating
AppIcon.icns from src/internal/app/icon.png), ad-hoc code-signs the
bundle, launches it through LaunchServices, and then prints the bundle
identity the running process actually got.
go build && ./simplenvim still works and is fine for iterating on editor
logic. What it cannot do is voice dictation — and it fails silently, with
no error and no log line.
The reason is that macOS resolves several per-app services — Dictation
(NSTextInputContext), microphone/TCC permission, and the Dock icon —
through the app's LaunchServices bundle identity. That identity comes from
Info.plist, which only exists inside a .app. A bare Mach-O binary has
none, so it inherits whatever launched it (Terminal, your editor, ...):
./simplenvim |
scripts/run-macos.sh |
|
|---|---|---|
CFBundleIdentifier |
[ NULL ] |
io.github.kgfly.simplenvimeditor |
fileType |
???? |
APPL |
| Dock icon | parent process's | the app's own |
| Voice dictation | never starts | works |
Dictation has nothing to attach a session to, so pressing the dictation shortcut does nothing at all. Everything else — typing, Nvim, rendering — is unaffected, which is what makes this confusing to diagnose.
The Dock icon is the quickest tell: the app's own icon means a real bundle identity; a generic or terminal icon means it has none, and dictation will not work. To confirm from the shell:
lsappinfo info -only bundleid "$(lsappinfo find pid=$(pgrep -n simplenvim) | head -1)"Note that running the executable from inside a built bundle also works,
since LaunchServices resolves identity from the enclosing .app — useful
when you want dictation and stdout at the same time:
./build/SimpleNvimEditor.app/Contents/MacOS/simplenvimFor shipping a .dmg, see Releasing;
packaging/macos/make-dmg.sh performs the same signing step.
| Flag | Description |
|---|---|
-nvim /path/to/nvim |
Overrides the nvim executable to launch (default: whatever the config file says, or plain nvim resolved via PATH). |
--maximized |
Starts the editor window maximized. |
Positional arguments open files as usual. To forward Nvim flags or commands,
put them after --; they are passed to Nvim unchanged and in order:
/Applications/SimpleNvimEditor.app/Contents/MacOS/simplenvim \
--maximized -- -c term -c edit /Users/k0g0kfq/data1/.nnn/n.todoAll simplenvim flags must come before --. This boundary prevents Nvim flags
such as -c from being mistaken for application flags.
For the config.toml file format (font/Nvim-launch settings, default
values, and file locations per OS), see the
Configuration section in the README.
Tests live under src/test/, split into three tiers: unit, integration,
and e2e. Run everything at once from src/ with:
go test ./...No external dependencies (no Nvim, no display). These test the protocol
state machine (uistate), key/mouse translation (input), config loading
(config), and the Gio grid painter (render) in isolation, using
hand-built redraw batches instead of a real Nvim connection.
go test ./test/unit/...
go test ./test/unit/... -race # check for data races (State is shared
# between the redraw-pump goroutine and
# the Gio frame goroutine)
go test ./test/unit/... -v # see each test nameThese always run and must always pass — they're the fast inner loop.
Spawn a real, isolated nvim -u NONE -n --embed child process (via the same
nvimproc.Process the app itself uses) and drive it through the actual
msgpack-rpc pipeline: attach, send key input, resize, send mouse events, and
quit — then verify the results both through our own mirrored uistate grid
and by asking the real Nvim process directly (e.g. getline(),
&columns), so a bug in our own decoding can't hide behind itself.
go test ./test/integration/...These skip themselves (not fail) if nvim isn't found on PATH, so the
suite stays green on a machine without Neovim installed. On a machine that
has it, they spawn real processes and take a few seconds.
Build the actual simplenvim binary, launch it under a virtual X11 display
(Xvfb), and verify it for real: a window with the right title appears, it
spawns an actual Nvim child process, a screenshot of the window shows
genuinely rendered content (not a blank frame), and killing the app cleans
up its Nvim child rather than leaking it.
go test ./test/e2e/...These require, all on PATH: nvim, go, and (Linux-only) Xvfb,
xdotool, and ImageMagick's import. They skip themselves on
non-Linux, or if any tool is missing — Xvfb is X11-specific, so this tier
intentionally doesn't try to run on macOS/Windows dev machines.
On Debian/Ubuntu, the extra e2e-only tools are:
sudo apt-get install -y xvfb xdotool imagemagick(Xvfb is provided by the xvfb package; ImageMagick provides import.)
Because the test tiers live in their own _test packages under test/...
rather than next to the source, plain -cover only reports on those
(mostly-empty) test packages themselves. To see real coverage of the
internal/... packages the tests exercise, use -coverpkg:
go test ./test/unit/... -coverpkg=./internal/... -coverprofile=cover.out
go tool cover -func=cover.outWorkflows live in .github/workflows/. The design
rationale (and the not-yet-implemented publishing channels) is in
ci-cd-setup.md.
| Workflow | Trigger | What it does |
|---|---|---|
pr.yml |
pull request / push to main |
Build, vet, gofmt, unit + integration (-race) on Linux/macOS/Windows, e2e on Linux, coverage gate |
build-matrix.yml |
called by others | Reusable: builds the binary natively on all 6 OS/arch combos, uploads archives |
package.yml |
called by release.yml |
Reusable: .deb/.rpm (nfpm), Windows .exe (Inno Setup), macOS .dmg (hdiutil) |
nightly.yml |
09:00 UTC daily, or manual | Builds only if main moved; publishes nightly-YYYYMMDD pre-release, keeps the newest 7 |
release.yml |
tag v*.*.*, or manual |
Full build + package + GitHub Release + build-provenance attestations |
pr.yml fails if total line coverage over internal/... drops below
80% (MIN_COVERAGE in the workflow). Coverage comes from the unit +
integration tiers only — e2e drives a separately-compiled binary, so its
execution isn't visible to -coverpkg (see §6).
The target in ci-cd-setup.md is 80%; the gate is set at 75% because
that's just under the measured 77.8% at the time it was added. Raise it as
the suite improves — it's a one-line change.
The gate, the gofmt check, and the e2e tier all run on Linux only. That's
deliberate: formatting is platform-independent, and the integration tier
skips itself when nvim is absent, so gating on a runner with a flaky
nvim install would fail for reasons unrelated to the change under test.
cd src
go build ./... && go vet ./... && gofmt -l .
go test ./test/unit/... ./test/integration/... -race \
-coverpkg=./internal/... -coverprofile=cover.out
go tool cover -func=cover.out | tail -1 # the gated number
go test ./test/e2e/...Validate workflow edits before pushing — this catches bad expressions and shell bugs that plain YAML linting misses:
actionlint # https://github.com/rhysd/actionlintGo to Actions → Nightly → Run workflow on GitHub, optionally check "Build even if there are no new commits since the last nightly", and click Run workflow.
Or use the CLI:
gh workflow run nightly.yml # normal run (skips if no new commits)
gh workflow run nightly.yml -f force=true # build regardlessPushing to main does not create a release automatically. An official
release can be triggered in 3 ways:
Go to Actions → Release → Run workflow, enter the version (e.g.
v1.0.0), and click Run workflow.
Or use the CLI:
gh workflow run release.yml -f version=v1.0.0Or push a server tag
git tag v1.0.0
git push origin v1.0.0Either way, release.yml:
- Validates the version matches
vX.Y.Z. - Builds the binary natively on all 6 OS/arch combinations
(Linux/macOS/Windows × amd64/arm64) via
build-matrix.yml. The version is stamped into the binary (simplenvim --version). - Produces native installers via
package.yml:.deband.rpm(nfpm), Windows.exe(Inno Setup), macOS.dmg(hdiutil). - Generates SHA-256 checksums and GitHub Artifact Attestations (supply-chain provenance).
- Creates a GitHub Release with all assets attached.
Builds are unsigned (Phase 1): macOS needs xattr -c
or right-click → Open on first launch, Windows needs More info →
Run anyway. Signing/notarization is Phase 2 — see ci-cd-setup.md §8.
Publishing to winget, Homebrew, AUR, and apt/rpm repos is not wired
up, since each needs secrets and external accounts that don't exist yet.
ci-cd-setup.md §7 has the recipes for when that changes.
pkg-config ... was not found in the pkg-config search path (Linux) —
install the missing -dev package(s) named in the error; see §2 above for
the full list up front.
Build succeeds but the window never opens / immediately exits — make
sure you actually have a display to render to ($DISPLAY or
$WAYLAND_DISPLAY set on Linux). For headless testing, run under Xvfb:
Xvfb :99 -screen 0 1024x768x24 & DISPLAY=:99 ./simplenvim.
Voice dictation does nothing (macOS) — you are almost certainly running
a bare binary rather than an app bundle. Build with ./scripts/run-macos.sh
instead of go build && ./simplenvim; see
the section above. The
Dock icon is the giveaway: a generic or terminal icon means the process has
no bundle identity, and Dictation has nothing to attach to.
Nvim seems to open but nothing happens after that — check that nvim
(or your configured -nvim path) actually starts standalone; a broken user
init.lua/init.vim that hangs on startup will hang simplenvim too, since
it's the same process.