A Neovim plugin for automatic coding activity tracking. Works with the ShellTime daemon to monitor your development time across projects.
- Automatic Time Tracking - Passively monitors coding activity
- Language Detection - Uses the buffer's filetype, falling back to the file extension, and reports it as the VS Code language id (
sh→shellscript,text→plaintext, …) so a language adds up across editors - Project Analytics - Tracks time per project/workspace
- Git Integration - Records activity by git branch
- Debug Detection - Marks activity as
debuggingwhile an nvim-dap session is active - Debouncing & Batching - At most one heartbeat per file every 30s, sent in batches every 2 minutes
- Offline Support - Keeps heartbeats in memory and retries them when the daemon is unavailable or has code tracking turned off
- Flush on Exit - Sends pending heartbeats when Neovim quits, and leaves what the daemon can't take for it to send later, so short sessions aren't lost
- CLI Update Check - Warns once per session when your ShellTime CLI is out of date
curl -sSL https://shelltime.xyz/i | bashAfter installation, reload your shell configuration:
- zsh:
source ~/.zshrc - fish:
source ~/.config/fish/config.fish - bash:
source ~/.bashrc
shelltime initThis will:
- Open your browser to authenticate with your ShellTime account
- Install shell hooks for your shell (zsh/fish/bash)
- Start the ShellTime daemon
Make sure your ShellTime config (~/.shelltime/config.yaml) contains:
codeTracking:
enabled: trueOr in TOML format (~/.shelltime/config.toml):
[codeTracking]
enabled = trueConfigs created by shelltime init already include this; older configs may not. The daemon ignores editor heartbeats unless it is true. Recent daemons pick the change up right away; older ones read it only at startup, so restart them after changing it (for example with shelltime daemon reinstall).
- Neovim >= 0.10.0 (Neovim only; classic Vim isn't supported)
- ShellTime daemon running (see Prerequisites)
curl(optional, for the CLI update check)
{
"shelltime/coding-extension-vim",
event = "VeryLazy",
opts = {},
}use {
"shelltime/coding-extension-vim",
config = function()
require("shelltime").setup()
end
}Plug 'shelltime/coding-extension-vim'Then add to your init.lua (or lua require("shelltime").setup() to init.vim):
require("shelltime").setup()Calling setup() is required: loading the plugin only registers its commands. With lazy.nvim, opts = {} calls it for you.
-
Complete the Prerequisites - Install CLI, authenticate, and enable code tracking
-
Install the plugin using your preferred plugin manager (see above)
-
Start coding - The plugin automatically tracks your activity!
The plugin reads the same files as the ShellTime CLI: the first of ~/.shelltime/config.yaml, config.yml and config.toml, with the first config.local.yaml, config.local.yml or config.local.toml merged over it.
-- Default setup (reads the CLI's config files)
require("shelltime").setup()
-- Read one file instead (YAML or TOML)
require("shelltime").setup({
config = "/path/to/your/config.yaml",
})config is the only setup() option. If there is no config file, the plugin uses the defaults below. The files are checked for changes every few seconds, so most settings apply without restarting Neovim, codeTracking.enabled included (heartbeatInterval needs a restart).
| Option | Type | Default | Description |
|---|---|---|---|
socketPath |
string | /tmp/shelltime.sock |
Unix socket path for daemon |
codeTracking.enabled |
boolean | true |
Enable/disable tracking in the plugin. The daemon also requires it to be true (see Prerequisites) |
debug |
boolean | false |
Enable debug logging |
heartbeatInterval |
number | 120000 |
How often pending heartbeats are sent, in ms |
debounceInterval |
number | 30000 |
Minimum time between heartbeats for the same file, in ms |
apiEndpoint |
string | - | ShellTime API URL used for the CLI update check (written by shelltime init) |
webEndpoint |
string | - | ShellTime web URL used in the update command (written by shelltime init) |
| Command | Description |
|---|---|
:ShellTimeStatus |
Show daemon connection status (version, uptime, platform) and pending heartbeats |
:ShellTimeFlush |
Manually flush pending heartbeats to daemon |
:ShellTimeEnable |
Enable tracking for this session (needs setup(); doesn't override codeTracking.enabled: false) |
:ShellTimeDisable |
Send pending heartbeats (what the daemon doesn't take goes to its resend file, as on exit) and disable tracking for this session |
The plugin monitors these Neovim events:
| Event | Trigger |
|---|---|
BufEnter |
Opening or switching to a file |
TextChanged / TextChangedI |
Editing text |
BufWritePost |
Saving a file |
CursorMoved / CursorMovedI |
Moving cursor |
Commands that pass through many buffers (:bufdo, :argdo, :cfdo, session restore) count only the buffer that is current when they finish, and the text change of a reload from disk (the file changed outside after a git checkout, or a formatter or agent rewrote it) isn't counted as editing, while an undo back to the saved state is. Saves with :wa are attributed to each buffer written, at its own cursor.
Only regular files are tracked. Special buffers (help, terminal, quickfix, etc.), unnamed buffers, files inside .git/, and non-file:// URLs such as oil:// or fugitive:// are skipped.
Heartbeats are:
- Deduplicated: Buffer and cursor events at the same file and cursor position as the last event are skipped (edits always count)
- Debounced: Max 1 heartbeat per file per 30 seconds. Saves count once per window too, since auto-save plugins write after every change: the last save inside the window is kept, at its own time and cursor, and sent with the file's next heartbeat or the next flush, so the save before
:wqalways counts - Batched: Sent to the daemon over its Unix socket every 2 minutes, at most 500 per message
- Flushed on exit: Pending heartbeats are sent on
VimLeavePre, and Neovim waits up to 1.5s for them and for any send already in progress. What the daemon doesn't take, or doesn't acknowledge in time, goes to~/.shelltime/coding-heartbeat.data.log, which the daemon resends once it runs - Queued: Kept in memory and retried on the next flush if the daemon is unavailable or has code tracking turned off (up to 5,000; oldest dropped first)
Each heartbeat includes:
- File info: Path, language, line count, cursor position
- Project info: Name, root path, git branch
- Editor info: Neovim version, plugin version
- System info: Hostname, OS, OS version
- Activity: Timestamp, whether it was a save event, category (
codingordebugging)
The project root is the git repository the file is in (as in VS Code and JetBrains, so a monorepo's packages count toward one project). Outside a repository it is the nearest parent directory containing package.json, Cargo.toml, go.mod, pyproject.toml, setup.py, Makefile, CMakeLists.txt, .project or .root, otherwise the file's directory. The project name is its last two path segments, e.g. code/my-app. The branch is read from the repository's HEAD, without running git, except in a reftable repository, whose HEAD is a stub: there it comes from git rev-parse --abbrev-ref HEAD, asked at most every 30 seconds per repository.
On startup, if the daemon is reachable, the plugin asks <apiEndpoint>/api/v1/cli/version-check (via curl) whether the daemon's CLI version is the latest. If not, it shows a warning once per session with the update command (curl -sSL <webEndpoint>/i | bash). The check is skipped when apiEndpoint or webEndpoint is not set.
-
Check if daemon is running:
shelltime daemon status # socket, running state, and whether Code Tracking is enabled # If not running, run: shelltime init
-
Verify config file exists:
cat ~/.shelltime/config.yaml -
Enable debug mode in config (messages are shown via
vim.notify):debug: true
-
Check status in Neovim:
:ShellTimeStatus
Run :ShellTimeStatus to check:
- If "Disconnected", ensure daemon is running and
socketPathmatches the daemon's socket - If pending heartbeats > 0, try
:ShellTimeFlush - If it warns that code tracking is off in the daemon config, set
codeTracking.enabledtotrue(and restart an older daemon). The daemon drops heartbeats until then; the plugin keeps them queued