Created
June 4, 2026 11:03
-
-
Save colegatron/b76e32edf9d294e698a7c12e8d1b3fa6 to your computer and use it in GitHub Desktop.
Nice prompt for shell. Kubernetes, git, python env and nice colors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ============================================================================ | |
| # Check Bash Version - This script requires Bash 4.0+ | |
| # ============================================================================ | |
| if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then | |
| echo "ERROR: This prompt requires Bash 4.0 or newer" | |
| echo "Current version: ${BASH_VERSION}" | |
| echo "" | |
| echo "Solution: Install bash with Homebrew:" | |
| echo " brew install bash" | |
| echo " chsh -s /opt/homebrew/bin/bash" | |
| echo " (Then close and reopen Terminal)" | |
| return 1 | |
| fi | |
| # ============================================================================ | |
| # Enhanced Bash Prompt (PS1) with Git, Kubernetes, Docker, and more | |
| # ============================================================================ | |
| # Features: | |
| # • Git branch & status (dirty/clean) | |
| # • Kubernetes context (with color coding per context) | |
| # • Docker context | |
| # • Smart directory truncation (~/previous/current when long) | |
| # • Exit status of last command (color-coded) | |
| # • Python virtualenv indicator | |
| # • Background job count | |
| # • User/hostname (useful for SSH sessions) | |
| # • Time (optional, default off) | |
| # ============================================================================ | |
| # Guard: skip re-declaration if already loaded (avoids readonly errors on re-source) | |
| [[ -n "${_BASH_PROMPT_LOADED:-}" ]] && { PROMPT_COMMAND=__build_prompt; return 0; } | |
| readonly _BASH_PROMPT_LOADED=1 | |
| # Color definitions | |
| readonly COLOR_RESET='\[\033[0m\]' | |
| readonly COLOR_BOLD='\[\033[1m\]' | |
| readonly COLOR_DIM='\[\033[2m\]' | |
| readonly COLOR_BLINK='\[\033[5m\]' | |
| readonly COLOR_BLINK_OFF='\[\033[25m\]' | |
| # Powerline separator character | |
| readonly SEPARATOR=$'' | |
| # Light grey gradient backgrounds (slightly darker left → lighter right) | |
| readonly BG_GREY_1='\[\033[48;5;246m\]' # Light grey (darkest in scheme) | |
| readonly BG_GREY_2='\[\033[48;5;248m\]' # Slightly lighter | |
| readonly BG_GREY_3='\[\033[48;5;250m\]' # Lighter | |
| readonly BG_GREY_4='\[\033[48;5;252m\]' # Very light | |
| readonly BG_GREY_5='\[\033[48;5;253m\]' # Near white | |
| readonly BG_GREY_6='\[\033[48;5;255m\]' # White-ish | |
| # Foreground colors matching backgrounds (for separator transitions) | |
| readonly FG_GREY_1='\[\033[38;5;246m\]' | |
| readonly FG_GREY_2='\[\033[38;5;248m\]' | |
| readonly FG_GREY_3='\[\033[38;5;250m\]' | |
| readonly FG_GREY_4='\[\033[38;5;252m\]' | |
| readonly FG_GREY_5='\[\033[38;5;253m\]' | |
| readonly FG_GREY_6='\[\033[38;5;255m\]' | |
| # Dark text colors (visible on light grey backgrounds) | |
| readonly COLOR_DARK_RED='\[\033[38;5;88m\]' | |
| readonly COLOR_DARK_GREEN='\[\033[38;5;28m\]' | |
| readonly COLOR_DARK_NAVY='\[\033[38;5;17m\]' | |
| readonly COLOR_DARK_TEAL='\[\033[38;5;23m\]' | |
| readonly COLOR_DARK_BLUE='\[\033[38;5;18m\]' | |
| readonly COLOR_DARK_ORANGE='\[\033[38;5;130m\]' | |
| readonly COLOR_DARK_PURPLE='\[\033[38;5;55m\]' | |
| readonly COLOR_DARK_GREY='\[\033[38;5;238m\]' | |
| # Kept for compatibility (prompt_set_k8s_color etc.) | |
| readonly COLOR_BLACK='\[\033[38;5;0m\]' | |
| readonly COLOR_RED="${COLOR_DARK_RED}" | |
| readonly COLOR_GREEN="${COLOR_DARK_GREEN}" | |
| readonly COLOR_BRIGHT_GREEN="${COLOR_DARK_GREEN}" | |
| readonly COLOR_BRIGHT_RED="${COLOR_DARK_RED}" | |
| readonly COLOR_BRIGHT_CYAN="${COLOR_DARK_TEAL}" | |
| readonly COLOR_BRIGHT_YELLOW="${COLOR_DARK_ORANGE}" | |
| readonly COLOR_BRIGHT_BLUE="${COLOR_DARK_BLUE}" | |
| readonly COLOR_BRIGHT_MAGENTA="${COLOR_DARK_PURPLE}" | |
| readonly COLOR_BRIGHT_BLACK="${COLOR_DARK_GREY}" | |
| # ============================================================================ | |
| # CONFIGURATION | |
| # ============================================================================ | |
| # Set to true to show time in prompt | |
| PROMPT_SHOW_TIME=false | |
| # Set to true to show full paths instead of smart truncation | |
| PROMPT_FULL_PATH=false | |
| # K8s context color mapping (context_name=color_code) | |
| declare -A K8S_COLORS=( | |
| [local]="${COLOR_DARK_GREEN}" | |
| [docker-desktop]="${COLOR_DARK_GREEN}" | |
| [minikube]="${COLOR_DARK_TEAL}" | |
| [staging]="${COLOR_DARK_BLUE}" | |
| [production]="${COLOR_DARK_RED}" | |
| [dev]="${COLOR_DARK_PURPLE}" | |
| ) | |
| # ============================================================================ | |
| # HELPER FUNCTIONS | |
| # ============================================================================ | |
| # Get the exit status of the last command with color | |
| __prompt_exit_status() { | |
| local status=$? | |
| if [[ $status -eq 0 ]]; then | |
| printf "%s" "${COLOR_DARK_GREEN}✓${COLOR_RESET}" | |
| else | |
| printf "%s" "${COLOR_DARK_RED}✗ ${status}${COLOR_RESET}" | |
| fi | |
| return $status | |
| } | |
| # Get git repo name and branch with status (raw, for conditional display) | |
| __prompt_git_info_raw() { | |
| local repo_name | |
| local branch | |
| local status | |
| local branch_color | |
| local display_branch | |
| local is_dirty | |
| # Check if we're in a git repo | |
| if ! git rev-parse --git-dir > /dev/null 2>&1; then | |
| return | |
| fi | |
| # Get repository name | |
| repo_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null) | |
| [[ -z "$repo_name" ]] && return | |
| # Get branch name | |
| branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) | |
| [[ -z "$branch" ]] && return | |
| # Default branch color | |
| branch_color="${COLOR_DARK_TEAL}" | |
| # Check if working directory is dirty | |
| if git diff --quiet --ignore-submodules HEAD 2>/dev/null; then | |
| status="${COLOR_DARK_GREEN}✓${COLOR_RESET}" | |
| is_dirty=false | |
| else | |
| status="${COLOR_DARK_RED}*${COLOR_RESET}" | |
| is_dirty=true | |
| fi | |
| # Special styling for master branch - add blink effect only if dirty | |
| if [[ "$branch" == "master" && "$is_dirty" == "true" ]]; then | |
| display_branch="${COLOR_BLINK}$(echo $branch | tr '[:lower:]' '[:upper:]')${COLOR_BLINK_OFF}" | |
| else | |
| display_branch="$branch" | |
| fi | |
| printf "%s" "${BG_GREY_4}${branch_color} 🔀 (${repo_name}) ${display_branch} ${COLOR_RESET}${BG_GREY_4}${status}${COLOR_RESET}" | |
| } | |
| # Get git repo name and branch with status | |
| __prompt_git_info() { | |
| __prompt_git_info_raw | |
| } | |
| # Get Kubernetes context (raw, for conditional display) | |
| __prompt_k8s_context_raw() { | |
| local context | |
| local color | |
| if ! command -v kubectl &> /dev/null; then | |
| return | |
| fi | |
| context=$(kubectl config current-context 2>/dev/null) | |
| [[ -z "$context" ]] && return | |
| # Look up color for this context | |
| color="${K8S_COLORS[$context]:-${COLOR_DARK_PURPLE}}" | |
| # Special styling for production context - add blink effect | |
| if [[ "$context" == "production" ]]; then | |
| context="${COLOR_BLINK}$(echo $context | tr '[:lower:]' '[:upper:]')${COLOR_BLINK_OFF}" | |
| fi | |
| printf "%s" "${BG_GREY_1}${color} 🔯 ${context} ${COLOR_RESET}" | |
| } | |
| # Get Kubernetes context with color coding | |
| __prompt_k8s_context() { | |
| __prompt_k8s_context_raw | |
| } | |
| # Get Docker context (raw, for conditional display) | |
| __prompt_docker_context_raw() { | |
| local context | |
| if ! command -v docker &> /dev/null; then | |
| return | |
| fi | |
| context=$(docker context show 2>/dev/null) | |
| [[ -z "$context" ]] && return | |
| printf "%s" "${BG_GREY_2}${COLOR_DARK_TEAL} 🐳 ${context} ${COLOR_RESET}" | |
| } | |
| # Get Docker context | |
| __prompt_docker_context() { | |
| __prompt_docker_context_raw | |
| } | |
| # Get Python virtualenv info (raw, for conditional display) | |
| __prompt_virtualenv_raw() { | |
| if [[ -z "$VIRTUAL_ENV" ]]; then | |
| return | |
| fi | |
| local venv_name | |
| venv_name=$(basename "$VIRTUAL_ENV") | |
| printf "%s" "${BG_GREY_5}${COLOR_DARK_GREEN} 🐍 ${venv_name} ${COLOR_RESET}" | |
| } | |
| # Get Python virtualenv info | |
| __prompt_virtualenv() { | |
| __prompt_virtualenv_raw | |
| } | |
| # Get background job count (raw, for conditional display) | |
| __prompt_jobs_raw() { | |
| local count | |
| count=$(jobs -p | wc -l) | |
| if [[ $count -gt 0 ]]; then | |
| printf "%s" "${BG_GREY_5}${COLOR_DARK_PURPLE} ⚙ ${count} ${COLOR_RESET}" | |
| fi | |
| } | |
| # Get background job count | |
| __prompt_jobs() { | |
| __prompt_jobs_raw | |
| } | |
| # Get time (optional) | |
| __prompt_time() { | |
| if [[ "$PROMPT_SHOW_TIME" == "true" ]]; then | |
| printf "%s" " ${COLOR_DIM}[ $(date '+%H:%M:%S') ]${COLOR_RESET}" | |
| fi | |
| } | |
| # Smart directory truncation | |
| __prompt_directory() { | |
| local pwd_path="$PWD" | |
| local max_length=40 | |
| if [[ "$PROMPT_FULL_PATH" == "true" ]]; then | |
| printf "%s" "${COLOR_DIM}[ ${COLOR_RESET}${COLOR_DARK_NAVY}${pwd_path}${COLOR_RESET}${COLOR_DIM} ]${COLOR_RESET}" | |
| return | |
| fi | |
| # Replace home with ~ | |
| pwd_path="${pwd_path/#$HOME/~}" | |
| # If too long, show ~/...previous/current | |
| if [[ ${#pwd_path} -gt $max_length ]]; then | |
| local current_dir | |
| local parent_dir | |
| current_dir="${pwd_path##*/}" | |
| parent_dir="${pwd_path%/*}" | |
| parent_dir="${parent_dir##*/}" | |
| pwd_path="~/${COLOR_DIM}...${COLOR_RESET}${parent_dir}/${current_dir}" | |
| fi | |
| printf "%s" "${COLOR_DIM}[ ${COLOR_RESET}${COLOR_DARK_NAVY}${pwd_path}${COLOR_RESET}${COLOR_DIM} ]${COLOR_RESET}" | |
| } | |
| # Create a clickable "../" link in iTerm2 when available | |
| __prompt_parent_link() { | |
| if [[ "${TERM_PROGRAM-}" == "iTerm.app" ]]; then | |
| printf "%s" $'\[\033]8;;iterm2://run?command=cd%20..\a\]../\[\033]8;;\a\]' | |
| else | |
| printf "%s" "../" | |
| fi | |
| } | |
| # ============================================================================ | |
| # MAIN PROMPT BUILDING | |
| # ============================================================================ | |
| __build_prompt() { | |
| local exit_status | |
| local k8s_info docker_info git_info venv_info jobs_info display_path | |
| # Save exit status before anything else | |
| exit_status=$? | |
| # Capture optional sections | |
| k8s_info="$(__prompt_k8s_context_raw)" | |
| docker_info="$(__prompt_docker_context_raw)" | |
| git_info="$(__prompt_git_info_raw)" | |
| venv_info="$(__prompt_virtualenv_raw)" | |
| jobs_info="$(__prompt_jobs_raw)" | |
| # Show only ../current-folder; use clickable ../ in iTerm2 if available | |
| local _full_path="${PWD/#$HOME/~}" | |
| if [[ "$_full_path" == "~" || "$_full_path" == "/" ]]; then | |
| display_path="$_full_path" | |
| else | |
| local _current="${_full_path##*/}" | |
| display_path="$(__prompt_parent_link)${_current}" | |
| fi | |
| # Line 1: [space] → K8s → Docker → Directory → Git → Venv → Jobs → [arrow] | |
| PS1="${BG_GREY_1} ${COLOR_RESET}${k8s_info}" | |
| # K8s → Docker or Directory | |
| if [[ -n "$docker_info" ]]; then | |
| PS1+="${FG_GREY_1}${BG_GREY_2}${SEPARATOR}${COLOR_RESET}${docker_info}" | |
| PS1+="${FG_GREY_2}${BG_GREY_3}${SEPARATOR}${COLOR_RESET}" | |
| else | |
| PS1+="${FG_GREY_1}${BG_GREY_3}${SEPARATOR}${COLOR_RESET}" | |
| fi | |
| # Directory (always shown) | |
| PS1+="${BG_GREY_3}${COLOR_DARK_NAVY} 📁 ${display_path} ${COLOR_RESET}" | |
| # Directory → Git → Venv → Jobs → [ending arrow] | |
| if [[ -n "$git_info" ]]; then | |
| PS1+="${FG_GREY_3}${BG_GREY_4}${SEPARATOR}${COLOR_RESET}${git_info}" | |
| if [[ -n "$venv_info" ]]; then | |
| PS1+="${FG_GREY_4}${BG_GREY_5}${SEPARATOR}${COLOR_RESET}${venv_info}" | |
| if [[ -n "$jobs_info" ]]; then | |
| PS1+="${FG_GREY_5}${BG_GREY_5}${SEPARATOR}${COLOR_RESET}${jobs_info}" | |
| fi | |
| PS1+="${FG_GREY_5}${SEPARATOR}${COLOR_RESET}" | |
| elif [[ -n "$jobs_info" ]]; then | |
| PS1+="${FG_GREY_4}${BG_GREY_5}${SEPARATOR}${COLOR_RESET}${jobs_info}" | |
| PS1+="${FG_GREY_5}${SEPARATOR}${COLOR_RESET}" | |
| else | |
| PS1+="${FG_GREY_4}${SEPARATOR}${COLOR_RESET}" | |
| fi | |
| elif [[ -n "$venv_info" ]]; then | |
| PS1+="${FG_GREY_3}${BG_GREY_5}${SEPARATOR}${COLOR_RESET}${venv_info}" | |
| if [[ -n "$jobs_info" ]]; then | |
| PS1+="${FG_GREY_5}${BG_GREY_5}${SEPARATOR}${COLOR_RESET}${jobs_info}" | |
| fi | |
| PS1+="${FG_GREY_5}${SEPARATOR}${COLOR_RESET}" | |
| elif [[ -n "$jobs_info" ]]; then | |
| PS1+="${FG_GREY_3}${BG_GREY_5}${SEPARATOR}${COLOR_RESET}${jobs_info}" | |
| PS1+="${FG_GREY_5}${SEPARATOR}${COLOR_RESET}" | |
| else | |
| PS1+="${FG_GREY_3}${SEPARATOR}${COLOR_RESET}" | |
| fi | |
| PS1+="\n" | |
| # Line 2: user@host → exit status → prompt symbol | |
| PS1+="${BG_GREY_4} ${COLOR_RESET}${BG_GREY_4}${COLOR_DARK_GREEN}\u${COLOR_RESET}${BG_GREY_4}@${COLOR_RESET}${BG_GREY_4}${COLOR_DARK_ORANGE}\h${COLOR_RESET}${BG_GREY_4} ${COLOR_RESET}" | |
| PS1+="${FG_GREY_4}${BG_GREY_5}${SEPARATOR}${COLOR_RESET}" | |
| PS1+="${BG_GREY_5} ${COLOR_RESET}${BG_GREY_5}" | |
| PS1+="$(__prompt_exit_status)" | |
| PS1+="${COLOR_RESET}${BG_GREY_5} ${COLOR_RESET}" | |
| PS1+="${FG_GREY_5}${BG_GREY_6}${SEPARATOR}${COLOR_RESET}" | |
| if [[ $EUID -eq 0 ]]; then | |
| PS1+="${BG_GREY_6} ${COLOR_RESET}${BG_GREY_6}${COLOR_DARK_RED}#${COLOR_RESET}${BG_GREY_6} ${COLOR_RESET}${FG_GREY_6}${SEPARATOR}${COLOR_RESET}" | |
| else | |
| PS1+="${BG_GREY_6} ${COLOR_RESET}${BG_GREY_6}${COLOR_DARK_GREEN}\$${COLOR_RESET}${BG_GREY_6} ${COLOR_RESET}${FG_GREY_6}${SEPARATOR}${COLOR_RESET}" | |
| fi | |
| PS1+=" " | |
| return $exit_status | |
| } | |
| # Set the prompt command | |
| PROMPT_COMMAND=__build_prompt | |
| # Optionally set secondary prompt (for multi-line commands) | |
| PS2="${COLOR_DARK_GREY}→${COLOR_RESET} " | |
| # ============================================================================ | |
| # CONFIGURATION FUNCTIONS | |
| # ============================================================================ | |
| # Function to toggle time display | |
| prompt_toggle_time() { | |
| if [[ "$PROMPT_SHOW_TIME" == "true" ]]; then | |
| PROMPT_SHOW_TIME=false | |
| echo "Time display: OFF" | |
| else | |
| PROMPT_SHOW_TIME=true | |
| echo "Time display: ON" | |
| fi | |
| } | |
| # Function to toggle full path | |
| prompt_toggle_fullpath() { | |
| if [[ "$PROMPT_FULL_PATH" == "true" ]]; then | |
| PROMPT_FULL_PATH=false | |
| echo "Full path display: OFF" | |
| else | |
| PROMPT_FULL_PATH=true | |
| echo "Full path display: ON" | |
| fi | |
| } | |
| # Function to set K8s context color | |
| prompt_set_k8s_color() { | |
| local context=$1 | |
| local color=$2 | |
| if [[ -z "$context" ]] || [[ -z "$color" ]]; then | |
| echo "Usage: prompt_set_k8s_color <context> <color_code>" | |
| echo "Example: prompt_set_k8s_color production '\033[91m'" | |
| return 1 | |
| fi | |
| K8S_COLORS[$context]=$color | |
| echo "Set ${context} → $color" | |
| } | |
| # Function to show current config | |
| prompt_show_config() { | |
| echo "=== Prompt Configuration ===" | |
| echo "Time display: $PROMPT_SHOW_TIME" | |
| echo "Full path: $PROMPT_FULL_PATH" | |
| echo "" | |
| echo "K8s context colors:" | |
| for context in "${!K8S_COLORS[@]}"; do | |
| echo " $context → ${K8S_COLORS[$context]}" | |
| done | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment