jinzhongjia/znvim
neovim remote rpc client implementation with zig
refsA lightweight Neovim RPC client for Zig that discovers the runtime API via nvim_get_api_info, manages transports, and ships a high-level MessagePack helper so application code never needs to touch the raw zig-msgpack API.
ApiInfo, ApiFunction, ApiParameter) describing exactly what the connected Neovim instance exposes.nvim --embed processes.znvim.msgpack wraps zig-msgpack with ergonomic constructors (msgpack.array, msgpack.object, msgpack.encode, …) plus type-safe readers (msgpack.expectString, msgpack.asArray, …).zig build examples produces runnable binaries that demonstrate common workflows.--embed support (standard builds already have it)Add the package to your project using zig fetch:
zig fetch --save https://github.com/jinzhongjia/znvim/archive/main.tar.gz
Wire the module in build.zig:
const znvim_dep = b.dependency("znvim", .{
.target = target,
.optimize = optimize,
});
const znvim = znvim_dep.module("znvim");
exe.root_module.addImport("znvim", znvim);
const std = @import("std");
const znvim = @import("znvim");
const msgpack = znvim.msgpack;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer switch (gpa.deinit()) {
.ok => {},
.leak => std.debug.print("warning: leaked allocations\n", .{}),
};
const allocator = gpa.allocator();
const address = std.process.getEnvVarOwned(allocator, "NVIM_LISTEN_ADDRESS") catch {
std.debug.print("Set NVIM_LISTEN_ADDRESS to your Neovim socket before running.\n", .{});
return error.MissingAddress;
};
defer allocator.free(address);
var client = try znvim.Client.init(allocator, .{ .socket_path = address });
defer client.deinit();
try client.connect();
// Ask Neovim to evaluate a small expression.
const expr = try msgpack.string(allocator, "join(['zig', 'nvim'], '-')");
defer msgpack.free(expr, allocator);
const params = [_]msgpack.Value{expr};
const response = try client.request("vim_eval", ¶ms);
defer msgpack.free(response, allocator);
if (msgpack.asString(response)) |result| {
std.debug.print("Neovim answered: {s}\n", .{result});
}
}
Launch Neovim and point NVIM_LISTEN_ADDRESS at the exposed socket:
nvim --headless --listen /tmp/nvim.sock &
export NVIM_LISTEN_ADDRESS=/tmp/nvim.sock
zig build examples
./zig-out/bin/print_api
| Example | Description |
|---|---|
print_api.zig |
Connects and prints the first few discovered API functions. |
run_command.zig |
Issues an nvim_command notification that writes to :messages. |
eval_expression.zig |
Evaluates a Vimscript expression and inspects the returned payload. |
buffer_lines.zig |
Reads the current buffer, replaces its content, then verifies the change. |
api_lookup.zig |
Looks up metadata for a single API function by name. |
Build all examples in one go with zig build examples; binaries are placed under zig-out/bin/.
Comprehensive documentation is available in the doc/ directory:
👉 Start here: doc/README.md
The znvim.msgpack namespace aims to be the only interface most users need:
const payload = try msgpack.object(allocator, .{
.name = "my-command",
.enabled = true,
.retries = 3,
});
defer msgpack.free(payload, allocator);
const arr = try msgpack.array(allocator, &.{ payload });
const parsed = msgpack.expectArray(arr) catch return error.NotArray;
Use msgpack.encode(allocator, value) for generic encoding of common Zig types, and the expect* / as* helpers when decoding responses.
Run tests:
zig build test
Generate API documentation:
zig build docs
The generated documentation will be in the zig-out/docs/ directory. Open zig-out/docs/index.html in your browser.
Run performance benchmarks:
zig build run-benchmark
To work on the documentation, keep the English README.md and the Chinese translation README.zh.md in sync.
MIT