#!/usr/bin/env bash # ------------------------------------------------------------------------------ # Copyright (c) 2013-2026 Geoffray Levasseur # 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" # Common helpers, then the ones private to this module (helpers/prompt.sh) # shellcheck source=../helpers/common.sh declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh" _profile_load_helpers "${BASH_SOURCE[0]}" # ------------------------------------------------------------------------------ # 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_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). # ------------------------------------------------------------------------------ # 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. # 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 # shellcheck disable=SC2154 # set by _timer_now (helpers/prompt.sh) _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. # ------------------------------------------------------------------------------ # 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 # When other commands precede set_prompt in PROMPT_COMMAND (profile.sh adds # _profile_save_status first for that case), $? is theirs: use the saved one. if [[ -n "${_profile_status+x}" ]]; then Last_Command="$_profile_status" unset _profile_status fi 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