https://github.com/quodlibetor/vcs-status-daemon
A tool to keep track of version control status in the background so your shell prompt can be fast
https://github.com/quodlibetor/vcs-status-daemon
git jujutsu prompt
Last synced: 3 months ago
JSON representation
A tool to keep track of version control status in the background so your shell prompt can be fast
- Host: GitHub
- URL: https://github.com/quodlibetor/vcs-status-daemon
- Owner: quodlibetor
- Created: 2026-03-15T23:34:04.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-31T23:39:56.000Z (3 months ago)
- Last Synced: 2026-04-01T01:55:39.685Z (3 months ago)
- Topics: git, jujutsu, prompt
- Language: Rust
- Homepage:
- Size: 3.2 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# vcs-status-daemon
A background daemon that pre-caches [Jujutsu](https://github.com/jj-vcs/jj) and Git repository status, so shell prompts can retrieve it in milliseconds instead of waiting for `jj` or `git` to run on every prompt.
> [!WARNING]
> This project is entirely AI generated but it seems to work 🤖 🤷
## Problem
VCS tools like `jj` and `git` can be slow in large repositories. Shell prompt integrations that call them on every prompt add noticeable latency. This daemon watches for repository changes via filesystem notifications and keeps a formatted status string in memory, ready to serve instantly — giving you a single, fast status tool for both Jujutsu and Git repos.
## Featrues
* Daemon pre-renders templates and writes them to a cache, fully formatted
status is read into an env var by the shell integration.
* Very fast (~2ms when run as a binary, microseconds with shell integration)
prompt display for both git and jujutsu.
* Several built-in themes, including clones of several popular ones.
* Write your on themes in tera, a Jinja-like templating language. (Take
inspiration from the [src/templates](./src/templates) directory.)
* Works well with starship since the fully rendered template can be displayed
directly by the env_var module.
## Installation
Requires a working `jj` and/or `git` CLI installation.
Install prebuilt binaries via shell script
```shell
curl --proto '=https' --tlsv1.2 -LsSf 'https://github.com/quodlibetor/vcs-status-daemon/releases/latest/download/vcs-status-daemon-installer.sh' | sh
```
Install prebuilt binaries via Homebrew or Linuxbrew:
```shell
brew install 'quodlibetor/tap/vcs-status-daemon'
```
Install prebuilt binaries via [mise](https://mise.jdx.dev/)
```shell
mise use -g 'github:quodlibetor/vcs-status-daemon@latest'
```
Install via cargo:
```shell
cargo install --git 'https://github.com/quodlibetor/vcs-status-daemon'
```
Or go to the [Releases](https://github.com/quodlibetor/vcs-status-daemon/releases) and download an artifact directly.
## Usage
### Shell prompt integration
The easiest thing to do is to just stick it in your prompt, this will work and averages ~2ms for me:
```zsh
PS1='$(vcs-status-daemon) \$ '
```
To be even faster you can avoid a subprocess spawn by initializing a shell function.
Add the following to your shell rc file. This sets a `VCS_STATUS` environment variable before each prompt — use it in your prompt however you like.
```zsh
# .zshrc
eval "$(vcs-status-daemon init zsh)"
PS1='%~ ${VCS_STATUS}%# '
```
```bash
# .bashrc
eval "$(vcs-status-daemon init bash)"
PS1='\w ${VCS_STATUS}\$ '
```
```fish
# config.fish
vcs-status-daemon init fish | source
set -g fish_prompt_vcs_status '$VCS_STATUS '
```
```nu
# config.nu (paste the output, or save to a file and source it)
# vcs-status-daemon init nu | save -f ~/.cache/vcs-status-daemon.nu
# source ~/.cache/vcs-status-daemon.nu
```
#### With starship
```zsh
# .zshrc
eval "$(vcs-status-daemon init zsh --starship)"
```
```bash
# .bashrc
eval "$(vcs-status-daemon init bash --starship)"
```
```fish
# config.fish
vcs-status-daemon init fish --starship | source
```
```nu
# config.nu
# vcs-status-daemon init nu --starship | save -f ~/.cache/vcs-status-daemon.nu
# source ~/.cache/vcs-status-daemon.nu
```
The `--starship` flag warns if it can't find your `starship.toml` or if it's missing the `[env_var.VCS_STATUS]` section. Add this to your `starship.toml`:
Recommended starship.toml configuration
```toml
# Disable starship's built-in git modules (vcs-status-daemon handles both jj and git)
[git_branch]
disabled = true
[git_commit]
disabled = true
[git_status]
disabled = true
[git_state]
disabled = true
[env_var.VCS_STATUS]
format = "$env_value "
```
### Themes
The exact output is configured via [tera](https://keats.github.io/tera/docs/#templates) templates.
Built in templates are defined in [src/templates](./src/templates), you can use them as inspiration
and define new ones in your config file via `vcs-status-daemon config edit`.
You can set any named template with `vcs-status-daemon config set template.name NAME`.
And you can view the source code defining a template with the
`vcs-status-daemon template print NAME` command.
There are several built-in templates that you can view with the
`vcs-status-daemon template show` command:
#### vcs-status-daemin detailed templates
These include line counts in addition to file counts:

#### `vcs-status-daemon` simple templates
These just use color and minimal symbols to represent the status:

#### Clones of popular git prompts
Clones of templates from [gitstatus / p10k](https://github.com/romkatv/gitstatus),
[starship](https://starship.rs/),
[oh-my-zsh](https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/git-prompt/README.md),
and [pure](https://github.com/factcondenser/pure-prompt):

And see "Format templates" and "Configuration" below to write your own.
### Commands
```sh
# Query status for the current directory (default, auto-starts daemon)
vcs-status-daemon
# Generate shell integration code (sets $VCS_STATUS before each prompt)
vcs-status-daemon init (zsh|bash|fish|nu) [--starship]
# Manage config
vcs-status-daemon config set K=V # set a config value
vcs-status-daemon config edit # open config in $EDITOR, with
vcs-status-daemon config path # print config file path
# Preview and manage templates
vcs-status-daemon template show [NAMES...] [-n] # show templates (all, or filtered by name)
vcs-status-daemon template print NAME # print raw template source
vcs-status-daemon template format "{{ change_id }}" # test a custom template
vcs-status-daemon template set --name nerdfont # set active template by name (alias for config set)
vcs-status-daemon template set --format "{{ ... }}" # set an inline format template (alias for config set)
vcs-status-daemon template debug [--repo PATH] # show template with variable values annotated inline
# Restart the daemon (graceful shutdown + restart)
vcs-status-daemon restart
# Show daemon status (running, PID, uptime, watched repos)
vcs-status-daemon status
```
The client sends its current directory to the daemon, which walks up the directory tree to find a repo root (`.jj/` or `.git/`). The mapping from directory to repo root is cached. When run outside a recognized repository, the client exits silently with exit code 0, making it safe for unconditional prompt use.
### Runtime directory
Both client and daemon resolve paths from a shared runtime directory:
1. `VCS_STATUS_DAEMON_DIR` environment variable (if set)
2. Default: `/tmp/vcs-status-daemon-$USER/`
The directory contains:
- `sock` — Unix domain socket
- `pid` — daemon PID file (for `restart` and `status`)
- `cache/` — cached status files (read by the shell function for the fastest path)
- `daemon.log` — log output (rotated at 5 MB)
The daemon also accepts a `--dir` CLI flag, which takes priority over the environment variable. When the client auto-starts the daemon, it always passes its resolved directory via `--dir` to ensure both sides agree.
## Configuration
Configuration is loaded from `~/.config/vcs-status-daemon/config.toml`. All fields are optional and have sensible defaults.
```toml
# How many ancestor commits to search for bookmarks in jj repos (default: 10)
bookmark_search_depth = 10
# Enable ANSI color output (default: true)
color = true
[template]
# Built-in template to use: "ascii" (default), "nerdfont", "unicode", "simple", or "minimal"
name = "ascii"
# Explicit format template (Tera syntax, overrides name if set)
# format = "..."
# User-defined variables injected into the template rendering context.
# Built-in templates use these to control bookmark limiting.
[template.vars]
# max_bookmarks = "5" # show at most 5 bookmarks, then "(+N more)"
# prioritize_branches = "bwm/*" # glob: matching bookmarks are shown first
# User-defined named templates (selected via template.name)
# [templates]
# my_template = "{{ change_id }} {{ description }}"
```
## Format template
The `format` field is a [Tera](https://keats.github.io/tera/docs/) template string. Tera uses `{{ variable }}` for interpolation, `{% if %}` / `{% elif %}` / `{% endif %}` for conditionals, and `{% for x in list %}` / `{% endfor %}` for loops.
### VCS type detection
The daemon detects whether a repository uses jj or git (jj wins if both `.jj/` and `.git/` are present) and exposes `is_jj` and `is_git` booleans. Use these to write templates that work for both:
```tera
{% if is_jj %}{{ change_id }}{% elif is_git %}{% if has_branch %}{{ branch }}{% else %}{{ commit_id }}{% endif %}{% endif %}
```
### Template variables
#### VCS type
| Variable | Type | Description |
|---|---|---|
| `is_jj` | bool | `true` if the repo is a jj repository. |
| `is_git` | bool | `true` if the repo is a plain git repository (no `.jj/`). |
#### Shared fields (both jj and git)
| Variable | Type | Description |
|---|---|---|
| `commit_id` | string | Short commit ID. For jj repos with `color = true`, includes jj's native ANSI coloring. |
| `description` | string | First line of the commit description (jj) or commit message summary (git). |
| `empty` | bool | `true` if the working commit (jj) or HEAD commit (git) has no changes. |
| `conflict` | bool | `true` if there are conflicts (jj conflict markers or git merge conflicts). |
#### Diff stats
There are three groups of diff stat variables. For jj repos (which have no staging area), `*_working_tree` and unsuffixed/`*_total` are identical, and `*_staged` is always 0. For git repos, all three groups are independently populated.
`file_mad_count` is the total number of **m**odified + **a**dded + **d**eleted files (i.e., `files_modified + files_added + files_deleted`).
| Variable | Type | jj | git |
|---|---|---|---|
| `file_mad_count_working_tree` | integer | Files changed in `@` vs parent | Unstaged: working tree vs index |
| `lines_added_working_tree` | integer | Lines added in `@` | Unstaged lines added |
| `lines_removed_working_tree` | integer | Lines removed in `@` | Unstaged lines removed |
| `file_mad_count_staged` | integer | Always 0 | Staged: index vs HEAD |
| `lines_added_staged` | integer | Always 0 | Staged lines added |
| `lines_removed_staged` | integer | Always 0 | Staged lines removed |
| `file_mad_count` | integer | Same as `file_mad_count_working_tree` | Total: working tree vs HEAD |
| `lines_added_total` | integer | Same as `lines_added_working_tree` | Total lines added |
| `lines_removed_total` | integer | Same as `lines_removed_working_tree` | Total lines removed |
The default template uses unsuffixed/`*_total` since it gives the complete picture for both VCS types.
#### jj-only fields
These are populated only for jj repositories. In git repositories they are empty/false/zero.
| Variable | Type | Description |
|---|---|---|
| `change_id` | string | Short change ID (8 chars). With `color = true`, includes jj's native ANSI coloring. |
| `bookmarks` | list | List of bookmark objects (see below). |
| `has_bookmarks` | bool | `true` if any bookmarks were found in the ancestor search range. |
| `divergent` | bool | `true` if the working commit is divergent. |
| `hidden` | bool | `true` if the working commit is hidden. |
| `immutable` | bool | `true` if the working commit is immutable. |
#### git-only fields
These are populated only for git repositories. In jj repositories they are empty/false.
| Variable | Type | Description |
|---|---|---|
| `branch` | string | Current branch name. Empty when HEAD is detached. |
| `has_branch` | bool | `true` if on a branch, `false` if HEAD is detached. |
| `rebasing` | bool | `true` if a rebase is in progress (interactive or non-interactive). |
#### Workspace/worktree fields
| Variable | Type | Description |
|---|---|---|
| `workspace_name` | string | Workspace name (jj) or worktree directory name (git). `"default"` / `"main"` for the primary workspace/worktree. |
| `is_default_workspace` | bool | `true` if this is the default workspace (jj) or main worktree (git). |
#### Bookmark objects (jj only)
Each item in the `bookmarks` list has:
| Field | Type | Description |
|---|---|---|
| `name` | string | Bookmark name, e.g. `"main"`. |
| `distance` | integer | Number of commits between `@` and the bookmarked commit. `0` means the bookmark is on `@`. |
| `display` | string | Pre-formatted display string: `"main"` when distance is 0, `"main+2"` otherwise. |
| `tracking` | string | Tracking status: `"tracked"`, `"untracked"`, `"ahead"`, `"behind"`, or `"sideways"`. |
#### Template variables (`[template.vars]`)
User-defined variables from `[template.vars]` are injected into the Tera rendering context alongside VCS variables. Built-in templates use these to control bookmark limiting:
| Variable | Effect |
|---|---|
| `max_bookmarks` | Maximum number of bookmarks to display. Excess bookmarks are replaced with a "(+N more)" entry. |
| `prioritize_branches` | Glob pattern (e.g. `bwm/*`, `*main*`). Matching bookmarks are sorted first before the limit is applied. |
Example config:
```toml
[template.vars]
max_bookmarks = "5"
prioritize_branches = "bwm/*"
```
You can also define arbitrary variables for use in custom templates — any key in `[template.vars]` becomes available as a Tera variable.
#### `limit_bookmarks` filter
The `limit_bookmarks` filter truncates a bookmark list and appends a "(+N more)" indicator:
```tera
{{ bookmarks | limit_bookmarks(count=3, prioritize="bwm/*") }}
```
| Argument | Type | Description |
|---|---|---|
| `count` | integer (required) | Maximum number of bookmarks to show. |
| `prioritize` | string (optional) | Glob pattern — matching bookmarks are sorted first. Supports `*` wildcards. |
When `bookmarks.len() > count`, the filter returns the first `count` items plus a synthetic entry with `display: "(+N more)"`. You can use it in custom templates directly, or let the built-in templates apply it automatically via `max_bookmarks` / `prioritize_branches` template vars.
#### Color filters
Colors are applied using Tera's filter syntax: `{{ value | green }}`. When `color = true`, filters wrap the value in ANSI escape codes. When `color = false`, filters are no-ops (the value passes through unchanged).
| Filter | ANSI code |
|---|---|
| `bold` | `\e[1m` |
| `dim` | `\e[2m` |
| `italic` | `\e[3m` |
| `underline` | `\e[4m` |
| `red` | `\e[31m` |
| `green` | `\e[32m` |
| `yellow` | `\e[33m` |
| `blue` | `\e[34m` |
| `magenta` | `\e[35m` |
| `cyan` | `\e[36m` |
| `white` | `\e[37m` |
| `bright_red` | `\e[91m` |
| `bright_green` | `\e[92m` |
| `bright_yellow` | `\e[93m` |
| `bright_blue` | `\e[94m` |
| `bright_magenta` | `\e[95m` |
| `bright_cyan` | `\e[96m` |
| `bright_white` | `\e[97m` |
You can apply filters to variables (`{{ branch | green }}`), string literals (`{{ "CONFLICT" | bright_red }}`), or concatenated values (`{{ "+" ~ lines_added_total | bright_green }}`).
### Built-in templates
Five templates are included. Select one with `template.name` in your config:
```toml
[template]
name = "nerdfont"
```
#### `ascii` (default)
Works in any terminal. Example output:
- **jj**: `JJ xlvlt main [3 +10-5]` or `JJ xlvlt (EMPTY)`
- **git**: `+- main [3 +10-5]` or `+- abc1234` (detached HEAD)
#### `nerdfont`
Requires a [Nerd Font](https://www.nerdfonts.com/). Uses iconic glyphs for VCS type, conflicts, divergence, etc. Example output:
- **jj**: `󱗆 xlvlt main [3 +10-5]` or `󱗆 xlvlt ∅`
- **git**: `ó°Š¢ main [3 +10-5]` or `ó°Š¢ abc1234` (detached HEAD)
#### `unicode`
Uses standard Unicode symbols (no Nerd Fonts needed). Example output:
- **jj**: `⋈ xlvlt ≡ main [3 +10-5]`
- **git**: `± main [3 +10-5]` or `± abc1234` (detached HEAD)
#### `simple`
Branch/bookmark name with compact diff-type indicators. Shows `[~+-?]` when there are changes: `~` modified, `+` added, `-` deleted, `?` untracked. For jj, shows the bookmark name, description, or change ID depending on context, with immutable commits highlighted in red.
#### `minimal`
Just the branch or bookmark name, color-coded by state. Green = clean, yellow = changes, red = unstaged (git), magenta = untracked (git). For jj, shows the bookmark name, description, or change ID depending on context.
### User-defined templates
You can define your own named templates in the config and select them with `template.name`:
```toml
[template]
name = "minimal"
[templates]
minimal = "{{ commit_id }} {{ description }}"
```
User templates take priority over built-in ones — you can override `ascii` or `nerdfont` with your own version.
Use `vcs-status-daemon template print ` to view the raw source of any template (includes are inlined automatically). This is a good starting point for writing your own.
Use `vcs-status-daemon template debug` to see the current template with each variable's value annotated inline (e.g. `{{ change_id_prefix=su | magenta }}`), plus a list of available variables not used by the template.
The built-in `ascii`, `nerdfont`, and `unicode` templates all share the same body (`detail.tera`) and differ only in their icon constants. You can use the same pattern in your own templates — set icon variables with `{% set %}` and then `{% include "detail.tera" %}`:
```toml
[template]
name = "my_icons"
[templates]
my_icons = '''
{% set jj_icon = "jj" -%}
{% set git_icon = "git" -%}
{% set bookmark_prefix = "" -%}
{% set rebasing_icon = "REBASING" -%}
{% set conflict_icon = "!!" -%}
{% set divergent_icon = "DIVERGENT" -%}
{% set hidden_icon = "HIDDEN" -%}
{% set immutable_icon = "IMMUTABLE" -%}
{% set empty_icon = "(empty)" -%}
{% set stale_icon = "STALE" -%}
{% set workspace_open = "(" -%}
{% set workspace_close = ")" -%}
{% include "detail.tera" %}'''
```
### Inline format override
The `format` field directly sets the template string, overriding `template.name`:
```toml
[template]
format = "{{ change_id }} {{ branch }}"
```
In TOML, use multi-line literal strings (`'''`) for readability. Use Tera's `{%-` whitespace trimming to prevent newlines from appearing in the output:
```toml
[template]
format = '''
{% if is_jj %}{{ change_id }}
{%- for b in bookmarks %} {{ b.display | blue }}{% endfor %}
{%- elif is_git %}{% if has_branch %}{{ branch | blue }}{% else %}{{ commit_id }}{% endif %}
{%- endif %}'''
```
### Custom template examples
**jj-only, minimal** -- just change ID and bookmarks, no color:
```toml
color = false
[template]
format = '''
{{ change_id }}
{%- for b in bookmarks %} {{ b.display }}{% endfor %}'''
```
**git-only, minimal** -- branch name (or commit hash when detached):
```toml
color = false
[template]
format = "{% if has_branch %}{{ branch }}{% else %}{{ commit_id }}{% endif %}"
```
**Verbose, both VCS** -- with description/state details:
```toml
[template]
format = '''
{% if is_jj %}{{ change_id }} {{ commit_id | dim }}
{%- for b in bookmarks %} {{ b.display | blue }}{% endfor %}
{%- if description %} {{ description | dim }}{% endif %}
{%- elif is_git %}{% if has_branch %}{{ branch | blue }}{% else %}{{ commit_id | dim }}{% endif %}
{%- endif %}
{%- if file_mad_count > 0 %} {{ file_mad_count | bright_blue }}f {{ "+" ~ lines_added_total | bright_green }} {{ "-" ~ lines_removed_total | bright_red }}{% endif %}
{%- if empty %} {{ "empty" | yellow }}{% endif %}
{%- if conflict %} {{ "conflict!" | bright_red }}{% endif %}'''
```
**Custom bookmark formatting** -- show distance differently (jj only):
```toml
[template]
format = '''
{{ change_id }}
{%- for b in bookmarks %} {{ b.name | cyan }}{% if b.distance > 0 %}~{{ b.distance }}{% endif %}{% endfor %}
{%- if empty %} {{ "empty" | dim }}{% endif %}'''
```
## How it works
### Architecture
```
Shell prompt calls: vcs-status-daemon (client mode, the default)
|
| connects to Unix domain socket
v
vcs-status-daemon daemon (background server)
|
+-- detects VCS type (jj wins if both .jj/ and .git/ exist)
+-- watches repo via filesystem notifications (notify)
+-- on change: shells out to jj or git, caches formatted status
+-- serves cached text to clients instantly
```
- **Single binary, two modes**: `daemon` (background server) and default (client/query)
- **Auto-start**: the client spawns the daemon automatically if it's not running
- **Multi-repo**: the daemon tracks multiple repositories, each with its own filesystem watcher
- **Dual VCS**: supports both jj and git repositories, with jj taking priority when both are present
### Step by step
1. **Client** connects to the daemon's Unix domain socket. If the daemon isn't running, the client spawns it as a detached background process and retries.
2. **Daemon** receives a query with a directory path and walks up to find the repo root. It detects the VCS type (`.jj/` takes priority over `.git/`). On first query for a repo, it:
- Sets up a filesystem watcher appropriate for the VCS type:
- **jj**: watches `.jj/repo/op_heads/heads/` (operations) and the working directory
- **git**: watches `.git/` and `.git/refs/` (ref changes) and the working directory
- Shells out to `jj` or `git` to gather status info (commit, branch/bookmarks, diff stats)
- Renders the format template and caches the result
3. **On filesystem changes**, the daemon immediately re-runs the appropriate VCS command and updates the cache (at most one refresh per repo at a time; events arriving during a refresh trigger a re-refresh on completion). For jj repos, it uses `--ignore-working-copy` when only `.jj/` internal files changed (faster, avoids unnecessary snapshots), and omits it when working copy files changed (so `jj` snapshots first, giving accurate diff stats).
4. **Subsequent queries** return the cached string instantly.
## Development
```sh
# Run tests (requires jj and git to be installed)
cargo nextest run
# Or with plain cargo
cargo test
# Build
cargo build --release
```