bash/prompt
2026-03-09 22:30:14 -04:00

175 lines
4.2 KiB
Text

# Detect graphical environment once at source time
if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then
_gfx=1
_py_sym=$'\ue73c'
_js_sym=$'\ue781'
_nix_sym=$'\ue843'
_proj_sym=$'\ueb45 '
_branch_sym=$'\uf418'
else
_gfx=""
_py_sym="py"
_js_sym="js"
_nix_sym="nix"
_proj_sym="../"
_branch_sym="git"
fi
# Pre-compute colored icons
_python_icon="\[\033[01;33m\]$_py_sym\[\033[00m\]"
_node_icon="\[\033[01;93m\]$_js_sym\[\033[00m\]"
_nix_icon="\[\033[01;34m\]$_nix_sym\[\033[00m\]"
# SSH check once at source time
if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then
_ssh_PS1="\n\[\033[01;37m\]\u@\h:\[\033[00m\]\n"
else
_ssh_PS1=""
fi
# Static prompt parts
_green_arrow="\[\033[01;32m\]>> "
_white_text="\[\033[00m\]"
# Cache: dir -> "git_root|superproject|" or "-" for non-git
declare -A _dir_cache
# Find git root by walking up (no git spawn)
_find_git_root() {
local dir="$PWD"
while [ -n "$dir" ]; do
if [ -e "$dir/.git" ]; then
_git_root="$dir"
return 0
fi
dir="${dir%/*}"
done
return 1
}
# Find superproject by walking up from git root
_find_superproject() {
local dir="${_git_root%/*}"
_superproject=""
while [ -n "$dir" ]; do
if [ -e "$dir/.git" ]; then
_superproject="$dir"
return 0
fi
dir="${dir%/*}"
done
return 1
}
# Read branch from .git/HEAD (no git spawn)
_read_branch() {
local git_dir="$_git_root/.git"
local head_file
# Submodule: .git is a file with "gitdir: <path>"
if [ -f "$git_dir" ]; then
read -r _ head_file < "$git_dir"
head_file="$head_file/HEAD"
else
head_file="$git_dir/HEAD"
fi
[ -f "$head_file" ] || return 1
local head
read -r head < "$head_file"
if [[ "$head" == "ref: refs/heads/"* ]]; then
_git_branch="${head#ref: refs/heads/}"
else
# Detached HEAD - short hash
_git_branch="${head:0:7}"
fi
}
# Build venv icons (must run every prompt - env can change)
_build_venv_icons() {
venv_icons=""
[ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon "
[ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon "
[ -d "$_git_root/node_modules" ] && venv_icons+="$_node_icon "
}
# Main prompt logic
_set_prompt() {
local cached="${_dir_cache[$PWD]}"
# Check cache
if [ -z "$cached" ]; then
if _find_git_root; then
_find_superproject
_dir_cache[$PWD]="$_git_root|$_superproject|"
else
_dir_cache[$PWD]="-"
cached="-"
fi
fi
# Non-git directory
if [ "$cached" = "-" ] || [ "${_dir_cache[$PWD]}" = "-" ]; then
venv_icons=""
[ -n "$IN_NIX_SHELL" ] && venv_icons+="$_nix_icon "
[ -n "$VIRTUAL_ENV" ] && venv_icons+="$_python_icon "
PS1="$_ssh_PS1\n\[\033[01;34m\]\w\[\033[00m\]\n$venv_icons$_green_arrow$_white_text"
return
fi
# Parse cache
[ -z "$cached" ] && cached="${_dir_cache[$PWD]}"
IFS='|' read -r _git_root _superproject _ <<< "$cached"
# Get branch (can change without cd) - if fails, git root is gone
if ! _read_branch; then
unset "_dir_cache[$PWD]"
_set_prompt
return
fi
# Build paths using bash string ops (no readlink spawn)
local git_curr_dir="${PWD#$_git_root}"
local git_root_dir="${_git_root##*/}"
# Build working_dir
local working_dir
if [ -n "$_superproject" ]; then
local super_name="${_superproject##*/}"
working_dir="\[\033[01;34m\]$_proj_sym$super_name/$git_root_dir$git_curr_dir\[\033[00m\]"
elif [ -z "$git_curr_dir" ]; then
working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir\[\033[00m\]"
else
working_dir="\[\033[01;34m\]$_proj_sym$git_root_dir$git_curr_dir\[\033[00m\]"
fi
# Build branch PS1
local git_branch_PS1
if [ -n "$_gfx" ]; then
git_branch_PS1="\[\033[01;31m\]$_git_branch $_branch_sym:\[\033[00m\]"
else
git_branch_PS1="\[\033[01;31m\]$_git_branch:\[\033[00m\]"
fi
# Build venv icons
_build_venv_icons
PS1="$_ssh_PS1\n$working_dir\n$venv_icons$_green_arrow$git_branch_PS1$_white_text"
}
# Invalidate cache for current directory
_prompt_cache_invalidate() {
unset "_dir_cache[$PWD]"
}
# Wrap git to invalidate cache on repo-creating commands
git() {
command git "$@"
local ret=$?
[[ "$1" =~ ^(init|clone)$ ]] && _prompt_cache_invalidate
return $ret
}
PROMPT_COMMAND="_set_prompt"