diff options
Diffstat (limited to 'common/config/zsh/user/prompt.zsh')
| -rw-r--r-- | common/config/zsh/user/prompt.zsh | 679 |
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 |
