#!/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. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # 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. # Allowed value forms: # $ColorName or ${ColorName} — colour variable from disp.sh (resolved by # indirection via ${!varname}) # \e[...m or \033[...m — raw ANSI escape literal (single block) # 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_dir="${2:-${PROMPT_THEME_DIR:-$MYPATH/profile.d/themes}}" local theme_file="" [[ -z "$theme_name" ]] && return 0 if [[ "$theme_name" == /* || "$theme_name" == */* ]]; then theme_file="$theme_name" else theme_file="$theme_dir/${theme_name}.theme" fi 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 ---------------------------------------- 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 ) # ---- Colour variable names exported by disp.sh -------------------------- local _lth_color_re _lth_color_re='Black|Red|Green|Yellow|Blue|Purple|Cyan|White' _lth_color_re+='|BBlack|BRed|BGreen|BYellow|BBlue|BPurple|BCyan|BWhite' _lth_color_re+='|UBlack|URed|UGreen|UYellow|UBlue|UPurple|UCyan|UWhite' _lth_color_re+='|On_Black|On_Red|On_Green|On_Yellow|On_Blue|On_Purple|On_Cyan|On_White' _lth_color_re+='|IBlack|IRed|IGreen|IYellow|IBlue|IPurple|ICyan|IWhite' _lth_color_re+='|BIBlack|BIRed|BIGreen|BIYellow|BIBlue|BIPurple|BICyan|BIWhite' _lth_color_re+='|On_IBlack|On_IRed|On_IGreen|On_IYellow|On_IBlue|On_IPurple|On_ICyan|On_IWhite' _lth_color_re+='|DEFAULTFG|DEFAULTBG|DEFAULTCOL|RESETCOL' # ---- Key whitelist: standard colour vars (same list as above) ----------- local _lth_cn for _lth_cn in \ 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; do _lth_allowed[$_lth_cn]=1 done unset _lth_cn # ERE: safe colour reference $Name or ${Name} local _lth_ref_re='^\$\{?('"$_lth_color_re"')\}?$' # ERE: raw ANSI escape literal \e[...m or \033[...m local _lth_ansi_re='^(\\e|\\033)\[[0-9;]*m$' # ---- Line parser --------------------------------------------------------- local _lth_line _lth_key _lth_value _lth_varname _lth_lineno=0 while IFS= read -r _lth_line || [[ -n "$_lth_line" ]]; do ((_lth_lineno++)) _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 [[ "$_lth_value" =~ $_lth_ref_re ]]; then # Safe colour variable reference — resolve via indirection _lth_varname="${_lth_value#\$}" _lth_varname="${_lth_varname#\{}" _lth_varname="${_lth_varname%\}}" export "$_lth_key"="${!_lth_varname}" elif [[ "$_lth_value" =~ $_lth_ansi_re ]]; then # Raw ANSI escape literal — accept as-is export "$_lth_key"="$_lth_value" 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 # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # 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}" # -- list mode ----------------------------------------------------------- if [[ $# -eq 0 || "$1" == "-l" || "$1" == "--list" ]]; then 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 # -- apply mode ---------------------------------------------------------- local theme_name="$1" # Reset colours to defaults before loading the new theme set_colors load_theme "$theme_name" || return 1 export PROMPT_THEME="$theme_name" disp I "Prompt theme set to $theme_name." } export -f set_theme # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # timer_* functions : internal timing function for prompt # Usage: timer_now # This function returns the current time in nanoseconds since the epoch. It # first tries to use the %N format specifier for nanoseconds, but if that is # not supported (e.g., on older systems), it falls back to seconds. function timer_now { date +%s%N 2>/dev/null || date +%s } # Usage: timer_start # This function initializes the timer_start variable with the current time in # nanoseconds. It is used to measure the elapsed time for the prompt. function timer_start { timer_start=${timer_start:-$(timer_now)} } # Usage: timer_stop # This function calculates the elapsed time since timer_start and formats it # into a human-readable string with appropriate units (us, ms, s, m, h function timer_stop { local delta_us=$((($(timer_now) - $timer_start) / 1000)) 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 timer_show=${s}.$(printf %03d $ms)s 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 } # ------------------------------------------------------------------------------ # 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! local FancyX='\342\234\227' local Checkmark='\342\234\223' # 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}" # 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. 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. timer_stop PS1+="($timer_show)\[${_ok_fg}${_bar_bg}\] ] " else PS1+="\[${_err_fg}${_err_bg}\] [ \$Last_Command " PS1+="\[${_err_mark}${_err_bg}\]${FancyX} " timer_stop 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 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 # # CONF_prompt is already populated by parse_conf (run in profile.sh before # modules are sourced). We extract PROMPT_THEME and PROMPT_THEME_DIR from the # raw associative array now so load_theme can run before load_conf "prompt" # exports remaining keys. That way any PROMPT_COLOR_* value set explicitly in # [prompt] wins over the same variable set by the theme file. _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 load_conf "prompt" # ------------------------------------------------------------------------------ # EOF