Files
profile/profile.d/prompt.sh
T
2026-10-13 14:35:00 +02:00

695 lines
27 KiB
Bash

#!/usr/bin/env bash
# ------------------------------------------------------------------------------
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
# Protected by the BSD3 license. Please read bellow for details.
#
# * Redistribution and use in source and binary forms,
# * with or without modification, are permitted provided
# * that the following conditions are met:
# *
# * Redistributions of source code must retain the above
# * copyright notice, this list of conditions and the
# * following disclaimer.
# *
# * Redistributions in binary form must reproduce the above
# * copyright notice, this list of conditions and the following
# * disclaimer in the documentation and/or other materials
# * provided with the distribution.
# *
# * Neither the name of the copyright holder nor the names
# * of any other contributors may be used to endorse or
# * promote products derived from this software without
# * specific prior written permission.
# *
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
load_conf "prompt"
# ------------------------------------------------------------------------------
# Colour variable names defined by disp.sh. They are the only names a theme or
# the [prompt] configuration may reference (as $Name or ${Name}), and the only
# palette variables a theme may redefine.
_PROMPT_COLOR_NAMES=(
Black Red Green Yellow Blue Purple Cyan White
BBlack BRed BGreen BYellow BBlue BPurple BCyan BWhite
UBlack URed UGreen UYellow UBlue UPurple UCyan UWhite
On_Black On_Red On_Green On_Yellow On_Blue On_Purple On_Cyan On_White
IBlack IRed IGreen IYellow IBlue IPurple ICyan IWhite
BIBlack BIRed BIGreen BIYellow BIBlue BIPurple BICyan BIWhite
On_IBlack On_IRed On_IGreen On_IYellow On_IBlue On_IPurple On_ICyan On_IWhite
DEFAULTFG DEFAULTBG DEFAULTCOL RESETCOL
)
# The same names as an ERE alternation (Black|Red|...)
_PROMPT_COLOR_RE=""
for _pc_name in "${_PROMPT_COLOR_NAMES[@]}"; do
_PROMPT_COLOR_RE+="|${_pc_name}"
done
_PROMPT_COLOR_RE="${_PROMPT_COLOR_RE#|}"
unset _pc_name
# ------------------------------------------------------------------------------
# Check a colour value and put its resolved form in _PROMPT_RESOLVED.
# A value is a sequence of blocks, each one either
# $ColorName or ${ColorName} — colour variable from disp.sh (resolved by
# indirection, never evaluated)
# \e[...m or \033[...m — raw ANSI escape literal
# Anything else is refused (return 1): colours end up inside PS1, so they must
# not be able to carry anything else than colour codes.
# Usage: _prompt_resolve_color <value>
_prompt_resolve_color()
{
local rest="$1" out="" name
local ref_re='^\$(\{('"$_PROMPT_COLOR_RE"')\}|('"$_PROMPT_COLOR_RE"'))'
local ansi_re='^(\\e|\\033)\[[0-9;]*m'
[[ -n $rest ]] || return 1
while [[ -n $rest ]]; do
if [[ $rest =~ $ref_re ]]; then
name="${BASH_REMATCH[2]:-${BASH_REMATCH[3]}}"
out+="${!name:-}"
elif [[ $rest =~ $ansi_re ]]; then
out+="${BASH_REMATCH[0]}"
else
return 1
fi
rest="${rest:${#BASH_REMATCH[0]}}"
done
_PROMPT_RESOLVED="$out"
}
# ------------------------------------------------------------------------------
# Path of the theme file for a bare name (searched in the theme directory) or an
# explicit path (anything containing a slash).
# Usage: _prompt_theme_file <theme_name_or_path> [theme_dir]
_prompt_theme_file()
{
local name="$1" dir="${2:-${PROMPT_THEME_DIR:-$MYPATH/profile.d/themes}}"
if [[ "$name" == */* ]]; then
printf '%s' "$name"
else
printf '%s' "$dir/${name}.theme"
fi
}
# ------------------------------------------------------------------------------
# Parse a prompt theme file safely — it is NEVER sourced or executed.
# Two categories of keys are accepted:
# PROMPT_COLOR_* — prompt slot colours (TIME_FG, BAR_BG, …)
# Standard colour variables from disp.sh (Blue, On_IBlack, …) — allows a
# theme to redefine the palette used everywhere in the shell session.
# Values follow the forms accepted by _prompt_resolve_color. Any other key or
# value is rejected with a warning.
# Usage: load_theme <theme_name_or_path> [theme_dir]
# theme_name_or_path : bare name (e.g. "dark") or an explicit path.
# theme_dir : directory to search for bare names; defaults to
# $MYPATH/profile.d/themes. Overridable via
# PROMPT_THEME_DIR.
load_theme()
{
local theme_name="$1"
local theme_file=""
[[ -z "$theme_name" ]] && return 0
theme_file=$(_prompt_theme_file "$theme_name" "${2:-}")
if [[ ! -f "$theme_file" || ! -r "$theme_file" ]]; then
printf "[ Warning ] load_theme: theme file not found: %s\n" "$theme_file" >&2
return 1
fi
# ---- Key whitelist: prompt slots and standard colour variables ----------
local -A _lth_allowed=(
[PROMPT_COLOR_TIME_FG]=1 [PROMPT_COLOR_TIME_BG]=1
[PROMPT_COLOR_BAR_BG]=1
[PROMPT_COLOR_OK_FG]=1 [PROMPT_COLOR_OK_MARK]=1
[PROMPT_COLOR_ERR_BG]=1 [PROMPT_COLOR_ERR_FG]=1 [PROMPT_COLOR_ERR_MARK]=1
[PROMPT_COLOR_ROOT_FG]=1 [PROMPT_COLOR_USER_FG]=1
[PROMPT_COLOR_DIR_FG]=1
[PROMPT_COLOR_CTX_FG]=1
)
local _lth_cn
for _lth_cn in "${_PROMPT_COLOR_NAMES[@]}"; do
_lth_allowed[$_lth_cn]=1
done
# ---- Line parser ---------------------------------------------------------
local _lth_line _lth_key _lth_value _lth_lineno=0
while IFS= read -r _lth_line || [[ -n "$_lth_line" ]]; do
_lth_lineno=$(( _lth_lineno + 1 ))
_lth_line="${_lth_line%$'\r'}" # strip CR
_lth_line="${_lth_line#"${_lth_line%%[![:space:]]*}"}" # ltrim
_lth_line="${_lth_line%"${_lth_line##*[![:space:]]}"}" # rtrim
[[ -z "$_lth_line" || "$_lth_line" == '#'* ]] && continue # blank/comment
[[ "$_lth_line" == 'export '* ]] && _lth_line="${_lth_line#export }" # strip prefix
if [[ "$_lth_line" != *=* ]]; then
printf "[ Warning ] load_theme: %s:%d: not a key=value pair, ignoring.\n" \
"$theme_file" "$_lth_lineno" >&2
continue
fi
_lth_key="${_lth_line%%=*}"
_lth_value="${_lth_line#*=}"
_lth_key="${_lth_key#"${_lth_key%%[![:space:]]*}"}"
_lth_key="${_lth_key%"${_lth_key##*[![:space:]]}"}" # trim key
if [[ -z "${_lth_allowed[$_lth_key]+x}" ]]; then
printf "[ Warning ] load_theme: %s:%d: key '%s' is not allowed, ignoring.\n" \
"$theme_file" "$_lth_lineno" "$_lth_key" >&2
continue
fi
# Strip surrounding quotes (handles inline trailing comments like KEY="val" # note)
if [[ "$_lth_value" == '"'* ]]; then
_lth_value="${_lth_value#\"}"
_lth_value="${_lth_value%%\"*}"
elif [[ "$_lth_value" == "'"* ]]; then
_lth_value="${_lth_value#\'}"
_lth_value="${_lth_value%%\'*}"
fi
if _prompt_resolve_color "$_lth_value"; then
export "$_lth_key"="$_PROMPT_RESOLVED"
else
printf "[ Warning ] load_theme: %s:%d: invalid value for '%s', ignoring.\n" \
"$theme_file" "$_lth_lineno" "$_lth_key" >&2
fi
done < "$theme_file"
}
# Not exported, it remains private (like the other _prompt_* helpers: set_theme
# and set_prompt are meant to be used from the interactive shell itself).
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Apply the PROMPT_COLOR_* keys of the [prompt] section on top of the current
# colours, so they win over the theme. Their values may use $ColorName, which
# parse_conf leaves untouched while the colour variables do not exist yet.
# A value that is not a valid colour is reported and not applied.
# Usage: _prompt_apply_overrides
_prompt_apply_overrides()
{
declare -p CONF_prompt >/dev/null 2>&1 || return 0
local k
for k in "${!CONF_prompt[@]}"; do
[[ $k == PROMPT_COLOR_* ]] || continue
if _prompt_resolve_color "${CONF_prompt[$k]}"; then
export "$k=$_PROMPT_RESOLVED"
else
printf "[ Warning ] [prompt] %s: invalid colour value '%s', ignoring.\n" \
"$k" "${CONF_prompt[$k]}" >&2
# load_conf exported the raw text: do not keep it as a colour
[[ "${!k:-}" == "${CONF_prompt[$k]}" ]] && unset "$k"
fi
done
return 0
}
# ------------------------------------------------------------------------------
# Switch to a theme from a clean state: default palette, no PROMPT_COLOR_*
# left over from the previous theme (a partial theme must not inherit them),
# the theme, then the [prompt] overrides. The theme file is checked first, so a
# wrong name changes nothing.
# Usage: _prompt_load_theme_clean <theme_name_or_path> [theme_dir]
_prompt_load_theme_clean()
{
local file v
file=$(_prompt_theme_file "$1" "${2:-}")
if [[ ! -f "$file" || ! -r "$file" ]]; then
printf "[ Warning ] set_theme: theme file not found: %s\n" "$file" >&2
return 1
fi
set_colors
for v in ${!PROMPT_COLOR_@}; do
unset "$v"
done
load_theme "$1" "${2:-}" || return 1
_prompt_apply_overrides
}
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Dynamically switch the prompt theme for the current shell session.
# Calls load_theme to apply the new colour values immediately, then updates
# PROMPT_THEME so subshells and the set_prompt fallback chain reflect the
# change. PROMPT_THEME_DIR is honoured when set.
# Usage: set_theme [theme_name_or_path]
# With no argument (or -l / --list), lists available .theme files.
set_theme()
{
local theme_dir="${PROMPT_THEME_DIR:-${MYPATH}/profile.d/themes}"
local preview=0
local save=0
local list_only=0
local theme_name=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
printf "set_theme: Switch the prompt colour theme for the current shell session.\n\n"
printf "Usage: set_theme [options] [theme]\n\n"
printf "Options:\n"
printf "\t-h, --help\tDisplay this help screen\n"
printf "\t-l, --list\tList available themes (default when no argument is given)\n"
printf "\t-p, --preview\tPreview a theme without applying it\n"
printf "\t-S, --save\tSave theme to configuration\n\n"
printf "Arguments:\n"
printf "\ttheme \tBare theme name (e.g. 'dark') or an explicit path to a .theme file.\n"
printf "\t \tThemes are searched in: %s\n" "$theme_dir"
printf "\t \tOverride with PROMPT_THEME_DIR in profile.conf [prompt].\n\n"
printf "Examples:\n"
printf "\tset_theme \t— list available themes\n"
printf "\tset_theme dark \t— apply the dark theme\n"
printf "\tset_theme -p dark \t— preview the dark theme\n"
printf "\tset_theme -S \t— save the current theme to the configuration\n"
printf "\tset_theme -S dark \t— apply and save the dark theme\n"
printf "\tset_theme ~/my.theme\t— apply a theme by path\n"
return 0
;;
-l|--list)
list_only=1
shift
;;
-p|--preview)
preview=1
shift
;;
-S|--save)
save=1
shift
;;
--)
shift
break
;;
-*)
disp E "Unknown option: $1, use \"set_theme --help\" to display usage."
return 1
;;
*)
if [[ -n "$theme_name" ]]; then
disp E "Too many arguments. Usage: set_theme [options] [theme]"
return 1
fi
theme_name="$1"
shift
;;
esac
done
if [[ $# -gt 0 ]]; then
if [[ -n "$theme_name" ]]; then
disp E "Too many arguments. Usage: set_theme [options] [theme]"
return 1
fi
theme_name="$1"
shift
fi
# -- list mode -----------------------------------------------------------
if (( list_only )) || [[ -z "$theme_name" && $preview -eq 0 ]]; then
if (( save )); then
if [[ -n "${PROMPT_THEME:-}" ]]; then
conf_save "prompt" "PROMPT_THEME" "$PROMPT_THEME" || return 1
disp I "Saved current prompt theme '$PROMPT_THEME' to configuration."
return 0
fi
disp E "No active theme to save. Apply a theme first or pass one with -S."
return 1
fi
printf "Available themes in %s:\n" "$theme_dir"
local f name
for f in "$theme_dir"/*.theme; do
[[ -f "$f" ]] || continue
name="${f##*/}"
name="${name%.theme}"
if [[ "$name" == "${PROMPT_THEME:-}" ]]; then
printf " * %s (active)\n" "$name"
else
printf " %s\n" "$name"
fi
done
return 0
fi
if (( preview )) && [[ -z "$theme_name" ]]; then
disp E "--preview requires a theme argument."
return 1
fi
if (( preview && save )); then
disp E "--preview and --save cannot be used together."
return 1
fi
# -- preview mode --------------------------------------------------------
if (( preview )); then
# Snapshot the palette and the prompt colours by value, to put back
# exactly what was there (including the PROMPT_COLOR_* that did not
# exist, which a previewed theme would otherwise leave behind).
local -A _snap=()
local _v
for _v in "${_PROMPT_COLOR_NAMES[@]}" ${!PROMPT_COLOR_@}; do
[[ -n "${!_v+x}" ]] && _snap[$_v]="${!_v}"
done
local _rc=0
_prompt_load_theme_clean "$theme_name" "$theme_dir" || _rc=1
if (( _rc == 0 )); then
printf "Preview for theme '%s':\n" "$theme_name"
printf "%b [ 13:37:00 ] %b%b [ 0 %b• (5s) %b] %b user@host%b [ git:main +1/-0 ] %b\n" \
"${PROMPT_COLOR_TIME_FG:-}${PROMPT_COLOR_TIME_BG:-}" "${DEFAULTCOL:-}" \
"${PROMPT_COLOR_OK_FG:-}${PROMPT_COLOR_BAR_BG:-}" \
"${PROMPT_COLOR_OK_MARK:-}${PROMPT_COLOR_BAR_BG:-}" \
"${PROMPT_COLOR_OK_FG:-}${PROMPT_COLOR_BAR_BG:-}" \
"${PROMPT_COLOR_USER_FG:-}${PROMPT_COLOR_BAR_BG:-}" \
"${PROMPT_COLOR_CTX_FG:-}${PROMPT_COLOR_BAR_BG:-}" "${DEFAULTCOL:-}"
printf "%b [ 13:37:00 ] %b%b [ 0 %b• (5s) %b] %b user@host%b [ git:main +1/-0 ] %b\n" \
"${PROMPT_COLOR_TIME_FG:-}${PROMPT_COLOR_TIME_BG:-}" "${DEFAULTCOL:-}" \
"${PROMPT_COLOR_OK_FG:-}${PROMPT_COLOR_ERR_BG:-}" \
"${PROMPT_COLOR_ERR_MARK:-}${PROMPT_COLOR_ERR_BG:-}" \
"${PROMPT_COLOR_OK_FG:-}${PROMPT_COLOR_ERR_BG:-}" \
"${PROMPT_COLOR_USER_FG:-}${PROMPT_COLOR_BAR_BG:-}" \
"${PROMPT_COLOR_CTX_FG:-}${PROMPT_COLOR_BAR_BG:-}" "${DEFAULTCOL:-}"
printf "%b/path/to/dir \$ %b\n" \
"${PROMPT_COLOR_DIR_FG:-}" "${DEFAULTCOL:-}"
fi
# Restore: every colour variable is either put back or removed
for _v in "${_PROMPT_COLOR_NAMES[@]}" ${!PROMPT_COLOR_@}; do
if [[ -n "${_snap[$_v]+x}" ]]; then
export "$_v=${_snap[$_v]}"
else
unset "$_v"
fi
done
return "$_rc"
fi
# -- apply mode ----------------------------------------------------------
_prompt_load_theme_clean "$theme_name" "$theme_dir" || return 1
export PROMPT_THEME="$theme_name"
if (( save )); then
conf_save "prompt" "PROMPT_THEME" "$theme_name" || return 1
disp I "Prompt theme set to $theme_name and saved to configuration."
return 0
fi
disp I "Prompt theme set to $theme_name."
}
export -f set_theme
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# timer_* functions : internal timing functions for the prompt.
# The start is recorded by the DEBUG trap (profile.sh) before each command and
# the elapsed time is computed by set_prompt, through PROMPT_COMMAND.
# Put the current time, in microseconds since the epoch, in _timer_us.
# EPOCHREALTIME (bash >= 5) costs no process; its decimal separator follows the
# locale ("." or ","), hence the removal of both. Otherwise "date +%s%N" is
# used (it prints a literal "N" where nanoseconds are not supported, which the
# digit check rejects), then plain seconds.
_timer_now()
{
if [[ -n "${EPOCHREALTIME:-}" ]]; then
_timer_us="${EPOCHREALTIME//[.,]/}"
else
local t
t=$(date +%s%N 2>/dev/null)
if [[ $t =~ ^[0-9]{16,}$ ]]; then
_timer_us=$(( t / 1000 ))
else
_timer_us=$(( $(date +%s) * 1000000 ))
fi
fi
}
# Usage: timer_start
# Record the start time of the command about to run, unless already recorded
# (the DEBUG trap fires for every simple command of a pipeline or function: only
# the first one counts).
timer_start()
{
if [[ -z "${_timer_start_us:-}" ]]; then
_timer_now
_timer_start_us="$_timer_us"
fi
}
# Usage: timer_stop
# Compute the time elapsed since timer_start and format it into timer_show with
# an appropriate unit (us, ms, s, m, h).
timer_stop()
{
_timer_now
local delta_us=$(( _timer_us - ${_timer_start_us:-$_timer_us} ))
(( delta_us < 0 )) && delta_us=0
local us=$((delta_us % 1000))
local ms=$(((delta_us / 1000) % 1000))
local s=$(((delta_us / 1000000) % 60))
local m=$(((delta_us / 60000000) % 60))
local h=$((delta_us / 3600000000))
# Goal: always show around 3 digits of accuracy
if ((h > 0)); then
timer_show=${h}h${m}m
elif ((m > 0)); then
timer_show=${m}m${s}s
elif ((s >= 10)); then
timer_show=${s}.$((ms / 100))s
elif ((s > 0)); then
printf -v timer_show '%d.%03ds' "$s" "$ms"
elif ((ms >= 100)); then
timer_show=${ms}ms
elif ((ms > 0)); then
timer_show=${ms}.$((us / 100))ms
else
timer_show=${us}us
fi
unset _timer_start_us
}
# ------------------------------------------------------------------------------
# Prompt segments. Each one puts its text in _PROMPT_SEG (empty when there is
# nothing to show), without a command substitution (no subshell per segment).
# They are defined once, at load time, and not inside set_prompt: a function
# definition resets $?, and set_prompt must read the exit status of the user's
# last command before anything else runs.
_prompt_seg_git()
{
_PROMPT_SEG=""
# Fast path: skip git lookup when feature is disabled.
[[ "${PROMPT_SHOW_GIT:-1}" == "0" ]] && return 0
local branch
branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || \
branch=$(git rev-parse --short HEAD 2>/dev/null) || return 0
local dirty="" sync="" timed_out=0
if [[ "${PROMPT_SHOW_GIT_STATUS:-1}" != "0" ]]; then
# Build a timeout wrapper if the 'timeout' command is available;
# fall back to direct execution on systems that lack it.
local -a _tw=()
command -v timeout >/dev/null 2>&1 && _tw=(timeout "${PROMPT_GIT_TIMEOUT:-2}")
# One call for everything: --branch gives the "# branch.ab +A -B" line
# (only when an upstream exists) and each changed tracked file gives a
# line starting with 1, 2 or u. Untracked files are not counted.
# GIT_OPTIONAL_LOCKS=0 keeps git from taking the index lock to refresh
# it, which would make a concurrent "git commit" fail.
local out ec
out=$(GIT_OPTIONAL_LOCKS=0 "${_tw[@]}" git status --porcelain=v2 --branch \
--untracked-files=no --ignore-submodules 2>/dev/null)
ec=$?
if [[ $ec -eq 124 ]]; then
timed_out=1
else
[[ $'\n'"$out" == *$'\n'[12u]\ * ]] && dirty="*"
if [[ "$out" =~ \#\ branch\.ab\ \+([0-9]+)\ -([0-9]+) ]]; then
if [[ "${BASH_REMATCH[1]}" -gt 0 || "${BASH_REMATCH[2]}" -gt 0 ]]; then
sync=" +${BASH_REMATCH[1]}/-${BASH_REMATCH[2]}"
fi
fi
fi
fi
if [[ $timed_out -eq 1 ]]; then
_PROMPT_SEG="git:${branch}?"
else
_PROMPT_SEG="git:${branch}${dirty}${sync}"
fi
}
_prompt_seg_conda()
{
_PROMPT_SEG=""
[[ "${PROMPT_SHOW_CONDA:-1}" == "0" ]] && return 0
[[ -z "${CONDA_DEFAULT_ENV:-}" ]] && return 0
_PROMPT_SEG="conda:${CONDA_DEFAULT_ENV}"
}
_prompt_seg_venv()
{
_PROMPT_SEG=""
[[ "${PROMPT_SHOW_VENV:-1}" == "0" ]] && return 0
[[ -n "${CONDA_DEFAULT_ENV:-}" ]] && return 0
[[ -z "${VIRTUAL_ENV:-}" ]] && return 0
_PROMPT_SEG="venv:${VIRTUAL_ENV##*/}"
}
_prompt_seg_session()
{
_PROMPT_SEG=""
[[ "${PROMPT_SHOW_SESSION:-1}" == "0" ]] && return 0
local -a tags=()
[[ -n "${SSH_CONNECTION:-}" ]] && tags+=("ssh")
[[ -n "${TMUX:-}" ]] && tags+=("tmux")
[[ -n "${STY:-}" ]] && tags+=("screen")
[[ ${#tags[@]} -eq 0 ]] && return 0
local out="${tags[0]}" i
for ((i=1; i<${#tags[@]}; ++i)); do
out+="+${tags[i]}"
done
_PROMPT_SEG="$out"
}
# Append the current _PROMPT_SEG to the context text _PROMPT_CTX (" | " between
# segments). An "if" keeps the status at 0 when the segment is empty.
_prompt_ctx_add()
{
if [[ -n "$_PROMPT_SEG" ]]; then
if [[ -n "$_PROMPT_CTX" ]]; then
_PROMPT_CTX+=" | $_PROMPT_SEG"
else
_PROMPT_CTX="$_PROMPT_SEG"
fi
fi
}
# ------------------------------------------------------------------------------
# Function triggered internally by bash : defining prompt
# Usage: set_prompt
# This function is called by bash before displaying the prompt. It sets the
# PS1 variable to a custom prompt that includes the exit status of the last
# command, the elapsed time of the last command, and the current user and host.
set_prompt()
{
local Last_Command=$? # Must come first: nothing may run before this line
local FancyX='\342\234\227'
local Checkmark='\342\234\223'
# The context segment is not copied into PS1. Bash expands PS1 when
# displaying it, including $(...) and `...`: a git branch (or conda
# environment) named 'x$(command)' would be executed. PS1 only contains the
# reference ${_PROMPT_CTX}, whose value is inserted as plain text, never
# re-scanned. This needs promptvars (the default).
shopt -q promptvars || shopt -s promptvars
# Resolve theme/config colours with hardcoded fallbacks
local _time_fg="${PROMPT_COLOR_TIME_FG:-$Blue}"
local _time_bg="${PROMPT_COLOR_TIME_BG:-$On_White}"
local _bar_bg="${PROMPT_COLOR_BAR_BG:-$On_Blue}"
local _ok_fg="${PROMPT_COLOR_OK_FG:-$BWhite}"
local _ok_mark="${PROMPT_COLOR_OK_MARK:-$BGreen}"
local _err_bg="${PROMPT_COLOR_ERR_BG:-$On_Red}"
local _err_fg="${PROMPT_COLOR_ERR_FG:-$White}"
local _err_mark="${PROMPT_COLOR_ERR_MARK:-$BYellow}"
local _root_fg="${PROMPT_COLOR_ROOT_FG:-$Red}"
local _user_fg="${PROMPT_COLOR_USER_FG:-$BGreen}"
local _dir_fg="${PROMPT_COLOR_DIR_FG:-$ICyan}"
local _ctx_fg="${PROMPT_COLOR_CTX_FG:-$BIYellow}"
# Begin with time (cursor-save is non-printing; all ANSI sequences wrapped
# in \[...\] so bash does not count them toward the visible line width).
# Every fg colour is combined with its section bg in the same \[...\] block
# so that even "reset" colours (0;Xm) cannot strip the background.
PS1="\[\e[s\]\[${_time_fg}${_time_bg}\] [ \t ] \[${_bar_bg}\]"
# Add exit status of the last command.
# If it was successful, print a green check mark. Otherwise, print a red X.
timer_stop
if [[ $Last_Command == 0 ]]; then
PS1+="\[${_ok_fg}${_bar_bg}\] [ $Last_Command "
PS1+="\[${_ok_mark}${_bar_bg}\]${Checkmark} "
# Add the elapsed time, then close the status section and return to bar bg.
PS1+="($timer_show)\[${_ok_fg}${_bar_bg}\] ] "
else
PS1+="\[${_err_fg}${_err_bg}\] [ $Last_Command "
PS1+="\[${_err_mark}${_err_bg}\]${FancyX} "
PS1+="($timer_show)\[${_err_fg}${_err_bg}\] ] "
fi
# If root, print the host in root colour. Otherwise use user colour.
if [[ $EUID -eq 0 ]]; then
PS1+="\[${_root_fg}${_bar_bg}\] \\u\[${_user_fg}${_bar_bg}\]@\\h"
else
PS1+="\[${_user_fg}${_bar_bg}\] \\u@\\h"
fi
# Optional context segment appended at the end of the top bar.
_PROMPT_CTX=""
_prompt_seg_git; _prompt_ctx_add
_prompt_seg_conda; _prompt_ctx_add
_prompt_seg_venv; _prompt_ctx_add
_prompt_seg_session; _prompt_ctx_add
if [[ -n "$_PROMPT_CTX" ]]; then
# Single quotes: the reference must reach PS1 literally (see above)
PS1+="\[${_ctx_fg}${_bar_bg}\] [ "'${_PROMPT_CTX}'" ]"
fi
PS1+="\[\e[K\e[u\]\[$RESETCOL\]\n"
# Print the working directory and prompt marker, then reset colour.
PS1+="\[${_dir_fg}\]\\w \\\$\[$RESETCOL\] "
}
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Theme and configuration loading.
# Precedence (lowest → highest):
# 1. Hardcoded fallbacks in set_prompt
# 2. Theme file (PROMPT_THEME key from [prompt] section)
# 3. Individual PROMPT_COLOR_* overrides in [prompt] section
#
# load_conf "prompt" (top of this file) has exported every key of the section,
# the theme file is loaded over them, and _prompt_apply_overrides then puts the
# PROMPT_COLOR_* keys of the section back on top (resolving $ColorName values).
# CONF_prompt is already populated by parse_conf (run in profile.sh before
# modules are sourced), which is where PROMPT_THEME and PROMPT_THEME_DIR are
# read from.
_pt_theme="${CONF_prompt[PROMPT_THEME]:-}"
_pt_dir="${CONF_prompt[PROMPT_THEME_DIR]:-}"
[[ -n "$_pt_theme" ]] && load_theme "$_pt_theme" ${_pt_dir:+"$_pt_dir"}
unset _pt_theme _pt_dir
_prompt_apply_overrides
# ------------------------------------------------------------------------------
# EOF