|
#----- ZSH - CUSTOM PROMPT ----- |
|
# Powerline Prompt Configuration |
|
|
|
# 1. FORMATTING OPTIONS (Pallete: ~ One Dark Pro) |
|
# Zsh Prompt Expansion for TrueColor (24-bit RGB) support: |
|
# Structure: %F{<Hex>} or %K{<Hex>} |
|
# - %F{...} : Sets Foreground (Text) color |
|
# - %K{...} : Sets Background color |
|
# - Hex : standard RGB Hex string (e.g., '#RRGGBB') |
|
# - %f / %k : Resets foreground / background color to default |
|
# Example: %F{#F44747} sets text color to Red |
|
|
|
local C_YEL='#FFD75F' # Yellow (Session BG / Timer FG) |
|
local C_ORG='#FF8700' # Vibrant Orange (Matching YEL intensity) |
|
local C_PUR='#D55FDE' # Purple (Git BG) |
|
local C_BLU='#61AFEF' # Soft Blue (Path BG / Prompt Arrow FG) |
|
local C_RED='#F44747' # Pastel Red (Error BG) |
|
local C_BLK='#000000' # Black (Local Session BG) |
|
local C_WHT='#FFFFFF' # White (Error Text) |
|
local C_TXT='#1C2C34' # Dark Slate (Text Color for colored blocks) |
|
local C_GRY='#7F848E' # Grey (Separators) |
|
|
|
local S_ITA=$'%{\e[3m%}' # Italic Start |
|
local S_NIT=$'%{\e[23m%}' # Italic Stop |
|
local S_REV=$'%{\e[7m%}' # Reverse (Invert) Start |
|
local S_NRV=$'%{\e[27m%}' # Reverse (Invert) Stop |
|
|
|
# 2. SYMBOL DEFINITIONS |
|
local S_CAP_L=$'\uE0B6' # Start Cap (Left rounded) |
|
local S_CAP_R=$'\uE0B4' # End Cap (Right rounded) |
|
local S_SEP_L=$'\uE0B0' # Segment Separator (Filled arrow) |
|
local S_GAP_R=$'\ue0d7' # Gap Separator (Thin right arrow) |
|
local S_ARR_R=$'\uf054' # Prompt Arrow (Simple chevron) |
|
local S_ARR_U=$'\uf062' # Up Arrow (Error indicator) |
|
local S_PIP_F=$'\u2503' # ┃ Full Height Heavy Pipe (Separator) |
|
local S_PTH_S=$'\uf0da' # Path Separator (Small arrow) |
|
local S_SEP_VR=$'\u2595' # ▕ Right Light Vertical Bar |
|
local S_SEP_VL=$'\u258F' # ▏ Left Light Vertical Bar |
|
local S_GIT_B=$'\uE0A0' # Git Branch Symbol |
|
local S_GIT_A=$'\u21E1' # ⇡ Git Ahead |
|
local S_GIT_D=$'\u21E3' # ⇣ Git Behind |
|
local S_TIM_C=$'\uf017' # Clock Symbol |
|
local S_ERR_S=$'\uf00d' # Error Symbol (Cross) |
|
local S_GIT_CLN=$'\U000F0E1E' # Clean State (Custom) |
|
local S_GIT_DRT=$S_ERR_S # Dirty State (Same as Error) |
|
|
|
# 3. HELPER FUNCTIONS |
|
|
|
# Global flag to track first run |
|
typeset -g _PROMPT_FIRST_RUN=1 |
|
# Global flag to determine if initial newline is needed based on cursor position |
|
typeset -g _PROMPT_INIT_NEWLINE=0 |
|
|
|
# Intelligent Cursor Detection at Startup |
|
# Checks if the cursor is currently at Row 1. If not (e.g. Welcome Message), we need a newline. |
|
# Run inside an anonymous function to keep scope clean |
|
() { |
|
# Only run if connected to a terminal |
|
if [[ -t 1 ]]; then |
|
local pos |
|
local row |
|
local col |
|
|
|
# Save TTY settings approach caused issues on some systems |
|
# Instead, explicitly toggle the specific settings we need. |
|
# -echo: don't show the response code |
|
# -icanon: raw mode (so we don't need to wait for Enter) |
|
stty -echo -icanon |
|
|
|
# Ask terminal for cursor position: ESC [ 6 n |
|
echo -n $'\e[6n' > /dev/tty |
|
|
|
# Read response silently, timeout 0.1s, stop at 'R' |
|
# Response format: ESC [ Row ; Col R |
|
read -s -t 0.1 -d R pos < /dev/tty |
|
|
|
# Restore TTY settings explicitly |
|
stty echo icanon |
|
|
|
# Parse output |
|
# Remove initial ESC [ (which is \e[) |
|
pos="${pos##*$'\e'\[}" |
|
|
|
# Split Row;Col |
|
row="${pos%%;*}" |
|
|
|
# If valid row and row > 1, prompt is not at top |
|
if [[ -n "$row" && "$row" -gt 1 ]]; then |
|
_PROMPT_INIT_NEWLINE=1 |
|
fi |
|
fi |
|
} |
|
|
|
# Helper: Prompt Transition (2-Char Solution) |
|
# Usage: prompt_trans [Previous BG] [Next BG] |
|
prompt_trans() { |
|
echo -n "%k%F{$1}${S_SEP_L}%k%F{$2}${S_GAP_R}" |
|
} |
|
|
|
# Timer Logic (Pre-exec/cmd hooks) |
|
zmodload zsh/datetime |
|
preexec() { cmd_start=$EPOCHREALTIME; } |
|
|
|
precmd() { |
|
local exit_code=$? # Must be the very first line to capture status |
|
|
|
# Calculate execution time |
|
local now=$EPOCHREALTIME |
|
local dur=$(( now - ${cmd_start:-$now} )) |
|
unset cmd_start |
|
|
|
# Rebuild the prompt dynamically, passing duration and exit code |
|
build_prompt "$dur" "$exit_code" |
|
} |
|
|
|
# 4. PROMPT FUNCTION |
|
build_prompt() { |
|
local dur=$1 |
|
local exit_code=$2 |
|
|
|
# --- Segment: ERROR (Conditional, Detached) --- |
|
# Logic: Red BG / Slate Text, pointing up to previous command |
|
local seg_error="" |
|
if [[ $exit_code -ne 0 ]]; then |
|
seg_error="%F{$C_RED}%k${S_CAP_L}%K{$C_RED}%F{$C_TXT} ${S_ERR_S} ${exit_code} ${S_ARR_U} %k%F{$C_RED}${S_CAP_R}%f" |
|
fi |
|
|
|
# --- Segment: ENVIRONMENT (Session) --- |
|
# Logic: |
|
# - Local: Yellow BG ($C_YEL) for both parts. |
|
# - SSH: Orange BG ($C_ORG) for User/Host, Yellow BG ($C_YEL) for Shell. |
|
|
|
local is_ssh="" |
|
[[ -n "$SSH_CLIENT" || -n "$SSH_TTY" ]] && is_ssh=1 |
|
|
|
local bg_user |
|
local bg_shell="$C_YEL" |
|
|
|
if [[ -n "$is_ssh" ]]; then |
|
bg_user="$C_ORG" |
|
else |
|
bg_user="$C_YEL" |
|
fi |
|
|
|
local fg_user="$C_TXT" |
|
local fg_shell="$C_TXT" |
|
|
|
local current_shell="${SHELL:t} ${ZSH_VERSION}" |
|
local env_text="" |
|
if [[ -n "$ANDROID_DATA" ]]; then |
|
env_text="Termux" |
|
else |
|
env_text="%n%F{$C_GRY}@%F{$fg_user}%m" |
|
fi |
|
|
|
# Construction |
|
# Part 1: Start Cap + User Text |
|
local seg_env_part1="%F{$bg_user}%k${S_CAP_L}%K{$bg_user}%F{$fg_user}${S_ITA} ${env_text}${S_NIT}" |
|
|
|
# Part 2: Separator (Dual-bar trick, always) |
|
# This ensures consistent spacing and alignment for both Local and SSH |
|
local seg_env_sep="%K{$bg_user}%F{$C_TXT}${S_SEP_VR}%K{$bg_shell}%F{$C_TXT}${S_SEP_VL}" |
|
|
|
# Part 3: Shell Text |
|
local seg_env_part2="%K{$bg_shell}%F{$fg_shell}${S_ITA}${current_shell} ${S_NIT}" |
|
|
|
local seg_env="${seg_env_part1}${seg_env_sep}${seg_env_part2}" |
|
|
|
# --- Second Line Alignment Calculation --- |
|
local pure_env_text=$([[ -n "$ANDROID_DATA" ]] && echo "Termux" || echo "${(%):-%n@%m}") |
|
local n_len=${#pure_env_text} |
|
|
|
# Pad 1: Center HH:MM (length 5) under user@host (starts at offset 2) |
|
# Center of user@host is at: 2 + (n_len / 2) |
|
# Time starts at: Center - 2.5, which is roughly n_len / 2 |
|
local pad1_len=$(( n_len / 2 )) |
|
local pad1="${(l:$pad1_len:: :)}" |
|
|
|
# Pad 2: Gap between HH:MM and Prompt Char |
|
# Prompt Char should be at offset 2 + n_len (the separator's position) |
|
# Current position after HH:MM: pad1_len + 5 |
|
local pad2_len=$(( 2 + n_len - (pad1_len + 5) )) |
|
[[ $pad2_len -lt 0 ]] && pad2_len=0 |
|
local pad2="${(l:$pad2_len:: :)}" |
|
|
|
# --- Transition: Environment -> Path --- |
|
# From Shell BG (Yellow) -> Soft Blue |
|
local trans_env_path="$(prompt_trans $bg_shell $C_BLU)" |
|
|
|
# --- Segment: PATH --- |
|
# Logic: Soft Blue BG / Slate Text with Smart Truncation |
|
|
|
# Responsive Layout Strategy: |
|
# - Desktop (>= 80 cols): Relaxed limit (90% width), almost no truncation. |
|
# - Mobile (< 80 cols): Aggressive limit (35% width, min 20 chars) to prevent wrapping. |
|
local term_width=${COLUMNS:-80} |
|
local max_path_len=20 |
|
|
|
if [[ $term_width -ge 80 ]]; then |
|
max_path_len=$(( term_width * 90 / 100 )) |
|
else |
|
max_path_len=$(( term_width * 35 / 100 )) |
|
[[ $max_path_len -lt 20 ]] && max_path_len=20 |
|
fi |
|
|
|
local full_path="${(D)PWD}" |
|
local styled_path="" |
|
|
|
if [[ ${#full_path} -gt $max_path_len ]]; then |
|
# Keep the last $max_path_len characters and add prefix |
|
local trunc_path="..${full_path: -$max_path_len}" |
|
styled_path="${trunc_path//\// ${S_PTH_S} }" |
|
else |
|
styled_path="${full_path//\// ${S_PTH_S} }" |
|
fi |
|
|
|
local seg_path="%K{$C_BLU}%F{$C_TXT} ${styled_path} " |
|
|
|
# --- Segment: GIT (Conditional) --- |
|
local seg_git="" |
|
# Check if in Git repo |
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
|
local ref=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null) |
|
|
|
# Transition: Path (Blue) -> Git (Purple) |
|
local trans_path_git="$(prompt_trans $C_BLU $C_PUR)" |
|
|
|
# Body: Purple BG, Slate Text |
|
local body_git="%K{$C_PUR}%F{$C_TXT} ${S_GIT_B} ${ref} " |
|
|
|
# Check Clean/Dirty |
|
local status_text=$(git status --porcelain --ignore-submodules 2>/dev/null) |
|
if [[ -n "$status_text" ]]; then |
|
body_git+="${S_GIT_DRT} " |
|
|
|
# Count modified, added, deleted files |
|
local gMod=0 |
|
local gAdd=0 |
|
local gDel=0 |
|
|
|
# Split status_text by newlines into an array |
|
local -a status_lines |
|
status_lines=("${(@f)status_text}") |
|
|
|
for line in $status_lines; do |
|
# Check for Modified (M at start or space-M) |
|
if [[ "$line" == M* || "$line" == " M"* ]]; then ((gMod++)); fi |
|
# Check for Added (??) |
|
if [[ "$line" == \?\?* ]]; then ((gAdd++)); fi |
|
# Check for Deleted (D at start or space-D) |
|
if [[ "$line" == D* || "$line" == " D"* ]]; then ((gDel++)); fi |
|
done |
|
|
|
[[ $gMod -gt 0 ]] && body_git+="~${gMod} " |
|
[[ $gAdd -gt 0 ]] && body_git+="+${gAdd} " |
|
[[ $gDel -gt 0 ]] && body_git+="-${gDel} " |
|
else |
|
body_git+="${S_GIT_CLN} " |
|
fi |
|
|
|
# Check Upstream Status (Ahead/Behind) |
|
local git_counts=$(git rev-list --left-right --count HEAD...@{u} 2>/dev/null) |
|
if [[ -n "$git_counts" ]]; then |
|
local -a counts |
|
counts=(${=git_counts}) |
|
local ahead=${counts[1]} |
|
local behind=${counts[2]} |
|
[[ $ahead -gt 0 ]] && body_git+="${S_GIT_A}${ahead} " |
|
[[ $behind -gt 0 ]] && body_git+="${S_GIT_D}${behind} " |
|
fi |
|
|
|
# Exit: Git (Purple) -> Transparent |
|
local exit_git="%k%F{$C_PUR}${S_SEP_L}%f" |
|
|
|
seg_git="${trans_path_git}${body_git}${exit_git}" |
|
else |
|
# No Git? Just exit Path (Soft Blue) -> Transparent |
|
seg_git="%k%F{$C_BLU}${S_SEP_L}%f" |
|
fi |
|
|
|
# --- Segment: TIMER (Conditional, Detached) --- |
|
# Logic: Yellow BG / Slate Text ($C_TXT) - Capsule style |
|
local seg_time="" |
|
if (( dur >= 2 )); then |
|
local time_str |
|
if (( dur >= 3600 )); then |
|
local h=$(( dur / 3600 )) |
|
local rem=$(( dur % 3600 )) |
|
local m=$(( rem / 60 )) |
|
time_str=$(printf "%dh %dm" $h $m) |
|
elif (( dur >= 60 )); then |
|
time_str=$(printf "%dm %.1fs" $(( dur / 60 )) $(( dur % 60 ))) |
|
else |
|
time_str=$(printf "%.1fs" $dur) |
|
fi |
|
time_str="${time_str/./,}" |
|
|
|
seg_time="%F{$C_YEL}%k${S_CAP_L}%K{$C_YEL}%F{$C_TXT} ${S_TIM_C} ${time_str} %k%F{$C_YEL}${S_CAP_R}%f" |
|
fi |
|
|
|
# --- Segment: PROMPT CHAR (Line 2) --- |
|
# Logic: Newline -> Pad1 -> Time -> Pad2 -> User/Root char -> Soft Blue Arrow |
|
local time_now="%D{%H:%M}" |
|
local prompt_char="%(!.#.$)" |
|
local seg_end="%k%f"$'\n'"${pad1}%F{$C_GRY}${time_now}${pad2} %F{$C_BLU}${prompt_char} ${S_ARR_R} %f" |
|
|
|
# --- Final: PROMPT ASSEMBLY --- |
|
|
|
# Handle Initial Newline logic using global flags |
|
local prefix=$'\n' |
|
if [[ -n "$_PROMPT_FIRST_RUN" ]]; then |
|
# If it's the first run, check if we detected a "dirty" screen (Welcome msg) |
|
if [[ "$_PROMPT_INIT_NEWLINE" -eq 1 ]]; then |
|
prefix=$'\n' |
|
else |
|
prefix="" |
|
fi |
|
unset _PROMPT_FIRST_RUN |
|
fi |
|
|
|
local top_line="" |
|
[[ -n "$seg_error" ]] && top_line+="${seg_error} " |
|
[[ -n "$seg_time" ]] && top_line+="${seg_time}" |
|
|
|
# Trim trailing space if only error existed |
|
top_line="${top_line% }" |
|
|
|
if [[ -n "$top_line" ]]; then |
|
# TopLine + Newline + MainPrompt |
|
# Note: top_line already contains colors, but we need separation |
|
PROMPT="${prefix}${top_line}"$'\n\n'"${seg_env}${trans_env_path}${seg_path}${seg_git}${seg_end}" |
|
else |
|
PROMPT="${prefix}${seg_env}${trans_env_path}${seg_path}${seg_git}${seg_end}" |
|
fi |
|
} |