Skip to content

waynehoover/git-status-watch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 status when 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--once for 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

brew install waynehoover/tap/git-status-watch

Or via Cargo:

cargo install git-status-watch

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

git-status-watch --once
# {"branch":"main","staged":0,"modified":2,"untracked":1,"conflicted":0,"ahead":1,"behind":0,"stash":0,"state":"clean"}

One-shot with custom format:

git-status-watch --once --format '{branch} +{staged} ~{modified} ?{untracked}'
# main +0 ~2 ?1

Watch mode (prints on each change):

git-status-watch --format '{branch} +{staged} ~{modified} ?{untracked} ⇡{ahead}⇣{behind}'
# 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:

[git_branch]
disabled = true

[git_status]
disabled = true

[custom.gitstatus]
command = "git-status-watch --once --format '{branch} +{staged} ~{modified} ?{untracked} ⇡{ahead}⇣{behind}'"
when = "git rev-parse --show-toplevel"
require_repo = true
format = "[$output]($style) "
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 --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=""

_git_status_watch_start() {
  if [[ -z "$__GSW_PID" ]] || ! kill -0 "$__GSW_PID" 2>/dev/null; then
    git-status-watch --format '{branch} +{staged} ~{modified} ?{untracked} ⇡{ahead}⇣{behind}' | while IFS= read -r line; do
      __gsw_line="$line"
      kill -USR1 $$ 2>/dev/null
    done &
    __GSW_PID=$!
    disown
  fi
}

TRAPUSR1() { zle && zle reset-prompt }
precmd_functions+=(_git_status_watch_start)
RPROMPT='${__gsw_line}'

Bash

__gsw_line=""

_git_status_watch_start() {
  if [[ -z "$__GSW_PID" ]] || ! kill -0 "$__GSW_PID" 2>/dev/null; then
    git-status-watch --format '{branch} +{staged} ~{modified} ?{untracked} ⇡{ahead}⇣{behind}' | while IFS= read -r line; do
      echo "$line" > /tmp/gsw_$$
    done &
    __GSW_PID=$!
    disown
  fi
  [[ -f /tmp/gsw_$$ ]] && __gsw_line=$(cat /tmp/gsw_$$)
}

PROMPT_COMMAND="_git_status_watch_start; $PROMPT_COMMAND"
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 [[ -n "$TMUX" ]]; then
  git-status-watch --format '{branch} +{staged} ~{modified} ?{untracked} ⇡{ahead}⇣{behind}' \
    | while IFS= read -r line; do echo "$line" > /tmp/gsw_tmux; done &
  disown
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
_zellij_git_status_watch() {
  [[ -n "$ZELLIJ" ]] || return
  local repo_root=$(git rev-parse --show-toplevel 2>/dev/null)
  git-status-watch --format ' {branch} +{staged} ~{modified} ?{untracked} ⇡{ahead}⇣{behind}' "$repo_root" | while IFS= read -r line; do
    zellij pipe "zjstatus::pipe::pipe_git_status::${line}"
  done &
  disown
}
precmd_functions+=(_zellij_git_status_watch)

How It Works

  1. Resolves the git repo root from the current directory (or a path argument)
  2. Computes and prints initial status immediately
  3. Watches .git/ and the worktree recursively via native filesystem events (notify)
  4. Debounces events (75ms default), filters to only relevant .git/ state files (HEAD, index, refs, sentinel files)
  5. On change: recomputes status, compares to previous, prints only if different
  6. Exits cleanly on broken pipe (consumer closed)

Status is computed by shelling out to git:

  • git status --porcelain=v2 --branch --no-optional-locks for 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

Multiple instances automatically coordinate via flock on a shared state file in $XDG_RUNTIME_DIR (or $TMPDIR): the first watcher becomes the leader, others become followers that watch the state file instead of the repo. This means N terminals = 1 git status call per change, not N. The --once fast path reads the cached state file when a leader is active (~0.1ms vs ~15ms).

License

MIT

About

Reactive git status watcher using native filesystem events

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages