Skip to main content

Neovim

Introduction

Neovim statuslines re-evaluate on every redraw. Traditional approaches — running git branch, parsing kubectl config current-context, or reading battery files — add latency on each redraw cycle. beachcomber eliminates that cost: cached data is returned in microseconds over a persistent Unix socket connection.

The Lua SDK auto-detects neovim and uses vim.uv (neovim's built-in libuv bindings) for non-blocking socket access. No external dependencies are required inside neovim. Outside neovim, the SDK falls back to luasocket if available, or shells out to comb as a last resort.

Prerequisites

  • The beachcomber daemon must be running. Verify with comb s from a terminal.
  • The Lua SDK must be available in neovim's Lua path. Install via LuaRocks:
luarocks install libbeachcomber

Or place the SDK's beachcomber.lua (or beachcomber/ directory) somewhere on package.path that neovim can find — for example, inside your Neovim config under lua/.

Without the SDK

If you do not want to install the Lua SDK, you can call the comb binary directly using vim.fn.system(). This forks a process on each call, but because comb g returns in under 1ms it is still fast enough for statusline use:

-- lua/beachcomber-status.lua (no SDK required)
-- comb g returns plain text by default. g = get, no suffix needed.
local M = {}

local function comb_get(key, path)
local cmd = path
and string.format('comb g %s %s', key, vim.fn.shellescape(path))
or string.format('comb g %s', key)
local result = vim.fn.system(cmd)
if vim.v.shell_error ~= 0 or result == '' then
return ''
end
return vim.trim(result)
end

function M.git_branch() return comb_get('git.branch', vim.fn.getcwd()) end
function M.git_dirty()
return comb_get('git.dirty', vim.fn.getcwd()) == 'true' and '*' or ''
end
function M.battery()
local pct = comb_get('battery.percent')
return pct ~= '' and pct .. '%' or ''
end
function M.kube() return comb_get('kubecontext.context') end

return M

This module has the same interface as the SDK-based version below, so lualine and heirline examples work with either.

With the Lua SDK

The SDK uses vim.uv for non-blocking socket access — no process forking. This is the recommended approach if you can install the SDK.

The SDK exposes a single connect() call that returns a persistent client. Call client:get(key, path) to retrieve a value. The second argument is the working directory path and is required for providers like git that are path-scoped.

Create a helper module so the same client is reused across your config:

-- lua/beachcomber-status.lua
local comb = require('beachcomber')
local client = comb.connect()

local M = {}

function M.git_branch()
local result = client:get('git.branch', vim.fn.getcwd())
if result and result:is_hit() then
return result.data
end
return ''
end

function M.git_dirty()
local result = client:get('git.dirty', vim.fn.getcwd())
if result and result:is_hit() and result.data == true then
return '*'
end
return ''
end

function M.battery()
local result = client:get('battery.percent')
if result and result:is_hit() then
return result.data .. '%'
end
return ''
end

function M.kube()
local result = client:get('kubecontext.context')
if result and result:is_hit() then
return result.data
end
return ''
end

return M

To use it with the built-in statusline, set vim.o.statusline to call your module functions. Because vim.o.statusline evaluates %{} expressions, use a small wrapper via statusline option with %! or set it from a function:

-- In init.lua or a status module
local bc = require('beachcomber-status')

local function build_statusline()
local branch = bc.git_branch()
local dirty = bc.git_dirty()
local kube = bc.kube()
local bat = bc.battery()

local left = ''
if branch ~= '' then
left = ' ' .. branch .. dirty .. ' '
end

local right = ''
if kube ~= '' then right = right .. ' ' .. kube end
if bat ~= '' then right = right .. ' ' .. bat end

return left .. '%=' .. right
end

vim.o.statusline = '%!v:lua.require("beachcomber-status").statusline()'

-- Expose the function at a module level so %! can reach it
local bc = require('beachcomber-status')
bc.statusline = build_statusline

lualine.nvim

lualine accepts plain Lua functions as components. Any function that returns a string can be used directly. Pass the helper module functions as component values:

local bc = require('beachcomber-status')

require('lualine').setup({
sections = {
lualine_a = { 'mode' },
lualine_b = {
{ bc.git_branch, icon = '' },
{ bc.git_dirty, color = { fg = '#ff6666' } },
},
lualine_c = { 'filename' },
lualine_x = {
{ bc.kube, icon = '☸', cond = function() return bc.kube() ~= '' end },
},
lualine_y = { 'filetype' },
lualine_z = {
{ bc.battery, icon = '', cond = function() return bc.battery() ~= '' end },
},
},
})

lualine calls each component function on every statusline redraw. Because beachcomber returns cached data over a persistent Unix socket connection, this is effectively free — no process spawning, no filesystem reads on the hot path.

The cond field suppresses a component entirely when its value is empty, which avoids rendering a bare icon with no content.

heirline.nvim

heirline is lower-level than lualine. Components are Lua tables with provider, condition, and hl fields. The provider function returns the string to render; condition controls whether the component is included at all.

local bc = require('beachcomber-status')

local GitBranch = {
condition = function() return bc.git_branch() ~= '' end,
provider = function() return ' ' .. bc.git_branch() .. bc.git_dirty() .. ' ' end,
hl = { fg = '#7aa2f7', bold = true },
}

local KubeContext = {
condition = function() return bc.kube() ~= '' end,
provider = function() return '☸ ' .. bc.kube() .. ' ' end,
hl = { fg = '#7dcfff' },
}

local Battery = {
condition = function() return bc.battery() ~= '' end,
provider = function() return ' ' .. bc.battery() .. ' ' end,
hl = { fg = '#9ece6a' },
}

require('heirline').setup({
statusline = {
GitBranch,
{ provider = '%=' }, -- center spacer
KubeContext,
Battery,
},
})

Note that condition and provider are both called on each redraw cycle. Calling bc.git_branch() twice per component (once in condition, once in provider) is fine — the SDK returns cached data from memory on the second call within the same redraw.

If you want to avoid the double call, cache the value locally inside a surrounding component table using heirline's init hook:

local GitBranch = {
init = function(self)
self.branch = bc.git_branch()
self.dirty = bc.git_dirty()
end,
condition = function(self) return self.branch ~= '' end,
provider = function(self) return ' ' .. self.branch .. self.dirty .. ' ' end,
hl = { fg = '#7aa2f7', bold = true },
}

Troubleshooting

  • SDK not found: if require('beachcomber') fails, the SDK is not on neovim's package.path. Check :lua print(package.path) and ensure the SDK's location is included.
  • Verify from neovim command line: :lua print(require('beachcomber').connect():get('git.branch', vim.fn.getcwd()).data) should print the branch name. If it prints nil, the daemon is not running or the socket cannot be found.
  • Without the SDK: the vim.fn.system() approach has no dependency to troubleshoot — if comb g git.branch . works in your terminal, it works in neovim.

See the Troubleshooting guide for general diagnostics.