From 5f9fa8e3ab03443fbdce647cc92534d4fea519ce Mon Sep 17 00:00:00 2001 From: Justin Hileman Date: Fri, 15 May 2026 02:33:01 -0400 Subject: [PATCH] Improve git prompt performance --- README.md | 8 + functions/__bobthefish_colors.fish | 30 ++- functions/__bobthefish_git_async_enabled.fish | 210 +++++++++++++++ functions/__bobthefish_git_async_worker.fish | 110 ++++++++ .../__bobthefish_git_branch_from_parts.fish | 25 ++ functions/__bobthefish_prompt_git_async.fish | 137 ++++++++++ functions/__bobthefish_prompt_git_pwd.fish | 13 + functions/__bobthefish_prompt_git_sync.fish | 212 +++++++++++++++ functions/fish_prompt.fish | 245 +----------------- 9 files changed, 752 insertions(+), 238 deletions(-) create mode 100644 functions/__bobthefish_git_async_enabled.fish create mode 100644 functions/__bobthefish_git_async_worker.fish create mode 100644 functions/__bobthefish_git_branch_from_parts.fish create mode 100644 functions/__bobthefish_prompt_git_async.fish create mode 100644 functions/__bobthefish_prompt_git_pwd.fish create mode 100644 functions/__bobthefish_prompt_git_sync.fish diff --git a/README.md b/README.md index 368469a..056d2dd 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,14 @@ This feature is disabled by default. Use `yes` to show the currently executing [ Use `no` to disable Git integration. If you're doing this for performance reasons, try some of the options below before disabling it entirely! +#### `set -g theme_display_git_async no` + +Git status is rendered asynchronously by default in interactive shells. Bobthefish shows the prompt immediately, computes branch and status flags in the background, then repaints the prompt when the Git metadata is ready. Use `no` to restore synchronous Git rendering. Async rendering is currently disabled when `theme_git_worktree_support` is `yes`. + +#### `set -g theme_git_async_refresh_interval 0` + +Bobthefish waits at least one second between async Git refresh workers by default. If a prompt render is skipped during that interval, Bobthefish schedules one trailing refresh. Set this to `0` to check Git state on every prompt render. + #### `set -g theme_display_git_dirty no` Use `no` to hide Git dirty state. Set the Git `bash.showDirtyState` option on a per-repository basis to disable it just for especially large repos. diff --git a/functions/__bobthefish_colors.fish b/functions/__bobthefish_colors.fish index 34a5d16..9d035c7 100644 --- a/functions/__bobthefish_colors.fish +++ b/functions/__bobthefish_colors.fish @@ -19,6 +19,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo green $colorfg set -x color_repo_work_tree black $colorfg --bold + set -x color_repo_pending black $colorfg --bold set -x color_repo_dirty brred $colorfg set -x color_repo_staged yellow $colorfg @@ -55,6 +56,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo green $colorfg set -x color_repo_work_tree white $colorfg --bold + set -x color_repo_pending white $colorfg --bold set -x color_repo_dirty brred $colorfg set -x color_repo_staged yellow $colorfg @@ -91,6 +93,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo green $colorfg set -x color_repo_work_tree brgrey $colorfg --bold + set -x color_repo_pending brgrey $colorfg --bold set -x color_repo_dirty brred $colorfg set -x color_repo_staged yellow $colorfg @@ -127,6 +130,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo green $colorfg set -x color_repo_work_tree grey $colorfg --bold + set -x color_repo_pending grey $colorfg --bold set -x color_repo_dirty brred $colorfg set -x color_repo_staged yellow $colorfg @@ -169,6 +173,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green $grey set -x color_repo_work_tree $grey $grey --bold + set -x color_repo_pending $grey $grey --bold set -x color_repo_dirty $red $grey set -x color_repo_staged $yellow $grey @@ -222,6 +227,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $base0B $colorfg set -x color_repo_work_tree $base06 $colorfg --bold + set -x color_repo_pending $base06 $colorfg --bold set -x color_repo_dirty $base08 $colorfg set -x color_repo_staged $base09 $colorfg @@ -275,6 +281,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $base0B $colorfg set -x color_repo_work_tree $base02 $colorfg --bold + set -x color_repo_pending $base02 $colorfg --bold set -x color_repo_dirty $base08 $colorfg set -x color_repo_staged $base09 $colorfg @@ -328,6 +335,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green $colorfg set -x color_repo_work_tree $base2 $colorfg --bold + set -x color_repo_pending $base2 $colorfg --bold set -x color_repo_dirty $red $colorfg set -x color_repo_staged $yellow $colorfg @@ -381,6 +389,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green $colorfg set -x color_repo_work_tree $base02 $colorfg --bold + set -x color_repo_pending $base02 $colorfg --bold set -x color_repo_dirty $red $colorfg set -x color_repo_staged $yellow $colorfg @@ -427,6 +436,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green[1] $green[3] set -x color_repo_work_tree $grey[1] $white --bold + set -x color_repo_pending $grey[1] $white --bold set -x color_repo_dirty $red[2] $white set -x color_repo_staged $orange[1] $orange[3] @@ -472,6 +482,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green[2] $bg[1] set -x color_repo_work_tree $bg[1] $fg[2] --bold + set -x color_repo_pending $bg[1] $fg[2] --bold set -x color_repo_dirty $red[2] $fg[2] set -x color_repo_staged $yellow[1] $bg[1] @@ -516,7 +527,8 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_path_nowrite_basename $red[1] $bg[2] --bold set -x color_repo $green[2] $fg[1] - set -x color_repo_work_tree $bg[1] $fg[2] --bold + set -x color_repo_work_tree $bg[1] $fg[2] --bold + set -x color_repo_pending $bg[1] $fg[2] --bold set -x color_repo_dirty $orange[2] $bg[2] set -x color_repo_staged $yellow[1] $bg[1] @@ -562,6 +574,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green $bg set -x color_repo_work_tree $selection $fg --bold + set -x color_repo_pending $selection $fg --bold set -x color_repo_dirty $red $bg set -x color_repo_staged $yellow $bg @@ -615,6 +628,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $base0E $colorfg set -x color_repo_work_tree $base02 $colorfg --bold + set -x color_repo_pending $base02 $colorfg --bold set -x color_repo_dirty $base0B $colorfg set -x color_repo_staged $base0D $colorfg @@ -675,6 +689,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green $mantle set -x color_repo_work_tree $surface2 $mantle --bold + set -x color_repo_pending $surface2 $mantle --bold set -x color_repo_dirty $red $mantle set -x color_repo_staged $yellow $mantle @@ -736,6 +751,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green $mantle set -x color_repo_work_tree $surface2 $mantle --bold + set -x color_repo_pending $surface2 $mantle --bold set -x color_repo_dirty $red $mantle set -x color_repo_staged $yellow $mantle @@ -797,6 +813,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green $mantle set -x color_repo_work_tree $surface2 $mantle --bold + set -x color_repo_pending $surface2 $mantle --bold set -x color_repo_dirty $red $mantle set -x color_repo_staged $yellow $mantle @@ -858,6 +875,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green $mantle set -x color_repo_work_tree $surface2 $mantle --bold + set -x color_repo_pending $surface2 $mantle --bold set -x color_repo_dirty $red $mantle set -x color_repo_staged $yellow $mantle @@ -917,7 +935,8 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_path_nowrite_basename $dark_grey $red_orange --bold set -x color_repo $light_green $darker_grey - set -x color_repo_work_tree $dark_grey $light_grey --bold + set -x color_repo_work_tree $dark_grey $light_grey --bold + set -x color_repo_pending $dark_grey $light_grey --bold set -x color_repo_dirty $red $darker_grey set -x color_repo_staged $pale_gold $darker_grey @@ -962,6 +981,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green[3] $white[4] set -x color_repo_work_tree $grey[4] $grey[4] --bold + set -x color_repo_pending $grey[4] $grey[4] --bold set -x color_repo_dirty $red[3] $grey[4] set -x color_repo_staged $yellow[3] $grey[4] @@ -1009,6 +1029,7 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_repo $green[1] $green[3] set -x color_repo_work_tree $grey[3] $white --bold + set -x color_repo_pending $grey[3] $white --bold set -x color_repo_dirty $red[2] $white set -x color_repo_staged $orange[1] $orange[3] @@ -1030,6 +1051,9 @@ function __bobthefish_colors -S -a color_scheme -d 'Define colors used by bobthe set -x color_desk $blue[2] $grey[1] --bold set -x color_nix $blue[2] $grey[1] --bold end + + set -q color_repo_pending + or set -x color_repo_pending $color_repo_work_tree end function __bobthefish_user_color_scheme_deprecated @@ -1043,6 +1067,7 @@ function __bobthefish_user_color_scheme_deprecated set -q __color_path_nowrite_basename; or set -l __color_path_nowrite_basename 660000 cc9999 --bold set -q __color_repo; or set -l __color_repo addc10 0c4801 set -q __color_repo_work_tree; or set -l __color_repo_work_tree 333333 ffffff --bold + set -q __color_repo_pending; or set -l __color_repo_pending 333333 ffffff --bold set -q __color_repo_dirty; or set -l __color_repo_dirty ce000f ffffff set -q __color_repo_staged; or set -l __color_repo_staged f6b117 3a2a03 set -q __color_vi_mode_default; or set -l __color_vi_mode_default 999999 333333 --bold @@ -1081,6 +1106,7 @@ function __bobthefish_user_color_scheme_deprecated set -x color_path_nowrite_basename $__color_path_nowrite_basename set -x color_repo $__color_repo set -x color_repo_work_tree $__color_repo_work_tree + set -x color_repo_pending $__color_repo_pending set -x color_repo_dirty $__color_repo_dirty set -x color_repo_staged $__color_repo_staged set -x color_vi_mode_default $__color_vi_mode_default diff --git a/functions/__bobthefish_git_async_enabled.fish b/functions/__bobthefish_git_async_enabled.fish new file mode 100644 index 0000000..042bcc0 --- /dev/null +++ b/functions/__bobthefish_git_async_enabled.fish @@ -0,0 +1,210 @@ +function __bobthefish_git_async_enabled -S -d 'Check whether async git prompt updates should be used' + if [ "$argv[1]" = --init ] + if status is-interactive; and __bobthefish_version_at_least 3.0.0 $version + # The git metadata var must be universal: the detached background fish + # writes it, and a universal variable is the only channel that can deliver + # a value back into this (the parent) shell. It's namespaced by pid so + # concurrent sessions don't clobber each other. + set -g __bobthefish_git_async_var __bobthefish_git_async_$fish_pid + set -U $__bobthefish_git_async_var + + # Fires when the background worker publishes new metadata. The pending + # flag tells the next fish_prompt that this render is the async repaint + # (so it must not spawn another worker). + function __bobthefish_git_async_repaint --on-variable $__bobthefish_git_async_var + set -g __bobthefish_git_async_repaint_pending 1 + commandline --function repaint + end + + function __bobthefish_git_async_cleanup --on-event fish_exit + set -q __bobthefish_git_async_pid + and command kill $__bobthefish_git_async_pid 2>/dev/null + set -q __bobthefish_git_async_trailing_pid + and command kill $__bobthefish_git_async_trailing_pid 2>/dev/null + + set -q __bobthefish_git_async_var + and set -e $__bobthefish_git_async_var + end + + __bobthefish_git_async_gc + end + + # The git metadata var must be universal: the detached background fish + # writes it, and a universal variable is the only channel that can deliver + # a value back into this (the parent) shell. It's namespaced by pid so + # concurrent sessions don't clobber each other. + set -g __bobthefish_git_async_var __bobthefish_git_async_$fish_pid + set -U $__bobthefish_git_async_var + + # Fires when the background worker publishes new metadata. The pending + # flag tells the next fish_prompt that this render is the async repaint + # (so it must not spawn another worker). + function __bobthefish_git_async_repaint --on-variable $__bobthefish_git_async_var + set -g __bobthefish_git_async_repaint_pending 1 + commandline --function repaint + end + + function __bobthefish_git_async_cleanup --on-event fish_exit + set -q __bobthefish_git_async_pid + and command kill $__bobthefish_git_async_pid 2>/dev/null + set -q __bobthefish_git_async_trailing_pid + and command kill $__bobthefish_git_async_trailing_pid 2>/dev/null + set -q __bobthefish_git_async_poll_pid + and command kill $__bobthefish_git_async_poll_pid 2>/dev/null + + set -q __bobthefish_git_async_var + and set -e $__bobthefish_git_async_var + end + + __bobthefish_git_async_gc +end + +function __bobthefish_git_async_enabled -S -d 'Check whether async git prompt updates should be used' + [ "$theme_display_git_async" = no ] + and return 1 + + [ "$theme_git_worktree_support" = yes ] + and return 1 + + __bobthefish_version_at_least 3.0.0 $version + and status is-interactive + and set -q __bobthefish_git_async_var + and command -q fish + and __bobthefish_git_async_worker_path >/dev/null +end + +function __bobthefish_git_async_worker_path -S -d 'Resolve and cache the async git worker file' + set -q __bobthefish_git_async_worker_path_cache + and echo $__bobthefish_git_async_worker_path_cache + and return + + set -l prompt_path (functions --details fish_prompt) + [ -f "$prompt_path" ] + or return + + set -l resolved (dirname "$prompt_path")/__bobthefish_git_async_worker.fish + [ -f "$resolved" ] + or return + + set -g __bobthefish_git_async_worker_path_cache $resolved + echo $resolved +end + +function __bobthefish_git_async_fish -S -d 'Resolve and cache the fish binary used for async git workers' + set -q __bobthefish_git_async_fish_path + or set -g __bobthefish_git_async_fish_path (command -s fish) + echo $__bobthefish_git_async_fish_path +end + +function __bobthefish_git_async_gc -S -d 'Remove async git metadata left behind by dead fish sessions' + set -l prefix __bobthefish_git_async_ + for var in (set --names --universal | string match "$prefix*") + [ "$var" = "$__bobthefish_git_async_var" ] + and continue + + set -l pid (string replace $prefix '' -- $var) + string match -qr '^\d+$' -- "$pid" + or continue + + command kill -0 $pid 2>/dev/null + or set -e $var + end +end + +function __bobthefish_git_async_refresh_interval -S -d 'Get the minimum seconds between async git workers' + set -l interval 1 + string match -qr '^\d+$' -- "$theme_git_async_refresh_interval" + and set interval $theme_git_async_refresh_interval + + echo $interval +end + +function __bobthefish_git_async_schedule_trailing -S -a git_root_dir -a delay -d 'Schedule a trailing async git refresh' + [ "$delay" -gt 0 ] + or return + set -q __bobthefish_git_async_var + or return + + set -l worker_path (__bobthefish_git_async_worker_path) + or return + set -l fish_bin (__bobthefish_git_async_fish) + + if set -q __bobthefish_git_async_trailing_pid + command kill -0 $__bobthefish_git_async_trailing_pid 2>/dev/null + and [ "$__bobthefish_git_async_trailing_root_dir" = "$git_root_dir" ] + and return + + __bobthefish_git_async_clear_trailing + end + + command $fish_bin --private -c ' + sleep $argv[1] + source $argv[2] + __bobthefish_git_async_worker $argv[3] $argv[4] $argv[5] $argv[6] $argv[7] $argv[8] + ' $delay $worker_path $__bobthefish_git_async_var $git_root_dir "$theme_display_git_dirty" "$theme_display_git_dirty_verbose" "$theme_display_git_untracked" "$theme_display_git_stashed_verbose" & + builtin -q disown + and builtin disown + set -g __bobthefish_git_async_trailing_pid $last_pid + set -g __bobthefish_git_async_trailing_root_dir $git_root_dir +end + +function __bobthefish_git_async_start -S -a git_root_dir -d 'Start a background git prompt metadata update' + set -q __bobthefish_git_async_var + or return + + # Keep at most one worker active per prompt session. Re-rendering while a + # worker is still collecting git state should not kill the worker that will + # publish the fresh cache. + if set -q __bobthefish_git_async_pid + if command kill -0 $__bobthefish_git_async_pid 2>/dev/null + [ "$__bobthefish_git_async_root_dir" = "$git_root_dir" ] + and return + + command kill $__bobthefish_git_async_pid 2>/dev/null + end + + set -e __bobthefish_git_async_pid + end + + if set -q __bobthefish_git_async_trailing_pid + if [ "$__bobthefish_git_async_trailing_root_dir" != "$git_root_dir" ] + __bobthefish_git_async_clear_trailing + else if not command kill -0 $__bobthefish_git_async_trailing_pid 2>/dev/null + set -e __bobthefish_git_async_trailing_pid + end + end + + set -l now (date +%s) + set -l interval (__bobthefish_git_async_refresh_interval) + # Git state can change inside a repo between prompts, so this is a short + # debounce rather than a same-repo cache. Set theme_git_async_refresh_interval + # to 0 to spawn a worker on every eligible prompt render. + if [ "$interval" -gt 0 -a "$__bobthefish_git_async_root_dir" = "$git_root_dir" -a -n "$__bobthefish_git_async_last_start" ] + set -l elapsed (math $now - $__bobthefish_git_async_last_start) + if [ "$elapsed" -lt "$interval" ] + __bobthefish_git_async_schedule_trailing $git_root_dir (math $interval - $elapsed) + return + end + end + + __bobthefish_git_async_clear_trailing + + set -l worker_path (__bobthefish_git_async_worker_path) + or return + set -l fish_bin (__bobthefish_git_async_fish) + + set -l cmd source (string escape -- $worker_path) ';' __bobthefish_git_async_worker \ + (string escape -- $__bobthefish_git_async_var) \ + (string escape -- $git_root_dir) \ + (string escape -- "$theme_display_git_dirty") \ + (string escape -- "$theme_display_git_dirty_verbose") \ + (string escape -- "$theme_display_git_untracked") \ + (string escape -- "$theme_display_git_stashed_verbose") + + set -g __bobthefish_git_async_root_dir $git_root_dir + set -g __bobthefish_git_async_last_start $now + command $fish_bin --private --command "$cmd" & + builtin -q disown + and builtin disown + set -g __bobthefish_git_async_pid $last_pid +end diff --git a/functions/__bobthefish_git_async_worker.fish b/functions/__bobthefish_git_async_worker.fish new file mode 100644 index 0000000..5114462 --- /dev/null +++ b/functions/__bobthefish_git_async_worker.fish @@ -0,0 +1,110 @@ +# __bobthefish_git_async_counts / __bobthefish_git_async_dirty_verbose are +# deliberate copies of __bobthefish_git_counts / __bobthefish_git_dirty_verbose +# from __bobthefish_prompt_git_sync.fish. The worker runs in a non-interactive +# `fish --private -c` that bootstraps by sourcing only this file, so it must not +# rely on the theme's autoload path being configured (some shells load omf for +# interactive sessions only). Keep these in sync with their sync counterparts. +function __bobthefish_git_async_counts -S -d 'Print behind and ahead counts for the current branch' + set -l counts (command git rev-list --left-right --count '@{upstream}...HEAD' 2>/dev/null) + or return + + set counts (string split \t -- $counts) + if [ (count $counts) -lt 2 ] + set counts (string split ' ' -- $counts) + end + + [ (count $counts) -ge 2 ] + or return + + printf '%s\n%s\n' $counts[1] $counts[2] +end + +function __bobthefish_git_async_dirty_verbose -S -d 'Print a more verbose dirty state for the current working tree' + set -l diff_stats (command git diff --numstat) + or return + + set -l added 0 + set -l removed 0 + for line in $diff_stats + set -l fields (string split \t -- $line) + string match -rq '^\d+$' -- $fields[1] + and set added (math $added + $fields[1]) + string match -rq '^\d+$' -- $fields[2] + and set removed (math $removed + $fields[2]) + end + + set -l changes "+$added/-$removed" + echo "$changes " | string replace -r '(\+0/(-0)?|/-0)' '' +end + +function __bobthefish_git_async_worker -S -a git_async_var -a git_root_dir -a display_dirty -a display_dirty_verbose -a display_untracked -a display_stashed_verbose -d 'Compute git prompt metadata in a background fish' + set -l tag (command git describe --tags --exact-match 2>/dev/null) + set -l branch (command git symbolic-ref --quiet --short HEAD 2>/dev/null) + set -l detached + [ -z "$tag" -a -z "$branch" ] + and set detached (command git rev-parse --short HEAD 2>/dev/null) + + # Only resolve the configured default branch when it's actually needed + set -l configured_default_branch + [ -n "$branch" -a -z "$theme_git_default_branches" ] + and set configured_default_branch (command git config init.defaultBranch 2>/dev/null) + + set -l dirty + set -l dirty_stats + if [ "$display_dirty" != no ] + set -l show_dirty (command git config --bool bash.showDirtyState 2>/dev/null) + if [ "$show_dirty" != false ] + command git diff --no-ext-diff --quiet --exit-code 2>/dev/null + or begin + set dirty 1 + [ "$display_dirty_verbose" = yes ] + and set dirty_stats (__bobthefish_git_async_dirty_verbose) + end + end + end + + set -l staged + command git diff --cached --no-ext-diff --quiet --exit-code 2>/dev/null + or set staged 1 + + set -l stashed + if [ "$display_stashed_verbose" = yes ] + set stashed (command git rev-list --walk-reflogs --count refs/stash 2>/dev/null) + or set stashed + else + command git rev-parse --verify --quiet refs/stash >/dev/null + and set stashed 1 + end + + set -l ahead 0 + set -l behind 0 + set -l counts (__bobthefish_git_async_counts) + and begin + set behind $counts[1] + set ahead $counts[2] + end + + set -l new + if [ "$display_untracked" != no ] + set -l show_untracked (command git config --bool bash.showUntrackedFiles 2>/dev/null) + if [ "$show_untracked" != false ] + set new (command git ls-files --other --exclude-standard --directory --no-empty-directory "$git_root_dir" 2>/dev/null) + [ "$new" ] + and set new 1 + end + end + + # Field order is a contract shared with __bobthefish_prompt_git_async, + # which unpacks these by position. Keep the two in sync. + set -l next "$git_root_dir" "$tag" "$branch" "$detached" "$dirty" "$dirty_stats" "$staged" "$stashed" "$ahead" "$behind" "$new" "$configured_default_branch" + + # Skip the universal-variable write (disk I/O + a repaint broadcast to + # every fish session) when nothing actually changed since the last run. + set -l current $$git_async_var + set -l next_key (string join \x1e -- $next) + set -l current_key (string join \x1e -- $current) + [ "$next_key" = "$current_key" ] + and return + + set -U $git_async_var $next +end diff --git a/functions/__bobthefish_git_branch_from_parts.fish b/functions/__bobthefish_git_branch_from_parts.fish new file mode 100644 index 0000000..4b3c4a0 --- /dev/null +++ b/functions/__bobthefish_git_branch_from_parts.fish @@ -0,0 +1,25 @@ +function __bobthefish_git_branch_from_parts -S -a tag -a branch -a detached -a configured_default_branch -d 'Format a git branch from git metadata' + [ -n "$tag" ] + and echo "$tag_glyph $tag " + + if [ -n "$branch" ] + [ -n "$theme_git_default_branches" ] + or set -l theme_git_default_branches master main $configured_default_branch + + [ "$theme_display_git_master_branch" != yes -a "$theme_display_git_default_branch" != yes ] + and contains $branch $theme_git_default_branches + and echo $branch_glyph + and return + + set -l truncname $branch + [ "$theme_use_abbreviated_branch_name" = yes ] + and set truncname (string replace -r '^(.{17}).{3,}(.{5})$' "\$1…\$2" $branch) + + echo $branch_glyph $truncname + and return + end + + if [ -z "$tag" -a -n "$detached" ] + echo "$detached_glyph $detached" + end +end diff --git a/functions/__bobthefish_prompt_git_async.fish b/functions/__bobthefish_prompt_git_async.fish new file mode 100644 index 0000000..5c248f8 --- /dev/null +++ b/functions/__bobthefish_prompt_git_async.fish @@ -0,0 +1,137 @@ +function __bobthefish_git_count_or_zero -S -a count -d 'Normalize a git count for numeric comparisons' + string match -qr '^\d+$' -- "$count" + and echo $count + or echo 0 +end + +function __bobthefish_git_pending_branch -S -a git_root_dir -d 'Get cheap branch metadata while async git state is pending' + # The placeholder is only shown until the worker first publishes for this + # repo. Caching by root keeps rapid renders (e.g. holding Enter in a fresh + # repo) from re-forking git on every prompt during that brief window. + if set -q __bobthefish_git_pending_root + and [ "$__bobthefish_git_pending_root" = "$git_root_dir" ] + echo $__bobthefish_git_pending_branch_cache + return + end + + set -l rendered + set -l branch (command git symbolic-ref --quiet --short HEAD 2>/dev/null) + if [ -n "$branch" ] + # Keep the common branch path cheap. We intentionally do not call + # `git config init.defaultBranch` here; the worker will apply the full + # default-branch rules when it publishes complete metadata. + set rendered (__bobthefish_git_branch_from_parts '' "$branch" '') + else + set -l detached (command git rev-parse --short HEAD 2>/dev/null) + [ -n "$detached" ] + and set rendered (__bobthefish_git_branch_from_parts '' '' "$detached") + end + + set -g __bobthefish_git_pending_root $git_root_dir + set -g __bobthefish_git_pending_branch_cache $rendered + echo $rendered +end + +function __bobthefish_prompt_git_pending -S -a git_root_dir -a real_pwd -d 'Display an indeterminate repo segment while async git state is pending' + __bobthefish_path_segment $git_root_dir project + + __bobthefish_start_segment $color_repo_pending + echo -ns (__bobthefish_git_pending_branch $git_root_dir) ' ' + set_color normal + + __bobthefish_prompt_git_pwd $git_root_dir $real_pwd +end + +function __bobthefish_prompt_git_async -S -a git_root_dir -a real_pwd -a repaint -d 'Display git state from cached async metadata' + if [ "$repaint" != 1 ] + __bobthefish_git_async_start $git_root_dir + end + + # Field order is the contract published by __bobthefish_git_async_worker. + set -l git_data $$__bobthefish_git_async_var + if [ "$git_data[1]" != "$git_root_dir" ] + if [ "$repaint" = 1 ] + __bobthefish_git_async_start $git_root_dir + end + + set -l budget_ms (__bobthefish_git_async_first_render_budget_ms) + set -l waited_ms 0 + while [ "$budget_ms" -gt 0 -a "$waited_ms" -lt "$budget_ms" ] + sleep 0.01 + set git_data $$__bobthefish_git_async_var + [ "$git_data[1]" = "$git_root_dir" ] + and break + set waited_ms (math $waited_ms + 10) + end + end + + if [ "$git_data[1]" != "$git_root_dir" ] + __bobthefish_prompt_git_pending $git_root_dir $real_pwd + return + end + + set -l tag $git_data[2] + set -l branch $git_data[3] + set -l detached $git_data[4] + set -l dirty $git_data[5] + set -l dirty_stats $git_data[6] + set -l staged $git_data[7] + set -l stashed $git_data[8] + set -l ahead (__bobthefish_git_count_or_zero $git_data[9]) + set -l behind (__bobthefish_git_count_or_zero $git_data[10]) + set -l new $git_data[11] + set -l configured_default_branch $git_data[12] + + [ "$dirty" ] + and set dirty "$git_dirty_glyph$dirty_stats" + [ "$staged" ] + and set staged "$git_staged_glyph" + if [ "$stashed" ] + [ "$theme_display_git_stashed_verbose" = yes ] + and set stashed "$git_stashed_glyph$stashed" + or set stashed "$git_stashed_glyph" + end + + set -l ahead_flag + set -l verbose + [ "$theme_display_git_ahead_verbose" = yes ] + and set verbose 1 + switch "$ahead $behind" + case '0 0' # equal to upstream + case '* 0' # ahead of upstream + [ -n "$verbose" ] + and set ahead_flag "$git_ahead_glyph$ahead" + or set ahead_flag "$git_plus_glyph" + case '0 *' # behind upstream + [ -n "$verbose" ] + and set ahead_flag "$git_behind_glyph$behind" + or set ahead_flag "$git_minus_glyph" + case '*' # diverged from upstream + [ -n "$verbose" ] + and set ahead_flag "$git_ahead_glyph$ahead$git_behind_glyph$behind" + or set ahead_flag "$git_plus_minus_glyph" + end + + [ "$new" ] + and set new "$git_untracked_glyph" + + set -l flags "$dirty$staged$stashed$ahead_flag$new" + + [ "$flags" ] + and set flags " $flags" + + set -l flag_colors $color_repo + if [ "$dirty" ] + set flag_colors $color_repo_dirty + else if [ "$staged" ] + set flag_colors $color_repo_staged + end + + __bobthefish_path_segment $git_root_dir project + + __bobthefish_start_segment $flag_colors + echo -ns (__bobthefish_git_branch_from_parts "$tag" "$branch" "$detached" "$configured_default_branch") $flags ' ' + set_color normal + + __bobthefish_prompt_git_pwd $git_root_dir $real_pwd +end diff --git a/functions/__bobthefish_prompt_git_pwd.fish b/functions/__bobthefish_prompt_git_pwd.fish new file mode 100644 index 0000000..c3ef09a --- /dev/null +++ b/functions/__bobthefish_prompt_git_pwd.fish @@ -0,0 +1,13 @@ +function __bobthefish_prompt_git_pwd -S -a git_root_dir -a real_pwd -d 'Display the in-project path segment' + set -l project_pwd (__bobthefish_project_pwd $git_root_dir $real_pwd) + [ "$project_pwd" ] + or return + + if [ -w "$real_pwd" ] + __bobthefish_start_segment $color_path + else + __bobthefish_start_segment $color_path_nowrite + end + + echo -ns $project_pwd ' ' +end diff --git a/functions/__bobthefish_prompt_git_sync.fish b/functions/__bobthefish_prompt_git_sync.fish new file mode 100644 index 0000000..27e4531 --- /dev/null +++ b/functions/__bobthefish_prompt_git_sync.fish @@ -0,0 +1,212 @@ +function __bobthefish_git_counts -S -d 'Print behind and ahead counts for the current branch' + set -l counts (command git rev-list --left-right --count '@{upstream}...HEAD' 2>/dev/null) + or return + + set counts (string split \t -- $counts) + if [ (count $counts) -lt 2 ] + set counts (string split ' ' -- $counts) + end + + [ (count $counts) -ge 2 ] + or return + + printf '%s\n%s\n' $counts[1] $counts[2] +end + +function __bobthefish_git_branch -S -d 'Get the current git branch (or commitish)' + set -l tag (command git describe --tags --exact-match 2>/dev/null) + set -l branch (command git symbolic-ref --quiet --short HEAD 2>/dev/null) + + set -l detached + [ -z "$tag" -a -z "$branch" ] + and set detached (command git rev-parse --short HEAD 2>/dev/null) + + # Only resolve the configured default branch when it's actually needed + set -l configured_default_branch + [ -n "$branch" -a -z "$theme_git_default_branches" ] + and set configured_default_branch (command git config init.defaultBranch 2>/dev/null) + + __bobthefish_git_branch_from_parts "$tag" "$branch" "$detached" "$configured_default_branch" +end + +function __bobthefish_git_ahead -S -d 'Print the ahead/behind state for the current branch' + if [ "$theme_display_git_ahead_verbose" = yes ] + __bobthefish_git_ahead_verbose + return + end + + set -l counts (__bobthefish_git_counts) + or return + + set -l behind $counts[1] + set -l ahead $counts[2] + + if [ $ahead -gt 0 -a $behind -gt 0 ] + echo "$git_plus_minus_glyph" + else if [ $ahead -gt 0 ] + echo "$git_plus_glyph" + else if [ $behind -gt 0 ] + echo "$git_minus_glyph" + end +end + +function __bobthefish_git_ahead_verbose -S -d 'Print a more verbose ahead/behind state for the current branch' + set -l counts (__bobthefish_git_counts) + or return + + set -l behind $counts[1] + set -l ahead $counts[2] + + switch "$ahead $behind" + case '' # no upstream + case '0 0' # equal to upstream + return + case '* 0' # ahead of upstream + echo "$git_ahead_glyph$ahead" + case '0 *' # behind upstream + echo "$git_behind_glyph$behind" + case '*' # diverged from upstream + echo "$git_ahead_glyph$ahead$git_behind_glyph$behind" + end +end + +function __bobthefish_git_dirty_verbose -S -d 'Print a more verbose dirty state for the current working tree' + set -l diff_stats (command git diff --numstat) + or return + + set -l added 0 + set -l removed 0 + for line in $diff_stats + set -l fields (string split \t -- $line) + string match -rq '^\d+$' -- $fields[1] + and set added (math $added + $fields[1]) + string match -rq '^\d+$' -- $fields[2] + and set removed (math $removed + $fields[2]) + end + + set -l changes "+$added/-$removed" + echo "$changes " | string replace -r '(\+0/(-0)?|/-0)' '' +end + +function __bobthefish_git_stashed -S -d 'Print the stashed state for the current branch' + if [ "$theme_display_git_stashed_verbose" = yes ] + set -l stashed (command git rev-list --walk-reflogs --count refs/stash 2>/dev/null) + or return + + echo -n "$git_stashed_glyph$stashed" + else + command git rev-parse --verify --quiet refs/stash >/dev/null + and echo -n "$git_stashed_glyph" + end +end + +function __bobthefish_prompt_git_sync -S -a git_root_dir -a real_pwd -d 'Display the actual git state synchronously' + set -l dirty '' + if [ "$theme_display_git_dirty" != no ] + set -l show_dirty (command git config --bool bash.showDirtyState 2>/dev/null) + if [ "$show_dirty" != false ] + set dirty (command git diff --no-ext-diff --quiet --exit-code 2>/dev/null; or echo -n "$git_dirty_glyph") + if [ "$dirty" -a "$theme_display_git_dirty_verbose" = yes ] + set dirty "$dirty"(__bobthefish_git_dirty_verbose) + end + end + end + + set -l staged (command git diff --cached --no-ext-diff --quiet --exit-code 2>/dev/null; or echo -n "$git_staged_glyph") + set -l stashed (__bobthefish_git_stashed) + set -l ahead (__bobthefish_git_ahead) + + set -l new '' + if [ "$theme_display_git_untracked" != no ] + set -l show_untracked (command git config --bool bash.showUntrackedFiles 2>/dev/null) + if [ "$show_untracked" != false ] + set new (command git ls-files --other --exclude-standard --directory --no-empty-directory "$git_root_dir" 2>/dev/null) + if [ "$new" ] + set new "$git_untracked_glyph" + end + end + end + + set -l flags "$dirty$staged$stashed$ahead$new" + + [ "$flags" ] + and set flags " $flags" + + set -l flag_colors $color_repo + if [ "$dirty" ] + set flag_colors $color_repo_dirty + else if [ "$staged" ] + set flag_colors $color_repo_staged + end + + __bobthefish_path_segment $git_root_dir project + + __bobthefish_start_segment $flag_colors + echo -ns (__bobthefish_git_branch) $flags ' ' + set_color normal + + if [ "$theme_git_worktree_support" != yes ] + __bobthefish_prompt_git_pwd $git_root_dir $real_pwd + return + end + + set -l project_pwd (command git rev-parse --show-prefix 2>/dev/null | string trim --right --chars=/) + set -l work_dir (command git rev-parse --show-toplevel 2>/dev/null) + + # only show work dir if it's a parent… + if [ "$work_dir" ] + switch $real_pwd/ + case $work_dir/\* + string match "$git_root_dir*" $work_dir >/dev/null + and set work_dir (string sub -s (math 1 + (string length $git_root_dir)) $work_dir) + case \* + set -e work_dir + end + end + + if [ "$project_pwd" -o "$work_dir" ] + set -l colors $color_path + if not [ -w "$real_pwd" ] + set colors $color_path_nowrite + end + + __bobthefish_start_segment $colors + + # handle work_dir != project dir + if [ "$work_dir" ] + set -l work_parent (__bobthefish_dirname $work_dir) + if [ "$work_parent" ] + echo -n "$work_parent/" + end + + set_color normal + set_color -b $color_repo_work_tree + echo -n (__bobthefish_basename $work_dir) + + set_color normal + set_color -b $colors + [ "$project_pwd" ] + and echo -n / + end + + echo -ns $project_pwd ' ' + else + set project_pwd $real_pwd + + string match "$git_root_dir*" $project_pwd >/dev/null + and set project_pwd (string sub -s (math 1 + (string length $git_root_dir)) $project_pwd) + + set project_pwd (string trim --left --chars=/ -- $project_pwd) + + if [ "$project_pwd" ] + set -l colors $color_path + if not [ -w "$real_pwd" ] + set colors $color_path_nowrite + end + + __bobthefish_start_segment $colors + + echo -ns $project_pwd ' ' + end + end +end diff --git a/functions/fish_prompt.fish b/functions/fish_prompt.fish index 31aa814..ebb43ae 100644 --- a/functions/fish_prompt.fish +++ b/functions/fish_prompt.fish @@ -65,35 +65,7 @@ function __bobthefish_version_at_least -S -a required -a actual -d 'Compare dott return 0 end -function __bobthefish_git_branch -S -d 'Get the current git branch (or commitish)' - set -l tag (command git describe --tags --exact-match 2>/dev/null) - and echo "$tag_glyph $tag " - - set -l branch (command git symbolic-ref --quiet --short HEAD 2>/dev/null) - and begin - [ -n "$theme_git_default_branches" ] - or set -l theme_git_default_branches master main (command git config init.defaultBranch 2>/dev/null) - - [ "$theme_display_git_master_branch" != yes -a "$theme_display_git_default_branch" != yes ] - and contains $branch $theme_git_default_branches - and echo $branch_glyph - and return - - # truncate the middle of the branch name, but only if it's 25+ characters - set -l truncname $branch - [ "$theme_use_abbreviated_branch_name" = yes ] - and set truncname (string replace -r '^(.{17}).{3,}(.{5})$' "\$1…\$2" $branch) - - echo $branch_glyph $truncname - and return - end - - # If we've already shown a tag we don't need to show a detached branch - if [ -z "$tag" ] - set -l branch (command git rev-parse --short HEAD 2>/dev/null) - echo "$detached_glyph $branch" - end -end +__bobthefish_git_async_init function __bobthefish_fossil_branch -S -d 'Get the current fossil branch' set -l branch (command fossil branch 2>/dev/null | string trim --left --chars=' *') @@ -271,96 +243,6 @@ function __bobthefish_project_pwd -S -a project_root_dir -a real_pwd -d 'Print t string replace -ar '(\.?[^/]{'"$theme_project_dir_length"'})[^/]*/' '$1/' $project_dir end -function __bobthefish_git_ahead -S -d 'Print the ahead/behind state for the current branch' - if [ "$theme_display_git_ahead_verbose" = yes ] - __bobthefish_git_ahead_verbose - return - end - - set -l ahead 0 - set -l behind 0 - for line in (command git rev-list --left-right '@{upstream}...HEAD' 2>/dev/null) - switch "$line" - case '>*' - if [ $behind -eq 1 ] - echo '±' - return - end - set ahead 1 - case '<*' - if [ $ahead -eq 1 ] - echo "$git_plus_minus_glyph" - return - end - set behind 1 - end - end - - if [ $ahead -eq 1 ] - echo "$git_plus_glyph" - else if [ $behind -eq 1 ] - echo "$git_minus_glyph" - end -end - -function __bobthefish_git_ahead_verbose -S -d 'Print a more verbose ahead/behind state for the current branch' - set -l commits (command git rev-list --left-right '@{upstream}...HEAD' 2>/dev/null) - or return - - set -l behind 0 - set -l ahead 0 - for commit in $commits - switch "$commit" - case '<*' - set behind (math $behind + 1) - case '>*' - set ahead (math $ahead + 1) - end - end - - switch "$ahead $behind" - case '' # no upstream - case '0 0' # equal to upstream - return - case '* 0' # ahead of upstream - echo "$git_ahead_glyph$ahead" - case '0 *' # behind upstream - echo "$git_behind_glyph$behind" - case '*' # diverged from upstream - echo "$git_ahead_glyph$ahead$git_behind_glyph$behind" - end -end - -function __bobthefish_git_dirty_verbose -S -d 'Print a more verbose dirty state for the current working tree' - set -l diff_stats (command git diff --numstat) - or return - - set -l added 0 - set -l removed 0 - for line in $diff_stats - set -l fields (string split \t -- $line) - string match -rq '^\d+$' -- $fields[1] - and set added (math $added + $fields[1]) - string match -rq '^\d+$' -- $fields[2] - and set removed (math $removed + $fields[2]) - end - - set -l changes "+$added/-$removed" - echo "$changes " | string replace -r '(\+0/(-0)?|/-0)' '' -end - -function __bobthefish_git_stashed -S -d 'Print the stashed state for the current branch' - if [ "$theme_display_git_stashed_verbose" = yes ] - set -l stashed (command git rev-list --walk-reflogs --count refs/stash 2>/dev/null) - or return - - echo -n "$git_stashed_glyph$stashed" - else - command git rev-parse --verify --quiet refs/stash >/dev/null - and echo -n "$git_stashed_glyph" - end -end - # ============================== # Segment functions # ============================== @@ -1179,123 +1061,11 @@ function __bobthefish_prompt_screen -S -d 'Display the screen name' set_color normal end -function __bobthefish_prompt_git -S -a git_root_dir -a real_pwd -d 'Display the actual git state' - set -l dirty '' - if [ "$theme_display_git_dirty" != no ] - set -l show_dirty (command git config --bool bash.showDirtyState 2>/dev/null) - if [ "$show_dirty" != false ] - set dirty (command git diff --no-ext-diff --quiet --exit-code 2>/dev/null; or echo -n "$git_dirty_glyph") - if [ "$dirty" -a "$theme_display_git_dirty_verbose" = yes ] - set dirty "$dirty"(__bobthefish_git_dirty_verbose) - end - end - end - - set -l staged (command git diff --cached --no-ext-diff --quiet --exit-code 2>/dev/null; or echo -n "$git_staged_glyph") - set -l stashed (__bobthefish_git_stashed) - set -l ahead (__bobthefish_git_ahead) - - set -l new '' - if [ "$theme_display_git_untracked" != no ] - set -l show_untracked (command git config --bool bash.showUntrackedFiles 2>/dev/null) - if [ "$show_untracked" != false ] - set new (command git ls-files --other --exclude-standard --directory --no-empty-directory "$git_root_dir" 2>/dev/null) - if [ "$new" ] - set new "$git_untracked_glyph" - end - end - end - - set -l flags "$dirty$staged$stashed$ahead$new" - - [ "$flags" ] - and set flags " $flags" - - set -l flag_colors $color_repo - if [ "$dirty" ] - set flag_colors $color_repo_dirty - else if [ "$staged" ] - set flag_colors $color_repo_staged - end - - __bobthefish_path_segment $git_root_dir project - - __bobthefish_start_segment $flag_colors - echo -ns (__bobthefish_git_branch) $flags ' ' - set_color normal - - if [ "$theme_git_worktree_support" != yes ] - set -l project_pwd (__bobthefish_project_pwd $git_root_dir $real_pwd) - if [ "$project_pwd" ] - if [ -w "$real_pwd" ] - __bobthefish_start_segment $color_path - else - __bobthefish_start_segment $color_path_nowrite - end - - echo -ns $project_pwd ' ' - end - return - end - - set -l project_pwd (command git rev-parse --show-prefix 2>/dev/null | string trim --right --chars=/) - set -l work_dir (command git rev-parse --show-toplevel 2>/dev/null) - - # only show work dir if it's a parent… - if [ "$work_dir" ] - switch $real_pwd/ - case $work_dir/\* - string match "$git_root_dir*" $work_dir >/dev/null - and set work_dir (string sub -s (math 1 + (string length $git_root_dir)) $work_dir) - case \* - set -e work_dir - end - end - - if [ "$project_pwd" -o "$work_dir" ] - set -l colors $color_path - if not [ -w "$real_pwd" ] - set colors $color_path_nowrite - end - - __bobthefish_start_segment $colors - - # handle work_dir != project dir - if [ "$work_dir" ] - set -l work_parent (__bobthefish_dirname $work_dir) - if [ "$work_parent" ] - echo -n "$work_parent/" - end - - set_color normal - set_color -b $color_repo_work_tree - echo -n (__bobthefish_basename $work_dir) - - set_color normal - set_color -b $colors - [ "$project_pwd" ] - and echo -n / - end - - echo -ns $project_pwd ' ' +function __bobthefish_prompt_git -S -a git_root_dir -a real_pwd -a repaint -d 'Display the actual git state' + if __bobthefish_git_async_enabled + __bobthefish_prompt_git_async $git_root_dir $real_pwd $repaint else - set project_pwd $real_pwd - - string match "$git_root_dir*" $project_pwd >/dev/null - and set project_pwd (string sub -s (math 1 + (string length $git_root_dir)) $project_pwd) - - set project_pwd (string trim --left --chars=/ -- $project_pwd) - - if [ "$project_pwd" ] - set -l colors $color_path - if not [ -w "$real_pwd" ] - set colors $color_path_nowrite - end - - __bobthefish_start_segment $colors - - echo -ns $project_pwd ' ' - end + __bobthefish_prompt_git_sync $git_root_dir $real_pwd end end @@ -1328,6 +1098,9 @@ end function fish_prompt -d 'bobthefish, a fish theme optimized for awesome' # Save the last status for later (do this before anything else) set -l last_status $status + set -l git_async_repaint + set -e __bobthefish_git_async_repaint_pending + and set git_async_repaint 1 # Use a simple prompt on dumb terminals. if [ "$TERM" = dumb ] @@ -1382,7 +1155,7 @@ function fish_prompt -d 'bobthefish, a fish theme optimized for awesome' case '' __bobthefish_prompt_dir $real_pwd case "$git_root_dir" - __bobthefish_prompt_git $git_root_dir $real_pwd + __bobthefish_prompt_git $git_root_dir $real_pwd $git_async_repaint case "$hg_root_dir" __bobthefish_prompt_hg $hg_root_dir $real_pwd case "$fossil_root_dir"