diff options
| author | srdusr <trevorgray@srdusr.com> | 2025-09-01 15:40:50 +0200 |
|---|---|---|
| committer | srdusr <trevorgray@srdusr.com> | 2025-09-01 15:40:50 +0200 |
| commit | 40fc89461967f0094911e8846117ed3190e42d0b (patch) | |
| tree | ed22b71d8a3036fb7e0e2a48ec6a09f99f9da365 /linux/home/.config/zsh/user/prompt.zsh | |
| parent | ecd73a73200a64b5c62614debd9f9ec08748a72d (diff) | |
| download | dotfiles-40fc89461967f0094911e8846117ed3190e42d0b.tar.gz dotfiles-40fc89461967f0094911e8846117ed3190e42d0b.zip | |
Testing
Diffstat (limited to 'linux/home/.config/zsh/user/prompt.zsh')
| -rw-r--r-- | linux/home/.config/zsh/user/prompt.zsh | 568 |
1 files changed, 522 insertions, 46 deletions
diff --git a/linux/home/.config/zsh/user/prompt.zsh b/linux/home/.config/zsh/user/prompt.zsh index d9fc148..b8b1b97 100644 --- a/linux/home/.config/zsh/user/prompt.zsh +++ b/linux/home/.config/zsh/user/prompt.zsh @@ -2,19 +2,91 @@ ########## Prompt(s) ########## -terminfo_down_sc=$terminfo[cud1]$terminfo[cuu1]$terminfo[sc]$terminfo[cud1] - +# 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 ) -setopt prompt_subst +# 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 @@ -22,76 +94,376 @@ git_branch_test_color() { else local gitstatuscolor='%F{82}' fi - echo "${gitstatuscolor}${ref}" + _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:*' stagedstr ' +%F{15}staged%f' -zstyle ':vcs_info:*' unstagedstr ' -%F{15}unstaged%f' -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 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 - hook_com[unstaged]+='%F{196} !%f%F{15}untracked%f' + + 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 - 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" + + 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 - if [ "${COLUMNS}" -gt 69 ]; then - job_length=$((${COLUMNS}-70)) - [ "${job_length}" -lt "0" ] && job_length=0 - fi + local available=$(available_space) - if [ "${job_length}" -gt 0 ]; then + # 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 - local title_jobs="jobs:" - job_name="${title_jobs}" - job_name+="%F{green}$(jobs | grep + | tr -s " " | cut -d " " -f 4- | cut -b 1-${job_length} | sed "s/\(.*\)/\1/")%f" + 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}" } -function job_count() { - local job_count - job_count=$(jobs -s | grep -c "suspended") - if [ "${job_count}" -gt 0 ]; then - echo "(${job_count})" +# 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 } -current_jobs=' $(job_name)$(job_count)' +# 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="%r" -chevron_right="" +empty_line_bottom="" +chevron_right="" color_reset="%{$(tput sgr0)%}" color_yellow="%{$(tput setaf 226)%}" color_blink="%{$(tput blink)%}" @@ -99,16 +471,27 @@ 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' -function insert-mode () { echo "-- INSERT --" } -function normal-mode () { echo "-- NORMAL --" } +# 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' @@ -119,32 +502,87 @@ vi-mode-indicator () { fi } -function set-prompt () { +# Prompt function to ensure the prompt stays on one line, even in narrow terminals +function set-prompt() { vi-mode-indicator - mode="%F{145}%{$terminfo_down_sc$vi_mode$terminfo[rc]%f%}" - #PS1="${relative_home}${vcs_info_msg_0_}${current_jobs} ${carriage_return}${mode}${dollar}${space}" - PS1="${v1}${user}${v2}${space}${relative_home}${vcs_info_msg_0_}${current_jobs}$(ssh_name) ${carriage_return}${mode}${v3}${dollar}${v4}${empty_line_bottom}" - #RPROMPT="$(ssh_name)" + 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 } -precmd () { - print -rP +# 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 -function update-mode-file() { +# Update mode file based on current mode +update-mode-file() { set-prompt - local current_mode=$(cat ~/.vi-mode) + 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="" @@ -165,8 +603,10 @@ function check-nvim-running() { fi } +# ZLE line initialization hook function zle-line-init() { zle reset-prompt + vi-mode-indicator case "${KEYMAP}" in vicmd) echo -ne '\e[1 q' @@ -177,9 +617,12 @@ function zle-line-init() { 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' @@ -190,13 +633,46 @@ function zle-keymap-select() { esac } -preexec () { print -rn -- $terminfo[el]; echo -ne '\e[5 q' ; } +# 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 +} -zle -N zle-line-init -zle -N zle-keymap-select +# 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() { - update-mode-file + 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 |
