Passive coding telemetry for Neovim. You code, your habits update themselves — zero check-ins, zero keystroke overhead, zero cloud.
The editor client for focusd, the
local-first focus engine. It talks raw HTTP over the daemon's unix socket
through libuv (vim.uv): no curl forks, no threads, and Neovim's
single-threaded main loop never waits on anything.
-
Neovim >= 0.10
-
the focusd daemon binary:
go install github.com/SiiahK/focusd@latest
or grab a prebuilt one from the releases page. The plugin autostarts the daemon when it isn't running (safe by design: the daemon is a flock singleton).
{
"SiiahK/focusd.nvim",
event = "VeryLazy",
opts = {},
}use({
"SiiahK/focusd.nvim",
config = function()
require("focusd").setup()
end,
})require("focusd").setup({
sock = nil, -- unix socket path; nil = auto-discovery
-- (FOCUSD_SOCK → XDG_RUNTIME_DIR → OS cache dir)
bin = "focusd", -- daemon binary for autostart (found in PATH)
autostart = true, -- start the daemon when the socket doesn't answer
timeout_ms = 250, -- hard cap: the editor NEVER waits longer than this
flush_interval_ms = 15000, -- heartbeat batch cadence (saves flush immediately)
status_interval_ms = 5000, -- statusline cache refresh
max_backoff_ms = 30000, -- exponential backoff ceiling while daemon is down
})require("focusd").status() is a pure memory read — call it on every redraw
for free. An async 5s poll fills the cache and pauses when Neovim loses OS
focus.
-- lualine
sections = { lualine_x = { require("focusd").status } }| Command | Effect |
|---|---|
:FocusdStatus |
show the current focus state |
:FocusdFocusStart {id} |
start a focus session for a habit |
:FocusdFocusStop |
stop the active session and log it |
:FocusdFlush |
send pending heartbeats now |
:checkhealth focusd |
socket, binary and a real daemon round-trip |
Autocmds on the typing path (TextChanged, BufEnter, BufWritePost, …)
do O(1) in-memory counter bumps only. A timer flushes the accumulated
activity every 15s — immediately on save — as one coalesced batch to
POST /heartbeat. Every request carries a hard 250ms timeout, requests of
the same class cancel their stale predecessor ("latest wins"), and when the
daemon is down the plugin backs off exponentially and degrades silently:
your editor never lags, telemetry at worst loses its last interval.
Your data lands in a local SQLite file owned by the daemon. See where the week went:
focusd report