git-status-watch
Reactive git status for your terminal. Watches your repo with native filesystem events (FSEvents/inotify) and outputs structured status instantly — no polling.
Why
Every existing tool for git status in your terminal (gitmux, tmux-gitbar, shell prompt plugins) works the same way: poll git status on a timer. Your status bar updates every 2-5 seconds whether anything changed or not, and misses changes between intervals.
git-status-watch flips this around. It uses native filesystem events to watch your repo and outputs a line only when something actually changes. This means:
- Instant updates — you see changes the moment they happen, not seconds later
- Zero wasted work — no CPU spent re-running
git statuswhen nothing changed - Works everywhere — a single compiled binary that outputs to stdout, so it plugs into any shell prompt, tmux, zellij, or anything else that can read a line of text
- Two modes —
--oncefor per-prompt freshness (like gitmux), watch mode for reactive updates between keypresses
A shell prompt can use both: --once on each Enter for immediate accuracy, plus a background watcher that triggers repaints when external changes happen (IDE saves, background fetches, other terminals).
Install
Or via Cargo:
Usage
git-status-watch [OPTIONS] [PATH]
Also works as a git subcommand:
git status-watch [OPTIONS] [PATH]
Options:
| Flag | Description |
|---|---|
--format <STR> |
Custom format string (see placeholders below) |
--once |
Print once and exit |
--state-dir <DIR> |
Write status to a file in this directory (keyed by repo path) |
--debounce-ms <MS> |
Debounce window in milliseconds (default: 75) |
--always-print |
Print on every filesystem event, even if unchanged |
By default, git-status-watch outputs JSON and keeps running, printing a new line whenever the git status changes.
Placeholders
| Placeholder | Description |
|---|---|
{branch} |
Branch name or short detached hash |
{staged} |
Staged file count |
{modified} |
Modified file count |
{untracked} |
Untracked file count |
{conflicted} |
Conflicted file count |
{ahead} |
Commits ahead of upstream |
{behind} |
Commits behind upstream |
{stash} |
Stash count |
{state} |
Operation state: merge, rebase, cherry-pick, bisect, revert, or empty |
Format strings support \t and \n escape sequences for tab and newline.
Examples
One-shot JSON:
# {"branch":"main","staged":0,"modified":2,"untracked":1,"conflicted":0,"ahead":1,"behind":0,"stash":0,"state":"clean"}
One-shot with custom format:
# main +0 ~2 ?1
Watch mode (prints on each change):
# main +0 ~2 ?1 ⇡1⇣0
# ... (updates reactively as files change)
Shell Integration
Starship
Add a custom module to ~/.config/starship.toml. Disable the built-in git modules to avoid duplicate info:
[]
= true
[]
= true
[]
= "git-status-watch --once --state-dir /tmp/gsw --format '{branch} +{staged} ~{modified} ?{untracked} ⇡{ahead}⇣{behind}'"
= "git rev-parse --show-toplevel"
= true
= "[$output]($style) "
= "bold purple"
Fish (with Tide)
Use --once in a custom Tide item for immediate status on Enter, plus a background watcher for reactive updates:
# ~/.config/fish/functions/_tide_item_gitstatus.fish
function _tide_item_gitstatus
set -l raw (command git-status-watch --once --state-dir /tmp/gsw --format \
'{branch}\t{staged}\t{modified}\t{untracked}\t{conflicted}\t{ahead}\t{behind}\t{stash}\t{state}' 2>/dev/null)
test -n "$raw"; or return
set -l fields (string split \t $raw)
set -l branch $fields[1]
# ... parse remaining fields, render with _tide_print_item
end
# ~/.config/fish/conf.d/gitstatus.fish — reactive repaints between keypresses
status is-interactive; or return
function _gitstatus_repaint --on-variable _gitstatus_signal_$fish_pid
commandline -f repaint
end
function _gitstatus_on_prompt --on-event fish_prompt
set -l repo_root (command git rev-parse --show-toplevel 2>/dev/null)
# start/restart git-status-watch in background, bump
# _gitstatus_signal_$fish_pid on each stdout line to trigger repaint
end
Zsh
Background watcher with TRAPUSR1 for reactive prompt refresh:
__gsw_line=""
precmd_functions+=(_git_status_watch_start)
RPROMPT='${__gsw_line}'
Bash
__gsw_line=""
PROMPT_COMMAND="_git_status_watch_start; "
PS1='\u@\h \w ${__gsw_line} \$ '
Tmux
Use --once for polling (like gitmux):
set -g status-right '#(git-status-watch --once --format " {branch} +{staged} ~{modified} ?{untracked} ⇡{ahead}⇣{behind}")'
set -g status-interval 2
Or use watch mode for reactive updates (no polling):
# In your shell startup:
if ; then
| while ; do ; done &
fi
set -g status-right '#(cat /tmp/gsw_tmux 2>/dev/null)'
set -g status-interval 1
Zellij (zjstatus)
Pipe directly into zjstatus for a reactive status bar:
# fish
function __zellij_git_status_watch --on-event fish_prompt
set -q ZELLIJ; or return
set -l repo_root (git rev-parse --show-toplevel 2>/dev/null)
# manage watcher lifecycle, pipe to zjstatus:
git-status-watch --format ' {branch} +{staged} ~{modified} ?{untracked} ⇡{ahead}⇣{behind}' "$repo_root" \
| while read -l line
zellij pipe "zjstatus::pipe::pipe_git_status::$line"
end &
end
# zsh
precmd_functions+=(_zellij_git_status_watch)
How It Works
- Resolves the git repo root from the current directory (or a path argument)
- Computes and prints initial status immediately
- Watches
.git/and the worktree recursively via native filesystem events (notify) - Debounces events (75ms default), filters to only relevant
.git/state files (HEAD, index, refs, sentinel files) - On change: recomputes status, compares to previous, prints only if different
- Exits cleanly on broken pipe (consumer closed)
Status is computed by shelling out to git:
git status --porcelain=v2 --branch --no-optional-locksfor branch, upstream, file counts- Stash reflog line count (
.git/logs/refs/stash) for stash count — no subprocess needed - Sentinel file checks (
.git/MERGE_HEAD, etc.) for operation state
When using --state-dir, multiple instances coordinate via flock: only one watches the repo (leader), others watch the state file (followers). This means N terminals = 1 git status call per change, not N.
License
MIT