An open API service indexing awesome lists of open source software.

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

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:

detailed templates

#### `vcs-status-daemon` simple templates

These just use color and minimal symbols to represent the status:

simple templates

#### 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):

cloned prompts

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
```