#!/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. # ------------------------------------------------------------------------------ # Private helpers of profile.d/prompt.sh, loaded by the module itself. # 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 _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_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 } # 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_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 } # 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 } # Prompt segment: set _PROMPT_SEG to the git branch (or short commit when # detached) with dirty/ahead/behind markers; empty outside a repository or when # disabled by PROMPT_SHOW_GIT / PROMPT_SHOW_GIT_STATUS. _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 segment: set _PROMPT_SEG to the active conda environment, if any. _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 segment: set _PROMPT_SEG to the active Python virtualenv name, unless # a conda environment (which sets its own) is active. _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 segment: set _PROMPT_SEG to the session kind tags (ssh, tmux, screen) # joined with '+', empty for a plain local session. _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 } # Store the exit status of the user's last command (first command of a # PROMPT_COMMAND that also contains other commands, see profile.sh). _profile_save_status() { _profile_status=$? }