#!/usr/bin/env bash # PS1 module with Nerd Font fallback + disable switch # Designed for bash 3.2+ (macOS) and bash 4/5 (Linux) # Load this in interactive shells. case "$-" in *i*) ;; *) return 0 ;; esac # Disable switch (per-user/per-shell) # 1) env: DISABLE_GLOBAL_PS1=1 # 2) file: ~/.config/ps1/disable if [[ "${DISABLE_GLOBAL_PS1:-0}" == "1" ]]; then return 0 fi if [[ -f "$HOME/.config/ps1/disable" ]]; then return 0 fi # Force NF separators globally (rounded caps) unless ASCII is forced. export PS1_FORCE_NF=1 # Nerd Font detection (best-effort) + overrides # PS1_FORCE_ASCII=1 -> always fallback separators # PS1_FORCE_NF=1 -> always Nerd Font separators _ps1_has_nf() { if [[ "${PS1_FORCE_ASCII:-0}" == "1" ]]; then return 1; fi if [[ "${PS1_FORCE_NF:-0}" == "1" ]]; then return 0; fi # Linux/WSL: fontconfig often available if command -v fc-list >/dev/null 2>&1; then fc-list 2>/dev/null | grep -qi "Nerd Font" && return 0 fi # macOS: fontconfig often not present; default to fallback unless forced return 1 } # Season selection (dynamic or static) # PS1_SEASON_MODE=dynamic|static # PS1_SEASON= winter|spring|summer|autumn # Also reads config from /etc/ps1-season or ~/.config/ps1/season _ps1_season_config() { local cfg="" if [[ -r /etc/ps1-season ]]; then cfg="/etc/ps1-season" elif [[ -r "$HOME/.config/ps1/season" ]]; then cfg="$HOME/.config/ps1/season" fi if [[ -n "$cfg" ]]; then while IFS='=' read -r k v; do k="${k//[[:space:]]/}" v="${v//[[:space:]]/}" case "$k" in PS1_SEASON_MODE) export PS1_SEASON_MODE="$v" ;; PS1_SEASON) export PS1_SEASON="$v" ;; esac done < "$cfg" fi } _ps1_season_dynamic() { local mm dd m d mm=$(TZ=Europe/Oslo date +%m); dd=$(TZ=Europe/Oslo date +%d) m=$((10#$mm)); d=$((10#$dd)) if (( m >= 3 && m <= 5 )); then echo "spring" elif (( m >= 6 && m <= 8 )); then echo "summer" elif (( m == 9 || m == 10 || (m == 11 && d < 15) )); then echo "autumn" else echo "winter" fi } _ps1_season() { _ps1_season_config local mode="${PS1_SEASON_MODE:-dynamic}" local season="${PS1_SEASON:-}" if [[ "$mode" == "static" && -n "$season" ]]; then echo "$season" else _ps1_season_dynamic fi } # Style selection (aurora/neon/forge/circuit/mono) # PS1_STYLE=aurora|neon|forge|circuit|mono # PS1_STYLE_PACK=standard|contest # PS1_CONTEST_LAYOUT=day-time-user|user-time|time-user|day-time # Also reads config from /etc/ps1-style or ~/.config/ps1/style _ps1_style_config() { local cfg="" if [[ -r /etc/ps1-style ]]; then cfg="/etc/ps1-style" elif [[ -r "$HOME/.config/ps1/style" ]]; then cfg="$HOME/.config/ps1/style" fi if [[ -n "$cfg" ]]; then while IFS='=' read -r k v; do k="${k//[[:space:]]/}" v="${v//[[:space:]]/}" case "$k" in PS1_STYLE) export PS1_STYLE="$v" ;; PS1_STYLE_PACK) export PS1_STYLE_PACK="$v" ;; PS1_CONTEST_LAYOUT) export PS1_CONTEST_LAYOUT="$v" ;; esac done < "$cfg" fi } _ps1_style() { _ps1_style_config local style="${PS1_STYLE:-aurora}" echo "$style" } _ps1_style_pack() { _ps1_style_config local pack="${PS1_STYLE_PACK:-standard}" echo "$pack" } _ps1_contest_layout() { _ps1_style_config local layout="${PS1_CONTEST_LAYOUT:-day-time-user}" echo "$layout" } # Time-based emoji with seasonal accents (Europe/Oslo) _ps1_pick_icon() { local list_name="$1" local season="$2" local day local -a list local idx local offset=0 day=$(TZ=Europe/Oslo date +%j) case "$season" in spring) offset=11 ;; summer) offset=23 ;; autumn) offset=37 ;; *) offset=0 ;; esac eval "list=(\"\${${list_name}[@]}\")" if ((${#list[@]} == 0)); then echo "" return fi idx=$(( (10#$day + offset) % ${#list[@]} )) echo "${list[$idx]}" } _ps1_symbol() { local hh mm h m hh=$(TZ=Europe/Oslo date +%H); mm=$(TZ=Europe/Oslo date +%M) h=$((10#$hh)); m=$((10#$mm)) local season="${__PS1_SEASON:-winter}" local pack="${__PS1_STYLE_PACK:-standard}" local rot_season="${season}" local sunrise coffee work evening night local -a sunrise_list work_list if [[ "$pack" == "contest" ]]; then rot_season="winter" sunrise_list=( "โšก๏ธ" "๐Ÿ›ฐ๏ธ" "๐Ÿงฌ" "๐Ÿง " ) work_list=( "๐Ÿ› ๏ธ" "๐Ÿ’พ" "๐Ÿงช" "๐Ÿงฉ" ) coffee="โ˜•"; evening="๐ŸŽ›๏ธ"; night="๐Ÿ•ถ๏ธ" else case "$season" in spring) sunrise_list=( "๐ŸŒท" "๐ŸŒฑ" "๐Ÿฃ" "๐ŸŒค" ) work_list=( "๐ŸŒฟ" "๐Ÿชด" "๐Ÿ" "๐ŸŒผ" ) coffee="โ˜•"; evening="๐ŸŒค"; night="๐ŸŒ™" ;; summer) sunrise_list=( "๐ŸŒž" "๐Ÿ–๏ธ" "๐ŸŒ…" "๐ŸŒค" ) work_list=( "๐Ÿ„" "๐Ÿ–๏ธ" "๐Ÿšค" "๐ŸŒด" ) coffee="๐Ÿงƒ"; evening="๐ŸŒ‡"; night="๐ŸŒ™" ;; autumn) sunrise_list=( "๐Ÿ" "๐Ÿ‚" "๐ŸŒซ๏ธ" "๐ŸŒฆ๏ธ" ) work_list=( "๐Ÿ‚" "๐Ÿ„" "๐ŸŽƒ" "๐Ÿชต" ) coffee="โ˜•"; evening="๐ŸŒ†"; night="๐ŸŒ™" ;; *) sunrise_list=( "๐Ÿ”๏ธ" "๐ŸŒจ๏ธ" "โ„๏ธ" "๐ŸŒŒ" ) work_list=( "๐ŸŽฟ" "โ›ท๏ธ" "๐Ÿ‚" "๐ŸงŠ" ) coffee="โ˜•"; evening="๐ŸŒ†"; night="๐ŸŒ™" ;; esac fi sunrise="$(_ps1_pick_icon sunrise_list "$rot_season")" work="$(_ps1_pick_icon work_list "$rot_season")" if (( h >= 5 && h <= 8 )); then echo "$sunrise" elif (( h >= 9 && h <= 10 )); then echo "$coffee" elif (( h == 11 && m < 30 )); then echo "๐Ÿฅช" elif (( (h == 11 && m >= 30) || (h >= 12 && h <= 15) )); then echo "$work" elif (( h == 16 )); then echo "๐Ÿฒ" elif (( h >= 17 && h <= 22 )); then echo "$evening" else echo "$night" fi } # Path shortening (keeps /home/user or /Users/user visible) # bash 3.2 safe _ps1_path() { local p="$PWD" local parts=() IFS='/' read -ra parts <<< "$p" if ((${#parts[@]} < 6)); then echo "$p"; return fi local n=${#parts[@]} echo "/${parts[1]}/${parts[2]}/โ€ฆ/${parts[$((n-2))]}/${parts[$((n-1))]}" } # Dynamic vars updated before each prompt __PS1_SYM="" __PS1_PATH="" __PS1_STATUS=0 __PS1_USE_NF=0 __PS1_SEASON="winter" __PS1_STYLE="aurora" __PS1_STYLE_PACK="standard" __PS1_CONTEST_LAYOUT="day-time-user" _ps1_set_prompt() { local RST="\[\e[0m\]" # Seasonal palettes (Z1 is darker than Z2) local Z1_BG Z1_FG Z2_BG Z2_FG PATH_FG FRAME if [[ "$__PS1_STYLE_PACK" == "contest" ]]; then Z1_BG="\[\e[48;5;24m\]" Z1_FG="\[\e[38;5;255m\]" Z2_BG="\[\e[48;5;31m\]" Z2_FG="\[\e[38;5;255m\]" PATH_FG="\[\e[38;5;51m\]" FRAME="\[\e[38;5;23m\]" else case "$__PS1_SEASON" in spring) Z1_BG="\[\e[48;5;71m\]" Z1_FG="\[\e[38;5;255m\]" Z2_BG="\[\e[48;5;120m\]" Z2_FG="\[\e[38;5;22m\]" PATH_FG="\[\e[38;5;22m\]" FRAME="\[\e[38;5;65m\]" ;; summer) Z1_BG="\[\e[48;5;142m\]" Z1_FG="\[\e[38;5;255m\]" Z2_BG="\[\e[48;5;214m\]" Z2_FG="\[\e[38;5;0m\]" PATH_FG="\[\e[38;5;232m\]" FRAME="\[\e[38;5;130m\]" ;; autumn) Z1_BG="\[\e[48;5;95m\]" Z1_FG="\[\e[38;5;255m\]" Z2_BG="\[\e[48;5;173m\]" Z2_FG="\[\e[38;5;255m\]" PATH_FG="\[\e[38;5;223m\]" FRAME="\[\e[38;5;95m\]" ;; *) # winter (default) Z1_BG="\[\e[48;5;61m\]" Z1_FG="\[\e[38;5;255m\]" Z2_BG="\[\e[48;5;37m\]" Z2_FG="\[\e[38;5;255m\]" PATH_FG="\[\e[38;5;194m\]" FRAME="\[\e[38;5;60m\]" ;; esac fi # Status colors local OK="\[\e[38;5;76m\]" local BAD="\[\e[38;5;203m\]" local BOLD="\[\e[1m\]" local NOBOLD="\[\e[22m\]" local left right sep if [[ "$__PS1_USE_NF" -eq 1 ]]; then left="๎‚ถ"; right="๎‚ด"; sep="๎‚ฐ" else left="["; right="]"; sep="โ–ถ" fi local prompt_sym if [[ "$__PS1_STATUS" -eq 0 ]]; then prompt_sym="${OK}${BOLD}โžœ${NOBOLD}${RST}" else prompt_sym="${BAD}${BOLD}โžœ${NOBOLD}${RST}" fi local style="${__PS1_STYLE:-aurora}" local L1="\\d \\A \\u" local L_HOST="@\\h" local PREFIX="" local HOST_PART="${L_HOST}" local PATH_PART="${__PS1_PATH}" local PATH_SEP=" " if [[ "$__PS1_STYLE_PACK" == "contest" ]]; then local layout="${__PS1_CONTEST_LAYOUT:-day-time-user}" case "$layout" in user-time) L1="\\u \\A" ;; time-user) L1="\\A \\u" ;; day-time) L1="\\d \\A" ;; *) L1="\\d \\A \\u" ;; esac HOST_PART="@\\h - ${__PS1_PATH}" PATH_PART="" PATH_SEP="" PREFIX="\n" fi case "$style" in neon) local N1_BG="\[\e[48;5;201m\]" local N1_FG="\[\e[38;5;255m\]" local N2_BG="\[\e[48;5;45m\]" local N2_FG="\[\e[38;5;16m\]" local N_PATH_FG="\[\e[38;5;16m\]" local N_FRAME="\[\e[38;5;199m\]" PS1="\ ${PREFIX}${N_FRAME}โ•ญโ”€${RST}\ ${N1_BG}${N1_FG}${left} ${BOLD}\\u${NOBOLD} \\d \\A ${RST}${N1_BG}${N2_BG}${N2_FG}${sep}${RST}\ ${N2_BG}${N2_FG} ${HOST_PART}${PATH_SEP}${N_PATH_FG}${PATH_PART}${RST}${N2_BG}${N2_FG}${right}${RST}\ \n${N_FRAME}โ•ฐโ”€โ”€ ${RST}${prompt_sym} ${__PS1_SYM} " ;; forge) local F1_BG="\[\e[48;5;94m\]" local F1_FG="\[\e[38;5;230m\]" local F2_BG="\[\e[48;5;166m\]" local F2_FG="\[\e[38;5;231m\]" local F_PATH_FG="\[\e[38;5;224m\]" local F_FRAME="\[\e[38;5;130m\]" PS1="\ ${PREFIX}${F_FRAME}โ•ญโ”€${RST}\ ${F1_BG}${F1_FG}${left} ${BOLD}${L1}${NOBOLD} ${RST}${F1_BG}${F2_BG}${F2_FG}${sep}${RST}\ ${F2_BG}${F2_FG} ${HOST_PART}${PATH_SEP}${F_PATH_FG}${PATH_PART}${RST}${F2_BG}${F2_FG}${right}${RST}\ \n${F_FRAME}โ•ฐโ”€โ”€ ${RST}${prompt_sym} ${__PS1_SYM} " ;; circuit) local C_FG="\[\e[38;5;46m\]" local C_DIM="\[\e[38;5;22m\]" local C_PATH="\[\e[38;5;120m\]" PS1="\ ${PREFIX}${C_DIM}โ”โ”${RST}${C_FG}[${BOLD}\\u${NOBOLD} ${RST}${C_FG}\\A${RST}${C_DIM}]${RST}\ ${C_DIM}โ”${RST}${C_FG}${HOST_PART}${RST}${PATH_SEP}${C_PATH}${PATH_PART}${RST}\ \n${C_DIM}โ”—โ”${RST}${prompt_sym} ${__PS1_SYM} " ;; mono) local M_FG="\[\e[38;5;250m\]" local M_DIM="\[\e[38;5;240m\]" PS1="\ ${PREFIX}${M_FG}โŸฆ${L1}โŸง ${M_DIM}${HOST_PART}${PATH_SEP}${M_FG}${PATH_PART}${RST}\ \n${M_DIM}โ””โ”€${RST}${prompt_sym} ${__PS1_SYM} " ;; *) # aurora (default) PS1="\ ${PREFIX}${FRAME}โ•ญโ”€${RST}\ ${Z1_BG}${Z1_FG}${left} ${L1} ${RST}${Z1_BG}${Z2_BG}${Z2_FG}${sep}${RST}\ ${Z2_BG}${Z2_FG} ${HOST_PART}${PATH_SEP}${PATH_FG}${PATH_PART}${RST}${Z2_BG}${Z2_FG}${right}${RST}\ \n${FRAME}โ•ฐโ”€โ”€ ${RST}${prompt_sym} ${__PS1_SYM} " ;; esac } _ps1_update() { __PS1_STATUS=$? __PS1_SEASON="$(_ps1_season)" __PS1_STYLE_PACK="$(_ps1_style_pack)" __PS1_CONTEST_LAYOUT="$(_ps1_contest_layout)" __PS1_STYLE="$(_ps1_style)" __PS1_SYM="$(_ps1_symbol)" __PS1_PATH="$(_ps1_path)" if _ps1_has_nf; then __PS1_USE_NF=1; else __PS1_USE_NF=0; fi _ps1_set_prompt } ps1_on() { if [[ "${PROMPT_COMMAND:-}" != *"_ps1_update"* ]]; then if [[ -n "${PROMPT_COMMAND:-}" ]]; then PROMPT_COMMAND="${PROMPT_COMMAND}; _ps1_update" else PROMPT_COMMAND="_ps1_update" fi fi _ps1_set_prompt } ps1_off() { :; } # Enable by default when loaded (global standard) ps1_on