@@ -5,13 +5,45 @@ autoload -U colors && colors
55
66if [ -z "$RUNNING_IN_VSCODE" ]; then
77 # Fancy prompt with git info - only when not in VS Code
8- # show git changes on the command line
98 autoload -Uz vcs_info add-zsh-hook
109 setopt prompt_subst
11- add-zsh-hook precmd vcs_info
1210
1311 zstyle ':vcs_info:*' enable git
1412
13+ ## Async git dirty-state rendering (Feb 2026)
14+ ## -------------------------------------------
15+ ## Previously, vcs_info ran with `check-for-changes true`, which calls `git diff`
16+ ## synchronously on every prompt. In repos with large LFS/xet-managed files
17+ ## (e.g. HuggingFace model repos with multi-GB binaries), `git diff` must
18+ ## SHA-hash the entire working-tree copy of those files, adding 4-5+ seconds
19+ ## of blocking delay per prompt.
20+ ##
21+ ## Fix: disable synchronous change detection so the prompt renders instantly
22+ ## with just the branch name (~10ms). Dirty state (staged/unstaged/untracked
23+ ## indicators: u, c, N) is computed in a background process. When it finishes,
24+ ## the prompt updates in-place via `zle reset-prompt`.
25+ ##
26+ ## Uses zsh's built-in `zle -F` fd-watcher -- no external plugins required.
27+ ## Works on zsh 5.8+.
28+ ##
29+ ## Indicators: u = unstaged changes, c = staged changes, N = untracked files
30+ ## e.g. [main] -> [mainucN] once the async check completes
31+
32+ # Disable the expensive synchronous change detection in vcs_info.
33+ # With this off, %u (unstaged) and %c (staged) are always empty.
34+ # We compute them asynchronously instead (see _async_git_callback below).
35+ zstyle ':vcs_info:*' check-for-changes false
36+
37+ # Simplified format: just branch (or branch|action during rebase/merge/etc).
38+ # Dirty indicators are appended separately via the async mechanism.
39+ zstyle ':vcs_info:git:*' formats '%b'
40+ zstyle ':vcs_info:git:*' actionformats '%b|%a'
41+
42+ # --- Async state variables ---
43+ typeset -g _git_info="" # Full formatted string for the prompt, e.g. "[mainucN]"
44+ typeset -g _async_git_fd="" # File descriptor for the current async worker process
45+ typeset -g _async_git_branch="" # Branch name captured when async was kicked off
46+
1547 # Detect if we're in a git worktree (fast filesystem check, no git commands)
1648 _get_worktree_name() {
1749 # Walk up to find .git file/dir
@@ -34,8 +66,46 @@ if [ -z "$RUNNING_IN_VSCODE" ]; then
3466 done
3567 }
3668
69+ # Called by zle when the async worker's fd becomes readable (i.e. it finished).
70+ # Reads the dirty indicators, rebuilds _git_info, and redraws the prompt.
71+ _async_git_callback() {
72+ local fd=$1
73+ local dirty=""
74+
75+ # Read the dirty indicators string from the background process
76+ read -r dirty <&$fd 2>/dev/null
77+
78+ # Clean up the fd watcher and close the descriptor
79+ zle -F "$fd"
80+ exec {fd}<&-
81+ _async_git_fd=""
82+
83+ # Rebuild _git_info with dirty indicators appended to the branch name
84+ if [[ -n "$_async_git_branch" ]]; then
85+ _git_info="[${_async_git_branch}${dirty}]"
86+ fi
87+
88+ # Redraw the prompt (only if zle is active, i.e. user is at a prompt)
89+ zle && zle reset-prompt
90+ }
91+
92+ # Cancel any in-flight async check (e.g. user pressed enter before it finished,
93+ # or cd'd to a different directory)
94+ _async_git_cancel() {
95+ if [[ -n "$_async_git_fd" ]]; then
96+ zle -F "$_async_git_fd" 2>/dev/null
97+ exec {_async_git_fd}<&- 2>/dev/null
98+ _async_git_fd=""
99+ fi
100+ }
101+
37102 precmd() {
38103 vcs_info
104+
105+ # Cancel any still-pending async check from the previous prompt cycle
106+ _async_git_cancel
107+
108+ # Worktree indicator (fast filesystem check, no git commands)
39109 local wt_name
40110 wt_name=$(_get_worktree_name)
41111 if [[ -n "$wt_name" ]]; then
@@ -53,42 +123,39 @@ if [ -z "$RUNNING_IN_VSCODE" ]; then
53123 else
54124 _venv_indicator=""
55125 fi
56- }
57-
58- # Enable checking for (un)staged changes, enabling use of %u and %c
59- zstyle ':vcs_info:*' check-for-changes true
60- # Use %m for misc info (we'll populate it with untracked indicator via hook)
61- zstyle ':vcs_info:git:*' formats '[%b%u%c%m]'
62- zstyle ':vcs_info:git:*' actionformats '[%b|%a%u%c%m]'
63- zstyle ':vcs_info:git:*' enable git
64126
65- # Hook to detect untracked files and show N indicator
66- zstyle ':vcs_info:git*+set-message:*' hooks git-untracked
127+ # Git info: render branch immediately, dirty state arrives async
128+ if [[ -n "$vcs_info_msg_0_" ]]; then
129+ _async_git_branch="$vcs_info_msg_0_"
130+ _git_info="[${_async_git_branch}]" # Immediate: just [branch], no dirty indicators yet
67131
68- +vi-git-untracked() {
69- if [[ $(git ls-files --other --exclude-standard 2>/dev/null | head -1) ]]; then
70- hook_com[misc]='N'
132+ # Kick off background process to compute dirty indicators.
133+ # This is where the expensive `git diff` runs without blocking the prompt.
134+ local async_pwd="$PWD"
135+ exec {_async_git_fd}< <(
136+ builtin cd -q "$async_pwd" 2>/dev/null || exit
137+ local indicators=""
138+ # Unstaged changes (u) -- this is the slow one in large/LFS repos
139+ git diff --no-ext-diff --quiet 2>/dev/null || indicators+="u"
140+ # Staged changes (c)
141+ git diff --cached --no-ext-diff --quiet 2>/dev/null || indicators+="c"
142+ # Untracked files (N)
143+ [[ -n "$(git ls-files --other --exclude-standard 2>/dev/null | head -1)" ]] && indicators+="N"
144+ echo "$indicators"
145+ )
146+ # Register callback for when the background process writes its result
147+ zle -F "$_async_git_fd" _async_git_callback
148+ else
149+ _git_info=""
150+ _async_git_branch=""
71151 fi
72152 }
73153
74- # %s - The current version control system, like git or svn.
75- # %r - The name of the root directory of the repository
76- # %S - The current path relative to the repository root directory
77- # %b - Branch information, like master
78- # %m - In case of Git, show information about stashes
79- # %u - Show unstaged changes in the repository
80- # %c - Show staged changes in the repository
81-
82- # %{$(tput setaf 60)%}%m # this is the machine IP/hostname
83-
84- # Right hand side of the prompt line
85- # RPROMPT="%{$(tput setaf 177)%}\$vcs_info_msg_0_"
86-
87- # PROMPT="%{$(tput setaf 60)%}%m %{$(tput setaf 105)%}%~${vcs_info_msg_0_}%{$(tput sgr0)%} %(?..[%?] )$ "
88-
89- # %{$(tput setaf 177)%}\$vcs_info_msg_0_ # means pink git information
154+ # %b - Branch information, like main
155+ # Indicators (computed async): u = unstaged, c = staged, N = untracked
156+ # e.g. [main], [mainuc], [mainucN], [main|rebase]
90157
91- PROMPT="\$_venv_indicator%{$(tput setaf 177)%}\$vcs_info_msg_0_ \$_worktree_indicator%{$(tput setaf 105)%}%~%{$(tput sgr0)%} %(?..[%?] )$ "
158+ PROMPT="\$_venv_indicator%{$(tput setaf 177)%}\$_git_info \$_worktree_indicator%{$(tput setaf 105)%}%~%{$(tput sgr0)%} %(?..[%?] )$ "
92159else
93160 # Simple prompt for VS Code terminal
94161 PROMPT="%{$(tput setaf 105)%}%~%{$(tput sgr0)%} %(?..[%?] )$ "
0 commit comments