Files
Dot-Zsh/Zsh/.zsh_prompt
2026-06-05 04:15:34 +01:00

286 lines
8.1 KiB
Plaintext

# Prompt styling and context
autoload -Uz add-zsh-hook vcs_info
zstyle ':vcs_info:*' enable git
zstyle ':vcs_info:git:*' formats ' %b'
typeset -g PROMPT_SHOW_SPACER=0
typeset -gA PROMPT_PROJECT_MATCH_CACHE
typeset -gA PROMPT_VERSION_CACHE
typeset -gA PROMPT_SEPARATOR_CACHE
typeset -g PROMPT_FIRST_RENDER=1
typeset -g PROMPT_PROJECT_ROOT_CACHE_PWD=''
typeset -g PROMPT_PROJECT_ROOT_CACHE=''
prompt_preexec() {
case "$1" in
clear|clear\ *|reset|reset\ *)
PROMPT_SHOW_SPACER=0
;;
*)
PROMPT_SHOW_SPACER=1
;;
esac
}
get_command_version() {
local command_name="$1"
local binary_path=""
local raw_version=""
if [[ -n ${PROMPT_VERSION_CACHE[$command_name]+x} ]]; then
print -r -- "${PROMPT_VERSION_CACHE[$command_name]}"
return 0
fi
binary_path="$(whence -p "$command_name" 2>/dev/null || true)"
if ! [ -n "$binary_path" ] || ! [ -x "$binary_path" ]; then
PROMPT_VERSION_CACHE[$command_name]=""
return 0
fi
case "$command_name" in
python)
raw_version="$($binary_path --version 2>&1 || true)"
raw_version="${raw_version#Python }"
;;
node)
raw_version="$($binary_path -v 2>/dev/null || true)"
raw_version="${raw_version#v}"
;;
go)
raw_version="$($binary_path version 2>/dev/null || true)"
raw_version="${raw_version#go version go}"
raw_version="${raw_version%% *}"
;;
rustc)
raw_version="$($binary_path -V 2>/dev/null || true)"
raw_version="${raw_version#rustc }"
raw_version="${raw_version%% *}"
;;
*)
PROMPT_VERSION_CACHE[$command_name]=""
return 0
;;
esac
[ -n "$raw_version" ] || {
PROMPT_VERSION_CACHE[$command_name]=""
return 0
}
PROMPT_VERSION_CACHE[$command_name]="$raw_version"
print -r -- "$raw_version"
}
get_project_root() {
if [ "$PROMPT_PROJECT_ROOT_CACHE_PWD" = "$PWD" ] && [ -n "$PROMPT_PROJECT_ROOT_CACHE" ]; then
print -r -- "$PROMPT_PROJECT_ROOT_CACHE"
return 0
fi
PROMPT_PROJECT_ROOT_CACHE_PWD="$PWD"
PROMPT_PROJECT_ROOT_CACHE="$(command git rev-parse --show-toplevel 2>/dev/null || print -r -- "$PWD")"
print -r -- "$PROMPT_PROJECT_ROOT_CACHE"
}
project_has_pattern() {
local project_root=""
local cache_key=""
local pattern
local matches=()
project_root="$(get_project_root)"
cache_key="${project_root}::${(j:|:)@}"
if [[ -n ${PROMPT_PROJECT_MATCH_CACHE[$cache_key]+x} ]]; then
[[ ${PROMPT_PROJECT_MATCH_CACHE[$cache_key]} == 1 ]]
return
fi
for pattern in "$@"; do
matches=("${project_root}"/${~pattern}(N))
if [ "${#matches[@]}" -gt 0 ]; then
PROMPT_PROJECT_MATCH_CACHE[$cache_key]=1
return 0
fi
done
PROMPT_PROJECT_MATCH_CACHE[$cache_key]=0
return 1
}
build_git_status_segment() {
local git_status_output=""
local line=""
local staged=0
local unstaged=0
local untracked=0
local status_parts=()
command git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
git_status_output="$(command git status --porcelain 2>/dev/null || true)"
if [ -z "$git_status_output" ]; then
print -r -- "%F{green}✓ clean%f"
return 0
fi
while IFS= read -r line; do
[ -n "$line" ] || continue
if [ "${line:0:2}" = "??" ]; then
((untracked++))
continue
fi
[ "${line:0:1}" = " " ] || ((staged++))
[ "${line:1:1}" = " " ] || ((unstaged++))
done <<< "$git_status_output"
[ "$staged" -eq 0 ] || status_parts+=("+$staged")
[ "$unstaged" -eq 0 ] || status_parts+=("~$unstaged")
[ "$untracked" -eq 0 ] || status_parts+=("?$untracked")
if [ "${#status_parts[@]}" -eq 0 ]; then
print -r -- "%F{green}✓ clean%f"
else
print -r -- "%F{yellow}${(j: :)status_parts}%f"
fi
}
build_prompt_separator_line() {
local last_status="$1"
local line_width="${COLUMNS:-80}"
local cache_key="${line_width}:${last_status}"
local separator_line=""
local color_prefix=""
local color_suffix=""
local i=0
if [[ -n ${PROMPT_SEPARATOR_CACHE[$cache_key]+x} ]]; then
print -r -- "${PROMPT_SEPARATOR_CACHE[$cache_key]}"
return 0
fi
if [ "$line_width" -lt 1 ]; then
line_width=1
fi
if [ "$last_status" -ne 0 ]; then
color_prefix="%F{red}"
color_suffix="%f"
fi
while [ "$i" -lt "$line_width" ]; do
separator_line+="─"
((i++))
done
PROMPT_SEPARATOR_CACHE[$cache_key]="${color_prefix}${separator_line}${color_suffix}"
print -r -- "${PROMPT_SEPARATOR_CACHE[$cache_key]}"
}
build_prompt() {
local last_status=$?
local git_segment=""
local git_status_segment=""
local env_segment=""
local project_symbols=()
local tool_versions=()
local python_version=""
local node_version=""
local go_version=""
local rust_version=""
local context_line=""
local newline=$'\n'
local prompt_spacer=""
local separator="::"
if [ "$last_status" -ne 0 ]; then
separator="%F{red}::%f"
fi
if [ "$PROMPT_FIRST_RENDER" -eq 1 ]; then
PROMPT_FIRST_RENDER=0
PROMPT="%B%~%b ${separator} "
RPROMPT="%n@%m"
return 0
fi
vcs_info
git_segment="${vcs_info_msg_0_}"
git_status_segment="$(build_git_status_segment)"
if [ -n "${CONDA_DEFAULT_ENV:-}" ]; then
env_segment=" ${CONDA_DEFAULT_ENV}"
elif [ -n "${DIRENV_DIR:-}" ] || [ -n "${DIRENV_FILE:-}" ]; then
env_segment=" direnv"
fi
if project_has_pattern '**/docker-compose*.yml' '**/docker-compose*.yaml' '**/compose*.yml' '**/compose*.yaml'; then
project_symbols+=("%F{blue}%f")
fi
if project_has_pattern '**/Makefile' '**/makefile' '**/GNUmakefile' '**/Justfile' '**/justfile' '**/.justfile'; then
project_symbols+=("%F{yellow}%f")
fi
if project_has_pattern '**/pyproject.toml' '**/requirements.txt' '**/.python-version' '**/setup.py'; then
python_version="$(get_command_version python)"
[ -z "$python_version" ] || tool_versions+=("%F{magenta} ${python_version}%f")
fi
if project_has_pattern '**/package.json' '**/.nvmrc' '**/pnpm-workspace.yaml' '**/yarn.lock' '**/package-lock.json' '**/bun.lock' '**/bun.lockb'; then
node_version="$(get_command_version node)"
[ -z "$node_version" ] || tool_versions+=("%F{green} ${node_version}%f")
fi
if project_has_pattern '**/go.mod' '**/go.work'; then
go_version="$(get_command_version go)"
[ -z "$go_version" ] || tool_versions+=("%F{cyan} ${go_version}%f")
fi
if project_has_pattern '**/Cargo.toml' '**/Cargo.lock' '**/rust-toolchain.toml' '**/rust-toolchain'; then
rust_version="$(get_command_version rustc)"
[ -z "$rust_version" ] || tool_versions+=("%F{yellow} ${rust_version}%f")
fi
if [ -n "$git_segment" ]; then
context_line="%F{cyan}${git_segment}%f"
[ -z "$git_status_segment" ] || context_line+=" ${git_status_segment}"
fi
if [ -n "$env_segment" ]; then
[ -z "$context_line" ] || context_line+=" %F{240}·%f "
context_line+="%F{green}${env_segment}%f"
fi
if [ "${#project_symbols[@]}" -gt 0 ]; then
[ -z "$context_line" ] || context_line+=" %F{240}·%f "
context_line+="${(j: :)project_symbols}"
fi
if [ "${#tool_versions[@]}" -gt 0 ]; then
[ -z "$context_line" ] || context_line+=" %F{240}·%f "
context_line+="${(j: :)tool_versions}"
fi
if [ -n "$context_line" ]; then
context_line+="${newline}"
fi
if [ "$PROMPT_SHOW_SPACER" -eq 1 ]; then
prompt_spacer="$(build_prompt_separator_line "$last_status")${newline}"
fi
PROMPT="${prompt_spacer}${context_line}%B%~%b ${separator} "
RPROMPT="%n@%m"
}
add-zsh-hook preexec prompt_preexec
add-zsh-hook precmd build_prompt
build_prompt