moved helpers separately

This commit is contained in:
2026-10-13 16:21:24 +02:00
parent ba1e07744e
commit 9911f6d01d
33 changed files with 1489 additions and 1089 deletions
+8
View File
@@ -82,6 +82,14 @@ profile gives access to numerous functions, aliases and to an advanced prompt.
All functions are organized into modules under the `profile.d/` directory and
are loaded automatically at startup.
The internal helpers (functions starting with `_`, not meant to be called by
hand) are kept apart from the modules, in the `helpers/` directory:
`helpers/common.sh` holds the ones shared by several modules (option parsing,
trap restoration, ...), and each module loads its own private helpers from
`helpers/<module name>.sh` when that file exists (`profile.d/rain.sh` loads
`helpers/rain.sh`). Both are loaded automatically; `helpers/` must be kept next
to `profile.sh` and `profile.d/`.
### 3.1. Prompt
A bar-style prompt showing current time, execution time of the last command
+9 -1
View File
@@ -22,6 +22,14 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
- `utaz` supports bare `.zst` files and `.tzst` archives; `taz -q` is now
effective.
### Changed
- Internal helpers are now delivered apart from the modules, in the new
`helpers/` directory: `helpers/common.sh` (shared helpers, loaded by
`profile.sh` before the modules) and `helpers/<module>.sh` (private helpers
of `profile.d/<module>.sh`, loaded by the module itself when the file exists).
`_fun_is_uint` became the shared `_is_uint`.
### Fixed
- `taz`: a plain file with the `tar` format could be deleted instead of being
@@ -174,7 +182,7 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
longer change the shell's directory stack.
- All modules: the option parsing no longer uses `xargs`, which broke on
arguments or file names containing a newline, nor `eval`: a new
`profile.d/args.sh` provides `_getopt_split` (and `_trap_restore`), pure bash
`helpers/common.sh` provides `_getopt_split` (and `_trap_restore`), pure bash
helpers; the `nullglob` and trap restorations no longer use `eval` either.
- `profile.sh`: the shell check used `$SHELL` (the login shell: unset under cron
or systemd, unrelated to the running shell) and made the profile refuse to
+25 -2
View File
@@ -120,7 +120,7 @@ brew install shellcheck
- Use `getopt` (not `getopts`) for option parsing — it handles long options and
`--` correctly.
Rebuild the arguments from its output with the `_getopt_split` helper
(`profile.d/args.sh`), never with `eval set --` or `xargs`:
(`helpers/common.sh`), never with `eval set --` or `xargs`:
```bash
PARSED=$(getopt -o hv --long help,verbose -n 'mycmd' -- "$@") || return 1
@@ -153,6 +153,29 @@ export -f my_function
Add the `load_conf` call near the top after any variable declarations.
### Helpers
Internal functions (prefixed with `_`) do not belong to the module file; they
live in the `helpers/` directory:
- `helpers/common.sh`: helpers used by several modules (`_getopt_split`,
`_trap_restore`, `_is_uint`, `_profile_load_helpers`). It is loaded by
`profile.sh` before any module. Put a helper there as soon as a second module
needs it, instead of copying it.
- `helpers/<module name>.sh`: helpers private to `profile.d/<module name>.sh`
(for instance `helpers/rain.sh` for `profile.d/rain.sh`). The file is
optional; the module loads it with the two lines below, which also load
`common.sh` when the module is sourced alone:
```bash
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
```
Helper files use the same copyright block as the modules and are only
definitions (no side effect besides the variables the helpers need).
Document each helper with a comment (purpose, `Usage:` line).
---
## 7. Configuration keys
@@ -176,7 +199,7 @@ See the existing themes and `README.md §4.4` for the allowed syntax.
## 9. Running shellcheck
```bash
shellcheck -x profile.sh profile.d/*.sh
shellcheck -x profile.sh profile.d/*.sh helpers/*.sh
```
All warnings must be resolved before a patch will be accepted. Accepted
+3 -1
View File
@@ -72,7 +72,9 @@ taz -p auto -f lz mydir
```
You can also source one module directly (for example
`profile.d/compress.sh`) if you only need a subset of functions.
`profile.d/compress.sh`) if you only need a subset of functions. The module
loads the helpers it needs from the `helpers/` directory, which must stay next
to `profile.d/`.
When you source a module directly, profile configuration parsing/loading from
`profile.sh` is skipped, so defaults from `profile.conf` are not applied unless
+34 -2
View File
@@ -36,8 +36,13 @@
# ------------------------------------------------------------------------------
# Helpers for the option parsing of the commands. The project rule is to never
# use eval: these functions rebuild argument lists in pure bash.
# Helpers shared by several modules. This file is loaded by profile.sh before
# any module (and by a module sourced alone); the helpers private to one module
# live in helpers/<module>.sh instead, loaded by the module itself through
# _profile_load_helpers.
#
# Option parsing: the project rule is to never use eval, so _getopt_split and
# _trap_restore rebuild argument lists and traps in pure bash.
# Split the output of getopt into the array whose name is $1, without eval and
# without xargs. getopt prints the normalised arguments as one shell-quoted
@@ -113,6 +118,33 @@ _trap_restore()
return 0
}
export -f _trap_restore
# True when $1 is a non-negative integer. Every value that ends up in an
# arithmetic context must pass this first (this includes values coming from the
# configuration file): bash evaluates non-numeric strings inside [[ -gt ]] or
# $(( )) as expressions, which can run arbitrary commands.
# Usage: _is_uint <value>
_is_uint()
{
[[ "$1" =~ ^[0-9]+$ ]]
}
export -f _is_uint
# Source the helpers private to a module: helpers/<module name>.sh, found next
# to the directory holding the module (profile.d/../helpers), the name being the
# module file name without ".sh". A module without helpers file is fine.
# Usage: _profile_load_helpers "${BASH_SOURCE[0]}"
_profile_load_helpers()
{
local dir=. name="${1##*/}"
[[ "$1" == */* ]] && dir="${1%/*}"
name="$dir/../helpers/${name%.sh}.sh"
[[ -r "$name" ]] || return 0
# shellcheck source=/dev/null
. "$name" || printf "[ Warning ] Failed to source helpers: %s\n" "$name" >&2
}
export -f _profile_load_helpers
# ------------------------------------------------------------------------------
+52
View File
@@ -0,0 +1,52 @@
#!/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.
# ------------------------------------------------------------------------------
# Private helpers of profile.d/conf.sh, loaded by the module itself.
# Print the path of the configuration file the conf_* functions work on:
# the user file ($HOME/.profile.conf) when it exists, otherwise the installation
# file ($PROFILE_CONF, defaulting to $MYPATH/profile.conf). The returned path
# may not exist yet.
# Usage: _conf_path
_conf_path()
{
if [[ -f "$HOME/.profile.conf" ]]; then
printf '%s\n' "$HOME/.profile.conf"
else
printf '%s\n' "${PROFILE_CONF:-${MYPATH}/profile.conf}"
fi
}
export -f _conf_path
+89
View File
@@ -0,0 +1,89 @@
#!/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.
# ------------------------------------------------------------------------------
# Private helpers of profile.d/fun.sh, loaded by the module itself.
# Convert a number of milliseconds into a seconds string usable by sleep
# (1500 -> 1.500), in pure bash (no awk fork per call).
# Usage: _fun_ms_to_s <ms>
_fun_ms_to_s()
{
printf '%d.%03d' $(( $1 / 1000 )) $(( $1 % 1000 ))
}
export -f _fun_ms_to_s
# Fail with a message unless a terminal is available: the demos wait for a key
# read from /dev/tty to stop.
# Usage: _fun_need_tty <command-name>
_fun_need_tty()
{
if ! { : </dev/tty; } 2>/dev/null; then
disp E "$1: a terminal is required (it runs until a key is pressed)."
return 1
fi
}
export -f _fun_need_tty
# Wait for a key press, then stop the background job $1 and its children.
# The job runs as "( ... ) &"; with job control (interactive shell) it is the
# leader of its own process group, which is signalled as a whole. Without job
# control that group does not exist, so its direct children (the pipeline) are
# killed first, then the subshell. SIGINT is trapped meanwhile so that Ctrl-C
# stops the job like a key press instead of aborting the caller and leaving it
# running. The wait is a polling loop of short timed reads: a blocking read is
# restarted by bash after a trapped signal and would never notice the Ctrl-C.
# Usage: _fun_stop_on_key <pid>
_fun_stop_on_key()
{
local pid=$1 old_int interrupted=0
old_int=$(trap -p INT)
trap 'interrupted=1' INT
while (( ! interrupted )); do
IFS= read -r -n 1 -s -t 0.3 _ </dev/tty && break
# Status above 128 is just the timeout; anything else (such as end of
# input on the terminal) ends the wait.
(( $? > 128 )) || break
done
kill -- -"$pid" 2>/dev/null || {
pkill -P "$pid" 2>/dev/null
kill "$pid" 2>/dev/null
}
wait "$pid" 2>/dev/null
trap - INT
_trap_restore "$old_int"
return 0
}
export -f _fun_stop_on_key
+99
View File
@@ -0,0 +1,99 @@
#!/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.
# ------------------------------------------------------------------------------
# Private helpers of profile.d/git.sh, loaded by the module itself.
# Internal helper: ensure git is available and cwd is a git worktree
_git_require_repo()
{
if ! command -v git >/dev/null 2>&1; then
disp E "git command not found."
return 1
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
disp E "Current directory is not inside a git repository."
return 1
fi
return 0
}
export -f _git_require_repo
# Internal helper: return default branch from remote HEAD, fallback to config
_git_default_branch()
{
local remote="${1:-$GIT_DEFAULT_REMOTE}"
local head
head=$(git symbolic-ref --quiet --short "refs/remotes/${remote}/HEAD" 2>/dev/null) || true
if [[ -n $head ]]; then
printf "%s\n" "${head#"${remote}"/}"
return 0
fi
printf "%s\n" "$GIT_MAIN_BRANCH"
return 0
}
export -f _git_default_branch
# Internal helper: print the name of the checked-out branch; fail with an error
# message on a detached HEAD (commands that push or rebase need a branch).
_git_current_branch()
{
local b
b=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || {
disp E "HEAD is detached: check out a branch first."
return 1
}
printf "%s\n" "$b"
}
export -f _git_current_branch
# Internal helper: print "<remote><TAB><branch>" for the remote branch tracked
# by local branch $1, read from its configuration (branch.<name>.remote/.merge).
# Unlike parsing the "origin/feature/x" form given by @{u}, this is exact for
# remote and branch names containing slashes. Fails when nothing is tracked or
# when the branch tracks another local branch (remote ".").
_git_tracking()
{
local branch=$1 remote merge
remote=$(git config --get "branch.${branch}.remote") || return 1
merge=$(git config --get "branch.${branch}.merge") || return 1
[[ $remote == "." ]] && return 1
printf "%s\t%s\n" "$remote" "${merge#refs/heads/}"
}
export -f _git_tracking
+72
View File
@@ -0,0 +1,72 @@
#!/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.
# ------------------------------------------------------------------------------
# Private helpers of profile.d/packages.sh, loaded by the module itself.
# Internal helper: run a package manager command, with root privileges when
# needed (sudo, else doas), or only display it in dry-run mode.
# Usage: _pkg_run <needs_root 0|1> <dry_run 0|1> <command> [args...]
_pkg_run()
{
local needs_root="$1" dry="$2"
shift 2
local -a prefix=()
if (( needs_root )) && [[ $EUID -ne 0 ]]; then
if command -v sudo >/dev/null 2>&1; then
prefix=(sudo)
elif command -v doas >/dev/null 2>&1; then
prefix=(doas)
elif (( ! dry )); then
disp E "Root privileges are required and neither sudo nor doas is available."
return 1
fi
fi
if (( dry )); then
# %q gives a copy-pasteable command line
printf '%q ' "${prefix[@]}" "$@"
printf '\n'
return 0
fi
if ! command -v "$1" >/dev/null 2>&1; then
disp E "'$1' is not installed."
return 1
fi
"${prefix[@]}" "$@"
}
export -f _pkg_run
+90
View File
@@ -0,0 +1,90 @@
#!/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.
# ------------------------------------------------------------------------------
# Private helpers of profile.d/processes.sh, loaded by the module itself.
# Internal helper: take the kill-style signal arguments (-9, -KILL, -SIGHUP)
# out of an argument list. getopt rejects them as unknown options, so they must
# be removed before it runs. They are recognised anywhere before a "--"
# (digits, or upper-case names, which cannot clash with the lower-case options
# of the callers); everything after "--" is left untouched.
# Usage: _proc_split_signals <signals_array> <rest_array> [args...]
_proc_split_signals()
{
local -n _pss_sigs="$1" _pss_rest="$2"
shift 2
_pss_sigs=()
_pss_rest=()
local a
while [[ $# -gt 0 ]]; do
a="$1"
shift
if [[ $a == "--" ]]; then
_pss_rest+=(-- "$@")
break
fi
if [[ $a =~ ^-([0-9]+|SIG[A-Z0-9+-]+|[A-Z][A-Z0-9+-]*)$ ]]; then
_pss_sigs+=("$a")
else
_pss_rest+=("$a")
fi
done
}
export -f _proc_split_signals
# Internal helper of kt: send a signal to a process and, first, to all its
# descendants (depth first, so a parent never outlives the children it could
# respawn from). A failure does not stop the others; the status is non-zero if
# any kill failed.
# Usage: _kt_tree <dry_run 0|1> <pid> [kill_args...]
_kt_tree()
{
local dry="$1" pid="$2" child rc=0
shift 2
for child in $(pgrep -P "$pid" 2>/dev/null); do
_kt_tree "$dry" "$child" "$@" || rc=1
done
if (( dry )); then
printf "DRY-RUN: "
printf "%q " kill "$@" "$pid"
printf "\n"
else
kill "$@" "$pid" || rc=1
fi
return "$rc"
}
export -f _kt_tree
+275
View File
@@ -0,0 +1,275 @@
#!/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.
# ------------------------------------------------------------------------------
# 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 <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
}
# 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
}
# 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=$?
}
+74
View File
@@ -0,0 +1,74 @@
#!/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.
# ------------------------------------------------------------------------------
# Private helpers of profile.d/pwd.sh, loaded by the module itself.
# Pool of random words read from /dev/urandom, refilled by _pwd_rand.
_PWD_POOL=()
# Internal helper: put in _PWD_RAND an integer uniformly distributed in
# [0, $1), for 1 <= $1 <= 2^32. $RANDOM is NOT usable for passwords: it only
# has 15 bits, a state seeded from the time and the PID, and is predictable.
# The source is $SRANDOM (bash >= 5.1, from the kernel's random generator), or
# else /dev/urandom read through od, 256 words at a time to limit the number
# of processes. Values above the largest multiple of $1 are rejected, so that
# "% $1" introduces no bias.
# Usage: _pwd_rand <max>
_pwd_rand()
{
local max="$1" limit r
limit=$(( 4294967296 - 4294967296 % max ))
while :; do
if [[ -n "${SRANDOM+x}" ]]; then
r="$SRANDOM"
else
if (( ${#_PWD_POOL[@]} == 0 )); then
# shellcheck disable=SC2207 # whitespace-separated numbers
_PWD_POOL=( $(od -An -v -N1024 -tu4 /dev/urandom 2>/dev/null) )
if (( ${#_PWD_POOL[@]} == 0 )); then
disp E "No source of random numbers available (/dev/urandom)."
return 1
fi
fi
r="${_PWD_POOL[-1]}"
unset '_PWD_POOL[-1]'
fi
if (( r < limit )); then
_PWD_RAND=$(( r % max ))
return 0
fi
done
}
+467
View File
@@ -0,0 +1,467 @@
#!/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.
# ------------------------------------------------------------------------------
# Private helpers of profile.d/rain.sh, loaded by the module itself.
# Fill RAIN_ENGINE_COLORS with the escape sequences of the drop colours (head to
# tail) of the colour theme $1: a 20-step 24-bit gradient when the terminal
# supports truecolor, a short 256-colour palette otherwise.
# Usage: _rain_build_colors <colour_name>
_rain_build_colors()
{
# Case-insensitive: the configuration documents capitalised names (Green)
local base_color="${1,,}"
local i
RAIN_ENGINE_COLORS=()
case "$base_color" in
green|blue|red|yellow|cyan|white) ;;
*)
disp E "Unknown color: ${1} (supported: green, blue, red, yellow, cyan, white)."
return 1
;;
esac
local use_truecolor=0
# term_set() already sets TERM=*-direct when truecolor is available;
# honour COLORTERM as a belt-and-suspenders fallback.
[[ "$TERM" == *direct* || "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]] && use_truecolor=1
if (( use_truecolor )); then
# 24-bit gradient from a near-black shade to a vivid hue.
# 20 steps provide smooth depth variation across simultaneous drops.
local steps=20
local r1 g1 b1 r2 g2 b2
case "${base_color}" in
green) r1=0; g1=12; b1=2; r2=50; g2=255; b2=90 ;;
blue) r1=0; g1=5; b1=25; r2=60; g2=140; b2=255 ;;
red) r1=15; g1=0; b1=0; r2=255; g2=40; b2=40 ;;
yellow) r1=25; g1=18; b1=0; r2=255; g2=240; b2=50 ;;
cyan) r1=0; g1=18; b1=18; r2=50; g2=255; b2=240 ;;
*) r1=40; g1=40; b1=45; r2=220; g2=220; b2=255 ;;
esac
local r g b
for ((i = 0; i < steps; i++)); do
r=$(( r1 + (r2 - r1) * i / (steps - 1) ))
g=$(( g1 + (g2 - g1) * i / (steps - 1) ))
b=$(( b1 + (b2 - b1) * i / (steps - 1) ))
RAIN_ENGINE_COLORS+=("\e[38;2;${r};${g};${b}m")
done
else
# Fallback: 256-colour palettes.
case $base_color in
green)
for i in {22..28} {34..40} {46..48}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
blue)
for i in {17..21} {27..33} {39..45}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
red)
for i in {52..52} {88..88} {124..124} {160..160} {196..201}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
yellow)
for i in {58..58} {100..100} {142..142} {184..184} {226..229}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
cyan)
for i in {30..31} {37..38} {44..45} {50..51}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
*)
RAIN_ENGINE_COLORS=("\e[37m" "\e[37;1m")
for i in {244..255}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
esac
fi
}
# Fill RAIN_ENGINE_CHARS with the characters a drop may be made of, according to
# the animation ($1: rain or matrix) and the matrix charset name ($2).
# Usage: _rain_build_chars <mode> <charset>
_rain_build_chars()
{
local mode="$1"
local charset="$2"
RAIN_ENGINE_CHARS=()
case "$mode" in
matrix)
case "$charset" in
""|binary)
RAIN_ENGINE_CHARS=("0" "1")
;;
kana|kanji)
# Half-width katakana set, generally rendered as single-cell glyphs.
RAIN_ENGINE_CHARS=("ア" "イ" "ウ" "エ" "オ" "カ" "キ" "ク" "ケ" "コ" "サ" "シ" "ス" "セ" "ソ" "タ" "チ" "ツ" "テ" "ト" "ナ" "ニ" "ヌ" "ネ" "ノ" "ハ" "ヒ" "フ" "ヘ" "ホ" "マ" "ミ" "ム" "メ" "モ" "ヤ" "ユ" "ヨ" "ラ" "リ" "ル" "レ" "ロ" "ワ" "ン")
;;
ascii)
RAIN_ENGINE_CHARS=("0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F")
;;
*)
disp E "Unknown charset: ${charset} (supported: binary, kana, ascii)."
return 1
;;
esac
;;
*)
RAIN_ENGINE_CHARS=("|" "│" "┃" "┆" "┇" "┊" "┋" "╽" "╿")
;;
esac
return 0
}
# Validate a speed and print it as a delay in seconds for read -t / sleep:
# values >= 1 are centiseconds (5 -> 0.050), values < 1 are raw seconds.
# Return 1 for a non-numeric or null value.
# Usage: _rain_normalize_speed <speed>
_rain_normalize_speed()
{
local raw_speed="$1"
# Accept integer/floating values. UI scale is centiseconds by default:
# 5 -> 0.05s, 2.5 -> 0.025s. Values < 1 are treated as direct seconds
# for backward compatibility (e.g. 0.03).
if [[ ! "$raw_speed" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
return 1
fi
# A null delay is refused: "read -t 0" does not wait, the animation loop
# would spin and use a whole CPU.
if LC_ALL=C awk -v s="$raw_speed" 'BEGIN { exit !(s == 0) }'; then
return 1
fi
if LC_ALL=C awk -v s="$raw_speed" 'BEGIN { exit !(s < 1) }'; then
printf "%s" "$raw_speed"
else
LC_ALL=C awk -v s="$raw_speed" 'BEGIN { printf "%.3f", s / 100 }'
fi
}
# Validate a density (maximum number of simultaneous drops, an integer >= 1)
# and print it; return 1 otherwise.
# Usage: _rain_normalize_density <density>
_rain_normalize_density()
{
local raw_density="$1"
if [[ ! "$raw_density" =~ ^[0-9]+$ || "$raw_density" -lt 1 ]]; then
return 1
fi
printf "%s" "$raw_density"
}
# Fill RAINBOW_BG_PALETTE with <width> background colour escapes sweeping the
# hue wheel: 6 bands of 256 steps in 24-bit colour, or 12 fixed ANSI colours
# spread over the width otherwise.
# Usage: _rainbow_build_palette <width> <use_truecolor (0|1)>
_rainbow_build_palette()
{
local palette_width="$1"
local use_truecolor="$2"
local x=0 wheel_pos=0 band=0 blend=0
local red=0 green=0 blue=0
RAINBOW_BG_PALETTE=()
if (( palette_width < 1 )); then
RAINBOW_BG_PALETTE+=("\e[41m")
return 0
fi
if (( use_truecolor )); then
for ((x = 0; x < palette_width; x++)); do
wheel_pos=$((x * 1536 / palette_width))
band=$((wheel_pos / 256))
blend=$((wheel_pos % 256))
case "$band" in
0)
red=255; green=$blend; blue=0
;;
1)
red=$((255 - blend)); green=255; blue=0
;;
2)
red=0; green=255; blue=$blend
;;
3)
red=0; green=$((255 - blend)); blue=255
;;
4)
red=$blend; green=0; blue=255
;;
*)
red=255; green=0; blue=$((255 - blend))
;;
esac
RAINBOW_BG_PALETTE+=("\e[48;2;${red};${green};${blue}m")
done
else
local ansi_palette=(41 101 43 103 42 102 46 106 44 104 45 105)
local ansi_count=${#ansi_palette[@]}
for ((x = 0; x < palette_width; x++)); do
RAINBOW_BG_PALETTE+=("\e[${ansi_palette[x * ansi_count / palette_width]}m")
done
fi
}
# Stop request of the animation loops: sets exit_st, a local of the running
# animation (dynamic scoping). Installed as the TERM/INT trap.
_rain_stop()
{
exit_st=1
}
# The animations read a key from the terminal and draw on it, and need tput.
# Usage: _rain_need_tty <command-name>
_rain_need_tty()
{
if [[ ! -t 0 || ! -t 1 ]]; then
disp E "$1: a terminal is required (standard input and output must be a terminal)."
return 1
fi
command -v tput >/dev/null 2>&1 || {
disp E "The program 'tput' is required but not installed."
return 1
}
}
# Prepare the terminal for an animation: save its settings and the TERM, INT
# and WINCH traps of the caller, install the animation traps, disable echo,
# hide the cursor and clear the screen.
# saved_stty and saved_traps are locals of the calling animation.
# Usage: _rain_enter <winch_handler_function>
_rain_enter()
{
saved_stty=$(stty -g 2>/dev/null)
saved_traps=$(trap -p TERM INT WINCH)
trap _rain_stop TERM INT
trap "$1" WINCH
stty -echo
printf '\e[?25l\e[2J'
}
# Undo _rain_enter: reset the colours (the last drawn drop colour would
# otherwise stay active), show the cursor, clear the screen, then restore the
# terminal settings and the caller's traps.
_rain_leave()
{
printf '\e[0m\e[?25h\e[2J\e[H'
[[ -n "$saved_stty" ]] && stty "$saved_stty"
trap - TERM INT WINCH
_trap_restore "$saved_traps"
return 0
}
# Shared engine of the rain and matrix animations: draws the falling drops until
# a key is pressed, a signal is received, or the terminal is resized (the
# state is rebuilt). The terminal state is always restored on exit.
# Usage: _rain_engine <delay_seconds> <colour> <rain|matrix> <charset> <density>
_rain_engine()
{
local step_duration="$1"
local base_color="$2"
local mode="$3"
local charset="$4"
local density_override="$5"
_rain_need_tty "$mode" || return 1
_rain_build_colors "$base_color" || return 1
_rain_build_chars "$mode" "$charset" || return 1
local rain_colors=("${RAIN_ENGINE_COLORS[@]}")
local rain_chars=("${RAIN_ENGINE_CHARS[@]}")
unset RAIN_ENGINE_COLORS RAIN_ENGINE_CHARS
local rain_color_tab=${#rain_colors[@]}
local rain_tab=${#rain_chars[@]}
local matrix_head_color=$'\e[1;97m'
local exit_st=0
local num_rain_metadata=5
local term_height=0 term_width=0
local X=0 Y=0 drop_length=0 rain_drop=0
local max_rain_width=0 max_rain_height=0
local new_rain_odd=0 falling_odd=0
local term_area=0
local frame_sleep="$step_duration"
local saved_stty="" saved_traps=""
# The drops: 5 consecutive entries each (X, Y of the top, character, colour,
# length).
local rains=()
local num_rains=0
local ch=""
# WINCH handler (and initial sizing). Called with the animation running, it
# sees its locals through dynamic scoping.
_rain_sigwinch()
{
term_width=$(tput cols)
term_height=$(tput lines)
term_area=$(( term_width * term_height ))
case "$mode" in
matrix)
max_rain_width=$(( term_area / 3 ))
max_rain_height=$(( term_height < 8 ? 1 : term_height / 6 ))
new_rain_odd=$(( term_height > 50 ? 100 : term_height * 2 ))
new_rain_odd=$(( new_rain_odd * 85 / 100 ))
falling_odd=100
# Adapt cadence and density to terminal size for smoother rendering.
if ((term_area < 1200)); then
max_rain_width=$(( term_area / 4 ))
frame_sleep=$(LC_ALL=C awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 1.15 }')
elif ((term_area > 5000)); then
max_rain_width=$(( term_area / 2 ))
frame_sleep=$(LC_ALL=C awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 0.85 }')
else
frame_sleep="$step_duration"
fi
;;
*)
max_rain_width=$(( term_area / 4 ))
max_rain_height=$(( term_height < 10 ? 1 : term_height / 10 ))
new_rain_odd=$(( term_height > 50 ? 100 : term_height * 2 ))
new_rain_odd=$(( new_rain_odd * 75 / 100 ))
falling_odd=$(( term_height > 25 ? 100 : term_height * 4 ))
falling_odd=$(( falling_odd * 90 / 100 ))
frame_sleep="$step_duration"
;;
esac
if [[ -n "$density_override" ]]; then
max_rain_width="$density_override"
fi
# The drops were placed for the previous size and the screen content
# does not fit the new one: start again from an empty screen.
rains=()
num_rains=0
printf '\e[2J'
}
# Draw one frame. The whole frame (erasing the previous drop positions,
# then the new ones) is built in a string and written with one printf: a
# write per character made the animation slow and flickering.
_rain_render()
{
local idx=0 y=0 drop_color="" current_char="" render_color="" out=""
local esc=$'\e'
for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do
X=${rains[idx]}
Y=${rains[idx + 1]}
drop_length=${rains[idx + 4]}
for ((y = Y; y < Y + drop_length; y++)); do
((y < 1 || y > term_height)) && continue
out+="${esc}[${y};${X}H "
done
done
for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do
if ((100 * RANDOM / 32768 < falling_odd)); then
if ((++rains[idx + 1] > term_height)); then
# The drop left the screen: remove it. The next drop moves
# into this slot, which must be processed again (the loop
# increment would skip it, and it would vanish for a frame).
rains=("${rains[@]:0:idx}" "${rains[@]:idx+num_rain_metadata}")
num_rains=$(( num_rains - 1 ))
idx=$(( idx - num_rain_metadata ))
continue
fi
fi
X=${rains[idx]}
Y=${rains[idx + 1]}
rain_drop=${rains[idx + 2]}
drop_color=${rains[idx + 3]}
drop_length=${rains[idx + 4]}
for ((y = Y; y < Y + drop_length; y++)); do
((y < 1 || y > term_height)) && continue
if [[ "$mode" == "matrix" ]]; then
current_char="${rain_chars[rain_tab * RANDOM / 32768]}"
if ((y == Y + drop_length - 1)); then
render_color="$matrix_head_color"
else
render_color="$drop_color"
fi
else
current_char="$rain_drop"
render_color="$drop_color"
fi
# render_color holds a literal \e[...m: hence %b below
out+="${esc}[${y};${X}H${render_color}${current_char}"
done
done
[[ -n "$out" ]] && printf '%b' "$out"
return 0
}
_rain_enter _rain_sigwinch
_rain_sigwinch
while ((exit_st <= 0)); do
ch=""
# A timeout makes read fail (status > 128): expected, not an error
read -r -n 1 -t "$frame_sleep" ch || true
case "$ch" in
q|Q)
_rain_stop
;;
esac
if ((num_rains < max_rain_width)) && ((100 * RANDOM / 32768 < new_rain_odd)); then
rain_drop="${rain_chars[rain_tab * RANDOM / 32768]}"
drop_color="${rain_colors[rain_color_tab * RANDOM / 32768]}"
if [[ "$mode" == "matrix" ]]; then
drop_length=$((max_rain_height * RANDOM / 32768 + 2))
else
drop_length=$((max_rain_height * RANDOM / 32768 + 1))
fi
X=$((term_width * RANDOM / 32768 + 1))
Y=$((1 - drop_length))
rains+=("$X" "$Y" "$rain_drop" "$drop_color" "$drop_length")
num_rains=$(( num_rains + 1 ))
fi
_rain_render
done
_rain_leave
unset -f _rain_sigwinch _rain_render
return 0
}
+63
View File
@@ -0,0 +1,63 @@
#!/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.
# ------------------------------------------------------------------------------
# Private helpers of profile.d/ssh.sh, loaded by the module itself.
# Internal helper: remove <key> (a name or an IP, in known_hosts notation, so
# also "[host]:port") from the known_hosts file <file> with ssh-keygen -R.
# ssh-keygen writes a new file: run by root on another user's file (--all-users)
# it would leave it owned by root with mode 0644, which keeps that user's ssh
# from adding host keys, or exposes a private file. The owner and mode of the
# file, and of its ".old" backup, are therefore put back.
# Returns ssh-keygen's status (non-zero when the key was not found).
# Usage: _rmhost_remove <key> <file>
_rmhost_remove()
{
local key="$1" file="$2" owner="" mode="" rc=0
if [[ ${EUID:-$(id -u)} -eq 0 ]]; then
owner=$(stat -c '%u:%g' -- "$file" 2>/dev/null)
mode=$(stat -c '%a' -- "$file" 2>/dev/null)
fi
ssh-keygen -R "$key" -f "$file" </dev/null >/dev/null 2>&1 || rc=$?
if [[ -n $owner && -n $mode ]]; then
chown "$owner" -- "$file" "${file}.old" 2>/dev/null
chmod "$mode" -- "$file" "${file}.old" 2>/dev/null
fi
return "$rc"
}
export -f _rmhost_remove
+62
View File
@@ -0,0 +1,62 @@
#!/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.
# ------------------------------------------------------------------------------
# Private helpers of profile.d/updates.sh, loaded by the module itself.
# Check a branch name. It ends up in URLs and in git command lines, so it is
# restricted to plain ref characters, without a leading "-" or "..".
# Usage: _updt_valid_branch <name>
_updt_valid_branch()
{
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ && "$1" != *..* ]]
}
# Tell whether the published version $2 is an update for the installed one $1.
# Versions look like MAJOR.MINOR.PATCH[-REVISION[_STAGE_N]]. The base version
# (before the first "-" or "_") decides, compared in version order; with the same
# base, any difference (a new revision, a release candidate becoming final) is
# an update. Returns 0 when it is, 1 otherwise (equal, or the installed version
# is the more recent one, as on a development branch).
# Usage: _updt_is_newer <installed> <published>
_updt_is_newer()
{
local cur="$1" new="$2" cur_base new_base
[[ "$cur" == "$new" ]] && return 1
cur_base="${cur%%[-_]*}"
new_base="${new%%[-_]*}"
[[ "$cur_base" == "$new_base" ]] && return 0
[[ "$(printf '%s\n%s\n' "$cur_base" "$new_base" | sort -V | tail -n 1)" == "$new_base" ]]
}
+3
View File
@@ -34,6 +34,9 @@
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
# Common helpers (helpers/common.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
load_conf "compress"
# ------------------------------------------------------------------------------
+3 -25
View File
@@ -34,23 +34,9 @@
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Print the path of the configuration file the conf_* functions work on:
# the user file ($HOME/.profile.conf) when it exists, otherwise the installation
# file ($PROFILE_CONF, defaulting to $MYPATH/profile.conf). The returned path
# may not exist yet.
# Usage: _conf_path
_conf_path()
{
if [[ -f "$HOME/.profile.conf" ]]; then
printf '%s\n' "$HOME/.profile.conf"
else
printf '%s\n' "${PROFILE_CONF:-${MYPATH}/profile.conf}"
fi
}
export -f _conf_path
# ------------------------------------------------------------------------------
# Common helpers, then the ones private to this module (helpers/conf.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Save or update a key=value pair in a section of the profile configuration file.
@@ -213,9 +199,6 @@ conf_save()
}
}
export -f conf_save
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Display the profile configuration file, with optional section and key filters.
# The same file resolution as conf_save is used: $HOME/.profile.conf when
@@ -401,9 +384,6 @@ conf_dump()
' "$conf_file" "$conf_file"
}
export -f conf_dump
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Set TERM to the best terminal capability available on this system.
# Called automatically at sourcing time.
@@ -526,8 +506,6 @@ term_set()
export -f term_set
# ------------------------------------------------------------------------------
term_set
# EOF
+3
View File
@@ -34,6 +34,9 @@
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
# Common helpers (helpers/common.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
load_conf "debug"
# ------------------------------------------------------------------------------
+3
View File
@@ -34,6 +34,9 @@
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
# Common helpers (helpers/common.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
load_conf disp
# ------------------------------------------------------------------------------
+3
View File
@@ -34,6 +34,9 @@
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
# Common helpers (helpers/common.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
load_conf "filefct"
# ------------------------------------------------------------------------------
+6 -77
View File
@@ -36,73 +36,9 @@
load_conf "fun"
# ------------------------------------------------------------------------------
# Shared helpers of the demo functions below (busy, fake_compile, hack).
# They are exported because the public functions are.
# True when $1 is a non-negative integer. Every value that ends up in an
# arithmetic context must pass this first (this includes values coming from the
# configuration file): bash evaluates non-numeric strings inside [[ -gt ]] or
# $(( )) as expressions, which can run arbitrary commands.
# Usage: _fun_is_uint <value>
_fun_is_uint()
{
[[ "$1" =~ ^[0-9]+$ ]]
}
export -f _fun_is_uint
# Convert a number of milliseconds into a seconds string usable by sleep
# (1500 -> 1.500), in pure bash (no awk fork per call).
# Usage: _fun_ms_to_s <ms>
_fun_ms_to_s()
{
printf '%d.%03d' $(( $1 / 1000 )) $(( $1 % 1000 ))
}
export -f _fun_ms_to_s
# Fail with a message unless a terminal is available: the demos wait for a key
# read from /dev/tty to stop.
# Usage: _fun_need_tty <command-name>
_fun_need_tty()
{
if ! { : </dev/tty; } 2>/dev/null; then
disp E "$1: a terminal is required (it runs until a key is pressed)."
return 1
fi
}
export -f _fun_need_tty
# Wait for a key press, then stop the background job $1 and its children.
# The job runs as "( ... ) &"; with job control (interactive shell) it is the
# leader of its own process group, which is signalled as a whole. Without job
# control that group does not exist, so its direct children (the pipeline) are
# killed first, then the subshell. SIGINT is trapped meanwhile so that Ctrl-C
# stops the job like a key press instead of aborting the caller and leaving it
# running. The wait is a polling loop of short timed reads: a blocking read is
# restarted by bash after a trapped signal and would never notice the Ctrl-C.
# Usage: _fun_stop_on_key <pid>
_fun_stop_on_key()
{
local pid=$1 old_int interrupted=0
old_int=$(trap -p INT)
trap 'interrupted=1' INT
while (( ! interrupted )); do
IFS= read -r -n 1 -s -t 0.3 _ </dev/tty && break
# Status above 128 is just the timeout; anything else (such as end of
# input on the terminal) ends the wait.
(( $? > 128 )) || break
done
kill -- -"$pid" 2>/dev/null || {
pkill -P "$pid" 2>/dev/null
kill "$pid" 2>/dev/null
}
wait "$pid" 2>/dev/null
trap - INT
_trap_restore "$old_int"
return 0
}
export -f _fun_stop_on_key
# ------------------------------------------------------------------------------
# Common helpers, then the ones private to this module (helpers/fun.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Make non-IT peoples think you're busy doing something hard: print the lines of
@@ -165,7 +101,7 @@ busy()
[[ $# -gt 0 ]] && pattern="$*"
# Checked here so that the configured default is validated as well as -d
if ! _fun_is_uint "$delay_ms"; then
if ! _is_uint "$delay_ms"; then
disp E "Invalid delay '${delay_ms}': must be an integer (milliseconds)."
return 1
fi
@@ -191,9 +127,6 @@ busy()
_fun_stop_on_key "$sub_pid"
}
export -f busy
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Simulate a long and complex compilation process
# Usage: fake_compile [options]
@@ -280,7 +213,7 @@ fake_compile()
# Validated here (not only in the options) so that the values coming from
# the configuration file are checked too.
if ! _fun_is_uint "$min_ms" || ! _fun_is_uint "$max_ms"; then
if ! _is_uint "$min_ms" || ! _is_uint "$max_ms"; then
disp E "min-delay and max-delay must be integers (milliseconds)."
return 1
fi
@@ -396,9 +329,6 @@ fake_compile()
_fun_stop_on_key "$sub_pid"
}
export -f fake_compile
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Simulate a dramatic hacking sequence
# Usage: hack [options]
@@ -470,7 +400,7 @@ hack()
esac
done
if ! _fun_is_uint "$min_ms" || ! _fun_is_uint "$max_ms"; then
if ! _is_uint "$min_ms" || ! _is_uint "$max_ms"; then
disp E "min-delay and max-delay must be integers (milliseconds)."
return 1
fi
@@ -598,5 +528,4 @@ hack()
export -f hack
# ------------------------------------------------------------------------------
# EOF
+4 -97
View File
@@ -36,6 +36,10 @@
load_conf git
# Common helpers, then the ones private to this module (helpers/git.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Built-in defaults (can be overridden from [git] section in profile.conf)
: "${GIT_MAIN_BRANCH:=main}"
@@ -43,81 +47,6 @@ load_conf git
: "${GIT_WIP_PREFIX:=wip}"
: "${GIT_GACP_AUTO_ADD:=1}"
# ------------------------------------------------------------------------------
# Internal helper: ensure git is available and cwd is a git worktree
_git_require_repo()
{
if ! command -v git >/dev/null 2>&1; then
disp E "git command not found."
return 1
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
disp E "Current directory is not inside a git repository."
return 1
fi
return 0
}
export -f _git_require_repo
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Internal helper: return default branch from remote HEAD, fallback to config
_git_default_branch()
{
local remote="${1:-$GIT_DEFAULT_REMOTE}"
local head
head=$(git symbolic-ref --quiet --short "refs/remotes/${remote}/HEAD" 2>/dev/null) || true
if [[ -n $head ]]; then
printf "%s\n" "${head#"${remote}"/}"
return 0
fi
printf "%s\n" "$GIT_MAIN_BRANCH"
return 0
}
export -f _git_default_branch
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Internal helper: print the name of the checked-out branch; fail with an error
# message on a detached HEAD (commands that push or rebase need a branch).
_git_current_branch()
{
local b
b=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || {
disp E "HEAD is detached: check out a branch first."
return 1
}
printf "%s\n" "$b"
}
export -f _git_current_branch
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Internal helper: print "<remote><TAB><branch>" for the remote branch tracked
# by local branch $1, read from its configuration (branch.<name>.remote/.merge).
# Unlike parsing the "origin/feature/x" form given by @{u}, this is exact for
# remote and branch names containing slashes. Fails when nothing is tracked or
# when the branch tracks another local branch (remote ".").
_git_tracking()
{
local branch=$1 remote merge
remote=$(git config --get "branch.${branch}.remote") || return 1
merge=$(git config --get "branch.${branch}.merge") || return 1
[[ $remote == "." ]] && return 1
printf "%s\t%s\n" "$remote" "${merge#refs/heads/}"
}
export -f _git_tracking
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Display compact git status + branch tracking information
# Usage: gst [path]
@@ -165,9 +94,6 @@ gst()
git -C "$target" status --short --branch
}
export -f gst
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Show a readable commit graph
# Usage: ggraph [-n limit]
@@ -219,9 +145,6 @@ ggraph()
git log --graph --decorate --oneline --all --max-count="$limit"
}
export -f ggraph
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Sync current branch with remote (fetch + rebase)
# Usage: gsync [remote]
@@ -288,9 +211,6 @@ gsync()
git rebase "$upstream"
}
export -f gsync
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Add, commit, and push changes with automatic pull/rebase if needed
# Usage: gacp -m "message" [file1 file2 ...]
@@ -414,9 +334,6 @@ gacp()
return 0
}
export -f gacp
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Reset local branch to exact upstream state (stash local changes first)
# Usage: greset [target]
@@ -553,9 +470,6 @@ greset()
return 0
}
export -f greset
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Create a quick WIP commit for local checkpointing
# Usage: gwip [message]
@@ -604,9 +518,6 @@ gwip()
git commit -m "$msg"
}
export -f gwip
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Delete merged local branches (except protected branches)
# Usage: gprune [-D|--deep] [main-branch]
@@ -728,9 +639,6 @@ gprune()
fi
}
export -f gprune
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Print repository root path
# Usage: groot
@@ -790,5 +698,4 @@ groot()
export -f groot
# ------------------------------------------------------------------------------
# EOF
+3
View File
@@ -34,6 +34,9 @@
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
# Common helpers (helpers/common.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
load_conf info
# ------------------------------------------------------------------------------
+3
View File
@@ -34,6 +34,9 @@
# * OF SUCH DAMAGE.
# ------------------------------------------------------------------------------
# Common helpers (helpers/common.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
load_conf lang
# ------------------------------------------------------------------------------
+4
View File
@@ -36,6 +36,10 @@
load_conf "net"
# Common helpers, then the ones private to this module (helpers/packages.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Download a resource using curl, wget, or fetch.
# Usage: dwl [-t <seconds>] [-r|--resume] <url> [output_file]
+3 -51
View File
@@ -36,6 +36,9 @@
load_conf "packages"
# Common helpers, then the ones private to this module (helpers/packages.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Detect the active package manager of the current distribution.
@@ -116,9 +119,6 @@ get_pkgmgr()
return 1
}
export -f get_pkgmgr
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Look for a package within installed one
# Usage: pkgs <string>
@@ -197,9 +197,6 @@ pkgs()
"${list_cmd[@]}" | grep ${grep_opt:+"$grep_opt"} -e "$pkg"
}
export -f pkgs
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Find which installed package a file belongs to.
# Usage: pkgf <file>
@@ -286,47 +283,6 @@ pkgf()
esac
}
export -f pkgf
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Internal helper: run a package manager command, with root privileges when
# needed (sudo, else doas), or only display it in dry-run mode.
# Usage: _pkg_run <needs_root 0|1> <dry_run 0|1> <command> [args...]
_pkg_run()
{
local needs_root="$1" dry="$2"
shift 2
local -a prefix=()
if (( needs_root )) && [[ $EUID -ne 0 ]]; then
if command -v sudo >/dev/null 2>&1; then
prefix=(sudo)
elif command -v doas >/dev/null 2>&1; then
prefix=(doas)
elif (( ! dry )); then
disp E "Root privileges are required and neither sudo nor doas is available."
return 1
fi
fi
if (( dry )); then
# %q gives a copy-pasteable command line
printf '%q ' "${prefix[@]}" "$@"
printf '\n'
return 0
fi
if ! command -v "$1" >/dev/null 2>&1; then
disp E "'$1' is not installed."
return 1
fi
"${prefix[@]}" "$@"
}
export -f _pkg_run
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Install packages with the package manager of the distribution.
# Usage: pkgi [options] <package> [package...]
@@ -441,9 +397,6 @@ pkgi()
_pkg_run "$needs_root" "$dry_run" "${cmd[@]}" "$@"
}
export -f pkgi
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Uninstall packages with the package manager of the distribution.
# Usage: pkgr [options] <package> [package...]
@@ -597,5 +550,4 @@ pkgr()
export -f pkgr
# ------------------------------------------------------------------------------
# EOF
+3 -76
View File
@@ -36,6 +36,9 @@
load_conf "processes"
# Common helpers, then the ones private to this module (helpers/processes.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Search processes matching the given string
@@ -106,9 +109,6 @@ ppg()
'
}
export -f ppg
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# List processes owned by a specific user
# Usage: ppu <username>
@@ -156,9 +156,6 @@ ppu()
ps -u "$1" -o "${PPU_DEFAULT_FORMAT:-pid,user,%cpu,%mem,start,time,command}"
}
export -f ppu
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# List processes by exact command name (no path/parameters)
# Usage: ppn <command_name>
@@ -217,9 +214,6 @@ ppn()
'
}
export -f ppn
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Get PID list of the given process name
# Usage: gpid <process_name [process_name2 ...]>
@@ -300,41 +294,6 @@ gpid()
(( found )) || return 1
}
export -f gpid
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Internal helper: take the kill-style signal arguments (-9, -KILL, -SIGHUP)
# out of an argument list. getopt rejects them as unknown options, so they must
# be removed before it runs. They are recognised anywhere before a "--"
# (digits, or upper-case names, which cannot clash with the lower-case options
# of the callers); everything after "--" is left untouched.
# Usage: _proc_split_signals <signals_array> <rest_array> [args...]
_proc_split_signals()
{
local -n _pss_sigs="$1" _pss_rest="$2"
shift 2
_pss_sigs=()
_pss_rest=()
local a
while [[ $# -gt 0 ]]; do
a="$1"
shift
if [[ $a == "--" ]]; then
_pss_rest+=(-- "$@")
break
fi
if [[ $a =~ ^-([0-9]+|SIG[A-Z0-9+-]+|[A-Z][A-Z0-9+-]*)$ ]]; then
_pss_sigs+=("$a")
else
_pss_rest+=("$a")
fi
done
}
export -f _proc_split_signals
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Kill all processes owned by the given users (kill user)
# Usage: ku [options] <username1 [username2 ...]>
@@ -460,36 +419,6 @@ ku()
return "$rc"
}
export -f ku
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Internal helper of kt: send a signal to a process and, first, to all its
# descendants (depth first, so a parent never outlives the children it could
# respawn from). A failure does not stop the others; the status is non-zero if
# any kill failed.
# Usage: _kt_tree <dry_run 0|1> <pid> [kill_args...]
_kt_tree()
{
local dry="$1" pid="$2" child rc=0
shift 2
for child in $(pgrep -P "$pid" 2>/dev/null); do
_kt_tree "$dry" "$child" "$@" || rc=1
done
if (( dry )); then
printf "DRY-RUN: "
printf "%q " kill "$@" "$pid"
printf "\n"
else
kill "$@" "$pid" || rc=1
fi
return "$rc"
}
export -f _kt_tree
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Kill all children of a process then the process (kill tree)
# Usage: kt [options] <pid>
@@ -580,6 +509,4 @@ kt()
export -f kt
# ------------------------------------------------------------------------------
# EOF
+3 -247
View File
@@ -34,74 +34,11 @@
# * 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
}
# Common helpers, then the ones private to this module (helpers/prompt.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.
@@ -192,59 +129,6 @@ load_theme()
}
# 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
@@ -419,34 +303,11 @@ set_theme()
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
@@ -498,107 +359,6 @@ timer_stop()
# 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
}
# 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=$?
}
# ------------------------------------------------------------------------------
# Function triggered internally by bash : defining prompt
# Usage: set_prompt
@@ -680,9 +440,6 @@ set_prompt()
# Print the working directory and prompt marker, then reset colour.
PS1+="\[${_dir_fg}\]\\w \\\$\[$RESETCOL\] "
}
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Theme and configuration loading.
# Precedence (lowest → highest):
@@ -703,5 +460,4 @@ unset _pt_theme _pt_dir
_prompt_apply_overrides
# ------------------------------------------------------------------------------
# EOF
+3 -39
View File
@@ -36,41 +36,9 @@
load_conf "pwd"
# ------------------------------------------------------------------------------
# Internal helper: put in _PWD_RAND an integer uniformly distributed in
# [0, $1), for 1 <= $1 <= 2^32. $RANDOM is NOT usable for passwords: it only
# has 15 bits, a state seeded from the time and the PID, and is predictable.
# The source is $SRANDOM (bash >= 5.1, from the kernel's random generator), or
# else /dev/urandom read through od, 256 words at a time to limit the number
# of processes. Values above the largest multiple of $1 are rejected, so that
# "% $1" introduces no bias.
# Usage: _pwd_rand <max>
_pwd_rand()
{
local max="$1" limit r
limit=$(( 4294967296 - 4294967296 % max ))
while :; do
if [[ -n "${SRANDOM+x}" ]]; then
r="$SRANDOM"
else
if (( ${#_PWD_POOL[@]} == 0 )); then
# shellcheck disable=SC2207 # whitespace-separated numbers
_PWD_POOL=( $(od -An -v -N1024 -tu4 /dev/urandom 2>/dev/null) )
if (( ${#_PWD_POOL[@]} == 0 )); then
disp E "No source of random numbers available (/dev/urandom)."
return 1
fi
fi
r="${_PWD_POOL[-1]}"
unset '_PWD_POOL[-1]'
fi
if (( r < limit )); then
_PWD_RAND=$(( r % max ))
return 0
fi
done
}
_PWD_POOL=()
# Common helpers, then the ones private to this module (helpers/pwd.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# genpwd : generate a password with different criteria
@@ -332,9 +300,6 @@ genpwd()
done
}
export -f genpwd
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# pwdscore : score a password quality from 1 to 100
# Usage: pwdscore [options] <password>
@@ -541,5 +506,4 @@ pwdscore()
export -f pwdscore
# ------------------------------------------------------------------------------
# EOF
+3 -410
View File
@@ -36,416 +36,9 @@
load_conf "rain"
# ------------------------------------------------------------------------------
# Generic rain-like engine and presets
_rain_build_colors()
{
# Case-insensitive: the configuration documents capitalised names (Green)
local base_color="${1,,}"
local i
RAIN_ENGINE_COLORS=()
case "$base_color" in
green|blue|red|yellow|cyan|white) ;;
*)
disp E "Unknown color: ${1} (supported: green, blue, red, yellow, cyan, white)."
return 1
;;
esac
local use_truecolor=0
# term_set() already sets TERM=*-direct when truecolor is available;
# honour COLORTERM as a belt-and-suspenders fallback.
[[ "$TERM" == *direct* || "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]] && use_truecolor=1
if (( use_truecolor )); then
# 24-bit gradient from a near-black shade to a vivid hue.
# 20 steps provide smooth depth variation across simultaneous drops.
local steps=20
local r1 g1 b1 r2 g2 b2
case "${base_color}" in
green) r1=0; g1=12; b1=2; r2=50; g2=255; b2=90 ;;
blue) r1=0; g1=5; b1=25; r2=60; g2=140; b2=255 ;;
red) r1=15; g1=0; b1=0; r2=255; g2=40; b2=40 ;;
yellow) r1=25; g1=18; b1=0; r2=255; g2=240; b2=50 ;;
cyan) r1=0; g1=18; b1=18; r2=50; g2=255; b2=240 ;;
*) r1=40; g1=40; b1=45; r2=220; g2=220; b2=255 ;;
esac
local r g b
for ((i = 0; i < steps; i++)); do
r=$(( r1 + (r2 - r1) * i / (steps - 1) ))
g=$(( g1 + (g2 - g1) * i / (steps - 1) ))
b=$(( b1 + (b2 - b1) * i / (steps - 1) ))
RAIN_ENGINE_COLORS+=("\e[38;2;${r};${g};${b}m")
done
else
# Fallback: 256-colour palettes.
case $base_color in
green)
for i in {22..28} {34..40} {46..48}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
blue)
for i in {17..21} {27..33} {39..45}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
red)
for i in {52..52} {88..88} {124..124} {160..160} {196..201}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
yellow)
for i in {58..58} {100..100} {142..142} {184..184} {226..229}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
cyan)
for i in {30..31} {37..38} {44..45} {50..51}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
*)
RAIN_ENGINE_COLORS=("\e[37m" "\e[37;1m")
for i in {244..255}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
esac
fi
}
_rain_build_chars()
{
local mode="$1"
local charset="$2"
RAIN_ENGINE_CHARS=()
case "$mode" in
matrix)
case "$charset" in
""|binary)
RAIN_ENGINE_CHARS=("0" "1")
;;
kana|kanji)
# Half-width katakana set, generally rendered as single-cell glyphs.
RAIN_ENGINE_CHARS=("ア" "イ" "ウ" "エ" "オ" "カ" "キ" "ク" "ケ" "コ" "サ" "シ" "ス" "セ" "ソ" "タ" "チ" "ツ" "テ" "ト" "ナ" "ニ" "ヌ" "ネ" "ノ" "ハ" "ヒ" "フ" "ヘ" "ホ" "マ" "ミ" "ム" "メ" "モ" "ヤ" "ユ" "ヨ" "ラ" "リ" "ル" "レ" "ロ" "ワ" "ン")
;;
ascii)
RAIN_ENGINE_CHARS=("0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F")
;;
*)
disp E "Unknown charset: ${charset} (supported: binary, kana, ascii)."
return 1
;;
esac
;;
*)
RAIN_ENGINE_CHARS=("|" "│" "┃" "┆" "┇" "┊" "┋" "╽" "╿")
;;
esac
return 0
}
_rain_normalize_speed()
{
local raw_speed="$1"
# Accept integer/floating values. UI scale is centiseconds by default:
# 5 -> 0.05s, 2.5 -> 0.025s. Values < 1 are treated as direct seconds
# for backward compatibility (e.g. 0.03).
if [[ ! "$raw_speed" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
return 1
fi
# A null delay is refused: "read -t 0" does not wait, the animation loop
# would spin and use a whole CPU.
if LC_ALL=C awk -v s="$raw_speed" 'BEGIN { exit !(s == 0) }'; then
return 1
fi
if LC_ALL=C awk -v s="$raw_speed" 'BEGIN { exit !(s < 1) }'; then
printf "%s" "$raw_speed"
else
LC_ALL=C awk -v s="$raw_speed" 'BEGIN { printf "%.3f", s / 100 }'
fi
}
_rain_normalize_density()
{
local raw_density="$1"
if [[ ! "$raw_density" =~ ^[0-9]+$ || "$raw_density" -lt 1 ]]; then
return 1
fi
printf "%s" "$raw_density"
}
_rainbow_build_palette()
{
local palette_width="$1"
local use_truecolor="$2"
local x=0 wheel_pos=0 band=0 blend=0
local red=0 green=0 blue=0
RAINBOW_BG_PALETTE=()
if (( palette_width < 1 )); then
RAINBOW_BG_PALETTE+=("\e[41m")
return 0
fi
if (( use_truecolor )); then
for ((x = 0; x < palette_width; x++)); do
wheel_pos=$((x * 1536 / palette_width))
band=$((wheel_pos / 256))
blend=$((wheel_pos % 256))
case "$band" in
0)
red=255; green=$blend; blue=0
;;
1)
red=$((255 - blend)); green=255; blue=0
;;
2)
red=0; green=255; blue=$blend
;;
3)
red=0; green=$((255 - blend)); blue=255
;;
4)
red=$blend; green=0; blue=255
;;
*)
red=255; green=0; blue=$((255 - blend))
;;
esac
RAINBOW_BG_PALETTE+=("\e[48;2;${red};${green};${blue}m")
done
else
local ansi_palette=(41 101 43 103 42 102 46 106 44 104 45 105)
local ansi_count=${#ansi_palette[@]}
for ((x = 0; x < palette_width; x++)); do
RAINBOW_BG_PALETTE+=("\e[${ansi_palette[x * ansi_count / palette_width]}m")
done
fi
}
# Stop request of the animation loops: sets exit_st, a local of the running
# animation (dynamic scoping). Installed as the TERM/INT trap.
_rain_stop()
{
exit_st=1
}
# The animations read a key from the terminal and draw on it, and need tput.
# Usage: _rain_need_tty <command-name>
_rain_need_tty()
{
if [[ ! -t 0 || ! -t 1 ]]; then
disp E "$1: a terminal is required (standard input and output must be a terminal)."
return 1
fi
command -v tput >/dev/null 2>&1 || {
disp E "The program 'tput' is required but not installed."
return 1
}
}
# Prepare the terminal for an animation: save its settings and the TERM, INT
# and WINCH traps of the caller, install the animation traps, disable echo,
# hide the cursor and clear the screen.
# saved_stty and saved_traps are locals of the calling animation.
# Usage: _rain_enter <winch_handler_function>
_rain_enter()
{
saved_stty=$(stty -g 2>/dev/null)
saved_traps=$(trap -p TERM INT WINCH)
trap _rain_stop TERM INT
trap "$1" WINCH
stty -echo
printf '\e[?25l\e[2J'
}
# Undo _rain_enter: reset the colours (the last drawn drop colour would
# otherwise stay active), show the cursor, clear the screen, then restore the
# terminal settings and the caller's traps.
_rain_leave()
{
printf '\e[0m\e[?25h\e[2J\e[H'
[[ -n "$saved_stty" ]] && stty "$saved_stty"
trap - TERM INT WINCH
_trap_restore "$saved_traps"
return 0
}
_rain_engine()
{
local step_duration="$1"
local base_color="$2"
local mode="$3"
local charset="$4"
local density_override="$5"
_rain_need_tty "$mode" || return 1
_rain_build_colors "$base_color" || return 1
_rain_build_chars "$mode" "$charset" || return 1
local rain_colors=("${RAIN_ENGINE_COLORS[@]}")
local rain_chars=("${RAIN_ENGINE_CHARS[@]}")
unset RAIN_ENGINE_COLORS RAIN_ENGINE_CHARS
local rain_color_tab=${#rain_colors[@]}
local rain_tab=${#rain_chars[@]}
local matrix_head_color=$'\e[1;97m'
local exit_st=0
local num_rain_metadata=5
local term_height=0 term_width=0
local X=0 Y=0 drop_length=0 rain_drop=0
local max_rain_width=0 max_rain_height=0
local new_rain_odd=0 falling_odd=0
local term_area=0
local frame_sleep="$step_duration"
local saved_stty="" saved_traps=""
# The drops: 5 consecutive entries each (X, Y of the top, character, colour,
# length).
local rains=()
local num_rains=0
local ch=""
# WINCH handler (and initial sizing). Called with the animation running, it
# sees its locals through dynamic scoping.
_rain_sigwinch()
{
term_width=$(tput cols)
term_height=$(tput lines)
term_area=$(( term_width * term_height ))
case "$mode" in
matrix)
max_rain_width=$(( term_area / 3 ))
max_rain_height=$(( term_height < 8 ? 1 : term_height / 6 ))
new_rain_odd=$(( term_height > 50 ? 100 : term_height * 2 ))
new_rain_odd=$(( new_rain_odd * 85 / 100 ))
falling_odd=100
# Adapt cadence and density to terminal size for smoother rendering.
if ((term_area < 1200)); then
max_rain_width=$(( term_area / 4 ))
frame_sleep=$(LC_ALL=C awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 1.15 }')
elif ((term_area > 5000)); then
max_rain_width=$(( term_area / 2 ))
frame_sleep=$(LC_ALL=C awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 0.85 }')
else
frame_sleep="$step_duration"
fi
;;
*)
max_rain_width=$(( term_area / 4 ))
max_rain_height=$(( term_height < 10 ? 1 : term_height / 10 ))
new_rain_odd=$(( term_height > 50 ? 100 : term_height * 2 ))
new_rain_odd=$(( new_rain_odd * 75 / 100 ))
falling_odd=$(( term_height > 25 ? 100 : term_height * 4 ))
falling_odd=$(( falling_odd * 90 / 100 ))
frame_sleep="$step_duration"
;;
esac
if [[ -n "$density_override" ]]; then
max_rain_width="$density_override"
fi
# The drops were placed for the previous size and the screen content
# does not fit the new one: start again from an empty screen.
rains=()
num_rains=0
printf '\e[2J'
}
# Draw one frame. The whole frame (erasing the previous drop positions,
# then the new ones) is built in a string and written with one printf: a
# write per character made the animation slow and flickering.
_rain_render()
{
local idx=0 y=0 drop_color="" current_char="" render_color="" out=""
local esc=$'\e'
for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do
X=${rains[idx]}
Y=${rains[idx + 1]}
drop_length=${rains[idx + 4]}
for ((y = Y; y < Y + drop_length; y++)); do
((y < 1 || y > term_height)) && continue
out+="${esc}[${y};${X}H "
done
done
for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do
if ((100 * RANDOM / 32768 < falling_odd)); then
if ((++rains[idx + 1] > term_height)); then
# The drop left the screen: remove it. The next drop moves
# into this slot, which must be processed again (the loop
# increment would skip it, and it would vanish for a frame).
rains=("${rains[@]:0:idx}" "${rains[@]:idx+num_rain_metadata}")
num_rains=$(( num_rains - 1 ))
idx=$(( idx - num_rain_metadata ))
continue
fi
fi
X=${rains[idx]}
Y=${rains[idx + 1]}
rain_drop=${rains[idx + 2]}
drop_color=${rains[idx + 3]}
drop_length=${rains[idx + 4]}
for ((y = Y; y < Y + drop_length; y++)); do
((y < 1 || y > term_height)) && continue
if [[ "$mode" == "matrix" ]]; then
current_char="${rain_chars[rain_tab * RANDOM / 32768]}"
if ((y == Y + drop_length - 1)); then
render_color="$matrix_head_color"
else
render_color="$drop_color"
fi
else
current_char="$rain_drop"
render_color="$drop_color"
fi
# render_color holds a literal \e[...m: hence %b below
out+="${esc}[${y};${X}H${render_color}${current_char}"
done
done
[[ -n "$out" ]] && printf '%b' "$out"
return 0
}
_rain_enter _rain_sigwinch
_rain_sigwinch
while ((exit_st <= 0)); do
ch=""
# A timeout makes read fail (status > 128): expected, not an error
read -r -n 1 -t "$frame_sleep" ch || true
case "$ch" in
q|Q)
_rain_stop
;;
esac
if ((num_rains < max_rain_width)) && ((100 * RANDOM / 32768 < new_rain_odd)); then
rain_drop="${rain_chars[rain_tab * RANDOM / 32768]}"
drop_color="${rain_colors[rain_color_tab * RANDOM / 32768]}"
if [[ "$mode" == "matrix" ]]; then
drop_length=$((max_rain_height * RANDOM / 32768 + 2))
else
drop_length=$((max_rain_height * RANDOM / 32768 + 1))
fi
X=$((term_width * RANDOM / 32768 + 1))
Y=$((1 - drop_length))
rains+=("$X" "$Y" "$rain_drop" "$drop_color" "$drop_length")
num_rains=$(( num_rains + 1 ))
fi
_rain_render
done
_rain_leave
unset -f _rain_sigwinch _rain_render
return 0
}
# Common helpers, then the ones private to this module (helpers/rain.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Let the rain fall (current style)
+3 -32
View File
@@ -36,34 +36,9 @@
load_conf "ssh"
# ------------------------------------------------------------------------------
# Internal helper: remove <key> (a name or an IP, in known_hosts notation, so
# also "[host]:port") from the known_hosts file <file> with ssh-keygen -R.
# ssh-keygen writes a new file: run by root on another user's file (--all-users)
# it would leave it owned by root with mode 0644, which keeps that user's ssh
# from adding host keys, or exposes a private file. The owner and mode of the
# file, and of its ".old" backup, are therefore put back.
# Returns ssh-keygen's status (non-zero when the key was not found).
# Usage: _rmhost_remove <key> <file>
_rmhost_remove()
{
local key="$1" file="$2" owner="" mode="" rc=0
if [[ ${EUID:-$(id -u)} -eq 0 ]]; then
owner=$(stat -c '%u:%g' -- "$file" 2>/dev/null)
mode=$(stat -c '%a' -- "$file" 2>/dev/null)
fi
ssh-keygen -R "$key" -f "$file" </dev/null >/dev/null 2>&1 || rc=$?
if [[ -n $owner && -n $mode ]]; then
chown "$owner" -- "$file" "${file}.old" 2>/dev/null
chmod "$mode" -- "$file" "${file}.old" 2>/dev/null
fi
return "$rc"
}
export -f _rmhost_remove
# ------------------------------------------------------------------------------
# Common helpers, then the ones private to this module (helpers/ssh.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Remove host entries (name and IP) from ~/.ssh/known_hosts for the active user
@@ -252,9 +227,6 @@ rmhost()
done
}
export -f rmhost
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Login root via SSH on the given machine
# Usage: ssr <server [ssh options]>
@@ -311,5 +283,4 @@ ssr()
export -f ssr
# ------------------------------------------------------------------------------
# EOF
+4 -29
View File
@@ -40,14 +40,11 @@ export BASE_URL="https://git.geoffray-levasseur.org/fatalerrors/profile"
# UPDT_DEFAULT_BRANCH (if set in profile.conf) is already available.
load_conf "updates"
# Common helpers, then the ones private to this module (helpers/updates.sh)
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
_profile_load_helpers "${BASH_SOURCE[0]}"
# ------------------------------------------------------------------------------
# Check a branch name. It ends up in URLs and in git command lines, so it is
# restricted to plain ref characters, without a leading "-" or "..".
# Usage: _updt_valid_branch <name>
_updt_valid_branch()
{
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ && "$1" != *..* ]]
}
if ! _updt_valid_branch "${UPDT_DEFAULT_BRANCH:-master}"; then
disp W "Invalid UPDT_DEFAULT_BRANCH '${UPDT_DEFAULT_BRANCH}', using master."
@@ -58,24 +55,6 @@ export UPDT_DEFAULT_BRANCH="${UPDT_DEFAULT_BRANCH:-master}"
export UPDT_URL="$BASE_URL/raw/branch/$UPDT_DEFAULT_BRANCH"
export ARCH_URL="$BASE_URL/archive/$UPDT_DEFAULT_BRANCH.tar.gz"
# ------------------------------------------------------------------------------
# Tell whether the published version $2 is an update for the installed one $1.
# Versions look like MAJOR.MINOR.PATCH[-REVISION[_STAGE_N]]. The base version
# (before the first "-" or "_") decides, compared in version order; with the same
# base, any difference (a new revision, a release candidate becoming final) is
# an update. Returns 0 when it is, 1 otherwise (equal, or the installed version
# is the more recent one, as on a development branch).
# Usage: _updt_is_newer <installed> <published>
_updt_is_newer()
{
local cur="$1" new="$2" cur_base new_base
[[ "$cur" == "$new" ]] && return 1
cur_base="${cur%%[-_]*}"
new_base="${new%%[-_]*}"
[[ "$cur_base" == "$new_base" ]] && return 0
[[ "$(printf '%s\n%s\n' "$cur_base" "$new_base" | sort -V | tail -n 1)" == "$new_base" ]]
}
# ------------------------------------------------------------------------------
# Check whether a newer profile version is available
# Usage: check_updates [-q]
@@ -168,9 +147,6 @@ check_updates()
return $result
}
export -f check_updates
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Apply the available profile upgrade
# Usage: profile_upgrade [options]
@@ -512,5 +488,4 @@ profile_upgrade()
export -f profile_upgrade
# ------------------------------------------------------------------------------
# EOF
+10
View File
@@ -455,6 +455,16 @@ fi
load_conf system # Load Bash system behavior configuration (history, pager, etc.)
load_conf general # General purpose configuration (compilation flags, etc.)
# Helpers shared by several modules (each module also loads the common helpers
# itself when it is sourced alone, see _profile_load_helpers).
if [[ -r "$MYPATH/helpers/common.sh" ]]; then
# shellcheck source=helpers/common.sh
. "$MYPATH/helpers/common.sh"
else
printf "[ Error ] Common helpers not found: %s\n" "$MYPATH/helpers/common.sh" >&2
return 1 2>/dev/null || exit 1
fi
# Load module scripts. nullglob is only needed for the expansion: the caller's
# setting is put back afterwards instead of being forced off.
_profile_ng=0