From 841fd064a5aba75fd362b97e29308bb52f0439ae Mon Sep 17 00:00:00 2001 From: allan Date: Sat, 10 Jan 2026 17:34:51 +0100 Subject: [PATCH] Fix Bash Git prompt cursor handling and stabilize rendering --- bash-git-prompt | 245 ++++++++++++++++++++---------------------------- 1 file changed, 100 insertions(+), 145 deletions(-) diff --git a/bash-git-prompt b/bash-git-prompt index d2ea5f2..120d0a4 100644 --- a/bash-git-prompt +++ b/bash-git-prompt @@ -8,6 +8,13 @@ # GIT_PROMPT_THEME="${GIT_PROMPT_THEME:-1}" +# +# ANSI-safe base colors (cursor safe) +# +C_USER='\[\e[0;32m\]' +C_PATH='\[\e[38;5;178m\]' +C_RESET='\[\e[0m\]' + # # Theme icon definitions # @@ -67,186 +74,143 @@ set_git_prompt_theme_icons() { ;; esac } + +# +# Theme-aware git colors (ANSI-safe) +# +set_git_prompt_theme_colors() { + case "$GIT_PROMPT_THEME" in + 4|5) + GC_BRANCH='\[\e[0;37m\]' + GC_STAGED='\[\e[0;31m\]' + GC_CONFLICT='\[\e[0;31m\]' + GC_CHANGED='\[\e[0;33m\]' + GC_UNTRACKED='\[\e[0;32m\]' + GC_STASHED='\[\e[0;36m\]' + GC_AHEAD='\[\e[0;37m\]' + GC_BEHIND='\[\e[0;37m\]' + GC_NOREMOTE='\[\e[0;37m\]' + GC_CLEAN='\[\e[0;32m\]' + GC_DIRTY='\[\e[0;31m\]' + ;; + *) + GC_BRANCH='\[\e[38;5;117m\]' + GC_STAGED='\[\e[38;5;196m\]' + GC_CONFLICT='\[\e[38;5;196m\]' + GC_CHANGED='\[\e[38;5;69m\]' + GC_UNTRACKED='\[\e[38;5;41m\]' + GC_STASHED='\[\e[38;5;226m\]' + GC_AHEAD='\[\e[0;37m\]' + GC_BEHIND='\[\e[0;37m\]' + GC_NOREMOTE='\[\e[38;5;250m\]' + GC_CLEAN='\[\e[0;32m\]' + GC_DIRTY='\[\e[38;5;196m\]' + ;; + esac +} + set_git_prompt_theme_icons +set_git_prompt_theme_colors # -# Caching configuration and setup +# Cache (per shell tree, auto-cleared on reboot) # -GIT_CACHE_FILE="/tmp/git_prompt_cache.$$" - -# Collected state variables -GIT_STATE_HEAD="" -GIT_STATE_STATUS="" -GIT_STATE_STASH_LIST="" -GIT_STATE_UNTRACKED="" -GIT_STATE_FINGERPRINT="" - -# Directories to skip for speed -HEAVY_DIRS=( - node_modules - vendor - dist - build - cache - uploads - public/uploads - tmp - .terraform - .cache -) +GIT_CACHE_FILE="/dev/shm/git_prompt_${USER}_${PPID}.cache" # -# Curent state collection +# State collection # git_collect_state() { - # HEAD GIT_STATE_HEAD=$(git rev-parse HEAD 2>/dev/null) || return 1 - # Full status (branch + staged/modified) - GIT_STATE_STATUS=$(GIT_OPTIONAL_LOCKS=0 git status --porcelain --branch 2>/dev/null) || return 1 + # Explicit porcelain contract + GIT_STATE_STATUS=$(git status --porcelain=v1 --branch 2>/dev/null) || return 1 - # Stash list - GIT_STATE_STASH_LIST=$(GIT_OPTIONAL_LOCKS=0 git stash list 2>/dev/null || true) + GIT_STATE_STASH_LIST=$(git stash list 2>/dev/null || true) - # Smart untracked detection - # Much faster than git status because it only lists untracked, not modified local untracked_raw untracked_raw=$(git ls-files --others --exclude-standard 2>/dev/null || true) - # Apply directory skipping GIT_STATE_UNTRACKED="" while IFS= read -r f; do - skip=0 - for d in "${HEAVY_DIRS[@]}"; do - [[ "$f" == "$d/"* ]] && skip=1 && break + for d in node_modules vendor dist build cache uploads public/uploads tmp .terraform .cache; do + [[ "$f" == "$d/"* ]] && continue 2 done - (( skip == 0 )) && GIT_STATE_UNTRACKED+="$f"$'\n' + GIT_STATE_UNTRACKED+="$f"$'\n' done <<< "$untracked_raw" - # Build fingerprint - if command -v sha1sum >/dev/null; then - GIT_STATE_FINGERPRINT=$( - printf '%s\n%s\n%s\n%s\n%s\n' \ - "$GIT_STATE_HEAD" \ - "$GIT_STATE_STATUS" \ - "$GIT_STATE_STASH_LIST" \ - "$GIT_STATE_UNTRACKED" \ - "$GIT_PROMPT_THEME" \ - | sha1sum | awk '{print $1}' - ) - else - GIT_STATE_FINGERPRINT=$(printf '%s\n%s\n%s\n%s\n' "$GIT_STATE_HEAD" "$GIT_STATE_STATUS" "$GIT_STATE_STASH_LIST" "$GIT_STATE_UNTRACKED") - fi - - return 0 + # POSIX fingerprint + GIT_STATE_FINGERPRINT=$(printf '%s%s%s%s%s' \ + "$GIT_STATE_HEAD" \ + "$GIT_STATE_STATUS" \ + "$GIT_STATE_STASH_LIST" \ + "$GIT_STATE_UNTRACKED" \ + "$GIT_PROMPT_THEME" | cksum | awk '{print $1}') } # -# Render logic +# Render logic (colored + cursor safe) # actual_git_prompt_info_logic() { local branch branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "(detached)") - local status="$GIT_STATE_STATUS" - local stash_list="$GIT_STATE_STASH_LIST" - - local staged conflicts changed untracked stashed is_clean has_remote - staged=$(grep -cE '^[AMDR] ' <<<"$status") - conflicts=$(grep -cE '^UU ' <<<"$status") - changed=$(grep -cE '^.[MD] ' <<<"$status") + local staged conflicts changed untracked stashed + staged=$(grep -cE '^[AMDR] ' <<<"$GIT_STATE_STATUS") + conflicts=$(grep -cE '^UU ' <<<"$GIT_STATE_STATUS") + changed=$(grep -cE '^.[MD] ' <<<"$GIT_STATE_STATUS") untracked=$(grep -c . <<<"$GIT_STATE_UNTRACKED") - stashed=$(grep -c . <<<"$stash_list") + stashed=$(grep -c . <<<"$GIT_STATE_STASH_LIST") - # Parse branch/remote info - local first_line=${status%%$'\n'*} - local ahead=0 behind=0 - has_remote=0 + local first_line=${GIT_STATE_STATUS%%$'\n'*} + local ahead=0 behind=0 has_remote=0 + [[ "$first_line" == *"..."* ]] && has_remote=1 + [[ "$first_line" =~ ahead\ ([0-9]+) ]] && ahead=${BASH_REMATCH[1]} + [[ "$first_line" =~ behind\ ([0-9]+) ]] && behind=${BASH_REMATCH[1]} - if [[ "$first_line" == "## "* ]]; then - [[ "$first_line" == *"..."* ]] && has_remote=1 - [[ "$first_line" =~ ahead\ ([0-9]+) ]] && ahead=${BASH_REMATCH[1]} - [[ "$first_line" =~ behind\ ([0-9]+) ]] && behind=${BASH_REMATCH[1]} - fi + local p="" + p+="${GC_BRANCH}${BRANCH_ICON}${branch}${C_RESET}" - is_clean=0 - (( staged == 0 && conflicts == 0 && changed == 0 && untracked == 0 )) && is_clean=1 + ((staged > 0)) && p+=" ${GC_STAGED}${STAGED_ICON}${staged}${C_RESET}" + ((conflicts > 0)) && p+=" ${GC_CONFLICT}${CONFLICT_ICON}${conflicts}${C_RESET}" + ((changed > 0)) && p+=" ${GC_CHANGED}${CHANGED_ICON}${changed}${C_RESET}" + ((untracked > 0)) && p+=" ${GC_UNTRACKED}${UNTRACKED_ICON}${untracked}${C_RESET}" + ((stashed > 0)) && p+=" ${GC_STASHED}${STASHED_ICON}${stashed}${C_RESET}" + ((ahead > 0)) && p+=" ${GC_AHEAD}${AHEAD_ICON}${ahead}${C_RESET}" + ((behind > 0)) && p+=" ${GC_BEHIND}${BEHIND_ICON}${behind}${C_RESET}" + ((has_remote == 0)) && p+=" ${GC_NOREMOTE}${NO_REMOTE_ICON}${C_RESET}" - # - # Render themes - # - if [[ "$GIT_PROMPT_THEME" == "2" ]]; then - printf "\[\e[30;44m\]" - printf "\[\e[97;44m\] %s%s " "$BRANCH_ICON" "$branch" - ((staged > 0)) && printf " %s%d" "$STAGED_ICON" "$staged" - ((conflicts > 0)) && printf " %s%d" "$CONFLICT_ICON" "$conflicts" - ((changed > 0)) && printf " %s%d" "$CHANGED_ICON" "$changed" - ((untracked > 0)) && printf " %s%d" "$UNTRACKED_ICON" "$untracked" - ((stashed > 0)) && printf " %s%d" "$STASHED_ICON" "$stashed" - ((ahead > 0)) && printf " %s%d" "$AHEAD_ICON" "$ahead" - ((behind > 0)) && printf " %s%d" "$BEHIND_ICON" "$behind" - ((has_remote == 0)) && printf " %s" "$NO_REMOTE_ICON" - ((is_clean)) && printf " %s" "$CLEAN_ICON" || printf " %s" "$DIRTY_ICON" - printf "\[\e[34;107m\]" - printf "\[\e[30;107m\] \w" - printf "\[\e[97;49m\]" - printf "\[\e[0m\]" - elif [[ "$GIT_PROMPT_THEME" == "3" ]]; then - printf "\n\[\e[0;37m\]┌──[\e[0m" - printf "\e[38;5;117m%s%s\e[0m" "$BRANCH_ICON" "$branch" - ((staged > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$STAGED_ICON" "$staged" - ((conflicts > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$CONFLICT_ICON" "$conflicts" - ((changed > 0)) && printf " \e[38;5;69m%s%d\e[0m" "$CHANGED_ICON" "$changed" - ((untracked > 0)) && printf " \e[38;5;41m%s%d\e[0m" "$UNTRACKED_ICON" "$untracked" - ((stashed > 0)) && printf " \e[38;5;226m%s%d\e[0m" "$STASHED_ICON" "$stashed" - ((ahead > 0)) && printf " \e[0;37m%s%d\e[0m" "$AHEAD_ICON" "$ahead" - ((behind > 0)) && printf " \e[0;37m%s%d\e[0m" "$BEHIND_ICON" "$behind" - ((has_remote == 0)) && printf " \e[38;5;250m%s\e[0m" "$NO_REMOTE_ICON" - ((is_clean)) && printf " \e[0;32m%s\e[0m" "$CLEAN_ICON" || printf " \e[38;5;196m%s\e[0m" "$DIRTY_ICON" - printf "\e[0;37m]\e[0m \[\e[38;5;178m\]\w\[\e[0m\]" - printf "\n\[\e[0;37m\]└──\[\e[0m\]" + if (( staged + conflicts + changed + untracked == 0 )); then + p+=" ${GC_CLEAN}${CLEAN_ICON}${C_RESET}" else - printf "\e[0;37m[\e[0m" - printf "\e[38;5;117m%s%s\e[0m" "$BRANCH_ICON" "$branch" - ((staged > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$STAGED_ICON" "$staged" - ((conflicts > 0)) && printf " \e[38;5;196m%s%d\e[0m" "$CONFLICT_ICON" "$conflicts" - ((changed > 0)) && printf " \e[38;5;69m%s%d\e[0m" "$CHANGED_ICON" "$changed" - ((untracked > 0)) && printf " \e[38;5;41m%s%d\e[0m" "$UNTRACKED_ICON" "$untracked" - ((stashed > 0)) && printf " \e[38;5;226m%s%d\e[0m" "$STASHED_ICON" "$stashed" - ((ahead > 0)) && printf " \e[0;37m%s%d\e[0m" "$AHEAD_ICON" "$ahead" - ((behind > 0)) && printf " \e[0;37m%s%d\e[0m" "$BEHIND_ICON" "$behind" - ((has_remote == 0)) && printf " \e[38;5;250m%s\e[0m" "$NO_REMOTE_ICON" - ((is_clean)) && printf " \e[0;32m%s\e[0m" "$CLEAN_ICON" || printf " \e[38;5;196m%s\e[0m" "$DIRTY_ICON" - printf "\e[0;37m]\e[0m" + p+=" ${GC_DIRTY}${DIRTY_ICON}${C_RESET}" fi + + echo "$p" } # -# Main prompt logic +# Prompt logic (cache-safe) # git_prompt_info() { - git rev-parse --is-inside-work-tree &>/dev/null || return - git_collect_state || return - local fingerprint="$GIT_STATE_FINGERPRINT" - if [[ -f "$GIT_CACHE_FILE" ]]; then local cached_fp - IFS= read -r cached_fp < "$GIT_CACHE_FILE" - if [[ -n "$cached_fp" && "$cached_fp" == "$fingerprint" ]]; then + read -r cached_fp < "$GIT_CACHE_FILE" + if [[ "$cached_fp" == "$GIT_STATE_FINGERPRINT" ]]; then sed '1d' "$GIT_CACHE_FILE" - return + return 0 fi fi local output - output=$(actual_git_prompt_info_logic) || return + output=$(actual_git_prompt_info_logic) { - echo "$fingerprint" + echo "$GIT_STATE_FINGERPRINT" echo "$output" } > "$GIT_CACHE_FILE" @@ -257,37 +221,28 @@ update_git_prompt() { local PROMPT_CHAR='$' [[ $EUID -eq 0 ]] && PROMPT_CHAR='#' - GIT_PS1="$(git_prompt_info)" - - # - # PS1 custom for all themes inside a git repository - # - local PS1_CUSTOM='\[\e[0;32m\]\u@\h\[\e[0m\]' + local GIT_PS1 + GIT_PS1=$(git_prompt_info) if [[ -n "$GIT_PS1" ]]; then - if [[ "$GIT_PROMPT_THEME" == "3" ]]; then - PS1="${PS1_CUSTOM} ${GIT_PS1} ${PROMPT_CHAR} " - elif [[ "$GIT_PROMPT_THEME" == "2" ]]; then - PS1="${PS1_CUSTOM} ${GIT_PS1}\[\e[0m\] ${PROMPT_CHAR} " - else - PS1="${PS1_CUSTOM} \[${GIT_PS1}\] \[\e[38;5;178m\]\w\[\e[0m\] ${PROMPT_CHAR} " - fi - + PS1="${C_USER}\u@\h${C_RESET} [${GIT_PS1}] ${C_PATH}\w${C_RESET} ${PROMPT_CHAR} " else - # - # PS1 cusom for all themes outside a git repository - # - PS1='\[\e[0;32m\]\u@\h\[\e[0m\]:\[\e[38;5;178m\]\w\[\e[0m\] '"${PROMPT_CHAR} " + PS1="${C_USER}\u@\h${C_RESET} ${C_PATH}\w${C_RESET} ${PROMPT_CHAR} " fi } PROMPT_COMMAND=update_git_prompt +# +# Theme switcher +# gpchange() { local theme="${1:-}" + if [[ "$theme" =~ ^[1-5]$ ]]; then export GIT_PROMPT_THEME="$theme" set_git_prompt_theme_icons + set_git_prompt_theme_colors update_git_prompt echo "Switched to Git prompt theme $theme" else