340 lines
14 KiB
Bash
340 lines
14 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.
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# 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_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_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)
|
|
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}\]${Checkmark} "
|
|
else
|
|
PS1+="\[${_err_fg}\]\[${_err_bg}\] [ \$Last_Command "
|
|
PS1+="\[${_err_mark}\]${FancyX} "
|
|
fi
|
|
|
|
# Add the elapsed time
|
|
timer_stop
|
|
PS1+="($timer_show)\[${_ok_fg}\] ] \[${_bar_bg}\] "
|
|
|
|
# If root, print the host in root colour. Otherwise use user colour.
|
|
if [[ $EUID -eq 0 ]]; then
|
|
PS1+="\[${_root_fg}\]\\u\[${_user_fg}\]@\\h"
|
|
else
|
|
PS1+="\[${_user_fg}\]\\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
|