aboutsummaryrefslogtreecommitdiff
path: root/common/config/zsh/user/prompt.zsh
diff options
context:
space:
mode:
authorsrdusr <trevorgray@srdusr.com>2025-09-24 00:49:52 +0200
committersrdusr <trevorgray@srdusr.com>2025-09-24 00:49:52 +0200
commita70909b2057bf8d5923241d53e8ef3daef328458 (patch)
tree4e215383912f7d035d61cc10ec06e86fc04deacc /common/config/zsh/user/prompt.zsh
parent280f7799be30cba8fa893b489c49ac511cefe229 (diff)
downloaddotfiles-a70909b2057bf8d5923241d53e8ef3daef328458.tar.gz
dotfiles-a70909b2057bf8d5923241d53e8ef3daef328458.zip
Zsh config changes
Diffstat (limited to 'common/config/zsh/user/prompt.zsh')
-rw-r--r--common/config/zsh/user/prompt.zsh679
1 files changed, 679 insertions, 0 deletions
diff --git a/common/config/zsh/user/prompt.zsh b/common/config/zsh/user/prompt.zsh
new file mode 100644
index 0000000..c55a835
--- /dev/null
+++ b/common/config/zsh/user/prompt.zsh
@@ -0,0 +1,679 @@
+#!/bin/zsh
+
+########## Prompt(s) ##########
+
+# Autoload necessary functions for vcs_info and coloring
+autoload -Uz vcs_info
+autoload -Uz add-zsh-hook
+autoload -U colors && colors
+
+# Enable prompt substitution
+setopt prompt_subst
+
+# Display git branch status and color
+precmd_vcs_info() { vcs_info }
+
+# Add vcs_info to precmd functions
+precmd_functions+=( precmd_vcs_info )
+
+# Manipulates cursor position: moves down by 2 lines, saves position, and restores cursor after an operation.
+terminfo_down_sc=$terminfo[cud1]$terminfo[cuu1]$terminfo[sc]$terminfo[cud1]
+
+# Track last executed command for exit code display
+typeset -g _last_executed_command=""
+typeset -g _cmd_start_time=0
+typeset -g _cmd_end_time=0
+typeset -g _cmd_duration=0
+typeset -g _spinner_idx=0
+typeset -ga _spinner_frames=('⣾' '⣽' '⣻' '⢿' '⡿' '⣟' '⣯' '⣷')
+typeset -g _cmd_is_running=0
+typeset -g _show_spinner=0
+typeset -g _SPINNER_DELAY=5 # Only show spinner after 5 seconds
+typeset -g _FINISHED_DELAY=10 # Only show finished message after 10 seconds
+
+# Register the ZLE widget for spinner updates - do this early
+zle -N update_spinner
+
+# Cache git information to avoid repeated expensive operations
+typeset -g _git_cached_info=""
+typeset -g _git_cache_timestamp=0
+typeset -g _git_cache_lifetime=2 # seconds before cache expires
+
+# Calculate how much space is available for the prompt components
+function available_space() {
+ local width=${COLUMNS:-80}
+ echo $width
+}
+
+# Check if we need to abbreviate git info
+function need_to_abbreviate_git() {
+ local available=$(available_space)
+ local vi_mode_len=13 # Length of "-- INSERT --"
+ local prompt_base_len=20 # Base prompt elements length
+ local path_len=${#${PWD/#$HOME/\~}}
+ local git_full_len=0
+
+ # Try to estimate git info length if available
+ if git rev-parse --is-inside-work-tree &>/dev/null; then
+ local branch=$(git symbolic-ref --short HEAD 2>/dev/null)
+ git_full_len=${#branch}
+
+ # Add length for status indicators
+ if [[ -n "$(git status --porcelain)" ]]; then
+ # Rough estimate for status text
+ git_full_len=$((git_full_len + 20))
+ fi
+ fi
+
+ # Calculate total space needed
+ local total_needed=$((vi_mode_len + prompt_base_len + path_len + git_full_len))
+
+ # Determine if we need to abbreviate
+ if [[ $total_needed -gt $available ]]; then
+ return 0 # Need to abbreviate
+ else
+ return 1 # Don't need to abbreviate
+ fi
+}
+
+# Custom git branch coloring based on status
+git_branch_test_color() {
+ local now=$(date +%s)
+ local cache_age=$((now - _git_cache_timestamp))
+
+ # Use cached value if available and not expired
+ if [[ -n "$_git_cached_info" && $cache_age -lt $_git_cache_lifetime ]]; then
+ echo "$_git_cached_info"
+ return
+ fi
+
+ local ref=$(git symbolic-ref --short HEAD 2> /dev/null)
+ if [ -n "${ref}" ]; then
+ if [ -n "$(git status --porcelain)" ]; then
+ local gitstatuscolor='%F{green}'
+ else
+ local gitstatuscolor='%F{82}'
+ fi
+ _git_cached_info="${gitstatuscolor}${ref}"
+ _git_cache_timestamp=$now
+ echo "$_git_cached_info"
+ else
+ _git_cached_info=""
+ _git_cache_timestamp=$now
+ echo ""
+ fi
+}
+
+# Git branch with dynamic abbreviation
+git_branch_dynamic() {
+ local now=$(date +%s)
+ local cache_age=$((now - _git_cache_timestamp))
+
+ # Only query git if cache is expired
+ if [[ $cache_age -ge $_git_cache_lifetime ]]; then
+ local ref=$(git symbolic-ref --short HEAD 2> /dev/null)
+ if [ -n "${ref}" ]; then
+ if need_to_abbreviate_git; then
+ # Abbreviated version for small terminals
+ case "${ref}" in
+ "main") _git_cached_info="m" ;;
+ "master") _git_cached_info="m" ;;
+ "development") _git_cached_info="d" ;;
+ "develop") _git_cached_info="d" ;;
+ "feature/"*) _git_cached_info="f/${ref#feature/}" | cut -c 1-4 ;;
+ "release/"*) _git_cached_info="r/${ref#release/}" | cut -c 1-4 ;;
+ "hotfix/"*) _git_cached_info="h/${ref#hotfix/}" | cut -c 1-4 ;;
+ *) _git_cached_info="${ref}" | cut -c 1-5 ;; # Truncate to first 5 chars for other branches
+ esac
+ else
+ # Full branch name when there's room
+ _git_cached_info="${ref}"
+ fi
+ _git_cache_timestamp=$now
+ echo "$_git_cached_info"
+ else
+ _git_cached_info=""
+ _git_cache_timestamp=$now
+ echo ""
+ fi
+ else
+ echo "$_git_cached_info"
+ fi
+}
+
+# VCS info styles (e.g., git)
+zstyle ':vcs_info:*' check-for-changes true
+zstyle ':vcs_info:*' enable git
+
+# Dynamically configure vcs_info formats based on available space
+function configure_vcs_styles() {
+ if need_to_abbreviate_git; then
+ # Abbreviated versions
+ zstyle ':vcs_info:*' stagedstr ' +%F{15}s%f'
+ zstyle ':vcs_info:*' unstagedstr ' -%F{15}u%f'
+ else
+ # Full versions
+ zstyle ':vcs_info:*' stagedstr ' +%F{15}staged%f'
+ zstyle ':vcs_info:*' unstagedstr ' -%F{15}unstaged%f'
+ fi
+
+ zstyle ':vcs_info:*' actionformats '%F{5}%F{2}%b%F{3}|%F{1}%a%F{5}%f '
+ zstyle ':vcs_info:*' formats '%F{208} '$'\uE0A0'' %f$(git_branch_test_color)%f%F{76}%c%F{3}%u%f '
+ zstyle ':vcs_info:git*+set-message:*' hooks git-untracked git-dynamic
+}
+
+# Show "untracked" status in git - with conditional abbreviation
++vi-git-untracked() {
+ if [[ $(git rev-parse --is-inside-work-tree 2> /dev/null) == 'true' ]] && \
+ git status --porcelain | grep '??' &> /dev/null ; then
+
+ if need_to_abbreviate_git; then
+ hook_com[unstaged]+='%F{196} !%f%F{15}u%f'
+ else
+ hook_com[unstaged]+='%F{196} !%f%F{15}untracked%f'
+ fi
+ fi
+}
+
+# Dynamic git branch hook
++vi-git-dynamic() {
+ hook_com[branch]=$(git_branch_dynamic)
+}
+
+# SSH info with conditional abbreviation
+ssh_name() {
+ if [[ -n $SSH_CONNECTION ]]; then
+ local ssh_info
+
+ if need_to_abbreviate_git; then
+ # Abbreviated SSH info
+ ssh_info="ssh:%F{green}%n$nc%f"
+ else
+ ssh_info="ssh:%F{green}%n$nc%f"
+ if [[ -n $SSH_CONNECTION ]]; then
+ local ip_address
+ ip_address=$(echo $SSH_CONNECTION | awk '{print $3}')
+ ssh_info="$ssh_info@%F{green}$ip_address%f"
+ fi
+ fi
+ echo " ${ssh_info}"
+ fi
+}
+
+# Job names (for job control) with conditional abbreviation
+function job_name() {
+ job_name=""
+ job_length=0
+ local available=$(available_space)
+
+ # Only show jobs if we have reasonable space
+ if [ "${available}" -gt 60 ]; then
+ local job_count=$(jobs | wc -l)
+ if [ "${job_count}" -gt 0 ]; then
+ if need_to_abbreviate_git; then
+ job_name+="%F{green}j:${job_count}%f"
+ else
+ local title_jobs="jobs:"
+ job_name="${title_jobs}"
+ job_length=$((${available}-70))
+ [ "${job_length}" -lt "0" ] && job_length=0
+
+ if [ "${job_length}" -gt 0 ]; then
+ job_name+="%F{green}$(jobs | grep + | tr -s " " | cut -d " " -f 4- | cut -b 1-${job_length} | sed "s/\(.*\)/\1/")%f"
+ else
+ job_name+="%F{green}${job_count}%f"
+ fi
+ fi
+ fi
+ fi
+
+ echo "${job_name}"
+}
+
+# Check if we should show the spinner based on elapsed time
+function should_show_spinner() {
+ if [[ $_cmd_is_running -eq 1 ]]; then
+ local current_time=$(date +%s)
+ local elapsed=$((current_time - _cmd_start_time))
+
+ # Show spinner only after delay threshold
+ if [[ $elapsed -ge $_SPINNER_DELAY ]]; then
+ _show_spinner=1
+ return 0 # Yes, show spinner
+ fi
+ fi
+
+ _show_spinner=0
+ return 1 # No, don't show spinner
+}
+
+# Update spinner animation - simplified version
+function update_spinner() {
+ # This function is now just a ZLE widget placeholder
+ # The actual spinner updates happen in the TRAPALRM handler
+ :
+}
+
+# Start spinner timer when command runs longer than threshold
+function start_spinner_timer() {
+ _spinner_idx=0
+ _cmd_is_running=1
+ _show_spinner=0 # Start with spinner hidden until delay passes
+
+ # Set up the TRAPALRM for periodic updates - CRITICAL FIX
+ TMOUT=0.5 # Update spinner every 0.5 seconds
+
+ # Define TRAPALRM function - this is key to the spinner working
+ TRAPALRM() {
+ if [[ $_cmd_is_running -eq 1 ]]; then
+ local current_time=$(date +%s)
+ local elapsed=$((current_time - _cmd_start_time))
+
+ # Show spinner only after delay threshold
+ if [[ $elapsed -ge $_SPINNER_DELAY ]]; then
+ _show_spinner=1
+ _spinner_idx=$(( (_spinner_idx + 1) % ${#_spinner_frames[@]} ))
+
+ # Force prompt refresh - critical for updating the spinner
+ if [[ -o zle ]]; then
+ zle reset-prompt 2>/dev/null || true
+ zle -R
+ fi
+ fi
+ fi
+ }
+}
+
+# Stop spinner when command finishes
+function stop_spinner_timer() {
+ _cmd_is_running=0
+ _show_spinner=0
+
+ # Disable the alarm trap and timer
+ TRAPALRM() { : }
+ TMOUT=0
+
+ # Force prompt refresh to clear spinner
+ if [[ -o zle ]]; then
+ zle reset-prompt 2>/dev/null || true
+ zle -R
+ fi
+}
+
+# Format time in a human-readable way
+function format_time() {
+ local seconds=$1
+ local result=""
+
+ # Format time as hours:minutes:seconds for long durations
+ if [[ $seconds -ge 3600 ]]; then
+ local hours=$((seconds / 3600))
+ local minutes=$(( (seconds % 3600) / 60 ))
+ local secs=$((seconds % 60))
+ result="${hours}h${minutes}m${secs}s"
+ elif [[ $seconds -ge 60 ]]; then
+ local minutes=$((seconds / 60))
+ local secs=$((seconds % 60))
+ result="${minutes}m${secs}s"
+ else
+ result="${seconds}s"
+ fi
+
+ echo "$result"
+}
+
+# Error code display for RPROMPT with spinner - fixed version
+function exit_code_info() {
+ local exit_code=$?
+
+ # If a command is running and we should show spinner
+ if [[ $_cmd_is_running -eq 1 && $_show_spinner -eq 1 ]]; then
+ local spinner=${_spinner_frames[$_spinner_idx]}
+ local current_time=$(date +%s)
+ local elapsed=$((current_time - _cmd_start_time))
+ echo "%F{yellow}${spinner} ${elapsed}s%f"
+ return
+ fi
+
+ # Don't show error code when line editor is active (user is typing)
+ if [[ -o zle ]]; then
+ echo ""
+ return
+ fi
+
+ # Show command finished message for completed commands that took longer than threshold
+ if [[ -n "$_last_executed_command" && $_cmd_duration -ge $_FINISHED_DELAY ]]; then
+ local duration_formatted=$(format_time $_cmd_duration)
+
+ # Show error code along with finished message if there was an error
+ if [[ $exit_code -ne 0 ]]; then
+ # Show TSTP (148) as a suspension indicator instead of error
+ if [[ $exit_code -eq 148 ]]; then
+ echo "%F{cyan}finished ${duration_formatted}%f %F{yellow}⏸ TSTP%f"
+ return
+ fi
+
+ local signal_name=""
+ # Check if it's a signal
+ if [[ $exit_code -gt 128 && $exit_code -le 165 ]]; then
+ local signal_num=$((exit_code - 128))
+ signal_name=$(kill -l $signal_num 2>/dev/null)
+ if [[ -n "$signal_name" ]]; then
+ signal_name=" ($signal_name)"
+ fi
+ fi
+
+ # Return formatted error code with finished message
+ echo "%F{cyan}finished ${duration_formatted}%f %F{red}✘ $exit_code$signal_name%f"
+ else
+ echo "%F{cyan}finished ${duration_formatted}%f %F{green}✓%f"
+ fi
+ return
+ fi
+
+ # Don't show anything for exit code 0 (success) if this is first command
+ if [[ -z "$_last_executed_command" && $exit_code -eq 0 ]]; then
+ echo ""
+ return
+ fi
+
+ # Show TSTP (148) as a suspension indicator instead of error
+ if [[ $exit_code -eq 148 ]]; then
+ echo "%F{yellow}⏸ TSTP%f"
+ return
+ fi
+
+ if [[ $exit_code -ne 0 ]]; then
+ local signal_name=""
+
+ # Check if it's a signal
+ if [[ $exit_code -gt 128 && $exit_code -le 165 ]]; then
+ local signal_num=$((exit_code - 128))
+ signal_name=$(kill -l $signal_num 2>/dev/null)
+ if [[ -n "$signal_name" ]]; then
+ signal_name=" ($signal_name)"
+ fi
+ fi
+
+ # Return formatted error code
+ echo "%F{red}✘ $exit_code$signal_name%f"
+ else
+ echo "%F{green}✓%f" # Success indicator
+ fi
+}
+
+abbreviated_path() {
+ local full_path="${PWD/#$HOME/~}" # Replace $HOME with ~
+ local available=$(available_space)
+
+ # If path is root
+ if [[ "$full_path" == "/" ]]; then
+ echo "%F{4}/%f"
+ return
+ fi
+
+ # If path is just ~
+ if [[ "$full_path" == "~" ]]; then
+ echo "%F{4}~%f"
+ return
+ fi
+
+ # If extremely small terminal, show nothing to avoid breaking prompt
+ if (( available < 20 )); then
+ echo ""
+ return
+ fi
+
+ # For very narrow terminals, just show the current dir
+ if (( available < 30 )); then
+ echo "%F{4}%1~%f"
+ return
+ fi
+
+ # For moderately narrow terminals, show last two components
+ if (( available < 40 )); then
+ echo "%F{4}%2~%f"
+ return
+ fi
+
+ # For wide terminals, show full path
+ if (( available > 70 )); then
+ echo "%F{4}${full_path}%f"
+ return
+ fi
+
+ # Otherwise, show abbreviated path (e.g. ~/d/p/n)
+ local parts=("${(s:/:)full_path}")
+ local result=""
+ local last_index=${#parts[@]}
+
+ for i in {1..$((last_index - 1))}; do
+ [[ -n ${parts[i]} ]] && result+="/${parts[i]:0:1}"
+ done
+
+ result+="/${parts[last_index]}"
+ echo "%F{4}${result}%f"
+}
+
+
+# Prompt variables
+user="%n"
+at="%F{15}at%{$reset_color%}"
+machine="%F{4}%m%{$reset_color%}"
+relative_home="%F{4}%~%{$reset_color%}"
+carriage_return=""$'\n'""
+empty_line_bottom=""
+chevron_right=""
+color_reset="%{$(tput sgr0)%}"
+color_yellow="%{$(tput setaf 226)%}"
+color_blink="%{$(tput blink)%}"
+prompt_symbol="$"
+dollar_sign="${color_yellow}${color_blink}${prompt_symbol}${color_reset}"
+dollar="%(?:%F{2}${dollar_sign}:%F{1}${dollar_sign})"
+space=" "
+#thin_space=$'\u2009'
+thin_space=$'\u202F'
+cmd_prompt="%(?:%F{2}${chevron_right} :%F{1}${chevron_right} )"
+git_info="\$vcs_info_msg_0_"
+v1="%{┌─[%}"
+v2="%{]%}"
+v3="└──["
+v4="]"
+newline=$'\n'
+
+# Indicate INSERT mode for vi - NEVER truncate this
+function insert-mode () {
+ echo "-- INSERT --"
+}
+
+# Indicate NORMAL mode for vi - NEVER truncate this
+function normal-mode () {
+ echo "-- NORMAL --"
+}
+
+# Vi mode indicator
+vi-mode-indicator () {
+ if [[ ${KEYMAP} == vicmd || ${KEYMAP} == vi-cmd-mode ]]; then
+ echo -ne '\e[1 q'
+ vi_mode=$(normal-mode)
+ elif [[ ${KEYMAP} == main || ${KEYMAP} == viins || ${KEYMAP} == '' ]]; then
+ echo -ne '\e[5 q'
+ vi_mode=$(insert-mode)
+ fi
+}
+
+# Prompt function to ensure the prompt stays on one line, even in narrow terminals
+function set-prompt() {
+ vi-mode-indicator
+ configure_vcs_styles # Dynamically set vcs styles based on available space
+ vcs_info # Refresh vcs info with new styles
+
+ local available=$(available_space)
+ if (( available < 14 )); then
+ # Extremely narrow terminal — use minimal prompt
+ PS1="${carriage_return}${dollar}${space}${empty_line_bottom}"
+ RPROMPT='$(exit_code_info)'
+
+ else
+ # Path display - always show something for path, but adapt based on space
+ local path_display="$(abbreviated_path)"
+
+ # Git info - omit entirely if not enough space
+ local gitinfo=""
+ if [[ $available -gt 40 ]]; then
+ gitinfo="${vcs_info_msg_0_}"
+ fi
+
+ # Jobs info
+ local jobs=" $(job_name)"
+
+ # SSH info
+ local sshinfo="$(ssh_name)"
+
+ # Vi mode is priority 1 - ALWAYS show it
+ mode="%F{145}%{$terminfo_down_sc$vi_mode$terminfo[rc]%f%}"
+
+ # Right prompt for error codes or spinner
+ RPROMPT='$(exit_code_info)'
+
+ PS1="${newline}${v1}${user}${v2} ${path_display}${gitinfo}${jobs}${sshinfo}${carriage_return}${mode}${v3}${dollar}${v4}${empty_line_bottom}"
+ fi
+}
+
+# Pre-command hook to set prompt
+my_precmd() {
+ # Calculate command duration if a command was run
+ if [[ -n "$_last_executed_command" && $_cmd_start_time -gt 0 ]]; then
+ _cmd_end_time=$(date +%s)
+ _cmd_duration=$((_cmd_end_time - _cmd_start_time))
+ else
+ _cmd_duration=0
+ fi
+
+ stop_spinner_timer # Make sure spinner is stopped
+ vcs_info
+ set-prompt
+ vi-mode-indicator
+}
+
+add-zsh-hook precmd my_precmd
+
+# Update mode file based on current mode
+update-mode-file() {
+ set-prompt
+ local current_mode=$(cat ~/.vi-mode 2>/dev/null || echo "")
+ local new_mode="$vi_mode"
+
+ if [[ "$new_mode" != "$current_mode" ]]; then
+ echo "$new_mode" >| ~/.vi-mode
+ fi
+
+ # Ensure we're in an interactive shell and ZLE is active
+ if [[ -o zle ]] && zle -l &>/dev/null; then
+ zle reset-prompt 2>/dev/null || true
+ else
+ # If ZLE is not active, fallback and print the prompt manually
+ set-prompt
+ print -Pn "$PS1"
+ fi
+
+ # Refresh tmux client if tmux is running
+ if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then
+ tmux refresh-client -S
+ fi
+}
+
+# Check if nvim is running and update mode
+function check-nvim-running() {
+ if pgrep -x "nvim" > /dev/null; then
+ vi_mode=""
+ update-mode-file
+ if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then
+ tmux refresh-client -S
+ fi
+ else
+ if [[ ${KEYMAP} == vicmd || ${KEYMAP} == vi-cmd-mode ]]; then
+ vi_mode=$(normal-mode)
+ elif [[ ${KEYMAP} == main || ${KEYMAP} == viins || ${KEYMAP} == '' ]]; then
+ vi_mode=$(insert-mode)
+ fi
+ update-mode-file
+ if command -v tmux &>/dev/null && [[ -n "$TMUX" ]]; then
+ tmux refresh-client -S
+ fi
+ fi
+}
+
+# ZLE line initialization hook
+function zle-line-init() {
+ zle reset-prompt
+ vi-mode-indicator
+ case "${KEYMAP}" in
+ vicmd)
+ echo -ne '\e[1 q'
+ ;;
+ main|viins|*)
+ echo -ne '\e[5 q'
+ ;;
+ esac
+}
+
+# ZLE keymap select hook
+function zle-keymap-select() {
+ update-mode-file
+ zle reset-prompt
+ zle -R
+ vi-mode-indicator
+ case "${KEYMAP}" in
+ vicmd)
+ echo -ne '\e[1 q'
+ ;;
+ main|viins|*)
+ echo -ne '\e[5 q'
+ ;;
+ esac
+}
+
+# Safer version of zle reset-prompt
+function safe_reset_prompt() {
+ # Only reset if ZLE is active
+ if [[ -o zle ]] && zle -l &>/dev/null; then
+ zle reset-prompt 2>/dev/null || true
+ fi
+}
+
+# Preexec hook for command execution - NO BACKGROUND JOBS VERSION
+function preexec() {
+ # Store the command being executed
+ _last_executed_command=$1
+ _cmd_start_time=$(date +%s)
+ _cmd_is_running=1
+ _show_spinner=0 # Reset spinner flag
+
+ # Start the spinner timer immediately
+ start_spinner_timer
+
+ print -rn -- $terminfo[el]
+ echo -ne '\e[5 q'
+ vi-mode-indicator
+}
+
+# Terminal resizing: resets the prompt if ZLE is active, updates the mode file.
+TRAPWINCH() {
+ if [[ -o zle ]] && zle -l &>/dev/null; then
+ zle -R
+ zle reset-prompt 2>/dev/null || true
+ fi
+ update-mode-file 2>/dev/null
+}
+
+# Register ZLE hooks
+zle -N zle-line-init
+zle -N zle-keymap-select
+zle -N update_spinner
+
+# Register hooks
+add-zsh-hook preexec preexec
+add-zsh-hook precmd my_precmd
+
+set-prompt