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
- 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 (saves always count), sent in batches every 2 minutes
- Offline Support - Keeps heartbeats in memory and retries them when the daemon is unavailable
- Flush on Exit - Sends pending heartbeats when Neovim quits, 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, and it reads the setting at startup, so restart the daemon 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)
- Git (optional, for branch tracking)
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 settings from ~/.shelltime/config.yaml by default:
-- Default setup (uses ~/.shelltime/config.yaml)
require("shelltime").setup()
-- Custom config path
require("shelltime").setup({
config = "/path/to/your/config.yaml",
})config is the only setup() option. The plugin reads just this one YAML file: not config.toml, config.yml or config.local.*. If the file is missing, it uses the defaults below. The file is re-read whenever it changes, so most settings apply without restarting Neovim (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 |
Flush pending heartbeats 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 |
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 (except saves)
- Batched: Sent to the daemon over its Unix socket every 2 minutes
- Flushed on exit: Pending heartbeats are sent on
VimLeavePre(Neovim waits up to 1.5s) - Queued: Kept in memory and retried on the next flush if the daemon is unavailable (up to 5,000; oldest dropped first). They are not written to disk, so they are lost if Neovim exits while the daemon is down
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 nearest parent directory containing .git, 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.
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) and copies that command to the + register when a clipboard is available. 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 "Connected" and flushes succeed but no activity shows up, check that
codeTracking.enabledistruefor the daemon and restart it. The daemon drops heartbeats otherwise, and the plugin can't tell