Go to file
2024-01-08 17:38:14 +01:00
src fix: options now use new (non-)API 2024-01-08 17:38:14 +01:00
.gitignore init 2023-04-23 16:11:05 +02:00
build.zig chore: update zig and nvim 2024-01-07 20:39:32 +01:00
generate_bindings.sh chore: update bindings & correct luajit library name 2023-11-26 10:14:58 +01:00
LICENSE init 2023-04-23 16:11:05 +02:00
nvim_all.h chore: update zig and nvim 2024-01-07 20:39:32 +01:00
nvim_c.zig chore: update zig and nvim 2024-01-07 20:39:32 +01:00
README.md docs: add README 2023-06-08 16:38:03 +02:00


Zig bindings for Neovim's internal C functions. The ABI follows Neovim's master branch!


  1. Add the dependency to your project's build.zig.zon:
    .name = "my-config",
    .version = "0.0.0",

    .dependencies = .{
        .znvim = .{
            .url = "https://mzte.de/git/LordMZTE/znvim/archive/<latest commit hash>.tar.gz",
            .hash = "<paste hash here; leave this field out when pasting the URL>",
  1. Structure your build.zig something like this:
const std = @import("std");

pub fn build(b: *std.build.Builder) !void {
    const target = b.standardTargetOptions(.{});

    if (target.os_tag orelse @import("builtin").os.tag == .windows)
        // windows is an error in many ways
        return error.Windows;

    const mode = b.standardOptimizeOption(.{});

    // Build your config as a dynamically linked library.
    const lib = b.addSharedLibrary(.{
        .name = "my-config",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = mode,

    // Declare the znvim dependency here.
    const znvim_dep = b.dependency("znvim", .{ .target = target, .optimize = mode });

    // Add both the nvim_c and znvim modules. You can import these independantly.
    lib.addModule("nvim", znvim_dep.module("nvim_c"));
    lib.addModule("znvim", znvim_dep.module("znvim"));

    // Link libc.

    // LuaJIT is required in order to create a native Lua module neovim can load as well as
    // for your Lua API.

    // IMPORTANT: without this, lua errors inside your module will cause nvim to SIGABRT!
    lib.unwind_tables = true;

    // Set the output path to be something sensical.
    b.getInstallStep().dependOn(&b.addInstallFile(lib.getOutputSource(), "share/nvim/mzte-nv.so").step);

  1. In your main.zig, you want a function called luaopen_my_config (where my_config is your module name) This function is loaded by neovim's LuaJIT runtime via dlsym and called as an entry point on each require of your module.
const znvim = @import("znvim");

// cImport LuaJIT
const c = @cImport({

pub const std_options = struct {
    // You may want to override logFn here in order to redirect std.log log messages to, for example
    // nvim notifications. Not doing so will clutter the GUI on log messages.

var is_initialized = false;

export fn luaopen_my_config(l: ?*c.lua_State) c_int {
    // Make sure we only run initiailzation code once.
    // I also recommend putting this into a seperate function you call from Lua once, but that's
    // outside the scope of this quick guide.
    if (!is_initialized) {
        is_initialized = true;

        // For example, you could set an option:
        // See docs of types inside the znvim module.
        znvim.OptionValue.of(true).setLog("number", .both) catch {};

    c.lua_newtable(l); // create a new lua table to return from `require`
    // ... add stuff to that table here
    return 1; // 1 indicates one value returned from the lua function
  1. Build your config to ~/.local
zig build \
  -Doptimize=ReleaseFast \ # Use ReleaseSafe for easier to debug segfaults :D
  -p ~/.local # The output path you set in build.zig will be appended to this.
  1. In Lua, add the path to your shared object to package.cpath:
package.cpath = package.cpath .. ";" .. vim.loop.os_homedir() .. "/.loca/share/nvim/my-config.so"

-- You can now require("my_config")!


The whole point of this is so I (and you too!) can write your neovim config in Zig, without going through the Lua API (or the rather bulky nvim_* API functions).

The way this works is by compiling your config's Zig part to a native Lua module, which is then loaded using just a few lines of Lua code. Here, you can also create a Lua API to call into.


znvim is split into 2 modules: nvim_c and znvim. The former is a translate-c'd file of all of neovim's headers while the latter contains a (work in progress) Zig API for them.


Apart from writing the Zig API, we need to keep nvim_c.zig updated. This file contains the raw C bindings. It's generated by generate_bindings.sh.